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.
Files changed (3) hide show
  1. package/README.md +5 -3
  2. package/dist/index.js +131 -95
  3. 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. Desktop notifications are a planned follow-up.
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 air quality is not provided |
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) >= nowMs);
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
- if (text.length <= width)
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").slice(0, 80),
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
- return `${prefix} ${left}${right}`.slice(0, width);
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
- if (tier === "xs")
90910
- return HINTS_XS;
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: line
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 truncate(text, width) {
91104
- if (text.length <= width)
91105
- return text;
91106
- return `${text.slice(0, Math.max(0, width - 1))}\u2026`;
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: truncate(line, budget)
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
- return parsed.data.results ?? [];
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 = "tuiweather/0.1 (github.com/brndnsh-labs/tuiweather)";
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 * 60000);
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
- if (tier !== "lg" && panels.nowcast && deriveNowcast(forecast, nowUtc).kind !== "dry") {
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.length <= width - 2)
94279
+ if (displayWidth(label) <= width - 2)
94192
94280
  return `${label} \u2026`;
94193
- return `${label.slice(0, width - 1)}\u2026`;
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
- return prev.kind === "dry" && next.kind !== "dry";
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 * 60000;
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuiweather",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Keyboard-driven terminal weather app powered by Open-Meteo",
5
5
  "license": "MIT",
6
6
  "type": "module",