@bobfrankston/rmfmail 1.0.667 → 1.0.669
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/TODO.md +2 -0
- package/bin/mailx.js +114 -111
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +117 -95
- package/client/app.bundle.js +119 -25
- package/client/app.bundle.js.map +2 -2
- package/client/components/alarms.js +106 -29
- package/client/components/alarms.js.map +1 -1
- package/client/components/alarms.ts +109 -31
- package/client/components/calendar-sidebar.js +19 -12
- package/client/components/calendar-sidebar.js.map +1 -1
- package/client/components/calendar-sidebar.ts +21 -12
- package/client/components/message-viewer.js +24 -1
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +23 -1
- package/client/compose/compose.bundle.js +4 -0
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/index.html +3 -0
- package/client/lib/api-client.js +7 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +8 -1
- package/package.json +3 -3
- package/packages/mailx-host/index.d.ts +5 -0
- package/packages/mailx-host/index.d.ts.map +1 -1
- package/packages/mailx-host/index.js +5 -0
- package/packages/mailx-host/index.js.map +1 -1
- package/packages/mailx-host/index.ts +5 -0
- package/packages/mailx-host/package.json +1 -1
- package/packages/mailx-service/index.d.ts +32 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +181 -40
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +168 -41
- package/packages/mailx-service/jsonrpc.js +2 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +2 -0
- package/packages/mailx-settings/index.d.ts +6 -1
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +24 -2
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +26 -4
package/bin/mailx.ts
CHANGED
|
@@ -23,8 +23,9 @@ import fs from "node:fs";
|
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import os from "node:os";
|
|
25
25
|
import net from "node:net";
|
|
26
|
+
import { execSync } from "node:child_process";
|
|
26
27
|
import { ports } from "@bobfrankston/miscinfo";
|
|
27
|
-
import { showMessageBox, showService, setAppName, setAppIcon } from "@bobfrankston/mailx-host";
|
|
28
|
+
import { showMessageBox, showMessageBoxEx, showService, setAppName, setAppIcon } from "@bobfrankston/mailx-host";
|
|
28
29
|
|
|
29
30
|
setAppName("rmfmail");
|
|
30
31
|
// Prefer the .ico (Windows Explorer / taskbar-pin shortcut uses the embedded
|
|
@@ -225,7 +226,16 @@ const __selfVersion: string = (() => {
|
|
|
225
226
|
})();
|
|
226
227
|
const __instanceFile = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".rmfmail", "instance.json");
|
|
227
228
|
|
|
228
|
-
|
|
229
|
+
interface InstanceFile {
|
|
230
|
+
pid: number;
|
|
231
|
+
version: string;
|
|
232
|
+
startedAt: number;
|
|
233
|
+
/** PIDs of children spawned by this daemon: msger WebView host,
|
|
234
|
+
* reminder popups, anything else we launched. Tracked so `-kill`
|
|
235
|
+
* can SIGTERM them too without resorting to name-based sweeps. */
|
|
236
|
+
childPids?: number[];
|
|
237
|
+
}
|
|
238
|
+
function readInstanceFile(): InstanceFile | null {
|
|
229
239
|
try {
|
|
230
240
|
const raw = fs.readFileSync(__instanceFile, "utf-8");
|
|
231
241
|
const inst = JSON.parse(raw);
|
|
@@ -233,12 +243,29 @@ function readInstanceFile(): { pid: number; version: string; startedAt: number }
|
|
|
233
243
|
} catch { /* missing or unreadable — treated as no instance */ }
|
|
234
244
|
return null;
|
|
235
245
|
}
|
|
236
|
-
function writeInstanceFile(pid: number): void {
|
|
246
|
+
function writeInstanceFile(pid: number, childPids: number[] = []): void {
|
|
237
247
|
try {
|
|
238
248
|
fs.mkdirSync(path.dirname(__instanceFile), { recursive: true });
|
|
239
|
-
|
|
249
|
+
const payload: InstanceFile = { pid, version: __selfVersion, startedAt: Date.now(), childPids };
|
|
250
|
+
fs.writeFileSync(__instanceFile, JSON.stringify(payload, null, 2));
|
|
240
251
|
} catch { /* non-fatal */ }
|
|
241
252
|
}
|
|
253
|
+
/** Add a child PID to the daemon's instance.json so a later `-kill`
|
|
254
|
+
* knows to reap it. Caller is expected to remove it (via
|
|
255
|
+
* removeChildPid) when the child exits naturally. */
|
|
256
|
+
function addChildPid(pid: number): void {
|
|
257
|
+
const inst = readInstanceFile();
|
|
258
|
+
if (!inst) return;
|
|
259
|
+
const set = new Set(inst.childPids || []);
|
|
260
|
+
set.add(pid);
|
|
261
|
+
writeInstanceFile(inst.pid, [...set]);
|
|
262
|
+
}
|
|
263
|
+
function removeChildPid(pid: number): void {
|
|
264
|
+
const inst = readInstanceFile();
|
|
265
|
+
if (!inst) return;
|
|
266
|
+
const next = (inst.childPids || []).filter(p => p !== pid);
|
|
267
|
+
writeInstanceFile(inst.pid, next);
|
|
268
|
+
}
|
|
242
269
|
function clearInstanceFile(): void {
|
|
243
270
|
try { fs.unlinkSync(__instanceFile); } catch { /* ignore */ }
|
|
244
271
|
}
|
|
@@ -253,16 +280,17 @@ function pidAlive(pid: number): boolean {
|
|
|
253
280
|
* with "already running". Verify it's actually us. Non-Windows: fall back
|
|
254
281
|
* to pidAlive — POSIX PID reuse is rare enough that we don't bother. */
|
|
255
282
|
function pidIsMailx(pid: number): boolean {
|
|
256
|
-
|
|
257
|
-
if
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
283
|
+
// Cross-platform: trust instance.json as the source of identity.
|
|
284
|
+
// We registered this PID at startup; if it's still alive, treat it
|
|
285
|
+
// as ours. Earlier Windows-only check used PowerShell + CimInstance
|
|
286
|
+
// to grep the command line, which violated the cross-platform rule
|
|
287
|
+
// (see memory: feedback_cross_platform_strong_rule). The downside is
|
|
288
|
+
// a brief PID-reuse window after a crash: if Windows recycles the
|
|
289
|
+
// PID between crash and the next launch, our SIGTERM hits an
|
|
290
|
+
// unrelated process. Mitigation: daemon clears instance.json on
|
|
291
|
+
// graceful exit; stale-instance file only persists across hard
|
|
292
|
+
// crashes, which is rare.
|
|
293
|
+
return pidAlive(pid);
|
|
266
294
|
}
|
|
267
295
|
|
|
268
296
|
// Version-mismatch upgrade: if a daemon from an older version is running when
|
|
@@ -291,23 +319,17 @@ if (!isDaemon && !__isCommandInvocation && !__keepOthers) {
|
|
|
291
319
|
if (inst && pidIsMailx(inst.pid) && inst.pid !== myPid) {
|
|
292
320
|
try { process.kill(inst.pid, "SIGTERM"); killedPids.push(inst.pid); } catch { /* */ }
|
|
293
321
|
}
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const pid = Number(line);
|
|
306
|
-
if (pid === myPid) continue;
|
|
307
|
-
try { process.kill(pid, "SIGTERM"); if (!killedPids.includes(pid)) killedPids.push(pid); } catch { /* */ }
|
|
308
|
-
}
|
|
309
|
-
} catch { /* sweep failed — proceed; instance.json kill above usually covers it */ }
|
|
310
|
-
}
|
|
322
|
+
// Replace-on-launch trusts the registered PID in instance.json. The
|
|
323
|
+
// graceful-shutdown handler in the prior daemon (this same file, below)
|
|
324
|
+
// is responsible for closing its own child msger/popup processes via
|
|
325
|
+
// handle.close() — name-based sweeps over `node.exe` / `msgernative.exe`
|
|
326
|
+
// / per-app exes are fragile and ALSO not portable (taskkill is
|
|
327
|
+
// Windows-only). If a daemon crashed without graceful shutdown, the
|
|
328
|
+
// instance file gets cleared next launch via the stale-PID check and
|
|
329
|
+
// the orphan stays until the user resolves it manually — preferable to
|
|
330
|
+
// a sweep that risks killing an unrelated `mailx` / `rmfmail` process
|
|
331
|
+
// owned by a different user (e.g., a developer running it from a
|
|
332
|
+
// sibling worktree).
|
|
311
333
|
// Brief wait + SIGKILL anything that didn't exit gracefully.
|
|
312
334
|
if (killedPids.length) {
|
|
313
335
|
const deadline = Date.now() + 2000;
|
|
@@ -371,7 +393,6 @@ function log(...msg: any[]): void { if (verbose) console.log("[rmfmail]", ...msg
|
|
|
371
393
|
function isElevated(): boolean {
|
|
372
394
|
try {
|
|
373
395
|
if (process.platform === "win32") {
|
|
374
|
-
const { execSync } = require("node:child_process");
|
|
375
396
|
execSync("net session >nul 2>&1", { stdio: "ignore", windowsHide: true });
|
|
376
397
|
return true;
|
|
377
398
|
}
|
|
@@ -410,60 +431,41 @@ if (hasFlag("kill")) {
|
|
|
410
431
|
const { execSync } = await import("node:child_process");
|
|
411
432
|
let killed = 0;
|
|
412
433
|
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
try { execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" }); console.log(`Killed PID ${pid} (port ${PORT})`); killed++; } catch { /* */ }
|
|
427
|
-
}
|
|
428
|
-
} catch { /* no process on port */ }
|
|
429
|
-
|
|
430
|
-
// Kill any node.exe running rmfmail (server or IPC service). Match
|
|
431
|
-
// the legacy `mailx` path too — source tree dir is still
|
|
432
|
-
// `Y:\dev\email\mailx\` and per-app exes from older installs are
|
|
433
|
-
// under `%LOCALAPPDATA%\mailx\` until those get cleaned up.
|
|
434
|
-
try {
|
|
435
|
-
const ps = execSync(
|
|
436
|
-
// Match both `\` and `/` path separators — node CommandLine
|
|
437
|
-
// can show either depending on how it was invoked. The
|
|
438
|
-
// earlier `\\packages` / `\\bin` variants missed forward-
|
|
439
|
-
// slash paths and left a daemon running. The character
|
|
440
|
-
// class [\\\\/] covers both. `(?:...)` keeps the regex
|
|
441
|
-
// unanchored — substring match anywhere in CommandLine.
|
|
442
|
-
`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name='node.exe'\\" | Where-Object { $_.CommandLine -match '(?:rmfmail|mailx)[\\\\/](?:bin|packages)|(?:rmfmail|mailx)-server' } | Select-Object -ExpandProperty ProcessId"`,
|
|
443
|
-
{ encoding: "utf-8" }
|
|
444
|
-
).trim();
|
|
445
|
-
for (const pid of ps.split("\n").map(s => s.trim()).filter(s => /^\d+$/.test(s))) {
|
|
446
|
-
try { execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" }); console.log(`Killed PID ${pid} (rmfmail node process)`); killed++; } catch { /* */ }
|
|
447
|
-
}
|
|
448
|
-
} catch { /* */ }
|
|
449
|
-
|
|
450
|
-
// Kill orphaned msgernative.exe windows. When the node process dies
|
|
451
|
-
// without cascade-killing its WebView child (old crash, forced
|
|
452
|
-
// taskkill, etc.), the msgernative.exe stays on screen and looks
|
|
453
|
-
// like a live rmfmail. rmfmail -kill should leave no trace.
|
|
454
|
-
// Scoped to exes launched for rmfmail by filtering CommandLine —
|
|
455
|
-
// don't touch msger windows started by other apps (msga, bbs, etc.).
|
|
434
|
+
// Cross-platform kill via instance.json. Daemon writes its PID +
|
|
435
|
+
// child PIDs (msger WebView, popups) at startup and on each spawn;
|
|
436
|
+
// -kill reads them and SIGTERMs each. No name-based sweeps, no
|
|
437
|
+
// PowerShell, no taskkill — those were Windows-only AND fragile
|
|
438
|
+
// (could match unrelated processes). See memory:
|
|
439
|
+
// feedback_cross_platform_strong_rule.
|
|
440
|
+
const inst = readInstanceFile();
|
|
441
|
+
const targets: number[] = [];
|
|
442
|
+
if (inst && pidAlive(inst.pid)) targets.push(inst.pid);
|
|
443
|
+
for (const cp of (inst?.childPids || [])) {
|
|
444
|
+
if (pidAlive(cp)) targets.push(cp);
|
|
445
|
+
}
|
|
446
|
+
for (const pid of targets) {
|
|
456
447
|
try {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
448
|
+
process.kill(pid, "SIGTERM");
|
|
449
|
+
console.log(`SIGTERM → PID ${pid}`);
|
|
450
|
+
killed++;
|
|
451
|
+
} catch (e: any) {
|
|
452
|
+
// ESRCH: stale PID, already dead. Anything else is unexpected.
|
|
453
|
+
if (e?.code !== "ESRCH") console.error(` kill ${pid}: ${e?.message || e}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Wait briefly for graceful shutdown, then SIGKILL anything still alive.
|
|
457
|
+
if (targets.length) {
|
|
458
|
+
const deadline = Date.now() + 2000;
|
|
459
|
+
while (Date.now() < deadline && targets.some(p => pidAlive(p))) {
|
|
460
|
+
const sab = new SharedArrayBuffer(4);
|
|
461
|
+
Atomics.wait(new Int32Array(sab), 0, 0, 100);
|
|
462
|
+
}
|
|
463
|
+
for (const pid of targets) {
|
|
464
|
+
if (pidAlive(pid)) {
|
|
465
|
+
try { process.kill(pid, "SIGKILL"); console.log(`SIGKILL → PID ${pid}`); }
|
|
466
|
+
catch { /* */ }
|
|
463
467
|
}
|
|
464
|
-
}
|
|
465
|
-
} else {
|
|
466
|
-
try { execSync(`fuser -k ${PORT}/tcp`, { stdio: "pipe" }); console.log(`Killed process on port ${PORT}`); killed++; } catch { /* */ }
|
|
468
|
+
}
|
|
467
469
|
}
|
|
468
470
|
|
|
469
471
|
// Clean up stale SQLite WAL/SHM files
|
|
@@ -1308,9 +1310,22 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
|
|
|
1308
1310
|
// Inject the popup function so MailxService.showReminderPopup can spawn
|
|
1309
1311
|
// an OS-level always-on-top window via mailx-host. Kept as injection
|
|
1310
1312
|
// (not import) so mailx-service stays host-agnostic.
|
|
1313
|
+
//
|
|
1314
|
+
// Uses showMessageBoxEx so we get a MessageBoxHandle back with
|
|
1315
|
+
// `pid` and `close()`. The pid goes into instance.json's childPids
|
|
1316
|
+
// for the lifetime of the popup, so a `rmfmail -kill` from another
|
|
1317
|
+
// shell can SIGTERM it without name-based sweeps (cross-platform —
|
|
1318
|
+
// see memory: feedback_cross_platform_strong_rule). Remove on
|
|
1319
|
+
// resolution so the file doesn't accumulate stale PIDs.
|
|
1311
1320
|
svc.setPopupFn(async (opts: any) => {
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1321
|
+
const h = showMessageBoxEx(opts);
|
|
1322
|
+
if (typeof h.pid === "number") addChildPid(h.pid);
|
|
1323
|
+
try {
|
|
1324
|
+
const r = await h.result;
|
|
1325
|
+
return { button: r.button, closed: r.closed, dismissed: r.dismissed, form: (r as any).form };
|
|
1326
|
+
} finally {
|
|
1327
|
+
if (typeof h.pid === "number") removeChildPid(h.pid);
|
|
1328
|
+
}
|
|
1314
1329
|
});
|
|
1315
1330
|
|
|
1316
1331
|
// Open msger in service mode — custom protocol serves files from client dir
|
|
@@ -1378,17 +1393,24 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
|
|
|
1378
1393
|
escapeCloses: false,
|
|
1379
1394
|
});
|
|
1380
1395
|
|
|
1381
|
-
// Register ourselves as the live instance so subsequent `
|
|
1382
|
-
// can detect version-mismatch and upgrade us (see top of
|
|
1383
|
-
// any of: SIGINT, SIGTERM, normal exit.
|
|
1396
|
+
// Register ourselves as the live instance so subsequent `rmfmail`
|
|
1397
|
+
// invocations can detect version-mismatch and upgrade us (see top of
|
|
1398
|
+
// file). Clear on any of: SIGINT, SIGTERM, normal exit.
|
|
1384
1399
|
//
|
|
1385
1400
|
// Critical: the SIGTERM handler must *close the WebView child process*
|
|
1386
|
-
// (handle.close() →
|
|
1387
|
-
// this, the auto-upgrade leaves the old WebView orphaned on
|
|
1388
|
-
// the user sees an apparently frozen "old
|
|
1389
|
-
// trying to spawn a second one. Cascade-
|
|
1390
|
-
// version-mismatch auto-upgrade
|
|
1391
|
-
|
|
1401
|
+
// (handle.close() → SIGTERM to the msger child) before Node exits.
|
|
1402
|
+
// Without this, the auto-upgrade leaves the old WebView orphaned on
|
|
1403
|
+
// screen and the user sees an apparently frozen "old rmfmail" while
|
|
1404
|
+
// the new Node is trying to spawn a second one. Cascade-closing the
|
|
1405
|
+
// child makes the version-mismatch auto-upgrade transparent.
|
|
1406
|
+
//
|
|
1407
|
+
// Child PIDs (the msger WebView host + any reminder popups) go into
|
|
1408
|
+
// instance.json so that a future `rmfmail -kill` can SIGTERM them
|
|
1409
|
+
// cross-platform without resorting to name-based process sweeps.
|
|
1410
|
+
const initialChildPids: number[] = [];
|
|
1411
|
+
const handleChild = (handle as any)?.pid ?? (handle as any)?.child?.pid;
|
|
1412
|
+
if (typeof handleChild === "number") initialChildPids.push(handleChild);
|
|
1413
|
+
writeInstanceFile(process.pid, initialChildPids);
|
|
1392
1414
|
const __cleanupInstance = () => {
|
|
1393
1415
|
// Only clear if WE are still the registered instance. Prevents the
|
|
1394
1416
|
// restart-daemon sequence (clear → spawn → new daemon writes its
|
package/client/app.bundle.js
CHANGED
|
@@ -66,6 +66,7 @@ __export(api_client_exports, {
|
|
|
66
66
|
moveMessages: () => moveMessages,
|
|
67
67
|
onEvent: () => onEvent,
|
|
68
68
|
onWsEvent: () => onWsEvent,
|
|
69
|
+
openInTextEditor: () => openInTextEditor,
|
|
69
70
|
openInWord: () => openInWord,
|
|
70
71
|
openLocalPath: () => openLocalPath,
|
|
71
72
|
readConfigHelp: () => readConfigHelp,
|
|
@@ -287,6 +288,9 @@ function addToDenylist(email) {
|
|
|
287
288
|
function openLocalPath(which) {
|
|
288
289
|
return ipc().openLocalPath(which);
|
|
289
290
|
}
|
|
291
|
+
function openInTextEditor(path) {
|
|
292
|
+
return ipc().openInTextEditor?.(path) ?? Promise.resolve({ ok: false, opener: "none", reason: "no host" });
|
|
293
|
+
}
|
|
290
294
|
function allowRemoteContent(type, value) {
|
|
291
295
|
return ipc().allowRemoteContent(type, value);
|
|
292
296
|
}
|
|
@@ -1224,7 +1228,8 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
|
|
|
1224
1228
|
if (srcBtn2) {
|
|
1225
1229
|
if (msg.emlPath) {
|
|
1226
1230
|
srcBtn2.hidden = false;
|
|
1227
|
-
srcBtn2.title = msg.emlPath
|
|
1231
|
+
srcBtn2.title = `${msg.emlPath}
|
|
1232
|
+
(click to copy path, double-click to open in default text editor)`;
|
|
1228
1233
|
srcBtn2.onclick = () => {
|
|
1229
1234
|
const path = currentMessage?.emlPath;
|
|
1230
1235
|
if (!path)
|
|
@@ -1237,9 +1242,24 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
|
|
|
1237
1242
|
prompt("EML file path:", path);
|
|
1238
1243
|
});
|
|
1239
1244
|
};
|
|
1245
|
+
srcBtn2.ondblclick = (e) => {
|
|
1246
|
+
e.preventDefault();
|
|
1247
|
+
const path = currentMessage?.emlPath;
|
|
1248
|
+
if (!path)
|
|
1249
|
+
return;
|
|
1250
|
+
Promise.resolve().then(() => (init_api_client(), api_client_exports)).then((m) => m.openInTextEditor(path)).then((r) => {
|
|
1251
|
+
const status = document.getElementById("status-sync");
|
|
1252
|
+
if (status) {
|
|
1253
|
+
status.textContent = r?.ok ? `Opened in ${r.opener}: ${path}` : `Open failed: ${r?.reason || "unknown"}`;
|
|
1254
|
+
}
|
|
1255
|
+
}).catch((e2) => {
|
|
1256
|
+
console.error("[mv] openInTextEditor failed:", e2);
|
|
1257
|
+
});
|
|
1258
|
+
};
|
|
1240
1259
|
} else {
|
|
1241
1260
|
srcBtn2.hidden = true;
|
|
1242
1261
|
srcBtn2.onclick = null;
|
|
1262
|
+
srcBtn2.ondblclick = null;
|
|
1243
1263
|
}
|
|
1244
1264
|
}
|
|
1245
1265
|
const editBtn2 = document.getElementById("mv-edit-draft");
|
|
@@ -3913,24 +3933,28 @@ function initCalendarSidebar() {
|
|
|
3913
3933
|
refresh();
|
|
3914
3934
|
});
|
|
3915
3935
|
}
|
|
3916
|
-
const
|
|
3917
|
-
|
|
3918
|
-
|
|
3936
|
+
const wireHolidayCheckbox = (cbId, settingsKey) => {
|
|
3937
|
+
const cb = document.getElementById(cbId);
|
|
3938
|
+
if (!cb || cb.__wired)
|
|
3939
|
+
return;
|
|
3940
|
+
cb.__wired = true;
|
|
3919
3941
|
getSettings().then((s) => {
|
|
3920
|
-
|
|
3942
|
+
cb.checked = !!s?.calendar?.[settingsKey];
|
|
3921
3943
|
}).catch(() => {
|
|
3922
3944
|
});
|
|
3923
|
-
|
|
3945
|
+
cb.addEventListener("change", async () => {
|
|
3924
3946
|
try {
|
|
3925
3947
|
const s = await getSettings();
|
|
3926
|
-
s.calendar = { ...s.calendar || {},
|
|
3948
|
+
s.calendar = { ...s.calendar || {}, [settingsKey]: cb.checked };
|
|
3927
3949
|
await saveSettings(s);
|
|
3928
3950
|
} catch (e) {
|
|
3929
|
-
console.error(
|
|
3951
|
+
console.error(`[cal] ${settingsKey} save failed:`, e);
|
|
3930
3952
|
}
|
|
3931
3953
|
refresh();
|
|
3932
3954
|
});
|
|
3933
|
-
}
|
|
3955
|
+
};
|
|
3956
|
+
wireHolidayCheckbox("cal-side-show-holidays", "showHolidays");
|
|
3957
|
+
wireHolidayCheckbox("cal-side-show-jewish-holidays", "showJewishHolidays");
|
|
3934
3958
|
const horizonInput = document.getElementById("cal-side-horizon");
|
|
3935
3959
|
if (horizonInput && !horizonInput.__wired) {
|
|
3936
3960
|
horizonInput.__wired = true;
|
|
@@ -4369,41 +4393,100 @@ async function firePopupForItem(item) {
|
|
|
4369
4393
|
if (openPopups.has(item.uuid))
|
|
4370
4394
|
return;
|
|
4371
4395
|
openPopups.add(item.uuid);
|
|
4372
|
-
const buttons = ["
|
|
4396
|
+
const buttons = ["snooze", "Dismiss"];
|
|
4373
4397
|
if (item.htmlLink)
|
|
4374
4398
|
buttons.push("Open");
|
|
4375
4399
|
if (item.kind === "calendar")
|
|
4376
4400
|
buttons.push("Delete");
|
|
4377
4401
|
const icon = item.kind === "calendar" ? "\u{1F4C5}" : "\u2611";
|
|
4378
4402
|
const kindLabel = item.kind === "calendar" ? "Calendar event" : "Task due";
|
|
4379
|
-
const
|
|
4403
|
+
const presets = [
|
|
4404
|
+
{ label: "5m", minutes: 5 },
|
|
4405
|
+
{ label: "15m", minutes: 15 },
|
|
4406
|
+
{ label: "30m", minutes: 30 },
|
|
4407
|
+
{ label: "1h", minutes: 60 },
|
|
4408
|
+
{ label: "2h", minutes: 120 },
|
|
4409
|
+
{ label: "1d", minutes: 60 * 24 }
|
|
4410
|
+
];
|
|
4411
|
+
const presetChips = presets.map((p) => `<button type="button" class="chip" data-snooze-min="${p.minutes}">${p.label}</button>`).join("");
|
|
4412
|
+
const actionBtns = ["Dismiss"];
|
|
4413
|
+
if (item.htmlLink)
|
|
4414
|
+
actionBtns.push("Open");
|
|
4415
|
+
if (item.kind === "calendar")
|
|
4416
|
+
actionBtns.push("Delete");
|
|
4417
|
+
const actionHtml = actionBtns.map((b) => `<button type="button" class="action" data-btn="${escapeHtml6(b)}">${escapeHtml6(b)}</button>`).join("");
|
|
4380
4418
|
const html = `<!DOCTYPE html>
|
|
4381
4419
|
<html><head><meta charset="utf-8"><style>
|
|
4382
4420
|
html, body { height: 100%; }
|
|
4383
|
-
body { font: 13px system-ui, sans-serif; margin: 0; padding: 16px
|
|
4421
|
+
body { font: 13px system-ui, sans-serif; margin: 0; padding: 14px 16px; background: #fff; color: #222; display:flex; flex-direction:column; box-sizing:border-box; gap: 8px; }
|
|
4384
4422
|
.icon { display: inline-block; width: 20px; text-align: center; margin-right: 4px; }
|
|
4385
|
-
.title { font-size: 1.1em; font-weight: 600;
|
|
4386
|
-
.when { color: #555; font-size: 0.95em;
|
|
4423
|
+
.title { font-size: 1.1em; font-weight: 600; }
|
|
4424
|
+
.when { color: #555; font-size: 0.95em; }
|
|
4387
4425
|
.kind { color: #888; font-size: 0.85em; }
|
|
4388
4426
|
.spacer { flex: 1; }
|
|
4389
|
-
.
|
|
4390
|
-
.
|
|
4391
|
-
|
|
4427
|
+
.row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
4428
|
+
.row-label { color: #555; font-size: 0.85em; margin-right: 2px; }
|
|
4429
|
+
button { font: 13px system-ui, sans-serif; cursor: pointer; }
|
|
4430
|
+
.chip { padding: 3px 9px; border: 1px solid #ccc; border-radius: 12px; background: #f7f7f7; color: #222; }
|
|
4431
|
+
.chip:hover { background: #e8f0fe; border-color: #0066cc; color: #0066cc; }
|
|
4432
|
+
.custom { display: flex; align-items: center; gap: 4px; }
|
|
4433
|
+
.custom input[type=number] { width: 48px; padding: 2px 4px; border: 1px solid #ccc; border-radius: 3px; font: inherit; }
|
|
4434
|
+
.custom select { padding: 2px 4px; border: 1px solid #ccc; border-radius: 3px; font: inherit; background: #fff; }
|
|
4435
|
+
.custom button.ok { padding: 2px 8px; border: 1px solid #0066cc; border-radius: 3px; background: #0066cc; color: #fff; }
|
|
4436
|
+
.custom button.ok:hover { background: #0052a3; }
|
|
4437
|
+
.actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
|
|
4438
|
+
.actions button.action { padding: 5px 14px; border: none; border-radius: 4px; background: #0066cc; color: #fff; min-width: 70px; }
|
|
4439
|
+
.actions button.action:hover { background: #0052a3; }
|
|
4440
|
+
.actions button.action[data-btn="Delete"] { background: #b00; }
|
|
4441
|
+
.actions button.action[data-btn="Delete"]:hover { background: #800; }
|
|
4392
4442
|
</style></head><body>
|
|
4393
4443
|
<div class="title"><span class="icon">${icon}</span>${escapeHtml6(item.title)}</div>
|
|
4394
4444
|
<div class="when">${escapeHtml6(formatWhen(item.whenMs))}</div>
|
|
4395
4445
|
<div class="kind">${kindLabel}</div>
|
|
4446
|
+
<div class="row">
|
|
4447
|
+
<span class="row-label">Snooze:</span>
|
|
4448
|
+
${presetChips}
|
|
4449
|
+
</div>
|
|
4450
|
+
<div class="row custom">
|
|
4451
|
+
<span class="row-label">Custom:</span>
|
|
4452
|
+
<input type="number" id="customN" value="5" min="1" max="999">
|
|
4453
|
+
<select id="customUnit">
|
|
4454
|
+
<option value="1">minutes</option>
|
|
4455
|
+
<option value="60">hours</option>
|
|
4456
|
+
<option value="1440">days</option>
|
|
4457
|
+
</select>
|
|
4458
|
+
<button type="button" class="ok" id="customGo">Snooze</button>
|
|
4459
|
+
</div>
|
|
4396
4460
|
<div class="spacer"></div>
|
|
4397
|
-
<div class="
|
|
4461
|
+
<div class="actions">${actionHtml}</div>
|
|
4398
4462
|
<script>
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4463
|
+
function send(payload) {
|
|
4464
|
+
try { window.ipc.postMessage(JSON.stringify(payload)); }
|
|
4465
|
+
catch (e) { try { window.close(); } catch (_) {} }
|
|
4466
|
+
}
|
|
4467
|
+
// Preset chips \u2192 snooze N minutes
|
|
4468
|
+
document.querySelectorAll("button[data-snooze-min]").forEach(function(b) {
|
|
4469
|
+
b.addEventListener("click", function() {
|
|
4470
|
+
var m = parseInt(b.getAttribute("data-snooze-min"), 10) || 15;
|
|
4471
|
+
send({ button: "snooze", form: { minutes: m } });
|
|
4472
|
+
});
|
|
4473
|
+
});
|
|
4474
|
+
// Custom snooze form \u2192 snooze N * unit-multiplier minutes
|
|
4475
|
+
var customGo = document.getElementById("customGo");
|
|
4476
|
+
var customN = document.getElementById("customN");
|
|
4477
|
+
var customUnit = document.getElementById("customUnit");
|
|
4478
|
+
function submitCustom() {
|
|
4479
|
+
var n = parseInt(customN.value, 10);
|
|
4480
|
+
if (!isFinite(n) || n < 1) n = 5;
|
|
4481
|
+
var mult = parseInt(customUnit.value, 10) || 1;
|
|
4482
|
+
send({ button: "snooze", form: { minutes: n * mult } });
|
|
4483
|
+
}
|
|
4484
|
+
customGo.addEventListener("click", submitCustom);
|
|
4485
|
+
customN.addEventListener("keydown", function(e) { if (e.key === "Enter") submitCustom(); });
|
|
4486
|
+
// Action buttons (Dismiss/Open/Delete) \u2192 discriminate by label.
|
|
4403
4487
|
document.querySelectorAll("button[data-btn]").forEach(function(b) {
|
|
4404
4488
|
b.addEventListener("click", function() {
|
|
4405
|
-
|
|
4406
|
-
catch (e) { /* IPC unavailable \u2014 close, parent treats as dismissed */ try { window.close(); } catch (_) {} }
|
|
4489
|
+
send({ button: b.getAttribute("data-btn") });
|
|
4407
4490
|
});
|
|
4408
4491
|
});
|
|
4409
4492
|
<\/script>
|
|
@@ -4417,7 +4500,10 @@ async function firePopupForItem(item) {
|
|
|
4417
4500
|
title,
|
|
4418
4501
|
html,
|
|
4419
4502
|
buttons,
|
|
4420
|
-
|
|
4503
|
+
// Sized for the new snooze form (chips row + custom input
|
|
4504
|
+
// row + action buttons) plus title/when/kind. Width fits
|
|
4505
|
+
// "5m 15m 30m 1h 2h 1d" on one line in Segoe UI.
|
|
4506
|
+
size: { width: 560, height: 300 }
|
|
4421
4507
|
});
|
|
4422
4508
|
} else {
|
|
4423
4509
|
r = await showInWebViewPopup({ title, html, buttons });
|
|
@@ -4445,6 +4531,14 @@ async function firePopupForItem(item) {
|
|
|
4445
4531
|
firedThisSession.delete(item.uuid);
|
|
4446
4532
|
snoozeItem(item, 2);
|
|
4447
4533
|
break;
|
|
4534
|
+
case "snooze": {
|
|
4535
|
+
const m = Math.max(1, Math.floor(r.form?.minutes ?? 15));
|
|
4536
|
+
snoozeItem(item, m);
|
|
4537
|
+
break;
|
|
4538
|
+
}
|
|
4539
|
+
// Legacy literal labels — retained so a pre-upgrade popup whose
|
|
4540
|
+
// bundle still says "Snooze 15m" / "Snooze 1h" continues to work
|
|
4541
|
+
// through a daemon-side rebuild without a UI restart.
|
|
4448
4542
|
case "Snooze 15m":
|
|
4449
4543
|
snoozeItem(item, 15);
|
|
4450
4544
|
break;
|