tuiweather 0.3.4 → 0.4.0

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/dist/index.js CHANGED
@@ -73193,8 +73193,143 @@ class PluginErrorBoundary extends import_react14.default.Component {
73193
73193
  }
73194
73194
  extend({ "time-to-first-draw": TimeToFirstDrawRenderable });
73195
73195
 
73196
+ // src/app/AppearanceApp.tsx
73197
+ var import_react40 = __toESM(require_react(), 1);
73198
+
73199
+ // src/theme/palette.ts
73200
+ var DARK_INK = {
73201
+ fg: "#c0caf5",
73202
+ fgDim: "#565f89",
73203
+ border: "#3b4261",
73204
+ surface: "#16161e"
73205
+ };
73206
+ var LIGHT_INK = {
73207
+ fg: "#343b58",
73208
+ fgDim: "#8990b3",
73209
+ border: "#a8b0d0",
73210
+ surface: "#f4f6fb"
73211
+ };
73212
+ var DAY_ACCENTS = {
73213
+ accent: "#2e7de9",
73214
+ ok: "#387068",
73215
+ warn: "#8c6c3e",
73216
+ danger: "#c64343",
73217
+ tempCold: "#007197",
73218
+ tempWarm: "#965027",
73219
+ rain: "#00807a"
73220
+ };
73221
+ var NIGHT_ACCENTS = {
73222
+ accent: "#7aa2f7",
73223
+ ok: "#9ece6a",
73224
+ warn: "#e0af68",
73225
+ danger: "#f7768e",
73226
+ tempCold: "#7dcfff",
73227
+ tempWarm: "#ff9e64",
73228
+ rain: "#41a6b5"
73229
+ };
73230
+ function parseHexColor(hex) {
73231
+ const m2 = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.exec(hex.trim());
73232
+ const body = m2?.[1];
73233
+ if (!body)
73234
+ return null;
73235
+ const full = body.length === 3 ? body.split("").map((c) => c + c).join("") : body;
73236
+ const r = Number.parseInt(full.slice(0, 2), 16);
73237
+ const g2 = Number.parseInt(full.slice(2, 4), 16);
73238
+ const b2 = Number.parseInt(full.slice(4, 6), 16);
73239
+ return [r, g2, b2];
73240
+ }
73241
+ function relativeLuminance(hex) {
73242
+ const rgb = parseHexColor(hex);
73243
+ if (!rgb)
73244
+ return null;
73245
+ const lin = rgb.map((channel) => {
73246
+ const s = channel / 255;
73247
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
73248
+ });
73249
+ const [r, g2, b2] = lin;
73250
+ return 0.2126 * (r ?? 0) + 0.7152 * (g2 ?? 0) + 0.0722 * (b2 ?? 0);
73251
+ }
73252
+ function contrastRatio(a, b2) {
73253
+ const la = relativeLuminance(a);
73254
+ const lb = relativeLuminance(b2);
73255
+ if (la === null || lb === null)
73256
+ return 1;
73257
+ const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
73258
+ return (hi + 0.05) / (lo + 0.05);
73259
+ }
73260
+ function isDarkBackground(hex) {
73261
+ if (!hex)
73262
+ return true;
73263
+ const lum = relativeLuminance(hex);
73264
+ if (lum === null)
73265
+ return true;
73266
+ return lum < 0.5;
73267
+ }
73268
+ function ensureContrast(color, bg2, minRatio) {
73269
+ if (contrastRatio(color, bg2) >= minRatio)
73270
+ return color;
73271
+ const toward = isDarkBackground(bg2) ? "#ffffff" : "#000000";
73272
+ const rgb = parseHexColor(color);
73273
+ const tgt = parseHexColor(toward);
73274
+ if (!rgb || !tgt)
73275
+ return color;
73276
+ for (let step = 1;step <= 20; step++) {
73277
+ const t2 = step / 20;
73278
+ const mixed = rgb.map((c, i) => Math.round(c + ((tgt[i] ?? c) - c) * t2)).map((c) => c.toString(16).padStart(2, "0")).join("");
73279
+ const candidate = `#${mixed}`;
73280
+ if (contrastRatio(candidate, bg2) >= minRatio)
73281
+ return candidate;
73282
+ }
73283
+ return toward;
73284
+ }
73285
+ var FOREGROUND_CONTRAST_FLOOR = 4.5;
73286
+ function buildPalette(theme, isDay, ink, terminalBackground) {
73287
+ const base = {
73288
+ ...ink === "dark" ? DARK_INK : LIGHT_INK,
73289
+ ...theme === "day" ? DAY_ACCENTS : theme === "night" ? NIGHT_ACCENTS : isDay ? DAY_ACCENTS : NIGHT_ACCENTS
73290
+ };
73291
+ const bg2 = terminalBackground ?? base.surface;
73292
+ return {
73293
+ ...base,
73294
+ fg: ensureContrast(base.fg, bg2, FOREGROUND_CONTRAST_FLOOR),
73295
+ fgDim: ensureContrast(base.fgDim, bg2, FOREGROUND_CONTRAST_FLOOR),
73296
+ warn: ensureContrast(base.warn, bg2, FOREGROUND_CONTRAST_FLOOR),
73297
+ danger: ensureContrast(base.danger, bg2, FOREGROUND_CONTRAST_FLOOR)
73298
+ };
73299
+ }
73300
+
73301
+ // src/theme/detect.ts
73302
+ var FALLBACK_APPEARANCE = { ink: "dark", background: null };
73303
+ function appearancesEqual(a, b2) {
73304
+ return a.ink === b2.ink && a.background === b2.background;
73305
+ }
73306
+ async function detectTerminalAppearance(query, timeoutMs = 300) {
73307
+ let timer;
73308
+ try {
73309
+ const colors = await Promise.race([
73310
+ query.getPalette({ timeout: timeoutMs }),
73311
+ new Promise((_2, reject) => {
73312
+ timer = setTimeout(() => reject(new Error("palette query timed out")), timeoutMs);
73313
+ })
73314
+ ]);
73315
+ const background = colors?.defaultBackground ?? null;
73316
+ return { ink: isDarkBackground(background) ? "dark" : "light", background };
73317
+ } catch {
73318
+ return FALLBACK_APPEARANCE;
73319
+ } finally {
73320
+ if (timer !== undefined)
73321
+ clearTimeout(timer);
73322
+ }
73323
+ }
73324
+ async function resolveTerminalAppearance(preference, query, timeoutMs = 300) {
73325
+ if (preference === "light" || preference === "dark") {
73326
+ return { ink: preference, background: null };
73327
+ }
73328
+ return detectTerminalAppearance(query, timeoutMs);
73329
+ }
73330
+
73196
73331
  // src/app/App.tsx
73197
- var import_react37 = __toESM(require_react(), 1);
73332
+ var import_react38 = __toESM(require_react(), 1);
73198
73333
 
73199
73334
  // src/components/DaylightBar.tsx
73200
73335
  var import_react18 = __toESM(require_react(), 1);
