tuiweather 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/index.js +131 -95
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Keyboard-driven terminal weather app: Dark Sky-style rain nowcasting, hourly and
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Rain nowcast** — minute-level "umbrella in N min" warnings derived from 15-minute precipitation buckets
|
|
7
|
+
- **Rain nowcast** — minute-level "umbrella in N min" warnings derived from 15-minute precipitation buckets (Open-Meteo only; NWS has no minute-level precipitation feed — the nowcast panel hides and the watch bell stays inactive under `provider = "nws"`, nothing renders a false "Dry")
|
|
8
8
|
- **Hourly + daily forecast** — sparkline temperature strip, emoji condition icons, precipitation probabilities, and (md+ widths) a UV/humidity/visibility summary row
|
|
9
9
|
- **Location search** — type `/`, search the Open-Meteo geocoder, enter to add; locations persist to config
|
|
10
10
|
- **Guided first run** — choose units and find a location without editing a config file
|
|
@@ -113,7 +113,9 @@ tuiweather watch --location seattle
|
|
|
113
113
|
|
|
114
114
|
Each poll prints the one-line status (prefixed with the location label). The bell rings only on a
|
|
115
115
|
dry → wet transition, not on every poll. Rain already in progress when the watch starts does not
|
|
116
|
-
bell.
|
|
116
|
+
bell. The bell depends on the minute-level nowcast, which is Open-Meteo only: under
|
|
117
|
+
`provider = "nws"` the nowcast panel hides and the bell stays inactive — nothing renders a false
|
|
118
|
+
"Dry". Desktop notifications are a planned follow-up.
|
|
117
119
|
|
|
118
120
|
## Configuration
|
|
119
121
|
|
|
@@ -154,7 +156,7 @@ longitude = -122.6765
|
|
|
154
156
|
| `time_format` | `12h` / `24h` / `auto` | `"auto"` | `auto` picks 12h when temperature units are imperial, else 24h |
|
|
155
157
|
| `refresh_minutes` | integer | `10` | Minimum `1` |
|
|
156
158
|
| `theme` | `day` / `night` / `auto` | `"auto"` | Accent palette; `auto` follows the location's sunrise/sunset. Text ink always adapts to your terminal's background |
|
|
157
|
-
| `provider` | `openmeteo` / `nws` | `"openmeteo"` | Weather data source; NWS
|
|
159
|
+
| `provider` | `openmeteo` / `nws` | `"openmeteo"` | Weather data source. Open-Meteo is the default full-feature experience; NWS is the official US source fallback — works: conditions, temperatures, precipitation probabilities; goes quiet: minute-level nowcast (panel hides, watch bell inactive), hourly/daily precip amounts (blank bars/chips), air quality |
|
|
158
160
|
| `daily_days` | integer | `7` | `1`–`16` forecast days |
|
|
159
161
|
| `hourly_hours` | integer | `24` | `12`–`48` forecast hours |
|
|
160
162
|
| `default_location` | string | none | Must match a `[[locations]]` slug |
|
package/dist/index.js
CHANGED
|
@@ -71422,7 +71422,7 @@ function peakProbability(points) {
|
|
|
71422
71422
|
}
|
|
71423
71423
|
function sliceUpcoming(points, nowUtc, max) {
|
|
71424
71424
|
const nowMs = Date.parse(nowUtc);
|
|
71425
|
-
const upcoming = points.filter((p) => Date.parse(p.timeUtc)
|
|
71425
|
+
const upcoming = points.filter((p) => Date.parse(p.timeUtc) > nowMs);
|
|
71426
71426
|
return upcoming.slice(0, Math.max(0, max));
|
|
71427
71427
|
}
|
|
71428
71428
|
function seriesWidthFor(pointCount, width) {
|
|
@@ -71714,6 +71714,8 @@ function sortedBuckets(f) {
|
|
|
71714
71714
|
})).sort((a, b2) => a.startMs - b2.startMs);
|
|
71715
71715
|
}
|
|
71716
71716
|
function deriveNowcast(f, nowUtc) {
|
|
71717
|
+
if (!f.hasMinutePrecip)
|
|
71718
|
+
return { kind: "unavailable" };
|
|
71717
71719
|
const nowMs = Date.parse(nowUtc);
|
|
71718
71720
|
const buckets = sortedBuckets(f);
|
|
71719
71721
|
const first = buckets[0];
|
|
@@ -71778,6 +71780,8 @@ function describeNowcast(n) {
|
|
|
71778
71780
|
switch (n.kind) {
|
|
71779
71781
|
case "dry":
|
|
71780
71782
|
return "Dry";
|
|
71783
|
+
case "unavailable":
|
|
71784
|
+
return "Nowcast unavailable";
|
|
71781
71785
|
case "starting": {
|
|
71782
71786
|
const word = INTENSITY_WORD[n.intensity];
|
|
71783
71787
|
return n.startsInMin < 1 ? `${word} starting any minute` : `${word} starting in ${n.startsInMin} min`;
|
|
@@ -71812,6 +71816,8 @@ var NowcastBanner = import_react23.memo(function NowcastBanner2({
|
|
|
71812
71816
|
nowUtc
|
|
71813
71817
|
}) {
|
|
71814
71818
|
const palette = usePalette();
|
|
71819
|
+
if (nowcast.kind === "unavailable")
|
|
71820
|
+
return null;
|
|
71815
71821
|
if (nowcast.kind === "dry") {
|
|
71816
71822
|
if (hideWhenDry)
|
|
71817
71823
|
return null;
|
|
@@ -71857,9 +71863,7 @@ function errorMessage(e) {
|
|
|
71857
71863
|
return e instanceof Error ? e.message : String(e);
|
|
71858
71864
|
}
|
|
71859
71865
|
function truncateTo(text, width) {
|
|
71860
|
-
|
|
71861
|
-
return text;
|
|
71862
|
-
return `${text.slice(0, Math.max(0, width - 1))}\u2026`;
|
|
71866
|
+
return truncateCells(text, width);
|
|
71863
71867
|
}
|
|
71864
71868
|
function slugifyCandidate(name, admin1, countryCode) {
|
|
71865
71869
|
const raw = `${name}${admin1 ? `-${admin1}` : ""}-${countryCode ?? ""}`;
|
|
@@ -71879,7 +71883,7 @@ function buildLocationEntry(result, existingSlugs) {
|
|
|
71879
71883
|
const label = region.length > 0 ? `${result.name}, ${region}` : result.name;
|
|
71880
71884
|
return {
|
|
71881
71885
|
slug: uniqueSlug(slugifyCandidate(result.name, result.admin1, result.country_code), existingSlugs),
|
|
71882
|
-
label: (label.length > 0 ? label : "unnamed"
|
|
71886
|
+
label: truncateCells(label.length > 0 ? label : "unnamed", 80),
|
|
71883
71887
|
latitude: result.latitude,
|
|
71884
71888
|
longitude: result.longitude
|
|
71885
71889
|
};
|
|
@@ -71895,10 +71899,11 @@ function regionText(result) {
|
|
|
71895
71899
|
function resultLine(result, selected, width) {
|
|
71896
71900
|
const right = formatCoords(result.latitude, result.longitude);
|
|
71897
71901
|
const leftRaw = `${result.name} \xB7 ${regionText(result)}`;
|
|
71898
|
-
const avail = Math.max(0, width - 2 - right.length);
|
|
71899
|
-
const left = truncateTo(leftRaw, avail).padEnd(avail);
|
|
71900
71902
|
const prefix = selected ? "\u203A" : " ";
|
|
71901
|
-
|
|
71903
|
+
const avail = Math.max(0, width - displayWidth(prefix) - 1 - displayWidth(right));
|
|
71904
|
+
const left = truncateTo(leftRaw, avail);
|
|
71905
|
+
const padded = `${left}${" ".repeat(Math.max(0, avail - displayWidth(left)))}`;
|
|
71906
|
+
return `${prefix} ${padded}${right}`;
|
|
71902
71907
|
}
|
|
71903
71908
|
function LocationPicker({
|
|
71904
71909
|
searchLocations,
|
|
@@ -90905,22 +90910,17 @@ var HINTS_FULL = "/ search \xB7 r refresh \xB7 d del\xD72 \xB7 u units \xB7 [ ]
|
|
|
90905
90910
|
var HINTS_MD = "/ search \xB7 r refresh \xB7 d del\xD72 \xB7 \u2191\u2193 scroll \xB7 ? help \xB7 q quit";
|
|
90906
90911
|
var HINTS_SM = "r refresh \xB7 \u2191\u2193 scroll \xB7 ? help \xB7 q quit";
|
|
90907
90912
|
var HINTS_XS = "r u ? q";
|
|
90908
|
-
function footerText(tier) {
|
|
90909
|
-
|
|
90910
|
-
|
|
90911
|
-
if (tier === "sm")
|
|
90912
|
-
return HINTS_SM;
|
|
90913
|
-
if (tier === "md")
|
|
90914
|
-
return HINTS_MD;
|
|
90915
|
-
return HINTS_FULL;
|
|
90913
|
+
function footerText(tier, width) {
|
|
90914
|
+
const hints = tier === "xs" ? HINTS_XS : tier === "sm" ? HINTS_SM : tier === "md" ? HINTS_MD : HINTS_FULL;
|
|
90915
|
+
return truncateCells(hints, Math.max(1, width - 3));
|
|
90916
90916
|
}
|
|
90917
|
-
function Footer({ tier }) {
|
|
90917
|
+
function Footer({ tier, width }) {
|
|
90918
90918
|
const palette = usePalette();
|
|
90919
90919
|
return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
|
|
90920
90920
|
flexDirection: "row",
|
|
90921
90921
|
children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
|
|
90922
90922
|
fg: palette.fgDim,
|
|
90923
|
-
children: footerText(tier)
|
|
90923
|
+
children: footerText(tier, width)
|
|
90924
90924
|
}, undefined, false, undefined, this)
|
|
90925
90925
|
}, undefined, false, undefined, this);
|
|
90926
90926
|
}
|
|
@@ -90951,18 +90951,20 @@ function Header({
|
|
|
90951
90951
|
tier,
|
|
90952
90952
|
fetchedAtMs,
|
|
90953
90953
|
stale = false,
|
|
90954
|
-
nowMs = Date.now()
|
|
90954
|
+
nowMs = Date.now(),
|
|
90955
|
+
width
|
|
90955
90956
|
}) {
|
|
90956
90957
|
const palette = usePalette();
|
|
90957
90958
|
if (tier === "sm" || tier === "xs") {
|
|
90958
90959
|
const clock = clockUtc !== undefined ? formatClock(clockUtc, utcOffsetSeconds, timeFormat) : "--:--";
|
|
90959
90960
|
const date5 = tier === "sm" && clockUtc !== undefined ? formatDayDate(clockUtc, utcOffsetSeconds, "short") : undefined;
|
|
90960
90961
|
const line = [label, date5, clock].filter((part) => part !== undefined).join(" \xB7 ");
|
|
90962
|
+
const clipped = width === undefined ? line : truncateCells(line, Math.max(1, width - 1));
|
|
90961
90963
|
return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
|
|
90962
90964
|
flexDirection: "row",
|
|
90963
90965
|
children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
|
|
90964
90966
|
fg: palette.fg,
|
|
90965
|
-
children:
|
|
90967
|
+
children: clipped
|
|
90966
90968
|
}, undefined, false, undefined, this)
|
|
90967
90969
|
}, undefined, false, undefined, this);
|
|
90968
90970
|
}
|
|
@@ -91100,21 +91102,19 @@ function Spinner() {
|
|
|
91100
91102
|
children: SPINNER_FRAMES[frame] ?? "|"
|
|
91101
91103
|
}, undefined, false, undefined, this);
|
|
91102
91104
|
}
|
|
91103
|
-
function
|
|
91104
|
-
|
|
91105
|
-
|
|
91106
|
-
return
|
|
91105
|
+
function deleteArmLine(label, width) {
|
|
91106
|
+
const line = `press d again to delete ${label}`;
|
|
91107
|
+
const budget = width === undefined ? displayWidth(line) : Math.max(0, width - 1);
|
|
91108
|
+
return truncateCells(line, budget);
|
|
91107
91109
|
}
|
|
91108
91110
|
function StatusArea({ loading, error: error61, stale, deleteArm, width }) {
|
|
91109
91111
|
const palette = usePalette();
|
|
91110
91112
|
if (deleteArm !== undefined) {
|
|
91111
|
-
const line = `press d again to delete ${deleteArm.label}`;
|
|
91112
|
-
const budget = width === undefined ? line.length : Math.max(0, width - 1);
|
|
91113
91113
|
return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
|
|
91114
91114
|
flexDirection: "row",
|
|
91115
91115
|
children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
|
|
91116
91116
|
fg: palette.warn,
|
|
91117
|
-
children:
|
|
91117
|
+
children: deleteArmLine(deleteArm.label, width)
|
|
91118
91118
|
}, undefined, false, undefined, this)
|
|
91119
91119
|
}, undefined, false, undefined, this);
|
|
91120
91120
|
}
|
|
@@ -92702,6 +92702,7 @@ var geocodingResponseSchema = exports_external.object({
|
|
|
92702
92702
|
// src/lib/providers/openmeteo/geocoding.ts
|
|
92703
92703
|
var GEOCODING_ENDPOINT = "https://geocoding-api.open-meteo.com/v1/search";
|
|
92704
92704
|
var TIMEOUT_MS = 1e4;
|
|
92705
|
+
var NAME_MAX_CELLS = 120;
|
|
92705
92706
|
function buildGeocodingUrl(query, count = 8) {
|
|
92706
92707
|
const params = new URLSearchParams({
|
|
92707
92708
|
name: query,
|
|
@@ -92720,7 +92721,17 @@ function parseGeocodingResponse(body) {
|
|
|
92720
92721
|
if (!parsed.success) {
|
|
92721
92722
|
throw new ProviderError("openmeteo geocoding response failed schema validation", "openmeteo", parsed.error);
|
|
92722
92723
|
}
|
|
92723
|
-
|
|
92724
|
+
const results = parsed.data.results ?? [];
|
|
92725
|
+
for (const r of results) {
|
|
92726
|
+
r.name = sanitizeText(r.name, NAME_MAX_CELLS);
|
|
92727
|
+
if (r.country !== undefined)
|
|
92728
|
+
r.country = sanitizeText(r.country, NAME_MAX_CELLS);
|
|
92729
|
+
if (r.admin1 !== undefined)
|
|
92730
|
+
r.admin1 = sanitizeText(r.admin1, NAME_MAX_CELLS);
|
|
92731
|
+
if (r.admin2 !== undefined)
|
|
92732
|
+
r.admin2 = sanitizeText(r.admin2, NAME_MAX_CELLS);
|
|
92733
|
+
}
|
|
92734
|
+
return results;
|
|
92724
92735
|
}
|
|
92725
92736
|
async function searchLocations(query, count = 8) {
|
|
92726
92737
|
let res;
|
|
@@ -92746,6 +92757,64 @@ async function searchLocations(query, count = 8) {
|
|
|
92746
92757
|
}
|
|
92747
92758
|
return parseGeocodingResponse(body);
|
|
92748
92759
|
}
|
|
92760
|
+
// package.json
|
|
92761
|
+
var package_default2 = {
|
|
92762
|
+
name: "tuiweather",
|
|
92763
|
+
version: "0.3.1",
|
|
92764
|
+
description: "Keyboard-driven terminal weather app powered by Open-Meteo",
|
|
92765
|
+
license: "MIT",
|
|
92766
|
+
type: "module",
|
|
92767
|
+
bin: {
|
|
92768
|
+
tuiweather: "bin/tuiweather.js"
|
|
92769
|
+
},
|
|
92770
|
+
files: [
|
|
92771
|
+
"bin",
|
|
92772
|
+
"dist"
|
|
92773
|
+
],
|
|
92774
|
+
repository: {
|
|
92775
|
+
type: "git",
|
|
92776
|
+
url: "git+https://github.com/brndnsh-labs/tuiweather.git"
|
|
92777
|
+
},
|
|
92778
|
+
homepage: "https://github.com/brndnsh-labs/tuiweather#readme",
|
|
92779
|
+
bugs: "https://github.com/brndnsh-labs/tuiweather/issues",
|
|
92780
|
+
keywords: [
|
|
92781
|
+
"tui",
|
|
92782
|
+
"weather",
|
|
92783
|
+
"terminal",
|
|
92784
|
+
"cli",
|
|
92785
|
+
"open-meteo",
|
|
92786
|
+
"forecast"
|
|
92787
|
+
],
|
|
92788
|
+
publishConfig: {
|
|
92789
|
+
access: "public"
|
|
92790
|
+
},
|
|
92791
|
+
engines: {
|
|
92792
|
+
bun: ">=1.3.0"
|
|
92793
|
+
},
|
|
92794
|
+
scripts: {
|
|
92795
|
+
dev: "bun run src/index.tsx",
|
|
92796
|
+
build: "bun build ./src/index.tsx --outdir dist --target bun --external '@opentui/core-*'",
|
|
92797
|
+
typecheck: "tsc --noEmit",
|
|
92798
|
+
lint: "biome check .",
|
|
92799
|
+
fmt: "biome check --write .",
|
|
92800
|
+
test: "bun test",
|
|
92801
|
+
prepublishOnly: "bun run typecheck && bun run lint && bun run test && bun run build"
|
|
92802
|
+
},
|
|
92803
|
+
dependencies: {
|
|
92804
|
+
"@opentui/core": "^0.5.9",
|
|
92805
|
+
"@opentui/react": "^0.5.9",
|
|
92806
|
+
react: "^19.2.8",
|
|
92807
|
+
"smol-toml": "^1.8.0",
|
|
92808
|
+
zod: "4.5.2",
|
|
92809
|
+
zustand: "^5.0.15"
|
|
92810
|
+
},
|
|
92811
|
+
devDependencies: {
|
|
92812
|
+
"@biomejs/biome": "^2.5.11",
|
|
92813
|
+
"@types/bun": "^1.4.0",
|
|
92814
|
+
"@types/react": "^19.2.18",
|
|
92815
|
+
typescript: "^7.0.2"
|
|
92816
|
+
}
|
|
92817
|
+
};
|
|
92749
92818
|
|
|
92750
92819
|
// src/lib/providers/nws/conditions.ts
|
|
92751
92820
|
var NWS_ICON_CODES = {
|
|
@@ -93049,6 +93118,7 @@ function normalizeNwsForecast(data, location, window2) {
|
|
|
93049
93118
|
utcOffsetSeconds,
|
|
93050
93119
|
fetchedAtUtc: new Date().toISOString(),
|
|
93051
93120
|
current: normalizeCurrent(data.obs),
|
|
93121
|
+
hasMinutePrecip: false,
|
|
93052
93122
|
minutely15: [],
|
|
93053
93123
|
hourly: normalizeHourly(data.hourly, window2?.forecastHours),
|
|
93054
93124
|
daily: normalizeDaily(data.daily, window2?.forecastDays)
|
|
@@ -93127,8 +93197,9 @@ var nwsProblemSchema = exports_external.object({
|
|
|
93127
93197
|
|
|
93128
93198
|
// src/lib/providers/nws/client.ts
|
|
93129
93199
|
var NWS_PROVIDER_ID = "nws";
|
|
93130
|
-
var NWS_USER_AGENT =
|
|
93200
|
+
var NWS_USER_AGENT = `tuiweather/${package_default2.version} (github.com/brndnsh-labs/tuiweather)`;
|
|
93131
93201
|
var API_ROOT = "https://api.weather.gov";
|
|
93202
|
+
var ALLOWED_HOST = new URL(API_ROOT).host;
|
|
93132
93203
|
var TIMEOUT_MS2 = 1e4;
|
|
93133
93204
|
var MAX_DETAIL_CHARS = 200;
|
|
93134
93205
|
var NWS_HEADERS = {
|
|
@@ -93146,6 +93217,15 @@ function problemDetail(body) {
|
|
|
93146
93217
|
return detail === undefined ? undefined : sanitizeText(detail, MAX_DETAIL_CHARS);
|
|
93147
93218
|
}
|
|
93148
93219
|
async function getJson(url2, label) {
|
|
93220
|
+
let target;
|
|
93221
|
+
try {
|
|
93222
|
+
target = new URL(url2);
|
|
93223
|
+
} catch {
|
|
93224
|
+
throw new ProviderError(`nws ${label} url is not absolute`, NWS_PROVIDER_ID);
|
|
93225
|
+
}
|
|
93226
|
+
if (target.protocol !== "https:" || target.host !== ALLOWED_HOST) {
|
|
93227
|
+
throw new ProviderError(`nws ${label} url host rejected`, NWS_PROVIDER_ID);
|
|
93228
|
+
}
|
|
93149
93229
|
let res;
|
|
93150
93230
|
try {
|
|
93151
93231
|
res = await fetch(url2, { headers: NWS_HEADERS, signal: AbortSignal.timeout(TIMEOUT_MS2) });
|
|
@@ -93403,6 +93483,7 @@ function normalizeForecast(data, locationOverride) {
|
|
|
93403
93483
|
utcOffsetSeconds: data.utc_offset_seconds,
|
|
93404
93484
|
fetchedAtUtc: new Date().toISOString(),
|
|
93405
93485
|
current,
|
|
93486
|
+
hasMinutePrecip: true,
|
|
93406
93487
|
minutely15,
|
|
93407
93488
|
hourly,
|
|
93408
93489
|
daily
|
|
@@ -93611,6 +93692,7 @@ var normalizedForecastSchema = exports_external.object({
|
|
|
93611
93692
|
utcOffsetSeconds: exports_external.number(),
|
|
93612
93693
|
fetchedAtUtc: exports_external.string(),
|
|
93613
93694
|
current: currentObsSchema,
|
|
93695
|
+
hasMinutePrecip: exports_external.boolean(),
|
|
93614
93696
|
minutely15: exports_external.array(precipIntervalSchema),
|
|
93615
93697
|
hourly: exports_external.array(hourlyPointSchema),
|
|
93616
93698
|
daily: exports_external.array(dailyPointSchema)
|
|
@@ -93779,6 +93861,11 @@ var DEFAULT_REFRESH_TIMERS = {
|
|
|
93779
93861
|
},
|
|
93780
93862
|
clearInterval: (handle) => timersClearInterval(handle)
|
|
93781
93863
|
};
|
|
93864
|
+
var REFRESH_LOOP_MARGIN_MS = 30000;
|
|
93865
|
+
var MIN_REFRESH_LOOP_PERIOD_MS = 60000;
|
|
93866
|
+
function refreshLoopPeriodMs(refreshMinutes) {
|
|
93867
|
+
return Math.max(MIN_REFRESH_LOOP_PERIOD_MS, refreshMinutes * 60000 - REFRESH_LOOP_MARGIN_MS);
|
|
93868
|
+
}
|
|
93782
93869
|
var defaultFetcher = (location, opts) => cachedForecast(selectProvider(opts.provider ?? "openmeteo"), location, {
|
|
93783
93870
|
maxAgeMinutes: opts.maxAgeMinutes,
|
|
93784
93871
|
window: opts.window
|
|
@@ -93842,7 +93929,7 @@ function createStoreInstance(deps = prodDeps()) {
|
|
|
93842
93929
|
if (slug === null)
|
|
93843
93930
|
return;
|
|
93844
93931
|
get().loadForecast(slug);
|
|
93845
|
-
}, get().config.refresh_minutes
|
|
93932
|
+
}, refreshLoopPeriodMs(get().config.refresh_minutes));
|
|
93846
93933
|
}
|
|
93847
93934
|
function launchAirQuality(slug, location, provider) {
|
|
93848
93935
|
(async () => {
|
|
@@ -94169,7 +94256,8 @@ function estimateMainContentRows(input2) {
|
|
|
94169
94256
|
if (panels.details === true && Number.isFinite(sunriseMs) && Number.isFinite(sunsetMs) && sunsetMs > sunriseMs && width - 1 >= DAYLIGHT_MIN_WIDTH - 1) {
|
|
94170
94257
|
sections.push(1);
|
|
94171
94258
|
}
|
|
94172
|
-
|
|
94259
|
+
const nowcastKind = deriveNowcast(forecast, nowUtc).kind;
|
|
94260
|
+
if (tier !== "lg" && panels.nowcast && nowcastKind !== "dry" && nowcastKind !== "unavailable") {
|
|
94173
94261
|
sections.push(NOWCAST_BANNER_ROWS);
|
|
94174
94262
|
}
|
|
94175
94263
|
const hourlyRows = hourlyRowsFor(tier, width, forecast, panels, nowUtc);
|
|
@@ -94188,9 +94276,9 @@ function truncateTo4(text, width) {
|
|
|
94188
94276
|
function clampLine(label, width) {
|
|
94189
94277
|
if (width <= 1)
|
|
94190
94278
|
return "";
|
|
94191
|
-
if (label
|
|
94279
|
+
if (displayWidth(label) <= width - 2)
|
|
94192
94280
|
return `${label} \u2026`;
|
|
94193
|
-
return `${label
|
|
94281
|
+
return `${truncateCells(label, width - 2)} \u2026`;
|
|
94194
94282
|
}
|
|
94195
94283
|
function XsChips({
|
|
94196
94284
|
forecast,
|
|
@@ -94466,7 +94554,8 @@ function App(props = {}) {
|
|
|
94466
94554
|
tier: tier === "lg" || tier === "md" ? undefined : tier,
|
|
94467
94555
|
fetchedAtMs: entry?.fetchedAtMs,
|
|
94468
94556
|
stale,
|
|
94469
|
-
nowMs
|
|
94557
|
+
nowMs,
|
|
94558
|
+
width: viewport.width
|
|
94470
94559
|
}, undefined, false, undefined, this);
|
|
94471
94560
|
const staleBadge = stale && !loading && error61 === undefined;
|
|
94472
94561
|
const status = /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(StatusArea, {
|
|
@@ -94476,8 +94565,10 @@ function App(props = {}) {
|
|
|
94476
94565
|
deleteArm: deleteArmed ? { label } : undefined,
|
|
94477
94566
|
width: viewport.width
|
|
94478
94567
|
}, undefined, false, undefined, this);
|
|
94568
|
+
const footerColumnWidth = tier === "lg" ? viewport.width - SIDEBAR_WIDTH : viewport.width;
|
|
94479
94569
|
const footer = /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Footer, {
|
|
94480
|
-
tier
|
|
94570
|
+
tier,
|
|
94571
|
+
width: footerColumnWidth
|
|
94481
94572
|
}, undefined, false, undefined, this);
|
|
94482
94573
|
const mainWidth = tier === "lg" ? viewport.width - SIDEBAR_WIDTH - 4 : viewport.width - 4;
|
|
94483
94574
|
const forecastVisible = forecast !== undefined && !helpOpen && !overlayOpen;
|
|
@@ -94643,6 +94734,7 @@ function nowcastSegment(forecast, nowUtc) {
|
|
|
94643
94734
|
const nowcast = deriveNowcast(forecast, nowUtc);
|
|
94644
94735
|
switch (nowcast.kind) {
|
|
94645
94736
|
case "dry":
|
|
94737
|
+
case "unavailable":
|
|
94646
94738
|
return "";
|
|
94647
94739
|
case "starting":
|
|
94648
94740
|
return `\u2602 in ${nowcast.startsInMin}min`;
|
|
@@ -94686,7 +94778,9 @@ function buildJsonLine(forecast, location, prefs, nowUtc) {
|
|
|
94686
94778
|
|
|
94687
94779
|
// src/app/watch.ts
|
|
94688
94780
|
function shouldBell(prev, next) {
|
|
94689
|
-
|
|
94781
|
+
if (prev.kind !== "dry")
|
|
94782
|
+
return false;
|
|
94783
|
+
return next.kind === "starting" || next.kind === "ongoing" || next.kind === "stopping";
|
|
94690
94784
|
}
|
|
94691
94785
|
async function runWatch(opts) {
|
|
94692
94786
|
const nowUtcFn = opts.nowUtc ?? (() => new Date().toISOString());
|
|
@@ -94714,64 +94808,6 @@ async function runWatch(opts) {
|
|
|
94714
94808
|
}
|
|
94715
94809
|
return polls;
|
|
94716
94810
|
}
|
|
94717
|
-
// package.json
|
|
94718
|
-
var package_default2 = {
|
|
94719
|
-
name: "tuiweather",
|
|
94720
|
-
version: "0.3.0",
|
|
94721
|
-
description: "Keyboard-driven terminal weather app powered by Open-Meteo",
|
|
94722
|
-
license: "MIT",
|
|
94723
|
-
type: "module",
|
|
94724
|
-
bin: {
|
|
94725
|
-
tuiweather: "bin/tuiweather.js"
|
|
94726
|
-
},
|
|
94727
|
-
files: [
|
|
94728
|
-
"bin",
|
|
94729
|
-
"dist"
|
|
94730
|
-
],
|
|
94731
|
-
repository: {
|
|
94732
|
-
type: "git",
|
|
94733
|
-
url: "git+https://github.com/brndnsh-labs/tuiweather.git"
|
|
94734
|
-
},
|
|
94735
|
-
homepage: "https://github.com/brndnsh-labs/tuiweather#readme",
|
|
94736
|
-
bugs: "https://github.com/brndnsh-labs/tuiweather/issues",
|
|
94737
|
-
keywords: [
|
|
94738
|
-
"tui",
|
|
94739
|
-
"weather",
|
|
94740
|
-
"terminal",
|
|
94741
|
-
"cli",
|
|
94742
|
-
"open-meteo",
|
|
94743
|
-
"forecast"
|
|
94744
|
-
],
|
|
94745
|
-
publishConfig: {
|
|
94746
|
-
access: "public"
|
|
94747
|
-
},
|
|
94748
|
-
engines: {
|
|
94749
|
-
bun: ">=1.3.0"
|
|
94750
|
-
},
|
|
94751
|
-
scripts: {
|
|
94752
|
-
dev: "bun run src/index.tsx",
|
|
94753
|
-
build: "bun build ./src/index.tsx --outdir dist --target bun --external '@opentui/core-*'",
|
|
94754
|
-
typecheck: "tsc --noEmit",
|
|
94755
|
-
lint: "biome check .",
|
|
94756
|
-
fmt: "biome check --write .",
|
|
94757
|
-
test: "bun test",
|
|
94758
|
-
prepublishOnly: "bun run typecheck && bun run lint && bun run test && bun run build"
|
|
94759
|
-
},
|
|
94760
|
-
dependencies: {
|
|
94761
|
-
"@opentui/core": "^0.5.9",
|
|
94762
|
-
"@opentui/react": "^0.5.9",
|
|
94763
|
-
react: "^19.2.8",
|
|
94764
|
-
"smol-toml": "^1.8.0",
|
|
94765
|
-
zod: "4.5.2",
|
|
94766
|
-
zustand: "^5.0.15"
|
|
94767
|
-
},
|
|
94768
|
-
devDependencies: {
|
|
94769
|
-
"@biomejs/biome": "^2.5.11",
|
|
94770
|
-
"@types/bun": "^1.4.0",
|
|
94771
|
-
"@types/react": "^19.2.18",
|
|
94772
|
-
typescript: "^7.0.2"
|
|
94773
|
-
}
|
|
94774
|
-
};
|
|
94775
94811
|
|
|
94776
94812
|
// src/cli.ts
|
|
94777
94813
|
class UsageError extends Error {
|
|
@@ -94994,7 +95030,7 @@ async function runWatchCli(args) {
|
|
|
94994
95030
|
const provider = selectProvider(config2.provider);
|
|
94995
95031
|
const prefs = resolveDisplayPrefs(config2);
|
|
94996
95032
|
const maxAgeMinutes = args.interval ?? config2.refresh_minutes;
|
|
94997
|
-
const intervalMs = maxAgeMinutes
|
|
95033
|
+
const intervalMs = refreshLoopPeriodMs(maxAgeMinutes);
|
|
94998
95034
|
const fetcher = () => cachedForecast(provider, { latitude, longitude }, { maxAgeMinutes });
|
|
94999
95035
|
const write = (text) => process.stdout.write(text);
|
|
95000
95036
|
const sleep2 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|