tuiweather 0.3.7 → 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/README.md +30 -12
- package/dist/index.js +735 -289
- package/package.json +3 -2
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
|
|
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:
|
|
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:
|
|
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(() =>
|
|
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 =
|
|
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
|
-
|
|
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
|
|
94444
|
-
if (typeof
|
|
94491
|
+
const raw = extractor ? extractor(parsed.data) : parsed.data.reason;
|
|
94492
|
+
if (typeof raw !== "string")
|
|
94445
94493
|
return;
|
|
94446
|
-
return sanitizeText(
|
|
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
|
|
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(
|
|
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
|
|
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.
|
|
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",
|
|
@@ -94671,7 +94804,8 @@ var package_default2 = {
|
|
|
94671
94804
|
lint: "biome check .",
|
|
94672
94805
|
fmt: "biome check --write .",
|
|
94673
94806
|
test: "bun test",
|
|
94674
|
-
|
|
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, {
|
|
95247
|
+
res = await fetch(url2, {
|
|
95248
|
+
headers: NWS_HEADERS,
|
|
95249
|
+
signal: AbortSignal.timeout(TIMEOUT_MS2),
|
|
95250
|
+
redirect: "error"
|
|
95251
|
+
});
|
|
95105
95252
|
} catch (cause) {
|
|
95106
|
-
|
|
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
|
|
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
|
-
|
|
95116
|
-
|
|
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
|
|
95129
|
-
const
|
|
95130
|
-
|
|
95131
|
-
|
|
95132
|
-
|
|
95133
|
-
|
|
95134
|
-
|
|
95135
|
-
|
|
95136
|
-
|
|
95137
|
-
|
|
95138
|
-
|
|
95139
|
-
|
|
95140
|
-
|
|
95141
|
-
|
|
95142
|
-
|
|
95143
|
-
|
|
95144
|
-
|
|
95145
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
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) {
|
|
@@ -95681,6 +95877,35 @@ class FsCacheIo {
|
|
|
95681
95877
|
await rm(join7(await this.baseDir(), key), { force: true });
|
|
95682
95878
|
}
|
|
95683
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
|
+
}
|
|
95684
95909
|
async function cachedForecast(provider, location, opts, io = new FsCacheIo) {
|
|
95685
95910
|
const maxAgeMinutes = opts?.maxAgeMinutes ?? DEFAULT_MAX_AGE_MINUTES;
|
|
95686
95911
|
const nowUtc = opts?.nowUtc ?? new Date().toISOString();
|
|
@@ -95698,7 +95923,14 @@ async function cachedForecast(provider, location, opts, io = new FsCacheIo) {
|
|
|
95698
95923
|
}
|
|
95699
95924
|
try {
|
|
95700
95925
|
const forecast = await provider.getForecast(location, opts?.window);
|
|
95701
|
-
await io.write(key, JSON.stringify({
|
|
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
|
+
});
|
|
95702
95934
|
return { forecast, stale: false };
|
|
95703
95935
|
} catch (error61) {
|
|
95704
95936
|
if (envelope && error61 instanceof ProviderError) {
|
|
@@ -95726,7 +95958,14 @@ async function cachedAirQuality(provider, location, opts, io = new FsCacheIo) {
|
|
|
95726
95958
|
}
|
|
95727
95959
|
try {
|
|
95728
95960
|
const airQuality = await provider.getAirQuality(location);
|
|
95729
|
-
await io.write(key, JSON.stringify({
|
|
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
|
+
});
|
|
95730
95969
|
return { airQuality, stale: false };
|
|
95731
95970
|
} catch (error61) {
|
|
95732
95971
|
if (envelope && error61 instanceof ProviderError) {
|
|
@@ -95763,8 +96002,14 @@ var DELETE_ARM_TTL_MS = 4000;
|
|
|
95763
96002
|
function isDeleteArmed(armedAtMs, nowMs) {
|
|
95764
96003
|
return armedAtMs !== null && nowMs >= armedAtMs && nowMs - armedAtMs < DELETE_ARM_TTL_MS;
|
|
95765
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
|
+
}
|
|
95766
96009
|
function errorMessage2(e) {
|
|
95767
|
-
|
|
96010
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
96011
|
+
return raw.split(`
|
|
96012
|
+
`)[0] ?? raw;
|
|
95768
96013
|
}
|
|
95769
96014
|
function findLocation(config2, slug) {
|
|
95770
96015
|
return config2.locations.find((loc) => loc.slug === slug);
|
|
@@ -95784,6 +96029,21 @@ function resolveDefaultSlug(config2, explicitSlug) {
|
|
|
95784
96029
|
}
|
|
95785
96030
|
return config2.locations[0]?.slug ?? null;
|
|
95786
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
|
+
}
|
|
95787
96047
|
function createStoreInstance(deps = prodDeps()) {
|
|
95788
96048
|
const fetcher = deps.fetchForecast;
|
|
95789
96049
|
const aqFetcher = deps.fetchAirQuality;
|
|
@@ -95793,6 +96053,36 @@ function createStoreInstance(deps = prodDeps()) {
|
|
|
95793
96053
|
let refreshHandle;
|
|
95794
96054
|
let disposed = false;
|
|
95795
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
|
+
}
|
|
95796
96086
|
function clearRefreshTimer() {
|
|
95797
96087
|
if (refreshHandle === undefined)
|
|
95798
96088
|
return;
|
|
@@ -95854,12 +96144,16 @@ function createStoreInstance(deps = prodDeps()) {
|
|
|
95854
96144
|
airQuality: null,
|
|
95855
96145
|
airQualityBySlug: {},
|
|
95856
96146
|
lastActionError: undefined,
|
|
96147
|
+
lastActionErrorAtMs: null,
|
|
95857
96148
|
helpOpen: false,
|
|
95858
96149
|
overlayOpen: false,
|
|
95859
96150
|
locationsOpen: false,
|
|
95860
96151
|
deleteArmedAtMs: null,
|
|
96152
|
+
onboardingSkipped: false,
|
|
96153
|
+
onboardingForced: false,
|
|
95861
96154
|
init: async (explicitSlug) => {
|
|
95862
|
-
|
|
96155
|
+
clearActionErrorTimer();
|
|
96156
|
+
set2({ initStatus: "loading", lastActionError: undefined, lastActionErrorAtMs: null });
|
|
95863
96157
|
try {
|
|
95864
96158
|
const config2 = await loadConfig(deps.configPath);
|
|
95865
96159
|
const slug = resolveDefaultSlug(config2, explicitSlug);
|
|
@@ -95872,7 +96166,8 @@ function createStoreInstance(deps = prodDeps()) {
|
|
|
95872
96166
|
}
|
|
95873
96167
|
scheduleRefreshLoop();
|
|
95874
96168
|
} catch (e) {
|
|
95875
|
-
|
|
96169
|
+
clearActionErrorTimer();
|
|
96170
|
+
set2({ initStatus: "error", lastActionError: errorMessage2(e), lastActionErrorAtMs: null });
|
|
95876
96171
|
}
|
|
95877
96172
|
},
|
|
95878
96173
|
loadForecast: async (slug, opts) => {
|
|
@@ -95928,12 +96223,14 @@ function createStoreInstance(deps = prodDeps()) {
|
|
|
95928
96223
|
switchLocation: (slug) => {
|
|
95929
96224
|
if (!findLocation(get().config, slug))
|
|
95930
96225
|
return;
|
|
96226
|
+
clearActionErrorState();
|
|
95931
96227
|
const aq = get().airQualityBySlug[slug] ?? null;
|
|
95932
96228
|
set2({ activeSlug: slug, airQuality: aq });
|
|
95933
96229
|
scheduleRefreshLoop();
|
|
95934
96230
|
get().loadForecast(slug);
|
|
95935
96231
|
},
|
|
95936
96232
|
cycleLocation: (delta) => {
|
|
96233
|
+
clearActionErrorState();
|
|
95937
96234
|
set2({ deleteArmedAtMs: null });
|
|
95938
96235
|
const locations = get().config.locations;
|
|
95939
96236
|
if (locations.length === 0)
|
|
@@ -95950,67 +96247,99 @@ function createStoreInstance(deps = prodDeps()) {
|
|
|
95950
96247
|
const prefs = resolveDisplayPrefs(config2);
|
|
95951
96248
|
const mixed = new Set([prefs.temp, prefs.wind, prefs.precip, prefs.pressure]).size > 1;
|
|
95952
96249
|
const units = config2.units === "metric" ? "imperial" : "metric";
|
|
95953
|
-
const
|
|
96250
|
+
const rawNext = mixed ? { ...config2, units, unit_prefs: { ...config2.unit_prefs, temp: units } } : {
|
|
95954
96251
|
...config2,
|
|
95955
96252
|
units,
|
|
95956
96253
|
unit_prefs: { temp: units, wind: units, precip: units, pressure: units }
|
|
95957
96254
|
};
|
|
96255
|
+
const next = withRepairedDefault(rawNext);
|
|
96256
|
+
clearActionErrorState();
|
|
95958
96257
|
set2({ config: next });
|
|
95959
96258
|
await saveConfig(next, deps.configPath).catch((e) => {
|
|
95960
|
-
|
|
96259
|
+
setActionErrorState(errorMessage2(e));
|
|
95961
96260
|
});
|
|
95962
96261
|
},
|
|
95963
|
-
toggleHelp: () =>
|
|
95964
|
-
|
|
95965
|
-
|
|
95966
|
-
|
|
95967
|
-
|
|
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
|
+
},
|
|
95968
96285
|
deleteArmed: (nowMs) => isDeleteArmed(get().deleteArmedAtMs, nowMs),
|
|
95969
96286
|
searchLocations: (query) => geocoder(query),
|
|
95970
96287
|
addLocation: async (entry) => {
|
|
96288
|
+
clearActionErrorState();
|
|
95971
96289
|
const config2 = get().config;
|
|
95972
96290
|
const slug = uniqueSlug(entry.slug, config2.locations.map((loc) => loc.slug));
|
|
95973
96291
|
const finalEntry = slug === entry.slug ? entry : { ...entry, slug };
|
|
95974
96292
|
const isFirstLocation = config2.locations.length === 0;
|
|
95975
|
-
const
|
|
96293
|
+
const rawNext = {
|
|
95976
96294
|
...config2,
|
|
95977
96295
|
locations: [...config2.locations, finalEntry]
|
|
95978
96296
|
};
|
|
95979
96297
|
if (isFirstLocation && config2.default_location === undefined) {
|
|
95980
|
-
|
|
96298
|
+
rawNext.default_location = slug;
|
|
95981
96299
|
}
|
|
96300
|
+
const next = withRepairedDefault(rawNext);
|
|
95982
96301
|
try {
|
|
95983
96302
|
await saveConfig(next, deps.configPath);
|
|
95984
96303
|
} catch (e) {
|
|
95985
|
-
|
|
95986
|
-
return;
|
|
96304
|
+
setActionErrorState(errorMessage2(e));
|
|
96305
|
+
return false;
|
|
95987
96306
|
}
|
|
95988
96307
|
set2({ config: next });
|
|
95989
96308
|
get().switchLocation(slug);
|
|
96309
|
+
clearActionErrorState();
|
|
96310
|
+
return true;
|
|
95990
96311
|
},
|
|
95991
96312
|
completeOnboarding: async (entry, units) => {
|
|
95992
96313
|
const config2 = get().config;
|
|
95993
|
-
|
|
95994
|
-
|
|
96314
|
+
const forced = get().onboardingForced;
|
|
96315
|
+
if (config2.locations.length > 0 && !forced) {
|
|
96316
|
+
setActionErrorState("onboarding is already complete");
|
|
95995
96317
|
return false;
|
|
95996
96318
|
}
|
|
95997
96319
|
const slug = uniqueSlug(entry.slug, config2.locations.map((loc) => loc.slug));
|
|
95998
96320
|
const finalEntry = slug === entry.slug ? entry : { ...entry, slug };
|
|
95999
|
-
const
|
|
96321
|
+
const rawNext = {
|
|
96000
96322
|
...config2,
|
|
96001
96323
|
units,
|
|
96002
96324
|
unit_prefs: { temp: units, wind: units, precip: units, pressure: units },
|
|
96003
96325
|
default_location: slug,
|
|
96004
|
-
locations: [finalEntry]
|
|
96326
|
+
locations: forced && config2.locations.length > 0 ? [...config2.locations, finalEntry] : [finalEntry]
|
|
96005
96327
|
};
|
|
96006
|
-
|
|
96328
|
+
const next = withRepairedDefault(rawNext);
|
|
96329
|
+
clearActionErrorState();
|
|
96007
96330
|
try {
|
|
96008
96331
|
await saveConfig(next, deps.configPath);
|
|
96009
96332
|
} catch (e) {
|
|
96010
|
-
|
|
96333
|
+
setActionErrorState(errorMessage2(e));
|
|
96011
96334
|
return false;
|
|
96012
96335
|
}
|
|
96013
|
-
|
|
96336
|
+
clearActionErrorState();
|
|
96337
|
+
set2({
|
|
96338
|
+
config: next,
|
|
96339
|
+
activeSlug: slug,
|
|
96340
|
+
onboardingForced: false,
|
|
96341
|
+
onboardingSkipped: false
|
|
96342
|
+
});
|
|
96014
96343
|
await get().loadForecast(slug);
|
|
96015
96344
|
scheduleRefreshLoop();
|
|
96016
96345
|
return true;
|
|
@@ -96026,31 +96355,34 @@ function createStoreInstance(deps = prodDeps()) {
|
|
|
96026
96355
|
set2({ deleteArmedAtMs: null });
|
|
96027
96356
|
const locations = config2.locations;
|
|
96028
96357
|
if (locations.length <= 1) {
|
|
96029
|
-
|
|
96358
|
+
setActionErrorState("cannot delete the only location");
|
|
96030
96359
|
return;
|
|
96031
96360
|
}
|
|
96361
|
+
clearActionErrorState();
|
|
96032
96362
|
const idx = locations.findIndex((loc) => loc.slug === slug);
|
|
96033
96363
|
if (idx === -1)
|
|
96034
96364
|
return;
|
|
96035
96365
|
const remaining = locations.filter((_2, i) => i !== idx);
|
|
96036
|
-
const
|
|
96366
|
+
const rawNext = { ...config2, locations: remaining };
|
|
96037
96367
|
if (config2.default_location === slug) {
|
|
96038
96368
|
const fallback = remaining[0];
|
|
96039
96369
|
if (fallback) {
|
|
96040
|
-
|
|
96370
|
+
rawNext.default_location = fallback.slug;
|
|
96041
96371
|
} else {
|
|
96042
|
-
delete
|
|
96372
|
+
delete rawNext.default_location;
|
|
96043
96373
|
}
|
|
96044
96374
|
}
|
|
96375
|
+
const next = withRepairedDefault(rawNext);
|
|
96045
96376
|
const nextActive = remaining[Math.min(idx, remaining.length - 1)];
|
|
96046
96377
|
set2((s) => ({
|
|
96047
96378
|
config: next,
|
|
96048
96379
|
airQualityBySlug: withoutKey(s.airQualityBySlug, slug)
|
|
96049
96380
|
}));
|
|
96381
|
+
let saveError;
|
|
96050
96382
|
try {
|
|
96051
96383
|
await saveConfig(next, deps.configPath);
|
|
96052
96384
|
} catch (e) {
|
|
96053
|
-
|
|
96385
|
+
saveError = errorMessage2(e);
|
|
96054
96386
|
}
|
|
96055
96387
|
if (slug === get().activeSlug) {
|
|
96056
96388
|
if (nextActive) {
|
|
@@ -96060,19 +96392,23 @@ function createStoreInstance(deps = prodDeps()) {
|
|
|
96060
96392
|
set2({ activeSlug: null, airQuality: null });
|
|
96061
96393
|
}
|
|
96062
96394
|
}
|
|
96395
|
+
if (saveError !== undefined)
|
|
96396
|
+
setActionErrorState(saveError);
|
|
96063
96397
|
},
|
|
96064
96398
|
setDefaultLocation: async (slug) => {
|
|
96065
96399
|
const config2 = get().config;
|
|
96066
96400
|
if (!findLocation(config2, slug))
|
|
96067
96401
|
return;
|
|
96402
|
+
clearActionErrorState();
|
|
96068
96403
|
const next = { ...config2, default_location: slug };
|
|
96069
96404
|
try {
|
|
96070
96405
|
await saveConfig(next, deps.configPath);
|
|
96071
96406
|
} catch (e) {
|
|
96072
|
-
|
|
96407
|
+
setActionErrorState(errorMessage2(e));
|
|
96073
96408
|
return;
|
|
96074
96409
|
}
|
|
96075
96410
|
set2({ config: next });
|
|
96411
|
+
clearActionErrorState();
|
|
96076
96412
|
},
|
|
96077
96413
|
moveLocation: async (slug, delta) => {
|
|
96078
96414
|
const config2 = get().config;
|
|
@@ -96082,23 +96418,34 @@ function createStoreInstance(deps = prodDeps()) {
|
|
|
96082
96418
|
const nextIdx = idx + delta;
|
|
96083
96419
|
if (nextIdx < 0 || nextIdx >= config2.locations.length)
|
|
96084
96420
|
return;
|
|
96421
|
+
clearActionErrorState();
|
|
96085
96422
|
const nextLocations = [...config2.locations];
|
|
96086
96423
|
const [moved] = nextLocations.splice(idx, 1);
|
|
96087
96424
|
if (!moved)
|
|
96088
96425
|
return;
|
|
96089
96426
|
nextLocations.splice(nextIdx, 0, moved);
|
|
96090
|
-
const next = { ...config2, locations: nextLocations };
|
|
96427
|
+
const next = withRepairedDefault({ ...config2, locations: nextLocations });
|
|
96091
96428
|
try {
|
|
96092
96429
|
await saveConfig(next, deps.configPath);
|
|
96093
96430
|
} catch (e) {
|
|
96094
|
-
|
|
96431
|
+
setActionErrorState(errorMessage2(e));
|
|
96095
96432
|
return;
|
|
96096
96433
|
}
|
|
96097
96434
|
set2({ config: next });
|
|
96435
|
+
clearActionErrorState();
|
|
96098
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
|
+
}),
|
|
96099
96445
|
dispose: () => {
|
|
96100
96446
|
disposed = true;
|
|
96101
96447
|
clearRefreshTimer();
|
|
96448
|
+
clearActionErrorTimer();
|
|
96102
96449
|
set2({ airQuality: null, airQualityBySlug: {} });
|
|
96103
96450
|
inFlight.clear();
|
|
96104
96451
|
}
|
|
@@ -96121,6 +96468,8 @@ function LocationsOverlay({ store, width, height }) {
|
|
|
96121
96468
|
const config2 = store((s) => s.config);
|
|
96122
96469
|
const activeSlug = store((s) => s.activeSlug);
|
|
96123
96470
|
const forecastBySlug = store((s) => s.forecastBySlug);
|
|
96471
|
+
const lastActionError = store((s) => s.lastActionError);
|
|
96472
|
+
const lastActionErrorAtMs = store((s) => s.lastActionErrorAtMs);
|
|
96124
96473
|
const [cursor, setCursor] = import_react27.useState(0);
|
|
96125
96474
|
const [offset, setOffset] = import_react27.useState(0);
|
|
96126
96475
|
const [armedSlug, setArmedSlug] = import_react27.useState(null);
|
|
@@ -96199,6 +96548,7 @@ function LocationsOverlay({ store, width, height }) {
|
|
|
96199
96548
|
setCursor((c) => Math.max(0, Math.min(c, count - 2)));
|
|
96200
96549
|
store.getState().deleteLocation(slug);
|
|
96201
96550
|
} else {
|
|
96551
|
+
store.getState().clearActionError();
|
|
96202
96552
|
setArmedSlug(slug);
|
|
96203
96553
|
setArmedAtMs(Date.now());
|
|
96204
96554
|
}
|
|
@@ -96230,6 +96580,7 @@ function LocationsOverlay({ store, width, height }) {
|
|
|
96230
96580
|
const left = Math.max(0, Math.floor((width - boxWidth) / 2));
|
|
96231
96581
|
const top = Math.max(0, Math.floor((height - boxHeight) / 2));
|
|
96232
96582
|
const armedLabel = armed && armedSlug !== null ? locations.find((loc) => loc.slug === armedSlug)?.label ?? null : null;
|
|
96583
|
+
const overlayActionError = isActionErrorActive(lastActionErrorAtMs, Date.now()) ? lastActionError : undefined;
|
|
96233
96584
|
return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
|
|
96234
96585
|
position: "absolute",
|
|
96235
96586
|
left,
|
|
@@ -96253,7 +96604,11 @@ function LocationsOverlay({ store, width, height }) {
|
|
|
96253
96604
|
bg: palette.surface,
|
|
96254
96605
|
children: "─".repeat(innerWidth)
|
|
96255
96606
|
}, undefined, false, undefined, this),
|
|
96256
|
-
|
|
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", {
|
|
96257
96612
|
fg: palette.danger,
|
|
96258
96613
|
bg: palette.surface,
|
|
96259
96614
|
children: truncateCells(`d again deletes ${armedLabel}`, innerWidth)
|
|
@@ -96470,6 +96825,10 @@ function FirstRun({ store, width, height, quit }) {
|
|
|
96470
96825
|
return;
|
|
96471
96826
|
}
|
|
96472
96827
|
if (step === "welcome") {
|
|
96828
|
+
if (key.name === "s" && !key.ctrl && !key.meta && !key.option && !key.shift) {
|
|
96829
|
+
store.getState().skipOnboarding();
|
|
96830
|
+
return;
|
|
96831
|
+
}
|
|
96473
96832
|
if (key.name === "return" || key.name === "enter" || key.name === "escape") {
|
|
96474
96833
|
setStep("units");
|
|
96475
96834
|
}
|
|
@@ -96567,7 +96926,7 @@ function FirstRun({ store, width, height, quit }) {
|
|
|
96567
96926
|
/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
|
|
96568
96927
|
fg: palette.accent,
|
|
96569
96928
|
bg: palette.surface,
|
|
96570
|
-
children: truncateTo2("enter continue ·
|
|
96929
|
+
children: truncateTo2("enter/esc continue · s skip · q quit", innerWidth)
|
|
96571
96930
|
}, undefined, false, undefined, this)
|
|
96572
96931
|
]
|
|
96573
96932
|
}, undefined, true, undefined, this)
|
|
@@ -96614,27 +96973,6 @@ function FirstRun({ store, width, height, quit }) {
|
|
|
96614
96973
|
}, undefined, false, undefined, this);
|
|
96615
96974
|
}
|
|
96616
96975
|
|
|
96617
|
-
// src/theme/detect.ts
|
|
96618
|
-
var FALLBACK_APPEARANCE = { ink: "dark", background: null };
|
|
96619
|
-
async function detectTerminalAppearance(query, timeoutMs = 300) {
|
|
96620
|
-
let timer;
|
|
96621
|
-
try {
|
|
96622
|
-
const colors = await Promise.race([
|
|
96623
|
-
query.getPalette({ timeout: timeoutMs }),
|
|
96624
|
-
new Promise((_2, reject) => {
|
|
96625
|
-
timer = setTimeout(() => reject(new Error("palette query timed out")), timeoutMs);
|
|
96626
|
-
})
|
|
96627
|
-
]);
|
|
96628
|
-
const background = colors?.defaultBackground ?? null;
|
|
96629
|
-
return { ink: isDarkBackground(background) ? "dark" : "light", background };
|
|
96630
|
-
} catch {
|
|
96631
|
-
return FALLBACK_APPEARANCE;
|
|
96632
|
-
} finally {
|
|
96633
|
-
if (timer !== undefined)
|
|
96634
|
-
clearTimeout(timer);
|
|
96635
|
-
}
|
|
96636
|
-
}
|
|
96637
|
-
|
|
96638
96976
|
// src/viewport/useViewport.ts
|
|
96639
96977
|
var import_react32 = __toESM(require_react(), 1);
|
|
96640
96978
|
|
|
@@ -96817,10 +97155,16 @@ var HELP_LINES = [
|
|
|
96817
97155
|
{ text: "l locations j/k focus enter open (lg)" },
|
|
96818
97156
|
{ text: "s default J/K reorder (lg) ↑↓ scroll" },
|
|
96819
97157
|
{ text: "/ search d delete (press twice)", dim: true },
|
|
97158
|
+
{ text: "o re-run setup", dim: true },
|
|
96820
97159
|
{ text: "esc close / clear focus" }
|
|
96821
97160
|
];
|
|
96822
|
-
function HelpOverlay({ width, height, providerLabel }) {
|
|
97161
|
+
function HelpOverlay({ store, width, height, providerLabel }) {
|
|
96823
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
|
+
});
|
|
96824
97168
|
const boxWidth = Math.max(1, Math.min(HELP_BOX_WIDTH, width >= 32 ? width - 2 : width));
|
|
96825
97169
|
const left = Math.max(0, Math.floor((width - boxWidth) / 2));
|
|
96826
97170
|
const top = Math.max(0, Math.floor((height - HELP_BOX_HEIGHT) / 2));
|
|
@@ -96853,12 +97197,12 @@ function HelpOverlay({ width, height, providerLabel }) {
|
|
|
96853
97197
|
}
|
|
96854
97198
|
|
|
96855
97199
|
// src/app/components/Sidebar.tsx
|
|
96856
|
-
var
|
|
97200
|
+
var import_react34 = __toESM(require_react(), 1);
|
|
96857
97201
|
var SIDEBAR_WIDTH = 26;
|
|
96858
97202
|
function truncateTo3(text, width) {
|
|
96859
97203
|
return truncateCells(text, width);
|
|
96860
97204
|
}
|
|
96861
|
-
var SidebarRow =
|
|
97205
|
+
var SidebarRow = import_react34.memo(function SidebarRow2({
|
|
96862
97206
|
slug,
|
|
96863
97207
|
label,
|
|
96864
97208
|
store,
|
|
@@ -96877,7 +97221,7 @@ var SidebarRow = import_react33.memo(function SidebarRow2({
|
|
|
96877
97221
|
children: truncateTo3(`${bullet} ${truncateTo3(label, labelBudget)}${tail}`, SIDEBAR_WIDTH - 3)
|
|
96878
97222
|
}, undefined, false, undefined, this);
|
|
96879
97223
|
});
|
|
96880
|
-
var Sidebar =
|
|
97224
|
+
var Sidebar = import_react34.memo(function Sidebar2({ store, focusedSlug, prefs }) {
|
|
96881
97225
|
const palette = usePalette();
|
|
96882
97226
|
const config2 = store((s) => s.config);
|
|
96883
97227
|
const activeSlug = store((s) => s.activeSlug);
|
|
@@ -96899,17 +97243,23 @@ var Sidebar = import_react33.memo(function Sidebar2({ store, focusedSlug, prefs
|
|
|
96899
97243
|
});
|
|
96900
97244
|
|
|
96901
97245
|
// src/app/components/StatusArea.tsx
|
|
96902
|
-
var
|
|
97246
|
+
var import_react35 = __toESM(require_react(), 1);
|
|
96903
97247
|
var SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
96904
97248
|
var SPINNER_INTERVAL_MS = 120;
|
|
96905
|
-
function Spinner() {
|
|
96906
|
-
const [frame, setFrame] =
|
|
96907
|
-
|
|
97249
|
+
function Spinner({ reducedMotion }) {
|
|
97250
|
+
const [frame, setFrame] = import_react35.useState(0);
|
|
97251
|
+
import_react35.useEffect(() => {
|
|
97252
|
+
if (reducedMotion)
|
|
97253
|
+
return;
|
|
96908
97254
|
const timer = setInterval(() => {
|
|
96909
97255
|
setFrame((f) => (f + 1) % SPINNER_FRAMES.length);
|
|
96910
97256
|
}, SPINNER_INTERVAL_MS);
|
|
96911
97257
|
return () => clearInterval(timer);
|
|
96912
|
-
}, []);
|
|
97258
|
+
}, [reducedMotion]);
|
|
97259
|
+
if (reducedMotion)
|
|
97260
|
+
return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
|
|
97261
|
+
children: "|"
|
|
97262
|
+
}, undefined, false, undefined, this);
|
|
96913
97263
|
return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
|
|
96914
97264
|
children: SPINNER_FRAMES[frame] ?? "|"
|
|
96915
97265
|
}, undefined, false, undefined, this);
|
|
@@ -96919,7 +97269,19 @@ function deleteArmLine(label, width) {
|
|
|
96919
97269
|
const budget = width === undefined ? displayWidth(line) : Math.max(0, width - 1);
|
|
96920
97270
|
return truncateCells(line, budget);
|
|
96921
97271
|
}
|
|
96922
|
-
function
|
|
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
|
+
}) {
|
|
96923
97285
|
const palette = usePalette();
|
|
96924
97286
|
if (deleteArm !== undefined) {
|
|
96925
97287
|
return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
|
|
@@ -96930,12 +97292,23 @@ function StatusArea({ loading, error: error61, stale, deleteArm, width }) {
|
|
|
96930
97292
|
}, undefined, false, undefined, this)
|
|
96931
97293
|
}, undefined, false, undefined, this);
|
|
96932
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
|
+
}
|
|
96933
97304
|
if (loading) {
|
|
96934
97305
|
return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
|
|
96935
97306
|
flexDirection: "row",
|
|
96936
97307
|
gap: 1,
|
|
96937
97308
|
children: [
|
|
96938
|
-
/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Spinner, {
|
|
97309
|
+
/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Spinner, {
|
|
97310
|
+
reducedMotion
|
|
97311
|
+
}, undefined, false, undefined, this),
|
|
96939
97312
|
/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
|
|
96940
97313
|
fg: palette.accent,
|
|
96941
97314
|
children: "syncing…"
|
|
@@ -96975,15 +97348,15 @@ function StatusArea({ loading, error: error61, stale, deleteArm, width }) {
|
|
|
96975
97348
|
}
|
|
96976
97349
|
|
|
96977
97350
|
// src/app/hooks/useNowMs.ts
|
|
96978
|
-
var
|
|
97351
|
+
var import_react36 = __toESM(require_react(), 1);
|
|
96979
97352
|
|
|
96980
97353
|
// src/app/tick.ts
|
|
96981
97354
|
var TICK_INTERVAL_MS = 30000;
|
|
96982
97355
|
|
|
96983
97356
|
// src/app/hooks/useNowMs.ts
|
|
96984
97357
|
function useNowMs(propsNowMs) {
|
|
96985
|
-
const [, setTick] =
|
|
96986
|
-
|
|
97358
|
+
const [, setTick] = import_react36.useState(0);
|
|
97359
|
+
import_react36.useEffect(() => {
|
|
96987
97360
|
if (propsNowMs !== undefined)
|
|
96988
97361
|
return;
|
|
96989
97362
|
const h2 = setInterval(() => setTick((v2) => v2 + 1), TICK_INTERVAL_MS);
|
|
@@ -97010,7 +97383,6 @@ function handleKey(nameOrEvent, api2) {
|
|
|
97010
97383
|
api2.toggleHelp();
|
|
97011
97384
|
return;
|
|
97012
97385
|
}
|
|
97013
|
-
api2.quit();
|
|
97014
97386
|
return;
|
|
97015
97387
|
}
|
|
97016
97388
|
if (api2.searchOpen() || api2.locationsOpen() || api2.helpOpen())
|
|
@@ -97249,7 +97621,7 @@ function XsChips({
|
|
|
97249
97621
|
children: truncateTo4(dailyChips(forecast.daily, prefs.temp), Math.max(0, width))
|
|
97250
97622
|
}, undefined, false, undefined, this);
|
|
97251
97623
|
}
|
|
97252
|
-
var MainContent =
|
|
97624
|
+
var MainContent = import_react38.memo(function MainContent2({
|
|
97253
97625
|
tier,
|
|
97254
97626
|
width,
|
|
97255
97627
|
forecast,
|
|
@@ -97444,17 +97816,20 @@ function App(props = {}) {
|
|
|
97444
97816
|
const locationsOpen = store((s) => s.locationsOpen);
|
|
97445
97817
|
const airQuality = store((s) => s.airQuality);
|
|
97446
97818
|
const lastActionError = store((s) => s.lastActionError);
|
|
97819
|
+
const lastActionErrorAtMs = store((s) => s.lastActionErrorAtMs);
|
|
97447
97820
|
const deleteArmedAtMs = store((s) => s.deleteArmedAtMs);
|
|
97821
|
+
const onboardingSkipped = store((s) => s.onboardingSkipped);
|
|
97822
|
+
const onboardingForced = store((s) => s.onboardingForced);
|
|
97448
97823
|
const viewport = useViewport();
|
|
97449
97824
|
const renderer = useRenderer();
|
|
97450
97825
|
const isDay = entry?.forecast.current.isDay ?? true;
|
|
97451
97826
|
const appearance = props.appearance ?? FALLBACK_APPEARANCE;
|
|
97452
|
-
const palette =
|
|
97453
|
-
const prefs =
|
|
97454
|
-
|
|
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(() => {
|
|
97455
97830
|
store.getState().init(props.initialSlug);
|
|
97456
97831
|
}, [store, props.initialSlug]);
|
|
97457
|
-
|
|
97832
|
+
import_react38.useEffect(() => {
|
|
97458
97833
|
return () => {
|
|
97459
97834
|
store.getState().dispose();
|
|
97460
97835
|
};
|
|
@@ -97466,17 +97841,18 @@ function App(props = {}) {
|
|
|
97466
97841
|
const nowUtc = props.nowUtc ?? new Date(nowMs).toISOString();
|
|
97467
97842
|
const tier = viewport.tier;
|
|
97468
97843
|
const forecast = entry?.forecast;
|
|
97469
|
-
const onboardingOpen = initStatus === "ready" && config2.locations.length === 0;
|
|
97844
|
+
const onboardingOpen = initStatus === "ready" && (config2.locations.length === 0 && !onboardingSkipped || onboardingForced);
|
|
97470
97845
|
const deleteArmed = isDeleteArmed(deleteArmedAtMs, nowMs);
|
|
97471
|
-
const
|
|
97472
|
-
|
|
97846
|
+
const actionError = isActionErrorActive(lastActionErrorAtMs, Date.now()) ? lastActionError : undefined;
|
|
97847
|
+
const [focusedSlug, setFocusedSlug] = import_react38.useState(null);
|
|
97848
|
+
import_react38.useEffect(() => {
|
|
97473
97849
|
if (tier !== "lg")
|
|
97474
97850
|
setFocusedSlug(null);
|
|
97475
97851
|
else if (focusedSlug !== null && !config2.locations.some((loc) => loc.slug === focusedSlug)) {
|
|
97476
97852
|
setFocusedSlug(null);
|
|
97477
97853
|
}
|
|
97478
97854
|
}, [config2.locations, focusedSlug, tier]);
|
|
97479
|
-
const api2 =
|
|
97855
|
+
const api2 = import_react38.useMemo(() => ({
|
|
97480
97856
|
quit,
|
|
97481
97857
|
activeSlug: () => store.getState().activeSlug,
|
|
97482
97858
|
refresh: (slug) => void store.getState().refresh(slug),
|
|
@@ -97486,7 +97862,8 @@ function App(props = {}) {
|
|
|
97486
97862
|
toggleHelp: () => store.getState().toggleHelp(),
|
|
97487
97863
|
searchOpen: () => {
|
|
97488
97864
|
const state = store.getState();
|
|
97489
|
-
|
|
97865
|
+
const onboardingActive = state.initStatus === "ready" && (state.config.locations.length === 0 && !state.onboardingSkipped || state.onboardingForced);
|
|
97866
|
+
return state.overlayOpen || onboardingActive;
|
|
97490
97867
|
},
|
|
97491
97868
|
openSearch: () => store.getState().setOverlayOpen(true),
|
|
97492
97869
|
locationsOpen: () => store.getState().locationsOpen,
|
|
@@ -97529,7 +97906,9 @@ function App(props = {}) {
|
|
|
97529
97906
|
error: error61,
|
|
97530
97907
|
stale: staleBadge,
|
|
97531
97908
|
deleteArm: deleteArmed ? { label } : undefined,
|
|
97532
|
-
|
|
97909
|
+
actionError,
|
|
97910
|
+
width: tier === "lg" ? viewport.width - SIDEBAR_WIDTH : viewport.width,
|
|
97911
|
+
reducedMotion: config2.reduced_motion
|
|
97533
97912
|
}, undefined, false, undefined, this);
|
|
97534
97913
|
const footerColumnWidth = tier === "lg" ? viewport.width - SIDEBAR_WIDTH : viewport.width;
|
|
97535
97914
|
const footer = /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Footer, {
|
|
@@ -97545,7 +97924,7 @@ function App(props = {}) {
|
|
|
97545
97924
|
panels: config2.panels,
|
|
97546
97925
|
nowUtc
|
|
97547
97926
|
}) : null;
|
|
97548
|
-
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;
|
|
97549
97928
|
const statusBlock = statusRows > 0 ? statusRows + 1 : 0;
|
|
97550
97929
|
const showOverflowHint = overflowEstimate !== null && viewport.height < overflowEstimate + MAIN_CHROME_ROWS + statusBlock;
|
|
97551
97930
|
const mainScrollHeight = Math.max(1, viewport.height - MAIN_CHROME_ROWS - statusBlock - (showOverflowHint ? OVERFLOW_HINT_ROWS : 0));
|
|
@@ -97659,6 +98038,7 @@ function App(props = {}) {
|
|
|
97659
98038
|
children: [
|
|
97660
98039
|
body,
|
|
97661
98040
|
helpOpen ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(HelpOverlay, {
|
|
98041
|
+
store,
|
|
97662
98042
|
width: viewport.width,
|
|
97663
98043
|
height: viewport.height,
|
|
97664
98044
|
providerLabel: config2.provider === "nws" ? "api.weather.gov" : "open-meteo.com"
|
|
@@ -97679,6 +98059,34 @@ function App(props = {}) {
|
|
|
97679
98059
|
}, undefined, false, undefined, this);
|
|
97680
98060
|
}
|
|
97681
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
|
+
|
|
97682
98090
|
// src/app/oneline.ts
|
|
97683
98091
|
var SEPARATOR = " · ";
|
|
97684
98092
|
var ARROWS_8 = ["↑", "↗", "→", "↘", "↓", "↙", "←", "↖"];
|
|
@@ -97821,6 +98229,18 @@ function parseInterval(raw) {
|
|
|
97821
98229
|
}
|
|
97822
98230
|
return value;
|
|
97823
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
|
+
}
|
|
97824
98244
|
function parseArgs(argv) {
|
|
97825
98245
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
97826
98246
|
if (argv.length !== 1)
|
|
@@ -97910,6 +98330,22 @@ function parseArgs(argv) {
|
|
|
97910
98330
|
return args;
|
|
97911
98331
|
}
|
|
97912
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
|
+
|
|
97913
98349
|
// src/index.tsx
|
|
97914
98350
|
function stderr(message) {
|
|
97915
98351
|
process.stderr.write(`${message}
|
|
@@ -97948,6 +98384,7 @@ function resolveLocationForCli(args, config2) {
|
|
|
97948
98384
|
}
|
|
97949
98385
|
async function runOneLine(args) {
|
|
97950
98386
|
const config2 = await loadConfig();
|
|
98387
|
+
warnStaleDefault(config2, stderr);
|
|
97951
98388
|
const resolved = resolveLocationForCli(args, config2);
|
|
97952
98389
|
if ("error" in resolved) {
|
|
97953
98390
|
const hint = config2.locations.length === 0 ? "; run tuiweather to set one up" : "";
|
|
@@ -97970,6 +98407,7 @@ async function runOneLine(args) {
|
|
|
97970
98407
|
}
|
|
97971
98408
|
async function runTui(locationArg) {
|
|
97972
98409
|
const config2 = await loadConfig();
|
|
98410
|
+
warnStaleDefault(config2, stderr);
|
|
97973
98411
|
let initialSlug;
|
|
97974
98412
|
if (locationArg !== null || config2.locations.length > 0) {
|
|
97975
98413
|
const resolved = resolveSlugFromConfig(config2, locationArg);
|
|
@@ -97980,17 +98418,25 @@ async function runTui(locationArg) {
|
|
|
97980
98418
|
}
|
|
97981
98419
|
initialSlug = resolved.slug;
|
|
97982
98420
|
}
|
|
98421
|
+
if (!await probeFfiAvailable()) {
|
|
98422
|
+
stderr(formatFfiUnavailableMessage(process.versions.node));
|
|
98423
|
+
return 1;
|
|
98424
|
+
}
|
|
97983
98425
|
const renderer = await createCliRenderer({ exitOnCtrlC: true });
|
|
97984
|
-
const appearance = await detectTerminalAppearance(renderer);
|
|
97985
98426
|
renderer.on("destroy", () => appStore.getState().dispose());
|
|
97986
|
-
|
|
98427
|
+
const appearancePromise = resolveTerminalAppearance(config2.ink, renderer);
|
|
98428
|
+
createRoot(renderer).render(/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(AppearanceApp, {
|
|
97987
98429
|
initialSlug,
|
|
97988
|
-
|
|
98430
|
+
appearancePromise
|
|
97989
98431
|
}, undefined, false, undefined, this));
|
|
98432
|
+
appearancePromise.catch(() => {
|
|
98433
|
+
return;
|
|
98434
|
+
});
|
|
97990
98435
|
return 0;
|
|
97991
98436
|
}
|
|
97992
98437
|
async function runWatchCli(args) {
|
|
97993
98438
|
const config2 = await loadConfig();
|
|
98439
|
+
warnStaleDefault(config2, stderr);
|
|
97994
98440
|
const resolved = resolveLocationForCli(args, config2);
|
|
97995
98441
|
if ("error" in resolved) {
|
|
97996
98442
|
const hint = config2.locations.length === 0 ? "; run tuiweather to set one up" : "";
|