@@ -73409,108 +73544,6 @@ function truncateCells(text, width) {
73409
73544
 
73410
73545
  // src/theme/tokens.ts
73411
73546
  var import_react17 = __toESM(require_react(), 1);
73412
-
73413
- // src/theme/palette.ts
73414
- var DARK_INK = {
73415
- fg: "#c0caf5",
73416
- fgDim: "#565f89",
73417
- border: "#3b4261",
73418
- surface: "#16161e"
73419
- };
73420
- var LIGHT_INK = {
73421
- fg: "#343b58",
73422
- fgDim: "#8990b3",
73423
- border: "#a8b0d0",
73424
- surface: "#f4f6fb"
73425
- };
73426
- var DAY_ACCENTS = {
73427
- accent: "#2e7de9",
73428
- ok: "#387068",
73429
- warn: "#8c6c3e",
73430
- danger: "#c64343",
73431
- tempCold: "#007197",
73432
- tempWarm: "#965027",
73433
- rain: "#00807a"
73434
- };
73435
- var NIGHT_ACCENTS = {
73436
- accent: "#7aa2f7",
73437
- ok: "#9ece6a",
73438
- warn: "#e0af68",
73439
- danger: "#f7768e",
73440
- tempCold: "#7dcfff",
73441
- tempWarm: "#ff9e64",
73442
- rain: "#41a6b5"
73443
- };
73444
- function parseHexColor(hex) {
73445
- const m2 = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.exec(hex.trim());
73446
- const body = m2?.[1];
73447
- if (!body)
73448
- return null;
73449
- const full = body.length === 3 ? body.split("").map((c) => c + c).join("") : body;
73450
- const r = Number.parseInt(full.slice(0, 2), 16);
73451
- const g2 = Number.parseInt(full.slice(2, 4), 16);
73452
- const b2 = Number.parseInt(full.slice(4, 6), 16);
73453
- return [r, g2, b2];
73454
- }
73455
- function relativeLuminance(hex) {
73456
- const rgb = parseHexColor(hex);
73457
- if (!rgb)
73458
- return null;
73459
- const lin = rgb.map((channel) => {
73460
- const s = channel / 255;
73461
- return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
73462
- });
73463
- const [r, g2, b2] = lin;
73464
- return 0.2126 * (r ?? 0) + 0.7152 * (g2 ?? 0) + 0.0722 * (b2 ?? 0);
73465
- }
73466
- function contrastRatio(a, b2) {
73467
- const la = relativeLuminance(a);
73468
- const lb = relativeLuminance(b2);
73469
- if (la === null || lb === null)
73470
- return 1;
73471
- const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
73472
- return (hi + 0.05) / (lo + 0.05);
73473
- }
73474
- function isDarkBackground(hex) {
73475
- if (!hex)
73476
- return true;
73477
- const lum = relativeLuminance(hex);
73478
- if (lum === null)
73479
- return true;
73480
- return lum < 0.5;
73481
- }
73482
- function ensureContrast(color, bg2, minRatio) {
73483
- if (contrastRatio(color, bg2) >= minRatio)
73484
- return color;
73485
- const toward = isDarkBackground(bg2) ? "#ffffff" : "#000000";
73486
- const rgb = parseHexColor(color);
73487
- const tgt = parseHexColor(toward);
73488
- if (!rgb || !tgt)
73489
- return color;
73490
- for (let step = 1;step <= 20; step++) {
73491
- const t2 = step / 20;
73492
- const mixed = rgb.map((c, i) => Math.round(c + ((tgt[i] ?? c) - c) * t2)).map((c) => c.toString(16).padStart(2, "0")).join("");
73493
- const candidate = `#${mixed}`;
73494
- if (contrastRatio(candidate, bg2) >= minRatio)
73495
- return candidate;
73496
- }
73497
- return toward;
73498
- }
73499
- var FOREGROUND_CONTRAST_FLOOR = 4.5;
73500
- function buildPalette(theme, isDay, ink, terminalBackground) {
73501
- const base = {
73502
- ...ink === "dark" ? DARK_INK : LIGHT_INK,
73503
- ...theme === "day" ? DAY_ACCENTS : theme === "night" ? NIGHT_ACCENTS : isDay ? DAY_ACCENTS : NIGHT_ACCENTS
73504
- };
73505
- const bg2 = terminalBackground ?? base.surface;
73506
- return {
73507
- ...base,
73508
- fg: ensureContrast(base.fg, bg2, FOREGROUND_CONTRAST_FLOOR),
73509
- fgDim: ensureContrast(base.fgDim, bg2, FOREGROUND_CONTRAST_FLOOR)
73510
- };
73511
- }
73512
-
73513
- // src/theme/tokens.ts
73514
73547
  var ThemeContext = import_react17.createContext(buildPalette("auto", true, "dark", null));
73515
73548
  function usePalette() {
73516
73549
  return import_react17.useContext(ThemeContext);
@@ -73890,13 +73923,14 @@ function StatLine({ parts, dim: dim2 }) {
73890
73923
  }
73891
73924
  var Hero = import_react20.memo(function Hero2({ obs, prefs, compact = false, mini = false }) {
73892
73925
  const palette = usePalette();
73926
+ const tempFg = lerpHex(palette.tempCold, palette.tempWarm, tempWarmthT(obs.temperatureC));
73893
73927
  if (mini) {
73894
73928
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
73895
73929
  flexDirection: "row",
73896
73930
  gap: 1,
73897
73931
  children: [
73898
73932
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
73899
- fg: palette.tempWarm,
73933
+ fg: tempFg,
73900
73934
  children: formatTemp(obs.temperatureC, prefs.temp)
73901
73935
  }, undefined, false, undefined, this),
73902
73936
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
@@ -73915,7 +73949,7 @@ var Hero = import_react20.memo(function Hero2({ obs, prefs, compact = false, min
73915
73949
  gap: 1,
73916
73950
  children: [
73917
73951
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
73918
- fg: palette.tempWarm,
73952
+ fg: tempFg,
73919
73953
  children: `${formatTemp(obs.temperatureC, prefs.temp)}`
73920
73954
  }, undefined, false, undefined, this),
73921
73955
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
@@ -73934,7 +73968,6 @@ var Hero = import_react20.memo(function Hero2({ obs, prefs, compact = false, min
73934
73968
  ]
73935
73969
  }, undefined, true, undefined, this);
73936
73970
  }
73937
- const tempFg = lerpHex(palette.tempCold, palette.tempWarm, tempWarmthT(obs.temperatureC));
73938
73971
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
73939
73972
  flexDirection: "column",
73940
73973
  flexShrink: 0,
@@ -74720,15 +74753,31 @@ function LocationPicker({
74720
74753
  }
74721
74754
  function SearchOverlay({ store, width, height }) {
74722
74755
  const searchLocations = store((s) => s.searchLocations);
74756
+ const [busy, setBusy] = import_react25.useState(false);
74757
+ const [actionError, setActionError] = import_react25.useState(undefined);
74723
74758
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(LocationPicker, {
74724
74759
  searchLocations,
74725
74760
  width,
74726
74761
  height,
74762
+ busy,
74763
+ actionError,
74764
+ onQueryChange: () => setActionError(undefined),
74727
74765
  onCancel: () => store.getState().setOverlayOpen(false),
74728
74766
  onSelect: (chosen) => {
74767
+ if (busy)
74768
+ return;
74769
+ setBusy(true);
74770
+ setActionError(undefined);
74729
74771
  const state = store.getState();
74730
74772
  const entry = buildLocationEntry(chosen, state.config.locations.map((loc) => loc.slug));
74731
- state.addLocation(entry).then(() => state.setOverlayOpen(false));
74773
+ state.addLocation(entry).then((ok) => {
74774
+ if (ok) {
74775
+ store.getState().setOverlayOpen(false);
74776
+ } else {
74777
+ setActionError(store.getState().lastActionError ?? "could not save config");
74778
+ setBusy(false);
74779
+ }
74780
+ });
74732
74781
  }
74733
74782
  }, undefined, false, undefined, this);
74734
74783
  }
@@ -94235,7 +94284,7 @@ class ProviderError extends Error {
94235
94284
 
94236
94285
  // src/lib/config/schema.ts
94237
94286
  var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
94238
- var SCHEMA_VERSION = 3;
94287
+ var SCHEMA_VERSION = 4;
94239
94288
  var locationSchema = exports_external.object({
94240
94289
  slug: exports_external.string().regex(SLUG_PATTERN),
94241
94290
  label: exports_external.string().min(1).max(80),
@@ -94261,23 +94310,15 @@ var tuiConfigSchema = exports_external.object({
94261
94310
  time_format: exports_external.enum(["12h", "24h", "auto"]).default("auto"),
94262
94311
  unit_prefs: exports_external.object(unitPrefsShape).prefault({}),
94263
94312
  refresh_minutes: exports_external.number().int().min(1).default(10),
94313
+ reduced_motion: exports_external.boolean().default(false),
94264
94314
  theme: exports_external.enum(["day", "night", "auto"]).default("auto"),
94315
+ ink: exports_external.enum(["auto", "light", "dark"]).default("auto"),
94265
94316
  provider: exports_external.enum(PROVIDER_IDS).default("openmeteo"),
94266
94317
  daily_days: exports_external.number().int().min(1).max(16).default(7),
94267
94318
  hourly_hours: exports_external.number().int().min(12).max(48).default(24),
94268
94319
  panels: panelsSchema.prefault({}),
94269
94320
  default_location: exports_external.string().optional(),
94270
94321
  locations: exports_external.array(locationSchema).prefault([])
94271
- }).superRefine((config2, ctx) => {
94272
- const target = config2.default_location;
94273
- if (target === undefined)
94274
- return;
94275
- if (!config2.locations.some((loc) => loc.slug === target)) {
94276
- ctx.addIssue({
94277
- code: "custom",
94278
- message: `default_location "${target}" does not match any [[locations]] slug`
94279
- });
94280
- }
94281
94322
  }).transform((config2) => {
94282
94323
  const legacy = config2.units;
94283
94324
  const unit_prefs = {
@@ -94304,7 +94345,7 @@ function migrateConfig(raw) {
94304
94345
  }
94305
94346
  const doc2 = { ...raw };
94306
94347
  const version2 = raw.schema_version;
94307
- if (version2 === 1 || version2 === 2) {
94348
+ if (version2 === 1 || version2 === 2 || version2 === 3) {
94308
94349
  doc2.schema_version = SCHEMA_VERSION;
94309
94350
  }
94310
94351
  const unitsField = doc2.units;
@@ -94368,7 +94409,12 @@ async function loadConfig(path4) {
94368
94409
  if (e.issues.some((i) => i.path.length === 0 || i.path[0] === "schema_version")) {
94369
94410
  issues.push("hint: bare keys must appear before any [table] or [[array]] headers in TOML");
94370
94411
  }
94371
- throw new ConfigError(`invalid config in ${target}: ${issues.length} issue(s)`, issues);
94412
+ const preview = issues.slice(0, 3).join(`
94413
+ `);
94414
+ const more = issues.length > 3 ? `
94415
+ ... and ${issues.length - 3} more` : "";
94416
+ throw new ConfigError(`invalid config in ${target}: ${issues.length} issue(s)
94417
+ ${preview}${more}`, issues);
94372
94418
  }
94373
94419
  return config2;
94374
94420
  }
@@ -94382,7 +94428,9 @@ function serialize(config2) {
94382
94428
  schema_version: config2.schema_version,
94383
94429
  time_format: config2.time_format,
94384
94430
  refresh_minutes: config2.refresh_minutes,
94431
+ reduced_motion: config2.reduced_motion,
94385
94432
  theme: config2.theme,
94433
+ ink: config2.ink,
94386
94434
  provider: config2.provider,
94387
94435
  daily_days: config2.daily_days,
94388
94436
  hourly_hours: config2.hourly_hours
@@ -94403,7 +94451,7 @@ async function saveConfig(config2, path4) {
94403
94451
  tuiConfigSchema.parse(config2);
94404
94452
  const target = path4 ?? defaultConfigPath();
94405
94453
  const dir = dirname3(target);
94406
- await mkdir2(dir, { recursive: true });
94454
+ await mkdir2(dir, { recursive: true, mode: 448 });
94407
94455
  const tmp = join6(dir, `${basename3(target)}.tmp-${process.pid}-${randomBytes2(8).toString("hex")}`);
94408
94456
  let created = false;
94409
94457
  try {
@@ -94436,19 +94484,99 @@ function sanitizeText(text, maxChars) {
94436
94484
  }
94437
94485
  return out.slice(0, maxChars);
94438
94486
  }
94439
- function errorReason(body, schema, maxChars = 200) {
94487
+ function errorReason(body, schema, maxChars = 200, extractor) {
94440
94488
  const parsed = schema.safeParse(body);
94441
94489
  if (!parsed.success)
94442
94490
  return;
94443
- const reason = parsed.data.reason;
94444
- if (typeof reason !== "string")
94491
+ const raw = extractor ? extractor(parsed.data) : parsed.data.reason;
94492
+ if (typeof raw !== "string")
94445
94493
  return;
94446
- return sanitizeText(reason, maxChars);
94494
+ return sanitizeText(raw, maxChars);
94495
+ }
94496
+ function causeSuffix(cause) {
94497
+ let raw = "";
94498
+ if (cause instanceof Error) {
94499
+ const inner = cause.cause;
94500
+ if (inner instanceof Error)
94501
+ raw = inner.message;
94502
+ else if (typeof inner === "string")
94503
+ raw = inner;
94504
+ else if (cause.message !== "fetch failed")
94505
+ raw = cause.message;
94506
+ } else if (typeof cause === "string") {
94507
+ raw = cause;
94508
+ } else if (cause !== null && typeof cause === "object" && "message" in cause && typeof cause.message === "string") {
94509
+ raw = cause.message;
94510
+ }
94511
+ const sanitized = sanitizeText(raw, 200).trim();
94512
+ if (!sanitized)
94513
+ return "";
94514
+ return `: ${sanitized}`;
94447
94515
  }
94448
94516
  function httpError(status, body, opts) {
94449
- const reason = errorReason(body, opts.schema, opts.maxChars ?? 200);
94517
+ const reason = errorReason(body, opts.schema, opts.maxChars ?? 200, opts.extractor);
94450
94518
  return new ProviderError(`${opts.providerId} ${opts.label} failed (HTTP ${status})${reason ? `: ${reason}` : ""}`, opts.providerId);
94451
94519
  }
94520
+ var PROVIDER_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
94521
+ function sizeCapError(providerId, label, cap) {
94522
+ return new ProviderError(`${providerId} ${label} response exceeded size limit (${cap} bytes)`, providerId);
94523
+ }
94524
+ async function readJsonCapped(res, opts) {
94525
+ const cap = opts.maxBytes ?? PROVIDER_RESPONSE_MAX_BYTES;
94526
+ const contentLength = res.headers.get("content-length");
94527
+ if (contentLength !== null) {
94528
+ const trimmed = contentLength.trim();
94529
+ if (/^\d+$/.test(trimmed)) {
94530
+ const parsed = Number(trimmed);
94531
+ if (Number.isSafeInteger(parsed) && parsed > cap) {
94532
+ try {
94533
+ await res.body?.cancel();
94534
+ } catch {}
94535
+ throw sizeCapError(opts.providerId, opts.label, cap);
94536
+ }
94537
+ }
94538
+ }
94539
+ let bytes;
94540
+ if (res.body) {
94541
+ const reader = res.body.getReader();
94542
+ const chunks = [];
94543
+ let total = 0;
94544
+ while (true) {
94545
+ const { done, value } = await reader.read();
94546
+ if (done)
94547
+ break;
94548
+ if (value) {
94549
+ total += value.byteLength;
94550
+ if (total > cap) {
94551
+ try {
94552
+ await reader.cancel();
94553
+ } catch {}
94554
+ throw sizeCapError(opts.providerId, opts.label, cap);
94555
+ }
94556
+ chunks.push(value);
94557
+ }
94558
+ }
94559
+ bytes = new Uint8Array(total);
94560
+ let offset = 0;
94561
+ for (const chunk of chunks) {
94562
+ bytes.set(chunk, offset);
94563
+ offset += chunk.byteLength;
94564
+ }
94565
+ } else {
94566
+ const text2 = await res.text();
94567
+ const encoded = new TextEncoder().encode(text2);
94568
+ if (encoded.byteLength > cap) {
94569
+ throw sizeCapError(opts.providerId, opts.label, cap);
94570
+ }
94571
+ bytes = encoded;
94572
+ }
94573
+ const text = new TextDecoder().decode(bytes);
94574
+ try {
94575
+ return JSON.parse(text);
94576
+ } catch (cause) {
94577
+ throw new ProviderError(`${opts.providerId} ${opts.label} returned a non-JSON body (HTTP ${res.status})`, opts.providerId, cause);
94578
+ }
94579
+ }
94452
94580
 
94453
94581
  // src/lib/providers/openmeteo/schemas.ts
94454
94582
  var LOCAL_NAIVE_TIME = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?)?$/;
@@ -94462,6 +94590,7 @@ var apiErrorBodySchema = exports_external.object({
94462
94590
  error: exports_external.literal(true),
94463
94591
  reason: exports_external.string().optional()
94464
94592
  });
94593
+ var MAX_REASON_CHARS = 200;
94465
94594
  var currentBlockSchema = exports_external.object({
94466
94595
  time: localNaiveTime,
94467
94596
  interval: exports_external.number().optional(),
@@ -94469,9 +94598,7 @@ var currentBlockSchema = exports_external.object({
94469
94598
  relative_humidity_2m: exports_external.number(),
94470
94599
  apparent_temperature: exports_external.number(),
94471
94600
  is_day: isDayStep,
94472
- precipitation: exports_external.number().nullable(),
94473
94601
  weather_code: exports_external.number(),
94474
- cloud_cover: exports_external.number().nullable(),
94475
94602
  pressure_msl: exports_external.number().nullable(),
94476
94603
  wind_speed_10m: exports_external.number(),
94477
94604
  wind_direction_10m: exports_external.number(),
@@ -94525,11 +94652,9 @@ var dailyBlockSchema = exports_external.object({
94525
94652
  temperature_2m_min: numArray,
94526
94653
  precipitation_sum: numArray,
94527
94654
  precipitation_probability_max: numArray.optional(),
94528
- uv_index_max: numArray.optional(),
94529
94655
  sunrise: strOrNullArray.optional(),
94530
94656
  sunset: strOrNullArray.optional(),
94531
- wind_speed_10m_max: numArray.optional(),
94532
- wind_gusts_10m_max: numArray.optional()
94657
+ wind_speed_10m_max: numArray.optional()
94533
94658
  }).superRefine((block, ctx) => {
94534
94659
  const n = block.time.length;
94535
94660
  for (const [key, value] of Object.entries(block)) {
@@ -94585,10 +94710,15 @@ function buildGeocodingUrl(query, count = 8) {
94585
94710
  });
94586
94711
  return `${GEOCODING_ENDPOINT}?${params.toString()}`;
94587
94712
  }
94588
- function parseGeocodingResponse(body) {
94713
+ function parseGeocodingResponse(body, status = 200) {
94589
94714
  const errorParsed = apiErrorBodySchema.safeParse(body);
94590
94715
  if (errorParsed.success) {
94591
- throw new ProviderError(`openmeteo geocoding failed: ${sanitizeText(errorParsed.data.reason ?? "unknown error", 200)}`, "openmeteo");
94716
+ throw httpError(status, body, {
94717
+ label: "geocoding",
94718
+ providerId: "openmeteo",
94719
+ schema: apiErrorBodySchema,
94720
+ maxChars: MAX_REASON_CHARS
94721
+ });
94592
94722
  }
94593
94723
  const parsed = geocodingResponseSchema.safeParse(body);
94594
94724
  if (!parsed.success) {
@@ -94613,19 +94743,22 @@ async function searchLocations(query, count = 8) {
94613
94743
  signal: AbortSignal.timeout(TIMEOUT_MS)
94614
94744
  });
94615
94745
  } catch (cause) {
94616
- throw new ProviderError("openmeteo geocoding request failed before an HTTP response", "openmeteo", cause);
94746
+ throw new ProviderError(`openmeteo geocoding request failed before an HTTP response${causeSuffix(cause)}`, "openmeteo", cause);
94617
94747
  }
94618
94748
  let body;
94619
94749
  try {
94620
- body = await res.json();
94750
+ body = await readJsonCapped(res, { providerId: "openmeteo", label: "geocoding" });
94621
94751
  } catch (cause) {
94752
+ if (cause instanceof ProviderError)
94753
+ throw cause;
94622
94754
  throw new ProviderError(`openmeteo geocoding returned a non-JSON body (HTTP ${res.status})`, "openmeteo", cause);
94623
94755
  }
94624
94756
  if (!res.ok || errorReason(body, apiErrorBodySchema) !== undefined) {
94625
94757
  throw httpError(res.status, body, {
94626
94758
  label: "geocoding",
94627
94759
  providerId: "openmeteo",
94628
- schema: apiErrorBodySchema
94760
+ schema: apiErrorBodySchema,
94761
+ maxChars: MAX_REASON_CHARS
94629
94762
  });
94630
94763
  }
94631
94764
  return parseGeocodingResponse(body);
@@ -94633,7 +94766,7 @@ async function searchLocations(query, count = 8) {
94633
94766
  // package.json
94634
94767
  var package_default2 = {
94635
94768
  name: "tuiweather",
94636
- version: "0.3.4",
94769
+ version: "0.4.0",
94637
94770
  description: "Keyboard-driven terminal weather app powered by Open-Meteo",
94638
94771
  license: "MIT",
94639
94772
  type: "module",
@@ -94662,7 +94795,7 @@ var package_default2 = {
94662
94795
  access: "public"
94663
94796
  },
94664
94797
  engines: {
94665
- node: ">=26.4.0"
94798
+ node: ">=20"
94666
94799
  },
94667
94800
  scripts: {
94668
94801
  dev: "bun run src/index.tsx",
@@ -94671,7 +94804,8 @@ var package_default2 = {
94671
94804
  lint: "biome check .",
94672
94805
  fmt: "biome check --write .",
94673
94806
  test: "bun test",
94674
- prepublishOnly: "bun run typecheck && bun run lint && bun run test && bun run build"
94807
+ verify: "bun run typecheck && bun run lint && bun run test && bun run build",
94808
+ prepublishOnly: "bun run verify"
94675
94809
  },
94676
94810
  dependencies: {
94677
94811
  "@opentui/core": "^0.5.9",
@@ -94903,11 +95037,9 @@ function normalizeCurrent(obs) {
94903
95037
  windDirectionDeg: quantityValue(obs.windDirection) ?? 0,
94904
95038
  windGustKmh: obsWindKmh(obs.windGust),
94905
95039
  pressureHpa: obsPressureHpa(obs.seaLevelPressure) ?? obsPressureHpa(obs.barometricPressure),
94906
- cloudCoverPct: null,
94907
95040
  dewPointC: obsTempC(obs.dewpoint),
94908
95041
  visibilityM: obsVisibilityM(obs.visibility),
94909
95042
  uvIndex: null,
94910
- precipLast1hMm: null,
94911
95043
  isDay: !icon.includes("/night/")
94912
95044
  };
94913
95045
  }
@@ -94972,11 +95104,9 @@ function normalizeDaily(periods, forecastDays) {
94972
95104
  quantityValue(group.day?.probabilityOfPrecipitation),
94973
95105
  quantityValue(group.night?.probabilityOfPrecipitation)
94974
95106
  ]),
94975
- uvIndexMax: null,
94976
95107
  sunriseUtc: null,
94977
95108
  sunsetUtc: null,
94978
- windSpeedMaxKmh: speeds.length > 0 ? Math.max(...speeds) : null,
94979
- windGustMaxKmh: null
95109
+ windSpeedMaxKmh: speeds.length > 0 ? Math.max(...speeds) : null
94980
95110
  });
94981
95111
  }
94982
95112
  return daily;
@@ -95075,6 +95205,26 @@ var API_ROOT = "https://api.weather.gov";
95075
95205
  var ALLOWED_HOST = new URL(API_ROOT).host;
95076
95206
  var TIMEOUT_MS2 = 1e4;
95077
95207
  var MAX_DETAIL_CHARS = 200;
95208
+ var NWS_METADATA_TTL_MS = 24 * 60 * 60 * 1000;
95209
+ var NWS_METADATA_MAX_ENTRIES = 64;
95210
+ function nwsProblemReason(data) {
95211
+ const v2 = data;
95212
+ const candidate = v2.detail ?? v2.title;
95213
+ return typeof candidate === "string" ? candidate : undefined;
95214
+ }
95215
+ function metadataKey(location) {
95216
+ return `${location.latitude.toFixed(4)}:${location.longitude.toFixed(4)}`;
95217
+ }
95218
+ var nwsMetadataMemo = new Map;
95219
+ var pendingForecasts = new Map;
95220
+ function evictOldestIfNeeded() {
95221
+ while (nwsMetadataMemo.size > NWS_METADATA_MAX_ENTRIES) {
95222
+ const oldest = nwsMetadataMemo.keys().next().value;
95223
+ if (oldest === undefined)
95224
+ break;
95225
+ nwsMetadataMemo.delete(oldest);
95226
+ }
95227
+ }
95078
95228
  var NWS_HEADERS = {
95079
95229
  "User-Agent": NWS_USER_AGENT,
95080
95230
  Accept: "application/geo+json"
@@ -95082,13 +95232,6 @@ var NWS_HEADERS = {
95082
95232
  function buildPointsUrl(location) {
95083
95233
  return `${API_ROOT}/points/${location.latitude},${location.longitude}`;
95084
95234
  }
95085
- function problemDetail(body) {
95086
- const parsed = nwsProblemSchema.safeParse(body);
95087
- if (!parsed.success)
95088
- return;
95089
- const detail = parsed.data.detail ?? parsed.data.title;
95090
- return detail === undefined ? undefined : sanitizeText(detail, MAX_DETAIL_CHARS);
95091
- }
95092
95235
  async function getJson(url2, label) {
95093
95236
  let target;
95094
95237
  try {
@@ -95101,19 +95244,45 @@ async function getJson(url2, label) {
95101
95244
  }
95102
95245
  let res;
95103
95246
  try {
95104
- res = await fetch(url2, { headers: NWS_HEADERS, signal: AbortSignal.timeout(TIMEOUT_MS2) });
95247
+ res = await fetch(url2, {
95248
+ headers: NWS_HEADERS,
95249
+ signal: AbortSignal.timeout(TIMEOUT_MS2),
95250
+ redirect: "error"
95251
+ });
95105
95252
  } catch (cause) {
95106
- throw new ProviderError(`nws ${label} request failed before an HTTP response`, NWS_PROVIDER_ID, cause);
95253
+ const message = cause instanceof Error ? cause.message : String(cause);
95254
+ if (message.toLowerCase().includes("redirect")) {
95255
+ throw new ProviderError(`nws ${label} redirected off-host`, NWS_PROVIDER_ID, cause);
95256
+ }
95257
+ throw new ProviderError(`nws ${label} request failed before an HTTP response${causeSuffix(cause)}`, NWS_PROVIDER_ID, cause);
95258
+ }
95259
+ if (res.url) {
95260
+ let final;
95261
+ try {
95262
+ final = new URL(res.url);
95263
+ } catch {
95264
+ throw new ProviderError(`nws ${label} redirected off-host`, NWS_PROVIDER_ID);
95265
+ }
95266
+ if (final.protocol !== "https:" || final.host !== ALLOWED_HOST) {
95267
+ throw new ProviderError(`nws ${label} redirected off-host`, NWS_PROVIDER_ID);
95268
+ }
95107
95269
  }
95108
95270
  let body;
95109
95271
  try {
95110
- body = await res.json();
95272
+ body = await readJsonCapped(res, { providerId: NWS_PROVIDER_ID, label });
95111
95273
  } catch (cause) {
95274
+ if (cause instanceof ProviderError)
95275
+ throw cause;
95112
95276
  throw new ProviderError(`nws ${label} returned a non-JSON body (HTTP ${res.status})`, NWS_PROVIDER_ID, cause);
95113
95277
  }
95114
95278
  if (!res.ok) {
95115
- const detail = problemDetail(body);
95116
- throw new ProviderError(`nws ${label} failed (HTTP ${res.status})${detail ? `: ${detail}` : ""}`, NWS_PROVIDER_ID);
95279
+ throw httpError(res.status, body, {
95280
+ label,
95281
+ providerId: NWS_PROVIDER_ID,
95282
+ schema: nwsProblemSchema,
95283
+ maxChars: MAX_DETAIL_CHARS,
95284
+ extractor: nwsProblemReason
95285
+ });
95117
95286
  }
95118
95287
  return body;
95119
95288
  }
@@ -95125,24 +95294,57 @@ function parseResponse(schema, body, label) {
95125
95294
  return parsed.data;
95126
95295
  }
95127
95296
  async function fetchForecast(location, window2) {
95128
- const points = parseResponse(pointsResponseSchema, await getJson(buildPointsUrl(location), "points"), "points");
95129
- const [hourlyBody, dailyBody, stationsBody] = await Promise.all([
95130
- getJson(points.properties.forecastHourly, "hourly forecast"),
95131
- getJson(points.properties.forecast, "daily forecast"),
95132
- getJson(points.properties.observationStations, "observation stations")
95133
- ]);
95134
- const stations = parseResponse(stationsResponseSchema, stationsBody, "observation stations");
95135
- const station = stations.features[0];
95136
- if (station === undefined) {
95137
- throw new ProviderError("nws observation stations list is empty; cannot fetch a current observation", NWS_PROVIDER_ID);
95138
- }
95139
- const obs = parseResponse(observationResponseSchema, await getJson(`${station.id}/observations/latest`, "latest observation"), "latest observation");
95140
- return normalizeNwsForecast({
95141
- points: points.properties,
95142
- hourly: parseResponse(forecastResponseSchema2, hourlyBody, "hourly forecast").properties.periods,
95143
- daily: parseResponse(forecastResponseSchema2, dailyBody, "daily forecast").properties.periods,
95144
- obs: obs.properties
95145
- }, location, window2);
95297
+ const key = metadataKey(location);
95298
+ const pendingKey = `${key}|${window2?.forecastDays ?? "*"}|${window2?.forecastHours ?? "*"}`;
95299
+ const pending = pendingForecasts.get(pendingKey);
95300
+ if (pending)
95301
+ return pending;
95302
+ const task = (async () => {
95303
+ const cached2 = nwsMetadataMemo.get(key);
95304
+ if (cached2 && Date.now() < cached2.expiresAt) {
95305
+ const meta4 = cached2.value;
95306
+ const [hourlyBody2, dailyBody2, obsBody] = await Promise.all([
95307
+ getJson(meta4.points.forecastHourly, "hourly forecast"),
95308
+ getJson(meta4.points.forecast, "daily forecast"),
95309
+ getJson(`${meta4.stationId}/observations/latest`, "latest observation")
95310
+ ]);
95311
+ return normalizeNwsForecast({
95312
+ points: meta4.points,
95313
+ hourly: parseResponse(forecastResponseSchema2, hourlyBody2, "hourly forecast").properties.periods,
95314
+ daily: parseResponse(forecastResponseSchema2, dailyBody2, "daily forecast").properties.periods,
95315
+ obs: parseResponse(observationResponseSchema, obsBody, "latest observation").properties
95316
+ }, location, window2);
95317
+ }
95318
+ if (cached2)
95319
+ nwsMetadataMemo.delete(key);
95320
+ const points = parseResponse(pointsResponseSchema, await getJson(buildPointsUrl(location), "points"), "points");
95321
+ const [hourlyBody, dailyBody, stationsBody] = await Promise.all([
95322
+ getJson(points.properties.forecastHourly, "hourly forecast"),
95323
+ getJson(points.properties.forecast, "daily forecast"),
95324
+ getJson(points.properties.observationStations, "observation stations")
95325
+ ]);
95326
+ const stations = parseResponse(stationsResponseSchema, stationsBody, "observation stations");
95327
+ const station = stations.features[0];
95328
+ if (station === undefined) {
95329
+ throw new ProviderError("nws observation stations list is empty; cannot fetch a current observation", NWS_PROVIDER_ID);
95330
+ }
95331
+ const obs = parseResponse(observationResponseSchema, await getJson(`${station.id}/observations/latest`, "latest observation"), "latest observation");
95332
+ const meta3 = { points: points.properties, stationId: station.id };
95333
+ nwsMetadataMemo.set(key, { value: meta3, expiresAt: Date.now() + NWS_METADATA_TTL_MS });
95334
+ evictOldestIfNeeded();
95335
+ return normalizeNwsForecast({
95336
+ points: points.properties,
95337
+ hourly: parseResponse(forecastResponseSchema2, hourlyBody, "hourly forecast").properties.periods,
95338
+ daily: parseResponse(forecastResponseSchema2, dailyBody, "daily forecast").properties.periods,
95339
+ obs: obs.properties
95340
+ }, location, window2);
95341
+ })();
95342
+ pendingForecasts.set(pendingKey, task);
95343
+ try {
95344
+ return await task;
95345
+ } finally {
95346
+ pendingForecasts.delete(pendingKey);
95347
+ }
95146
95348
  }
95147
95349
  var nwsProvider = {
95148
95350
  id: NWS_PROVIDER_ID,
@@ -95161,9 +95363,7 @@ var aqResponseSchema = exports_external.object({
95161
95363
  current: exports_external.object({
95162
95364
  time: localNaiveTime2,
95163
95365
  interval: exports_external.number().optional(),
95164
- us_aqi: exports_external.number().nullable().optional(),
95165
- pm2_5: exports_external.number().nullable().optional(),
95166
- ozone: exports_external.number().nullable().optional()
95366
+ us_aqi: exports_external.number().nullable().optional()
95167
95367
  }).passthrough(),
95168
95368
  current_units: exports_external.unknown().optional()
95169
95369
  }).passthrough();
@@ -95171,7 +95371,7 @@ function buildAirQualityUrl(location) {
95171
95371
  const params = new URLSearchParams({
95172
95372
  latitude: String(location.latitude),
95173
95373
  longitude: String(location.longitude),
95174
- current: ["us_aqi", "pm2_5", "ozone"].join(",")
95374
+ current: "us_aqi"
95175
95375
  });
95176
95376
  return `${AIR_QUALITY_ENDPOINT}?${params.toString()}`;
95177
95377
  }
@@ -95179,7 +95379,8 @@ function httpErrorFor(status, body) {
95179
95379
  return httpError(status, body, {
95180
95380
  label: "air-quality",
95181
95381
  providerId: "openmeteo",
95182
- schema: apiErrorBodySchema
95382
+ schema: apiErrorBodySchema,
95383
+ maxChars: MAX_REASON_CHARS
95183
95384
  });
95184
95385
  }
95185
95386
  function normalizeAirQuality(data) {
@@ -95187,8 +95388,6 @@ function normalizeAirQuality(data) {
95187
95388
  const observedAtUtc = new Date(Date.parse(`${data.current.time}Z`) - offsetMs).toISOString();
95188
95389
  return {
95189
95390
  usAqi: data.current.us_aqi ?? null,
95190
- pm25UgM3: data.current.pm2_5 ?? null,
95191
- ozoneUgM3: data.current.ozone ?? null,
95192
95391
  observedAtUtc
95193
95392
  };
95194
95393
  }
@@ -95203,8 +95402,10 @@ async function fetchAirQuality(location) {
95203
95402
  }
95204
95403
  let body;
95205
95404
  try {
95206
- body = await res.json();
95405
+ body = await readJsonCapped(res, { providerId: "openmeteo", label: "air-quality" });
95207
95406
  } catch (cause) {
95407
+ if (cause instanceof ProviderError)
95408
+ throw cause;
95208
95409
  throw new ProviderError(`openmeteo air-quality returned a non-JSON body (HTTP ${res.status})`, "openmeteo", cause);
95209
95410
  }
95210
95411
  if (!res.ok || errorReason(body, apiErrorBodySchema) !== undefined) {
@@ -95296,11 +95497,9 @@ function normalizeForecast(data, locationOverride) {
95296
95497
  windDirectionDeg: cur.wind_direction_10m,
95297
95498
  windGustKmh: cur.wind_gusts_10m ?? null,
95298
95499
  pressureHpa: cur.pressure_msl ?? null,
95299
- cloudCoverPct: cur.cloud_cover ?? null,
95300
95500
  dewPointC: cur.dew_point_2m ?? null,
95301
95501
  visibilityM: null,
95302
95502
  uvIndex: null,
95303
- precipLast1hMm: cur.precipitation ?? null,
95304
95503
  isDay: coerceIsDay(cur.is_day)
95305
95504
  };
95306
95505
  const minutely15 = normalizeMinutely(data.minutely_15, toUtc);
@@ -95342,11 +95541,9 @@ function normalizeForecast(data, locationOverride) {
95342
95541
  tempMaxC: requireNum(d2.temperature_2m_max, i, "daily.temperature_2m_max"),
95343
95542
  precipSumMm: requireNum(d2.precipitation_sum, i, "daily.precipitation_sum"),
95344
95543
  precipProbabilityMaxPct: optNum(d2.precipitation_probability_max, i),
95345
- uvIndexMax: optNum(d2.uv_index_max, i),
95346
95544
  sunriseUtc: typeof sunrise === "string" ? toUtc(sunrise) : null,
95347
95545
  sunsetUtc: typeof sunset === "string" ? toUtc(sunset) : null,
95348
- windSpeedMaxKmh: optNum(d2.wind_speed_10m_max, i),
95349
- windGustMaxKmh: optNum(d2.wind_gusts_10m_max, i)
95546
+ windSpeedMaxKmh: optNum(d2.wind_speed_10m_max, i)
95350
95547
  });
95351
95548
  }
95352
95549
  return {
@@ -95383,9 +95580,7 @@ var CURRENT_VARIABLES = [
95383
95580
  "relative_humidity_2m",
95384
95581
  "apparent_temperature",
95385
95582
  "is_day",
95386
- "precipitation",
95387
95583
  "weather_code",
95388
- "cloud_cover",
95389
95584
  "pressure_msl",
95390
95585
  "wind_speed_10m",
95391
95586
  "wind_direction_10m",
@@ -95413,11 +95608,9 @@ var DAILY_VARIABLES = [
95413
95608
  "temperature_2m_min",
95414
95609
  "precipitation_sum",
95415
95610
  "precipitation_probability_max",
95416
- "uv_index_max",
95417
95611
  "sunrise",
95418
95612
  "sunset",
95419
- "wind_speed_10m_max",
95420
- "wind_gusts_10m_max"
95613
+ "wind_speed_10m_max"
95421
95614
  ];
95422
95615
  function buildForecastUrl(location, opts = {}) {
95423
95616
  const params = new URLSearchParams({
@@ -95430,7 +95623,6 @@ function buildForecastUrl(location, opts = {}) {
95430
95623
  timezone: "auto",
95431
95624
  timeformat: "iso8601",
95432
95625
  forecast_days: String(opts.forecastDays ?? 3),
95433
- past_hours: String(opts.pastHours ?? 1),
95434
95626
  past_minutely_15: String(opts.pastMinutely15 ?? 8),
95435
95627
  forecast_minutely_15: String(opts.forecastMinutely15 ?? 12)
95436
95628
  });
@@ -95443,7 +95635,8 @@ function httpErrorFor2(status, body) {
95443
95635
  return httpError(status, body, {
95444
95636
  label: "forecast",
95445
95637
  providerId: OPENMETEO_PROVIDER_ID,
95446
- schema: apiErrorBodySchema
95638
+ schema: apiErrorBodySchema,
95639
+ maxChars: MAX_REASON_CHARS
95447
95640
  });
95448
95641
  }
95449
95642
  async function fetchForecast2(location, opts = {}) {
@@ -95453,12 +95646,17 @@ async function fetchForecast2(location, opts = {}) {
95453
95646
  signal: AbortSignal.timeout(TIMEOUT_MS4)
95454
95647
  });
95455
95648
  } catch (cause) {
95456
- throw new ProviderError("openmeteo forecast request failed before an HTTP response", OPENMETEO_PROVIDER_ID, cause);
95649
+ throw new ProviderError(`openmeteo forecast request failed before an HTTP response${causeSuffix(cause)}`, OPENMETEO_PROVIDER_ID, cause);
95457
95650
  }
95458
95651
  let body;
95459
95652
  try {
95460
- body = await res.json();
95653
+ body = await readJsonCapped(res, {
95654
+ providerId: OPENMETEO_PROVIDER_ID,
95655
+ label: "forecast"
95656
+ });
95461
95657
  } catch (cause) {
95658
+ if (cause instanceof ProviderError)
95659
+ throw cause;
95462
95660
  throw new ProviderError(`openmeteo forecast returned a non-JSON body (HTTP ${res.status})`, OPENMETEO_PROVIDER_ID, cause);
95463
95661
  }
95464
95662
  if (!res.ok || errorReason(body, apiErrorBodySchema) !== undefined) {
@@ -95483,12 +95681,14 @@ function selectProvider(id) {
95483
95681
 
95484
95682
  // src/lib/weather/cache.ts
95485
95683
  import { createHash, randomBytes as randomBytes3 } from "node:crypto";
95486
- import { chmod as chmod2, mkdir as mkdir3, open as open4, readFile as readFile3, rename as rename3, rm, unlink as unlink3 } from "node:fs/promises";
95684
+ import { chmod as chmod2, mkdir as mkdir3, open as open4, readdir, readFile as readFile3, rename as rename3, rm, stat as stat2, unlink as unlink3 } from "node:fs/promises";
95487
95685
  import { homedir as homedir2 } from "node:os";
95488
95686
  import { join as join7 } from "node:path";
95687
+ var CACHE_SCHEMA_VERSION = 2;
95489
95688
  var DEFAULT_MAX_AGE_MINUTES = 10;
95490
95689
  var AQ_TTL_MINUTES = 60;
95491
95690
  var MIN_MS = 60000;
95691
+ var ORPHAN_TTL_MS = 7 * 24 * 60 * MIN_MS;
95492
95692
  var conditionSchema = exports_external.enum([
95493
95693
  "clear",
95494
95694
  "mostly-clear",
@@ -95517,11 +95717,9 @@ var currentObsSchema = exports_external.object({
95517
95717
  windDirectionDeg: numberField,
95518
95718
  windGustKmh: nullableNumber,
95519
95719
  pressureHpa: nullableNumber,
95520
- cloudCoverPct: nullableNumber,
95521
95720
  dewPointC: nullableNumber,
95522
95721
  visibilityM: nullableNumber,
95523
95722
  uvIndex: nullableNumber,
95524
- precipLast1hMm: nullableNumber,
95525
95723
  isDay: exports_external.boolean()
95526
95724
  });
95527
95725
  var precipIntervalSchema = exports_external.object({
@@ -95552,11 +95750,9 @@ var dailyPointSchema = exports_external.object({
95552
95750
  tempMaxC: numberField,
95553
95751
  precipSumMm: numberField,
95554
95752
  precipProbabilityMaxPct: nullableNumber,
95555
- uvIndexMax: nullableNumber,
95556
95753
  sunriseUtc: exports_external.string().nullable(),
95557
95754
  sunsetUtc: exports_external.string().nullable(),
95558
- windSpeedMaxKmh: nullableNumber,
95559
- windGustMaxKmh: nullableNumber
95755
+ windSpeedMaxKmh: nullableNumber
95560
95756
  });
95561
95757
  var normalizedForecastSchema = exports_external.object({
95562
95758
  providerId: exports_external.string(),
@@ -95571,6 +95767,7 @@ var normalizedForecastSchema = exports_external.object({
95571
95767
  daily: exports_external.array(dailyPointSchema)
95572
95768
  });
95573
95769
  var envelopeSchema = exports_external.object({
95770
+ version: exports_external.literal(CACHE_SCHEMA_VERSION),
95574
95771
  fetchedAtUtc: exports_external.string().refine((s) => !Number.isNaN(Date.parse(s)), {
95575
95772
  message: "fetchedAtUtc is not a parseable instant"
95576
95773
  }),
@@ -95578,13 +95775,12 @@ var envelopeSchema = exports_external.object({
95578
95775
  });
95579
95776
  var airQualitySchema = exports_external.object({
95580
95777
  usAqi: exports_external.number().nullable(),
95581
- pm25UgM3: exports_external.number().nullable(),
95582
- ozoneUgM3: exports_external.number().nullable(),
95583
95778
  observedAtUtc: exports_external.string().refine((s) => !Number.isNaN(Date.parse(s)), {
95584
95779
  message: "observedAtUtc is not a parseable instant"
95585
95780
  })
95586
95781
  });
95587
95782
  var aqEnvelopeSchema = exports_external.object({
95783
+ version: exports_external.literal(CACHE_SCHEMA_VERSION),
95588
95784
  fetchedAtUtc: exports_external.string().refine((s) => !Number.isNaN(Date.parse(s)), {
95589
95785
  message: "fetchedAtUtc is not a parseable instant"
95590
95786
  }),
@@ -95592,11 +95788,11 @@ var aqEnvelopeSchema = exports_external.object({
95592
95788
  });
95593
95789
  function cacheKey(providerId, latitude, longitude, window2) {
95594
95790
  const windowTag = window2 === undefined ? "" : `|${window2.forecastDays ?? "*"}|${window2.forecastHours ?? "*"}`;
95595
- const digest = createHash("sha256").update(`${providerId}|${latitude.toFixed(3)}|${longitude.toFixed(3)}${windowTag}`).digest("hex");
95791
+ const digest = createHash("sha256").update(`${CACHE_SCHEMA_VERSION}|${providerId}|${latitude.toFixed(3)}|${longitude.toFixed(3)}${windowTag}`).digest("hex");
95596
95792
  return `${digest}.json`;
95597
95793
  }
95598
95794
  function airQualityCacheKey(providerId, latitude, longitude) {
95599
- const digest = createHash("sha256").update(`${providerId}|aq|${latitude.toFixed(3)}|${longitude.toFixed(3)}`).digest("hex");
95795
+ const digest = createHash("sha256").update(`${CACHE_SCHEMA_VERSION}|${providerId}|aq|${latitude.toFixed(3)}|${longitude.toFixed(3)}`).digest("hex");
95600
95796
  return `${digest}.json`;
95601
95797
  }
95602
95798
  function parseEnvelope(raw) {
@@ -95627,10 +95823,21 @@ function parseAqEnvelope(raw) {
95627
95823
  return null;
95628
95824
  return result.data;
95629
95825
  }
95826
+ function cacheRoot(platform, env2) {
95827
+ const xdg = env2.XDG_CACHE_HOME?.trim();
95828
+ if (xdg)
95829
+ return xdg;
95830
+ if (platform === "win32") {
95831
+ const localAppData = env2.LOCALAPPDATA?.trim();
95832
+ if (localAppData)
95833
+ return localAppData;
95834
+ }
95835
+ return join7(homedir2(), ".cache");
95836
+ }
95630
95837
 
95631
95838
  class FsCacheIo {
95632
95839
  async baseDir() {
95633
- const root = process.env.XDG_CACHE_HOME?.trim() || join7(homedir2(), ".cache");
95840
+ const root = cacheRoot(process.platform, process.env);
95634
95841
  const dir = join7(root, "tuiweather");
95635
95842
  await mkdir3(dir, { recursive: true, mode: 448 });
95636
95843
  return dir;
@@ -95670,6 +95877,35 @@ class FsCacheIo {
95670
95877
  await rm(join7(await this.baseDir(), key), { force: true });
95671
95878
  }
95672
95879
  }
95880
+ async function sweepStaleCacheFiles(io, nowMs) {
95881
+ let dir;
95882
+ try {
95883
+ dir = await io.baseDir();
95884
+ } catch {
95885
+ return;
95886
+ }
95887
+ let entries;
95888
+ try {
95889
+ entries = await readdir(dir);
95890
+ } catch {
95891
+ return;
95892
+ }
95893
+ for (const entry of entries) {
95894
+ if (!entry.endsWith(".json"))
95895
+ continue;
95896
+ let mtimeMs;
95897
+ try {
95898
+ mtimeMs = (await stat2(join7(dir, entry))).mtimeMs;
95899
+ } catch {
95900
+ continue;
95901
+ }
95902
+ if (nowMs - mtimeMs > ORPHAN_TTL_MS) {
95903
+ await io.remove(entry).catch(() => {
95904
+ return;
95905
+ });
95906
+ }
95907
+ }
95908
+ }
95673
95909
  async function cachedForecast(provider, location, opts, io = new FsCacheIo) {
95674
95910
  const maxAgeMinutes = opts?.maxAgeMinutes ?? DEFAULT_MAX_AGE_MINUTES;
95675
95911
  const nowUtc = opts?.nowUtc ?? new Date().toISOString();
@@ -95687,7 +95923,14 @@ async function cachedForecast(provider, location, opts, io = new FsCacheIo) {
95687
95923
  }
95688
95924
  try {
95689
95925
  const forecast = await provider.getForecast(location, opts?.window);
95690
- await io.write(key, JSON.stringify({ fetchedAtUtc: nowUtc, forecast }));
95926
+ await io.write(key, JSON.stringify({
95927
+ version: CACHE_SCHEMA_VERSION,
95928
+ fetchedAtUtc: nowUtc,
95929
+ forecast
95930
+ }));
95931
+ await sweepStaleCacheFiles(io, nowMs).catch(() => {
95932
+ return;
95933
+ });
95691
95934
  return { forecast, stale: false };
95692
95935
  } catch (error61) {
95693
95936
  if (envelope && error61 instanceof ProviderError) {
@@ -95715,7 +95958,14 @@ async function cachedAirQuality(provider, location, opts, io = new FsCacheIo) {
95715
95958
  }
95716
95959
  try {
95717
95960
  const airQuality = await provider.getAirQuality(location);
95718
- await io.write(key, JSON.stringify({ fetchedAtUtc: nowUtc, airQuality }));
95961
+ await io.write(key, JSON.stringify({
95962
+ version: CACHE_SCHEMA_VERSION,
95963
+ fetchedAtUtc: nowUtc,
95964
+ airQuality
95965
+ }));
95966
+ await sweepStaleCacheFiles(io, nowMs).catch(() => {
95967
+ return;
95968
+ });
95719
95969
  return { airQuality, stale: false };
95720
95970
  } catch (error61) {
95721
95971
  if (envelope && error61 instanceof ProviderError) {
@@ -95752,8 +96002,14 @@ var DELETE_ARM_TTL_MS = 4000;
95752
96002
  function isDeleteArmed(armedAtMs, nowMs) {
95753
96003
  return armedAtMs !== null && nowMs >= armedAtMs && nowMs - armedAtMs < DELETE_ARM_TTL_MS;
95754
96004
  }
96005
+ var ACTION_ERROR_TTL_MS = DELETE_ARM_TTL_MS;
96006
+ function isActionErrorActive(atMs, nowMs) {
96007
+ return atMs !== null && nowMs >= atMs && nowMs - atMs < ACTION_ERROR_TTL_MS;
96008
+ }
95755
96009
  function errorMessage2(e) {
95756
- return e instanceof Error ? e.message : String(e);
96010
+ const raw = e instanceof Error ? e.message : String(e);
96011
+ return raw.split(`
96012
+ `)[0] ?? raw;
95757
96013
  }
95758
96014
  function findLocation(config2, slug) {
95759
96015
  return config2.locations.find((loc) => loc.slug === slug);
@@ -95773,6 +96029,21 @@ function resolveDefaultSlug(config2, explicitSlug) {
95773
96029
  }
95774
96030
  return config2.locations[0]?.slug ?? null;
95775
96031
  }
96032
+ function withRepairedDefault(config2) {
96033
+ if (config2.default_location === undefined)
96034
+ return config2;
96035
+ if (config2.locations.some((loc) => loc.slug === config2.default_location))
96036
+ return config2;
96037
+ if (config2.locations.length === 0) {
96038
+ const next = { ...config2 };
96039
+ delete next.default_location;
96040
+ return next;
96041
+ }
96042
+ const first = config2.locations[0];
96043
+ if (!first)
96044
+ return config2;
96045
+ return { ...config2, default_location: first.slug };
96046
+ }
95776
96047
  function createStoreInstance(deps = prodDeps()) {
95777
96048
  const fetcher = deps.fetchForecast;
95778
96049
  const aqFetcher = deps.fetchAirQuality;
@@ -95782,6 +96053,36 @@ function createStoreInstance(deps = prodDeps()) {
95782
96053
  let refreshHandle;
95783
96054
  let disposed = false;
95784
96055
  const inFlight = new Map;
96056
+ let actionErrorTimer;
96057
+ function clearActionErrorTimer() {
96058
+ if (actionErrorTimer !== undefined) {
96059
+ clearTimeout(actionErrorTimer);
96060
+ actionErrorTimer = undefined;
96061
+ }
96062
+ }
96063
+ function clearActionErrorState() {
96064
+ clearActionErrorTimer();
96065
+ const s = get();
96066
+ if (s.lastActionError !== undefined || s.lastActionErrorAtMs !== null) {
96067
+ set2({ lastActionError: undefined, lastActionErrorAtMs: null });
96068
+ }
96069
+ }
96070
+ function setActionErrorState(message) {
96071
+ clearActionErrorTimer();
96072
+ const now = Date.now();
96073
+ const flat = message.replace(/\s+/g, " ").trim();
96074
+ set2({ lastActionError: flat, lastActionErrorAtMs: now });
96075
+ actionErrorTimer = setTimeout(() => {
96076
+ if (disposed)
96077
+ return;
96078
+ const cur = get();
96079
+ if (cur.lastActionErrorAtMs === now) {
96080
+ set2({ lastActionError: undefined, lastActionErrorAtMs: null });
96081
+ }
96082
+ actionErrorTimer = undefined;
96083
+ }, ACTION_ERROR_TTL_MS);
96084
+ actionErrorTimer.unref?.();
96085
+ }
95785
96086
  function clearRefreshTimer() {
95786
96087
  if (refreshHandle === undefined)
95787
96088
  return;
@@ -95843,12 +96144,16 @@ function createStoreInstance(deps = prodDeps()) {
95843
96144
  airQuality: null,
95844
96145
  airQualityBySlug: {},
95845
96146
  lastActionError: undefined,
96147
+ lastActionErrorAtMs: null,
95846
96148
  helpOpen: false,
95847
96149
  overlayOpen: false,
95848
96150
  locationsOpen: false,
95849
96151
  deleteArmedAtMs: null,
96152
+ onboardingSkipped: false,
96153
+ onboardingForced: false,
95850
96154
  init: async (explicitSlug) => {
95851
- set2({ initStatus: "loading", lastActionError: undefined });
96155
+ clearActionErrorTimer();
96156
+ set2({ initStatus: "loading", lastActionError: undefined, lastActionErrorAtMs: null });
95852
96157
  try {
95853
96158
  const config2 = await loadConfig(deps.configPath);
95854
96159
  const slug = resolveDefaultSlug(config2, explicitSlug);
@@ -95861,7 +96166,8 @@ function createStoreInstance(deps = prodDeps()) {
95861
96166
  }
95862
96167
  scheduleRefreshLoop();
95863
96168
  } catch (e) {
95864
- set2({ initStatus: "error", lastActionError: errorMessage2(e) });
96169
+ clearActionErrorTimer();
96170
+ set2({ initStatus: "error", lastActionError: errorMessage2(e), lastActionErrorAtMs: null });
95865
96171
  }
95866
96172
  },
95867
96173
  loadForecast: async (slug, opts) => {
@@ -95917,12 +96223,14 @@ function createStoreInstance(deps = prodDeps()) {
95917
96223
  switchLocation: (slug) => {
95918
96224
  if (!findLocation(get().config, slug))
95919
96225
  return;
96226
+ clearActionErrorState();
95920
96227
  const aq = get().airQualityBySlug[slug] ?? null;
95921
96228
  set2({ activeSlug: slug, airQuality: aq });
95922
96229
  scheduleRefreshLoop();
95923
96230
  get().loadForecast(slug);
95924
96231
  },
95925
96232
  cycleLocation: (delta) => {
96233
+ clearActionErrorState();
95926
96234
  set2({ deleteArmedAtMs: null });
95927
96235
  const locations = get().config.locations;
95928
96236
  if (locations.length === 0)
@@ -95939,67 +96247,99 @@ function createStoreInstance(deps = prodDeps()) {
95939
96247
  const prefs = resolveDisplayPrefs(config2);
95940
96248
  const mixed = new Set([prefs.temp, prefs.wind, prefs.precip, prefs.pressure]).size > 1;
95941
96249
  const units = config2.units === "metric" ? "imperial" : "metric";
95942
- const next = mixed ? { ...config2, units, unit_prefs: { ...config2.unit_prefs, temp: units } } : {
96250
+ const rawNext = mixed ? { ...config2, units, unit_prefs: { ...config2.unit_prefs, temp: units } } : {
95943
96251
  ...config2,
95944
96252
  units,
95945
96253
  unit_prefs: { temp: units, wind: units, precip: units, pressure: units }
95946
96254
  };
96255
+ const next = withRepairedDefault(rawNext);
96256
+ clearActionErrorState();
95947
96257
  set2({ config: next });
95948
96258
  await saveConfig(next, deps.configPath).catch((e) => {
95949
- set2({ lastActionError: errorMessage2(e) });
96259
+ setActionErrorState(errorMessage2(e));
95950
96260
  });
95951
96261
  },
95952
- toggleHelp: () => set2((s) => ({ helpOpen: !s.helpOpen })),
95953
- setOverlayOpen: (open5) => set2(open5 ? { overlayOpen: true, locationsOpen: false, deleteArmedAtMs: null } : { overlayOpen: false }),
95954
- setLocationsOpen: (open5) => set2(open5 ? { locationsOpen: true, overlayOpen: false, deleteArmedAtMs: null } : { locationsOpen: false }),
95955
- armDelete: () => set2({ deleteArmedAtMs: Date.now() }),
95956
- disarmDelete: () => set2({ deleteArmedAtMs: null }),
96262
+ toggleHelp: () => {
96263
+ clearActionErrorState();
96264
+ set2((s) => ({ helpOpen: !s.helpOpen }));
96265
+ },
96266
+ setOverlayOpen: (open5) => {
96267
+ clearActionErrorState();
96268
+ set2(open5 ? { overlayOpen: true, locationsOpen: false, deleteArmedAtMs: null } : { overlayOpen: false });
96269
+ },
96270
+ setLocationsOpen: (open5) => {
96271
+ clearActionErrorState();
96272
+ set2(open5 ? { locationsOpen: true, overlayOpen: false, deleteArmedAtMs: null } : { locationsOpen: false });
96273
+ },
96274
+ armDelete: () => {
96275
+ clearActionErrorState();
96276
+ set2({ deleteArmedAtMs: Date.now() });
96277
+ },
96278
+ disarmDelete: () => {
96279
+ clearActionErrorState();
96280
+ set2({ deleteArmedAtMs: null });
96281
+ },
96282
+ clearActionError: () => {
96283
+ clearActionErrorState();
96284
+ },
95957
96285
  deleteArmed: (nowMs) => isDeleteArmed(get().deleteArmedAtMs, nowMs),
95958
96286
  searchLocations: (query) => geocoder(query),
95959
96287
  addLocation: async (entry) => {
96288
+ clearActionErrorState();
95960
96289
  const config2 = get().config;
95961
96290
  const slug = uniqueSlug(entry.slug, config2.locations.map((loc) => loc.slug));
95962
96291
  const finalEntry = slug === entry.slug ? entry : { ...entry, slug };
95963
96292
  const isFirstLocation = config2.locations.length === 0;
95964
- const next = {
96293
+ const rawNext = {
95965
96294
  ...config2,
95966
96295
  locations: [...config2.locations, finalEntry]
95967
96296
  };
95968
96297
  if (isFirstLocation && config2.default_location === undefined) {
95969
- next.default_location = slug;
96298
+ rawNext.default_location = slug;
95970
96299
  }
96300
+ const next = withRepairedDefault(rawNext);
95971
96301
  try {
95972
96302
  await saveConfig(next, deps.configPath);
95973
96303
  } catch (e) {
95974
- set2({ lastActionError: errorMessage2(e) });
95975
- return;
96304
+ setActionErrorState(errorMessage2(e));
96305
+ return false;
95976
96306
  }
95977
96307
  set2({ config: next });
95978
96308
  get().switchLocation(slug);
96309
+ clearActionErrorState();
96310
+ return true;
95979
96311
  },
95980
96312
  completeOnboarding: async (entry, units) => {
95981
96313
  const config2 = get().config;
95982
- if (config2.locations.length > 0) {
95983
- set2({ lastActionError: "onboarding is already complete" });
96314
+ const forced = get().onboardingForced;
96315
+ if (config2.locations.length > 0 && !forced) {
96316
+ setActionErrorState("onboarding is already complete");
95984
96317
  return false;
95985
96318
  }
95986
96319
  const slug = uniqueSlug(entry.slug, config2.locations.map((loc) => loc.slug));
95987
96320
  const finalEntry = slug === entry.slug ? entry : { ...entry, slug };
95988
- const next = {
96321
+ const rawNext = {
95989
96322
  ...config2,
95990
96323
  units,
95991
96324
  unit_prefs: { temp: units, wind: units, precip: units, pressure: units },
95992
96325
  default_location: slug,
95993
- locations: [finalEntry]
96326
+ locations: forced && config2.locations.length > 0 ? [...config2.locations, finalEntry] : [finalEntry]
95994
96327
  };
95995
- set2({ lastActionError: undefined });
96328
+ const next = withRepairedDefault(rawNext);
96329
+ clearActionErrorState();
95996
96330
  try {
95997
96331
  await saveConfig(next, deps.configPath);
95998
96332
  } catch (e) {
95999
- set2({ lastActionError: errorMessage2(e) });
96333
+ setActionErrorState(errorMessage2(e));
96000
96334
  return false;
96001
96335
  }
96002
- set2({ config: next, activeSlug: slug, lastActionError: undefined });
96336
+ clearActionErrorState();
96337
+ set2({
96338
+ config: next,
96339
+ activeSlug: slug,
96340
+ onboardingForced: false,
96341
+ onboardingSkipped: false
96342
+ });
96003
96343
  await get().loadForecast(slug);
96004
96344
  scheduleRefreshLoop();
96005
96345
  return true;
@@ -96015,31 +96355,34 @@ function createStoreInstance(deps = prodDeps()) {
96015
96355
  set2({ deleteArmedAtMs: null });
96016
96356
  const locations = config2.locations;
96017
96357
  if (locations.length <= 1) {
96018
- set2({ lastActionError: "cannot delete the only location" });
96358
+ setActionErrorState("cannot delete the only location");
96019
96359
  return;
96020
96360
  }
96361
+ clearActionErrorState();
96021
96362
  const idx = locations.findIndex((loc) => loc.slug === slug);
96022
96363
  if (idx === -1)
96023
96364
  return;
96024
96365
  const remaining = locations.filter((_2, i) => i !== idx);
96025
- const next = { ...config2, locations: remaining };
96366
+ const rawNext = { ...config2, locations: remaining };
96026
96367
  if (config2.default_location === slug) {
96027
96368
  const fallback = remaining[0];
96028
96369
  if (fallback) {
96029
- next.default_location = fallback.slug;
96370
+ rawNext.default_location = fallback.slug;
96030
96371
  } else {
96031
- delete next.default_location;
96372
+ delete rawNext.default_location;
96032
96373
  }
96033
96374
  }
96375
+ const next = withRepairedDefault(rawNext);
96034
96376
  const nextActive = remaining[Math.min(idx, remaining.length - 1)];
96035
96377
  set2((s) => ({
96036
96378
  config: next,
96037
96379
  airQualityBySlug: withoutKey(s.airQualityBySlug, slug)
96038
96380
  }));
96381
+ let saveError;
96039
96382
  try {
96040
96383
  await saveConfig(next, deps.configPath);
96041
96384
  } catch (e) {
96042
- set2({ lastActionError: errorMessage2(e) });
96385
+ saveError = errorMessage2(e);
96043
96386
  }
96044
96387
  if (slug === get().activeSlug) {
96045
96388
  if (nextActive) {
@@ -96049,19 +96392,23 @@ function createStoreInstance(deps = prodDeps()) {
96049
96392
  set2({ activeSlug: null, airQuality: null });
96050
96393
  }
96051
96394
  }
96395
+ if (saveError !== undefined)
96396
+ setActionErrorState(saveError);
96052
96397
  },
96053
96398
  setDefaultLocation: async (slug) => {
96054
96399
  const config2 = get().config;
96055
96400
  if (!findLocation(config2, slug))
96056
96401
  return;
96402
+ clearActionErrorState();
96057
96403
  const next = { ...config2, default_location: slug };
96058
96404
  try {
96059
96405
  await saveConfig(next, deps.configPath);
96060
96406
  } catch (e) {
96061
- set2({ lastActionError: errorMessage2(e) });
96407
+ setActionErrorState(errorMessage2(e));
96062
96408
  return;
96063
96409
  }
96064
96410
  set2({ config: next });
96411
+ clearActionErrorState();
96065
96412
  },
96066
96413
  moveLocation: async (slug, delta) => {
96067
96414
  const config2 = get().config;
@@ -96071,23 +96418,34 @@ function createStoreInstance(deps = prodDeps()) {
96071
96418
  const nextIdx = idx + delta;
96072
96419
  if (nextIdx < 0 || nextIdx >= config2.locations.length)
96073
96420
  return;
96421
+ clearActionErrorState();
96074
96422
  const nextLocations = [...config2.locations];
96075
96423
  const [moved] = nextLocations.splice(idx, 1);
96076
96424
  if (!moved)
96077
96425
  return;
96078
96426
  nextLocations.splice(nextIdx, 0, moved);
96079
- const next = { ...config2, locations: nextLocations };
96427
+ const next = withRepairedDefault({ ...config2, locations: nextLocations });
96080
96428
  try {
96081
96429
  await saveConfig(next, deps.configPath);
96082
96430
  } catch (e) {
96083
- set2({ lastActionError: errorMessage2(e) });
96431
+ setActionErrorState(errorMessage2(e));
96084
96432
  return;
96085
96433
  }
96086
96434
  set2({ config: next });
96435
+ clearActionErrorState();
96087
96436
  },
96437
+ skipOnboarding: () => set2({ onboardingSkipped: true, onboardingForced: false }),
96438
+ requestOnboarding: () => set2({
96439
+ onboardingSkipped: false,
96440
+ onboardingForced: true,
96441
+ helpOpen: false,
96442
+ overlayOpen: false,
96443
+ locationsOpen: false
96444
+ }),
96088
96445
  dispose: () => {
96089
96446
  disposed = true;
96090
96447
  clearRefreshTimer();
96448
+ clearActionErrorTimer();
96091
96449
  set2({ airQuality: null, airQualityBySlug: {} });
96092
96450
  inFlight.clear();
96093
96451
  }
@@ -96110,6 +96468,8 @@ function LocationsOverlay({ store, width, height }) {
96110
96468
  const config2 = store((s) => s.config);
96111
96469
  const activeSlug = store((s) => s.activeSlug);
96112
96470
  const forecastBySlug = store((s) => s.forecastBySlug);
96471
+ const lastActionError = store((s) => s.lastActionError);
96472
+ const lastActionErrorAtMs = store((s) => s.lastActionErrorAtMs);
96113
96473
  const [cursor, setCursor] = import_react27.useState(0);
96114
96474
  const [offset, setOffset] = import_react27.useState(0);
96115
96475
  const [armedSlug, setArmedSlug] = import_react27.useState(null);
@@ -96188,6 +96548,7 @@ function LocationsOverlay({ store, width, height }) {
96188
96548
  setCursor((c) => Math.max(0, Math.min(c, count - 2)));
96189
96549
  store.getState().deleteLocation(slug);
96190
96550
  } else {
96551
+ store.getState().clearActionError();
96191
96552
  setArmedSlug(slug);
96192
96553
  setArmedAtMs(Date.now());
96193
96554
  }
@@ -96219,6 +96580,7 @@ function LocationsOverlay({ store, width, height }) {
96219
96580
  const left = Math.max(0, Math.floor((width - boxWidth) / 2));
96220
96581
  const top = Math.max(0, Math.floor((height - boxHeight) / 2));
96221
96582
  const armedLabel = armed && armedSlug !== null ? locations.find((loc) => loc.slug === armedSlug)?.label ?? null : null;
96583
+ const overlayActionError = isActionErrorActive(lastActionErrorAtMs, Date.now()) ? lastActionError : undefined;
96222
96584
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
96223
96585
  position: "absolute",
96224
96586
  left,
@@ -96242,7 +96604,11 @@ function LocationsOverlay({ store, width, height }) {
96242
96604
  bg: palette.surface,
96243
96605
  children: "─".repeat(innerWidth)
96244
96606
  }, undefined, false, undefined, this),
96245
- armedLabel !== null ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
96607
+ overlayActionError !== undefined ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
96608
+ fg: palette.danger,
96609
+ bg: palette.surface,
96610
+ children: truncateCells(overlayActionError, Math.max(0, innerWidth - 1))
96611
+ }, undefined, false, undefined, this) : armedLabel !== null ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
96246
96612
  fg: palette.danger,
96247
96613
  bg: palette.surface,
96248
96614
  children: truncateCells(`d again deletes ${armedLabel}`, innerWidth)
@@ -96459,6 +96825,10 @@ function FirstRun({ store, width, height, quit }) {
96459
96825
  return;
96460
96826
  }
96461
96827
  if (step === "welcome") {
96828
+ if (key.name === "s" && !key.ctrl && !key.meta && !key.option && !key.shift) {
96829
+ store.getState().skipOnboarding();
96830
+ return;
96831
+ }
96462
96832
  if (key.name === "return" || key.name === "enter" || key.name === "escape") {
96463
96833
  setStep("units");
96464
96834
  }
@@ -96556,7 +96926,7 @@ function FirstRun({ store, width, height, quit }) {
96556
96926
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
96557
96927
  fg: palette.accent,
96558
96928
  bg: palette.surface,
96559
- children: truncateTo2("enter continue · esc skip tour · q quit", innerWidth)
96929
+ children: truncateTo2("enter/esc continue · s skip · q quit", innerWidth)
96560
96930
  }, undefined, false, undefined, this)
96561
96931
  ]
96562
96932
  }, undefined, true, undefined, this)
@@ -96603,25 +96973,6 @@ function FirstRun({ store, width, height, quit }) {
96603
96973
  }, undefined, false, undefined, this);
96604
96974
  }
96605
96975
 
96606
- // src/theme/detect.ts
96607
- var FALLBACK_APPEARANCE = { ink: "dark", background: null };
96608
- async function detectTerminalAppearance(query, timeoutMs = 300) {
96609
- try {
96610
- const colors = await Promise.race([
96611
- query.getPalette({ timeout: timeoutMs }),
96612
- new Promise((_2, reject) => {
96613
- const timer = setTimeout(() => reject(new Error("palette query timed out")), timeoutMs);
96614
- if (typeof timer === "object" && "unref" in timer)
96615
- timer.unref();
96616
- })
96617
- ]);
96618
- const background = colors?.defaultBackground ?? null;
96619
- return { ink: isDarkBackground(background) ? "dark" : "light", background };
96620
- } catch {
96621
- return FALLBACK_APPEARANCE;
96622
- }
96623
- }
96624
-
96625
96976
  // src/viewport/useViewport.ts
96626
96977
  var import_react32 = __toESM(require_react(), 1);
96627
96978
 
@@ -96804,10 +97155,16 @@ var HELP_LINES = [
96804
97155
  { text: "l locations j/k focus enter open (lg)" },
96805
97156
  { text: "s default J/K reorder (lg) ↑↓ scroll" },
96806
97157
  { text: "/ search d delete (press twice)", dim: true },
97158
+ { text: "o re-run setup", dim: true },
96807
97159
  { text: "esc close / clear focus" }
96808
97160
  ];
96809
- function HelpOverlay({ width, height, providerLabel }) {
97161
+ function HelpOverlay({ store, width, height, providerLabel }) {
96810
97162
  const palette = usePalette();
97163
+ useKeyboard((key) => {
97164
+ if (key.name === "o" && !key.ctrl && !key.meta && !key.option && !key.shift) {
97165
+ store.getState().requestOnboarding();
97166
+ }
97167
+ });
96811
97168
  const boxWidth = Math.max(1, Math.min(HELP_BOX_WIDTH, width >= 32 ? width - 2 : width));
96812
97169
  const left = Math.max(0, Math.floor((width - boxWidth) / 2));
96813
97170
  const top = Math.max(0, Math.floor((height - HELP_BOX_HEIGHT) / 2));
@@ -96840,12 +97197,12 @@ function HelpOverlay({ width, height, providerLabel }) {
96840
97197
  }
96841
97198
 
96842
97199
  // src/app/components/Sidebar.tsx
96843
- var import_react33 = __toESM(require_react(), 1);
97200
+ var import_react34 = __toESM(require_react(), 1);
96844
97201
  var SIDEBAR_WIDTH = 26;
96845
97202
  function truncateTo3(text, width) {
96846
97203
  return truncateCells(text, width);
96847
97204
  }
96848
- var SidebarRow = import_react33.memo(function SidebarRow2({
97205
+ var SidebarRow = import_react34.memo(function SidebarRow2({
96849
97206
  slug,
96850
97207
  label,
96851
97208
  store,
@@ -96864,7 +97221,7 @@ var SidebarRow = import_react33.memo(function SidebarRow2({
96864
97221
  children: truncateTo3(`${bullet} ${truncateTo3(label, labelBudget)}${tail}`, SIDEBAR_WIDTH - 3)
96865
97222
  }, undefined, false, undefined, this);
96866
97223
  });
96867
- var Sidebar = import_react33.memo(function Sidebar2({ store, focusedSlug, prefs }) {
97224
+ var Sidebar = import_react34.memo(function Sidebar2({ store, focusedSlug, prefs }) {
96868
97225
  const palette = usePalette();
96869
97226
  const config2 = store((s) => s.config);
96870
97227
  const activeSlug = store((s) => s.activeSlug);
@@ -96886,17 +97243,23 @@ var Sidebar = import_react33.memo(function Sidebar2({ store, focusedSlug, prefs
96886
97243
  });
96887
97244
 
96888
97245
  // src/app/components/StatusArea.tsx
96889
- var import_react34 = __toESM(require_react(), 1);
97246
+ var import_react35 = __toESM(require_react(), 1);
96890
97247
  var SPINNER_FRAMES = ["|", "/", "-", "\\"];
96891
97248
  var SPINNER_INTERVAL_MS = 120;
96892
- function Spinner() {
96893
- const [frame, setFrame] = import_react34.useState(0);
96894
- import_react34.useEffect(() => {
97249
+ function Spinner({ reducedMotion }) {
97250
+ const [frame, setFrame] = import_react35.useState(0);
97251
+ import_react35.useEffect(() => {
97252
+ if (reducedMotion)
97253
+ return;
96895
97254
  const timer = setInterval(() => {
96896
97255
  setFrame((f) => (f + 1) % SPINNER_FRAMES.length);
96897
97256
  }, SPINNER_INTERVAL_MS);
96898
97257
  return () => clearInterval(timer);
96899
- }, []);
97258
+ }, [reducedMotion]);
97259
+ if (reducedMotion)
97260
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
97261
+ children: "|"
97262
+ }, undefined, false, undefined, this);
96900
97263
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
96901
97264
  children: SPINNER_FRAMES[frame] ?? "|"
96902
97265
  }, undefined, false, undefined, this);
@@ -96906,7 +97269,19 @@ function deleteArmLine(label, width) {
96906
97269
  const budget = width === undefined ? displayWidth(line) : Math.max(0, width - 1);
96907
97270
  return truncateCells(line, budget);
96908
97271
  }
96909
- function StatusArea({ loading, error: error61, stale, deleteArm, width }) {
97272
+ function actionErrorLine(error61, width) {
97273
+ const budget = width === undefined ? displayWidth(error61) : Math.max(0, width - 1);
97274
+ return truncateCells(error61, budget);
97275
+ }
97276
+ function StatusArea({
97277
+ loading,
97278
+ error: error61,
97279
+ stale,
97280
+ deleteArm,
97281
+ actionError,
97282
+ width,
97283
+ reducedMotion
97284
+ }) {
96910
97285
  const palette = usePalette();
96911
97286
  if (deleteArm !== undefined) {
96912
97287
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
@@ -96917,12 +97292,23 @@ function StatusArea({ loading, error: error61, stale, deleteArm, width }) {
96917
97292
  }, undefined, false, undefined, this)
96918
97293
  }, undefined, false, undefined, this);
96919
97294
  }
97295
+ if (actionError !== undefined) {
97296
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
97297
+ flexDirection: "row",
97298
+ children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
97299
+ fg: palette.danger,
97300
+ children: actionErrorLine(actionError, width)
97301
+ }, undefined, false, undefined, this)
97302
+ }, undefined, false, undefined, this);
97303
+ }
96920
97304
  if (loading) {
96921
97305
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
96922
97306
  flexDirection: "row",
96923
97307
  gap: 1,
96924
97308
  children: [
96925
- /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Spinner, {}, undefined, false, undefined, this),
97309
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Spinner, {
97310
+ reducedMotion
97311
+ }, undefined, false, undefined, this),
96926
97312
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
96927
97313
  fg: palette.accent,
96928
97314
  children: "syncing…"
@@ -96962,15 +97348,15 @@ function StatusArea({ loading, error: error61, stale, deleteArm, width }) {
96962
97348
  }
96963
97349
 
96964
97350
  // src/app/hooks/useNowMs.ts
96965
- var import_react35 = __toESM(require_react(), 1);
97351
+ var import_react36 = __toESM(require_react(), 1);
96966
97352
 
96967
97353
  // src/app/tick.ts
96968
97354
  var TICK_INTERVAL_MS = 30000;
96969
97355
 
96970
97356
  // src/app/hooks/useNowMs.ts
96971
97357
  function useNowMs(propsNowMs) {
96972
- const [, setTick] = import_react35.useState(0);
96973
- import_react35.useEffect(() => {
97358
+ const [, setTick] = import_react36.useState(0);
97359
+ import_react36.useEffect(() => {
96974
97360
  if (propsNowMs !== undefined)
96975
97361
  return;
96976
97362
  const h2 = setInterval(() => setTick((v2) => v2 + 1), TICK_INTERVAL_MS);
@@ -96997,7 +97383,6 @@ function handleKey(nameOrEvent, api2) {
96997
97383
  api2.toggleHelp();
96998
97384
  return;
96999
97385
  }
97000
- api2.quit();
97001
97386
  return;
97002
97387
  }
97003
97388
  if (api2.searchOpen() || api2.locationsOpen() || api2.helpOpen())
@@ -97236,7 +97621,7 @@ function XsChips({
97236
97621
  children: truncateTo4(dailyChips(forecast.daily, prefs.temp), Math.max(0, width))
97237
97622
  }, undefined, false, undefined, this);
97238
97623
  }
97239
- var MainContent = import_react37.memo(function MainContent2({
97624
+ var MainContent = import_react38.memo(function MainContent2({
97240
97625
  tier,
97241
97626
  width,
97242
97627
  forecast,
@@ -97431,17 +97816,20 @@ function App(props = {}) {
97431
97816
  const locationsOpen = store((s) => s.locationsOpen);
97432
97817
  const airQuality = store((s) => s.airQuality);
97433
97818
  const lastActionError = store((s) => s.lastActionError);
97819
+ const lastActionErrorAtMs = store((s) => s.lastActionErrorAtMs);
97434
97820
  const deleteArmedAtMs = store((s) => s.deleteArmedAtMs);
97821
+ const onboardingSkipped = store((s) => s.onboardingSkipped);
97822
+ const onboardingForced = store((s) => s.onboardingForced);
97435
97823
  const viewport = useViewport();
97436
97824
  const renderer = useRenderer();
97437
97825
  const isDay = entry?.forecast.current.isDay ?? true;
97438
97826
  const appearance = props.appearance ?? FALLBACK_APPEARANCE;
97439
- const palette = import_react37.useMemo(() => buildPalette(config2.theme, isDay, appearance.ink, appearance.background), [config2.theme, isDay, appearance]);
97440
- const prefs = import_react37.useMemo(() => resolveDisplayPrefs(config2), [config2]);
97441
- import_react37.useEffect(() => {
97827
+ const palette = import_react38.useMemo(() => buildPalette(config2.theme, isDay, appearance.ink, appearance.background), [config2.theme, isDay, appearance]);
97828
+ const prefs = import_react38.useMemo(() => resolveDisplayPrefs(config2), [config2]);
97829
+ import_react38.useEffect(() => {
97442
97830
  store.getState().init(props.initialSlug);
97443
97831
  }, [store, props.initialSlug]);
97444
- import_react37.useEffect(() => {
97832
+ import_react38.useEffect(() => {
97445
97833
  return () => {
97446
97834
  store.getState().dispose();
97447
97835
  };
@@ -97453,17 +97841,18 @@ function App(props = {}) {
97453
97841
  const nowUtc = props.nowUtc ?? new Date(nowMs).toISOString();
97454
97842
  const tier = viewport.tier;
97455
97843
  const forecast = entry?.forecast;
97456
- const onboardingOpen = initStatus === "ready" && config2.locations.length === 0;
97844
+ const onboardingOpen = initStatus === "ready" && (config2.locations.length === 0 && !onboardingSkipped || onboardingForced);
97457
97845
  const deleteArmed = isDeleteArmed(deleteArmedAtMs, nowMs);
97458
- const [focusedSlug, setFocusedSlug] = import_react37.useState(null);
97459
- import_react37.useEffect(() => {
97846
+ const actionError = isActionErrorActive(lastActionErrorAtMs, Date.now()) ? lastActionError : undefined;
97847
+ const [focusedSlug, setFocusedSlug] = import_react38.useState(null);
97848
+ import_react38.useEffect(() => {
97460
97849
  if (tier !== "lg")
97461
97850
  setFocusedSlug(null);
97462
97851
  else if (focusedSlug !== null && !config2.locations.some((loc) => loc.slug === focusedSlug)) {
97463
97852
  setFocusedSlug(null);
97464
97853
  }
97465
97854
  }, [config2.locations, focusedSlug, tier]);
97466
- const api2 = import_react37.useMemo(() => ({
97855
+ const api2 = import_react38.useMemo(() => ({
97467
97856
  quit,
97468
97857
  activeSlug: () => store.getState().activeSlug,
97469
97858
  refresh: (slug) => void store.getState().refresh(slug),
@@ -97473,7 +97862,8 @@ function App(props = {}) {
97473
97862
  toggleHelp: () => store.getState().toggleHelp(),
97474
97863
  searchOpen: () => {
97475
97864
  const state = store.getState();
97476
- return state.overlayOpen || state.initStatus === "ready" && state.config.locations.length === 0;
97865
+ const onboardingActive = state.initStatus === "ready" && (state.config.locations.length === 0 && !state.onboardingSkipped || state.onboardingForced);
97866
+ return state.overlayOpen || onboardingActive;
97477
97867
  },
97478
97868
  openSearch: () => store.getState().setOverlayOpen(true),
97479
97869
  locationsOpen: () => store.getState().locationsOpen,
@@ -97516,7 +97906,9 @@ function App(props = {}) {
97516
97906
  error: error61,
97517
97907
  stale: staleBadge,
97518
97908
  deleteArm: deleteArmed ? { label } : undefined,
97519
- width: viewport.width
97909
+ actionError,
97910
+ width: tier === "lg" ? viewport.width - SIDEBAR_WIDTH : viewport.width,
97911
+ reducedMotion: config2.reduced_motion
97520
97912
  }, undefined, false, undefined, this);
97521
97913
  const footerColumnWidth = tier === "lg" ? viewport.width - SIDEBAR_WIDTH : viewport.width;
97522
97914
  const footer = /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Footer, {
@@ -97532,7 +97924,7 @@ function App(props = {}) {
97532
97924
  panels: config2.panels,
97533
97925
  nowUtc
97534
97926
  }) : null;
97535
- const statusRows = deleteArmed ? 1 : loading ? 1 : error61 !== undefined ? ERROR_PANEL_ROWS : staleBadge ? 1 : 0;
97927
+ const statusRows = deleteArmed ? 1 : actionError !== undefined ? 1 : loading ? 1 : error61 !== undefined ? ERROR_PANEL_ROWS : staleBadge ? 1 : 0;
97536
97928
  const statusBlock = statusRows > 0 ? statusRows + 1 : 0;
97537
97929
  const showOverflowHint = overflowEstimate !== null && viewport.height < overflowEstimate + MAIN_CHROME_ROWS + statusBlock;
97538
97930
  const mainScrollHeight = Math.max(1, viewport.height - MAIN_CHROME_ROWS - statusBlock - (showOverflowHint ? OVERFLOW_HINT_ROWS : 0));
@@ -97646,6 +98038,7 @@ function App(props = {}) {
97646
98038
  children: [
97647
98039
  body,
97648
98040
  helpOpen ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(HelpOverlay, {
98041
+ store,
97649
98042
  width: viewport.width,
97650
98043
  height: viewport.height,
97651
98044
  providerLabel: config2.provider === "nws" ? "api.weather.gov" : "open-meteo.com"
@@ -97666,6 +98059,34 @@ function App(props = {}) {
97666
98059
  }, undefined, false, undefined, this);
97667
98060
  }
97668
98061
 
98062
+ // src/app/AppearanceApp.tsx
98063
+ function AppearanceApp({ initialSlug, appearancePromise, store }) {
98064
+ const [appearance, setAppearance] = import_react40.useState(FALLBACK_APPEARANCE);
98065
+ const renderer = useRenderer();
98066
+ import_react40.useEffect(() => {
98067
+ let cancelled = false;
98068
+ appearancePromise.then((detected) => {
98069
+ if (cancelled)
98070
+ return;
98071
+ if (renderer.isDestroyed)
98072
+ return;
98073
+ try {
98074
+ setAppearance((prev) => appearancesEqual(prev, detected) ? prev : detected);
98075
+ } catch {}
98076
+ }).catch(() => {
98077
+ return;
98078
+ });
98079
+ return () => {
98080
+ cancelled = true;
98081
+ };
98082
+ }, [appearancePromise, renderer]);
98083
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(App, {
98084
+ store,
98085
+ initialSlug,
98086
+ appearance
98087
+ }, undefined, false, undefined, this);
98088
+ }
98089
+
97669
98090
  // src/app/oneline.ts
97670
98091
  var SEPARATOR = " · ";
97671
98092
  var ARROWS_8 = ["↑", "↗", "→", "↘", "↓", "↙", "←", "↖"];
@@ -97808,6 +98229,18 @@ function parseInterval(raw) {
97808
98229
  }
97809
98230
  return value;
97810
98231
  }
98232
+ function warnStaleDefault(config2, write) {
98233
+ if (config2.default_location !== undefined && config2.locations.length > 0 && !config2.locations.some((loc) => loc.slug === config2.default_location)) {
98234
+ const fallback = config2.locations[0]?.slug ?? "";
98235
+ let stale = "";
98236
+ for (const ch of config2.default_location) {
98237
+ const code = ch.codePointAt(0) ?? 0;
98238
+ if (code > 31 && code !== 127)
98239
+ stale += ch;
98240
+ }
98241
+ write(`warning: default_location "${stale}" does not match any [[locations]] slug — using "${fallback}"`);
98242
+ }
98243
+ }
97811
98244
  function parseArgs(argv) {
97812
98245
  if (argv.includes("--help") || argv.includes("-h")) {
97813
98246
  if (argv.length !== 1)
@@ -97897,6 +98330,22 @@ function parseArgs(argv) {
97897
98330
  return args;
97898
98331
  }
97899
98332
 
98333
+ // src/lib/runtime/ffi.ts
98334
+ function formatFfiUnavailableMessage(nodeVersion) {
98335
+ return `interactive TUI requires Bun or Node >= 26.4 (detected node ${nodeVersion}) — CLI-only flags (--version, --one-line) still work`;
98336
+ }
98337
+ async function probeFfiAvailable() {
98338
+ const bunVersion = process.versions.bun;
98339
+ if (bunVersion !== undefined && bunVersion !== "")
98340
+ return true;
98341
+ try {
98342
+ await import("node:ffi");
98343
+ return true;
98344
+ } catch {
98345
+ return false;
98346
+ }
98347
+ }
98348
+
97900
98349
  // src/index.tsx
97901
98350
  function stderr(message) {
97902
98351
  process.stderr.write(`${message}
@@ -97935,6 +98384,7 @@ function resolveLocationForCli(args, config2) {
97935
98384
  }
97936
98385
  async function runOneLine(args) {
97937
98386
  const config2 = await loadConfig();
98387
+ warnStaleDefault(config2, stderr);
97938
98388
  const resolved = resolveLocationForCli(args, config2);
97939
98389
  if ("error" in resolved) {
97940
98390
  const hint = config2.locations.length === 0 ? "; run tuiweather to set one up" : "";
@@ -97957,6 +98407,7 @@ async function runOneLine(args) {
97957
98407
  }
97958
98408
  async function runTui(locationArg) {
97959
98409
  const config2 = await loadConfig();
98410
+ warnStaleDefault(config2, stderr);
97960
98411
  let initialSlug;
97961
98412
  if (locationArg !== null || config2.locations.length > 0) {
97962
98413
  const resolved = resolveSlugFromConfig(config2, locationArg);
@@ -97967,17 +98418,25 @@ async function runTui(locationArg) {
97967
98418
  }
97968
98419
  initialSlug = resolved.slug;
97969
98420
  }
98421
+ if (!await probeFfiAvailable()) {
98422
+ stderr(formatFfiUnavailableMessage(process.versions.node));
98423
+ return 1;
98424
+ }
97970
98425
  const renderer = await createCliRenderer({ exitOnCtrlC: true });
97971
- const appearance = await detectTerminalAppearance(renderer);
97972
98426
  renderer.on("destroy", () => appStore.getState().dispose());
97973
- createRoot(renderer).render(/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(App, {
98427
+ const appearancePromise = resolveTerminalAppearance(config2.ink, renderer);
98428
+ createRoot(renderer).render(/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(AppearanceApp, {
97974
98429
  initialSlug,
97975
- appearance
98430
+ appearancePromise
97976
98431
  }, undefined, false, undefined, this));
98432
+ appearancePromise.catch(() => {
98433
+ return;
98434
+ });
97977
98435
  return 0;
97978
98436
  }
97979
98437
  async function runWatchCli(args) {
97980
98438
  const config2 = await loadConfig();
98439
+ warnStaleDefault(config2, stderr);
97981
98440
  const resolved = resolveLocationForCli(args, config2);
97982
98441
  if ("error" in resolved) {
97983
98442
  const hint = config2.locations.length === 0 ? "; run tuiweather to set one up" : "";