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/README.md +33 -13
- package/bin/tuiweather.js +39 -2
- package/dist/index.js +748 -289
- package/package.json +4 -3
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",
|
|
@@ -94662,7 +94795,7 @@ var package_default2 = {
|
|
|
94662
94795
|
access: "public"
|
|
94663
94796
|
},
|
|
94664
94797
|
engines: {
|
|
94665
|
-
node: ">=
|
|
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
|
-
|
|
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) {
|
|
@@ -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.
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
96259
|
+
setActionErrorState(errorMessage2(e));
|
|
95950
96260
|
});
|
|
95951
96261
|
},
|
|
95952
|
-
toggleHelp: () =>
|
|
95953
|
-
|
|
95954
|
-
|
|
95955
|
-
|
|
95956
|
-
|
|
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
|
|
96293
|
+
const rawNext = {
|
|
95965
96294
|
...config2,
|
|
95966
96295
|
locations: [...config2.locations, finalEntry]
|
|
95967
96296
|
};
|
|
95968
96297
|
if (isFirstLocation && config2.default_location === undefined) {
|
|
95969
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95983
|
-
|
|
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
|
|
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
|
-
|
|
96328
|
+
const next = withRepairedDefault(rawNext);
|
|
96329
|
+
clearActionErrorState();
|
|
95996
96330
|
try {
|
|
95997
96331
|
await saveConfig(next, deps.configPath);
|
|
95998
96332
|
} catch (e) {
|
|
95999
|
-
|
|
96333
|
+
setActionErrorState(errorMessage2(e));
|
|
96000
96334
|
return false;
|
|
96001
96335
|
}
|
|
96002
|
-
|
|
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
|
-
|
|
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
|
|
96366
|
+
const rawNext = { ...config2, locations: remaining };
|
|
96026
96367
|
if (config2.default_location === slug) {
|
|
96027
96368
|
const fallback = remaining[0];
|
|
96028
96369
|
if (fallback) {
|
|
96029
|
-
|
|
96370
|
+
rawNext.default_location = fallback.slug;
|
|
96030
96371
|
} else {
|
|
96031
|
-
delete
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ·
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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] =
|
|
96894
|
-
|
|
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
|
|
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, {
|
|
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
|
|
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] =
|
|
96973
|
-
|
|
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 =
|
|
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 =
|
|
97440
|
-
const prefs =
|
|
97441
|
-
|
|
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
|
-
|
|
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
|
|
97459
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98427
|
+
const appearancePromise = resolveTerminalAppearance(config2.ink, renderer);
|
|
98428
|
+
createRoot(renderer).render(/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(AppearanceApp, {
|
|
97974
98429
|
initialSlug,
|
|
97975
|
-
|
|
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" : "";
|