mysticals 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +331 -239
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createElement } from "react";
|
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
|
|
7
7
|
// package.json
|
|
8
|
-
var version = "0.
|
|
8
|
+
var version = "0.2.0";
|
|
9
9
|
|
|
10
10
|
// src/client.ts
|
|
11
11
|
import { spawn } from "child_process";
|
|
@@ -256,6 +256,10 @@ var SyncEngine = class {
|
|
|
256
256
|
timers = /* @__PURE__ */ new Map();
|
|
257
257
|
inflight = /* @__PURE__ */ new Map();
|
|
258
258
|
failures = /* @__PURE__ */ new Map();
|
|
259
|
+
/** Follow-up passes queued behind a running one (see `fresh`). */
|
|
260
|
+
queued = /* @__PURE__ */ new Map();
|
|
261
|
+
/** Bumped by `edited`: a pass that started before a local edit must not overwrite it. */
|
|
262
|
+
edits = /* @__PURE__ */ new Map();
|
|
259
263
|
running = false;
|
|
260
264
|
unsubscribe;
|
|
261
265
|
start() {
|
|
@@ -276,14 +280,30 @@ var SyncEngine = class {
|
|
|
276
280
|
* Sync one account, or all accounts independently when id omitted.
|
|
277
281
|
* A single-account call rejects with that account's error; the all-accounts call never rejects.
|
|
278
282
|
*/
|
|
279
|
-
/**
|
|
283
|
+
/**
|
|
284
|
+
* `quiet`: the change came from this app (own edit/RSVP), so don't notify about it.
|
|
285
|
+
* `fresh`: the caller just changed the account on the server, so a pass already running (it may have fetched
|
|
286
|
+
* before the change) is followed by one more instead of being reused.
|
|
287
|
+
*/
|
|
280
288
|
async syncNow(accountId, opts = {}) {
|
|
281
289
|
if (accountId === void 0) {
|
|
282
290
|
await Promise.allSettled(this.store.list().map((a) => this.syncNow(a.id)));
|
|
283
291
|
return;
|
|
284
292
|
}
|
|
285
293
|
const existing = this.inflight.get(accountId);
|
|
286
|
-
if (existing) return existing;
|
|
294
|
+
if (existing && !opts.fresh) return existing;
|
|
295
|
+
if (existing) {
|
|
296
|
+
let next = this.queued.get(accountId);
|
|
297
|
+
if (!next) {
|
|
298
|
+
next = existing.catch(() => {
|
|
299
|
+
}).then(() => {
|
|
300
|
+
this.queued.delete(accountId);
|
|
301
|
+
return this.syncNow(accountId, { quiet: opts.quiet });
|
|
302
|
+
});
|
|
303
|
+
this.queued.set(accountId, next);
|
|
304
|
+
}
|
|
305
|
+
return next;
|
|
306
|
+
}
|
|
287
307
|
const p = this.syncAccount(accountId, !!opts.quiet).finally(() => {
|
|
288
308
|
this.inflight.delete(accountId);
|
|
289
309
|
this.schedule(accountId);
|
|
@@ -293,6 +313,10 @@ var SyncEngine = class {
|
|
|
293
313
|
this.opts.onSyncing?.(accountId);
|
|
294
314
|
return p;
|
|
295
315
|
}
|
|
316
|
+
/** A local edit was applied to this account's cache; a pass already running won't overwrite it. */
|
|
317
|
+
edited(accountId) {
|
|
318
|
+
this.edits.set(accountId, (this.edits.get(accountId) ?? 0) + 1);
|
|
319
|
+
}
|
|
296
320
|
/** True while a sync of this account is running. */
|
|
297
321
|
isSyncing(accountId) {
|
|
298
322
|
return this.inflight.has(accountId);
|
|
@@ -316,6 +340,7 @@ var SyncEngine = class {
|
|
|
316
340
|
// ponytail: quiet silences the whole sync, so an external change landing in the same pass is not announced.
|
|
317
341
|
async syncAccount(id2, quiet) {
|
|
318
342
|
const hadError = !!this.store.list().find((a) => a.id === id2)?.error;
|
|
343
|
+
const edits = this.edits.get(id2) ?? 0;
|
|
319
344
|
try {
|
|
320
345
|
const provider = this.store.getProvider(id2);
|
|
321
346
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -327,6 +352,10 @@ var SyncEngine = class {
|
|
|
327
352
|
)
|
|
328
353
|
);
|
|
329
354
|
const next = { calendars, events: lists.flat(), syncedAt: now.toISOString() };
|
|
355
|
+
if ((this.edits.get(id2) ?? 0) !== edits) {
|
|
356
|
+
this.failures.delete(id2);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
330
359
|
let prev;
|
|
331
360
|
try {
|
|
332
361
|
prev = this.store.readCache(id2);
|
|
@@ -495,7 +524,20 @@ function createApi(store, sync, deps) {
|
|
|
495
524
|
return a;
|
|
496
525
|
};
|
|
497
526
|
const syncOne = (accountId) => {
|
|
498
|
-
sync.syncNow(accountId, { quiet: true }).catch((e) => console.error(`sync ${accountId} failed`, e));
|
|
527
|
+
sync.syncNow(accountId, { quiet: true, fresh: true }).catch((e) => console.error(`sync ${accountId} failed`, e));
|
|
528
|
+
};
|
|
529
|
+
const applyLocal = (accountId, fn) => {
|
|
530
|
+
try {
|
|
531
|
+
store.patchCache(accountId, (c) => ({ ...c, events: fn(c.events) }));
|
|
532
|
+
sync.edited(accountId);
|
|
533
|
+
deps.onChanged?.(accountId);
|
|
534
|
+
} catch (e) {
|
|
535
|
+
console.error(`local update of ${accountId} failed`, e);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
const upsert = (ev, accountId, calendarId) => {
|
|
539
|
+
const own = { ...ev, accountId, calendarId };
|
|
540
|
+
applyLocal(accountId, (events) => [...events.filter((e) => e.id !== own.id), own]);
|
|
499
541
|
};
|
|
500
542
|
const nextColor = () => PALETTE[store.list().length % PALETTE.length];
|
|
501
543
|
const cachedEvent = (ref) => {
|
|
@@ -566,6 +608,7 @@ function createApi(store, sync, deps) {
|
|
|
566
608
|
if (!cal) throw new Error("calendar does not belong to account");
|
|
567
609
|
if (cal.readOnly) throw new Error("calendar is read-only");
|
|
568
610
|
const ev = await store.getProvider(a.id).createEvent(cal.id, { ...input, accountId: a.id });
|
|
611
|
+
upsert(ev, a.id, cal.id);
|
|
569
612
|
syncOne(a.id);
|
|
570
613
|
return ev;
|
|
571
614
|
},
|
|
@@ -574,17 +617,21 @@ function createApi(store, sync, deps) {
|
|
|
574
617
|
const cached2 = writable(cachedEvent(edit));
|
|
575
618
|
const { title: title2, start, end, allDay, location, description, attendees } = edit;
|
|
576
619
|
const ev = await store.getProvider(cached2.accountId).updateEvent({ ...cached2, title: title2, start, end, allDay, location, description, attendees });
|
|
620
|
+
upsert(ev, cached2.accountId, cached2.calendarId);
|
|
577
621
|
syncOne(cached2.accountId);
|
|
578
622
|
return ev;
|
|
579
623
|
},
|
|
580
624
|
delete: async (raw, scope) => {
|
|
581
625
|
const cached2 = writable(cachedEvent(EventRef.parse(raw)));
|
|
582
|
-
|
|
626
|
+
const how = Scope.parse(scope);
|
|
627
|
+
await store.getProvider(cached2.accountId).deleteEvent(cached2, how);
|
|
628
|
+
applyLocal(cached2.accountId, (events) => events.filter((e) => !deletedBy(cached2, how, e)));
|
|
583
629
|
syncOne(cached2.accountId);
|
|
584
630
|
},
|
|
585
631
|
respond: async (raw, status) => {
|
|
586
632
|
const cached2 = cachedEvent(EventRef.parse(raw));
|
|
587
633
|
const ev = await store.getProvider(cached2.accountId).respond(cached2, RsvpStatus.parse(status));
|
|
634
|
+
upsert(ev, cached2.accountId, cached2.calendarId);
|
|
588
635
|
syncOne(cached2.accountId);
|
|
589
636
|
return ev;
|
|
590
637
|
}
|
|
@@ -597,6 +644,12 @@ function createApi(store, sync, deps) {
|
|
|
597
644
|
}
|
|
598
645
|
};
|
|
599
646
|
}
|
|
647
|
+
function deletedBy(target, scope, e) {
|
|
648
|
+
if (e.id === target.id) return true;
|
|
649
|
+
const series = target.recurringEventId;
|
|
650
|
+
if (!series || scope === "one" || e.recurringEventId !== series || e.calendarId !== target.calendarId) return false;
|
|
651
|
+
return scope === "all" || Date.parse(e.start) >= Date.parse(target.start);
|
|
652
|
+
}
|
|
600
653
|
|
|
601
654
|
// ../../packages/core/src/accounts/store.ts
|
|
602
655
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -748,6 +801,11 @@ var AccountStore = class {
|
|
|
748
801
|
this.caches.set(id2, cache);
|
|
749
802
|
});
|
|
750
803
|
}
|
|
804
|
+
/** In-memory only: shows a local edit at once; the next sync writes the server's copy to disk. */
|
|
805
|
+
patchCache(id2, fn) {
|
|
806
|
+
this.require(id2);
|
|
807
|
+
this.caches.set(id2, fn(this.readCache(id2)));
|
|
808
|
+
}
|
|
751
809
|
hiddenCalendars(id2) {
|
|
752
810
|
const file = join4(this.accountDir(id2), "prefs.json");
|
|
753
811
|
try {
|
|
@@ -1442,8 +1500,8 @@ async function postToken(params) {
|
|
|
1442
1500
|
if (!res.ok && !json.error) json.error = `http_${res.status}`;
|
|
1443
1501
|
return parseTokenResponse(json);
|
|
1444
1502
|
}
|
|
1445
|
-
var DONE_HTML = (msg) => `<!doctype html><meta charset="utf-8"><title>Mysticals</title><body style="font:15px -apple-system,sans-serif;text-align:center;padding-top:80px"><h2>${msg}</h2><p>You can close this tab and return to Mysticals.</p></body
|
|
1446
|
-
async function runOAuthFlow(openUrl3) {
|
|
1503
|
+
var DONE_HTML = (msg, returnUrl) => `<!doctype html><meta charset="utf-8"><title>Mysticals</title><body style="font:15px -apple-system,sans-serif;text-align:center;padding-top:80px"><h2>${msg}</h2>` + (returnUrl ? `<p>Returning to Mysticals\u2026</p><p><a href="${returnUrl}" style="display:inline-block;padding:8px 16px;border-radius:6px;background:#bd93f9;color:#0b0b10;font-weight:600;text-decoration:none">Open Mysticals</a></p><p style="color:#888">You can close this tab.</p><script>location.href=${JSON.stringify(returnUrl)}</script></body>` : `<p>You can close this tab and return to Mysticals.</p></body>`);
|
|
1504
|
+
async function runOAuthFlow(openUrl3, opts = {}) {
|
|
1447
1505
|
const cfg = getClientConfig();
|
|
1448
1506
|
const { verifier, challenge } = createPkce();
|
|
1449
1507
|
const state = randomBytes(16).toString("base64url");
|
|
@@ -1465,7 +1523,7 @@ async function runOAuthFlow(openUrl3) {
|
|
|
1465
1523
|
const params = new URL(req.url ?? "/", redirectUri).searchParams;
|
|
1466
1524
|
const reply = (status, msg, then) => {
|
|
1467
1525
|
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", Connection: "close" });
|
|
1468
|
-
res.end(DONE_HTML(msg), then);
|
|
1526
|
+
res.end(DONE_HTML(msg, status === 200 ? opts.returnUrl : void 0), then);
|
|
1469
1527
|
};
|
|
1470
1528
|
if (!params.has("code") && !params.has("error")) {
|
|
1471
1529
|
res.writeHead(404, { Connection: "close" }).end();
|
|
@@ -1484,6 +1542,7 @@ async function runOAuthFlow(openUrl3) {
|
|
|
1484
1542
|
(e) => finish(e instanceof Error ? e : new Error(String(e)))
|
|
1485
1543
|
);
|
|
1486
1544
|
});
|
|
1545
|
+
opts.onCode?.();
|
|
1487
1546
|
const tok = await postToken({
|
|
1488
1547
|
grant_type: "authorization_code",
|
|
1489
1548
|
code,
|
|
@@ -1689,8 +1748,8 @@ function createGoogleProviderImpl(ctx) {
|
|
|
1689
1748
|
|
|
1690
1749
|
// ../../packages/core/src/providers/google/index.ts
|
|
1691
1750
|
var createGoogleProvider = createGoogleProviderImpl;
|
|
1692
|
-
async function googleSignIn(openUrl3) {
|
|
1693
|
-
return runOAuthFlow(openUrl3);
|
|
1751
|
+
async function googleSignIn(openUrl3, opts) {
|
|
1752
|
+
return runOAuthFlow(openUrl3, opts);
|
|
1694
1753
|
}
|
|
1695
1754
|
|
|
1696
1755
|
// src/telemetry.ts
|
|
@@ -2461,6 +2520,11 @@ var cleanNotes = (s = "") => s.replace(/^@color(hex)?:.*$\n?/gim, "").trim();
|
|
|
2461
2520
|
function meetingUrl(location) {
|
|
2462
2521
|
return location?.match(/https?:\/\/[^\s<>"]+/i)?.[0].replace(/[.,;:!?)\]}'>]+$/, "");
|
|
2463
2522
|
}
|
|
2523
|
+
function place(location = "") {
|
|
2524
|
+
const url = meetingUrl(location);
|
|
2525
|
+
const rest = (url ? location.replace(url, "") : location).replace(/^[\s/|,·-]+|[\s/|,·-]+$/g, "");
|
|
2526
|
+
return rest || (url ? "video call" : "");
|
|
2527
|
+
}
|
|
2464
2528
|
|
|
2465
2529
|
// ../../packages/core/src/logic/status.ts
|
|
2466
2530
|
import { differenceInMinutes, format as format6 } from "date-fns";
|
|
@@ -2683,11 +2747,6 @@ function useScroll(first, last, height, total) {
|
|
|
2683
2747
|
}
|
|
2684
2748
|
var WHEEL_STEP = 3;
|
|
2685
2749
|
var rsvpMark = (e) => e.myStatus && e.myStatus !== "accepted" ? `${STATUS_ICON[e.myStatus]} ` : "";
|
|
2686
|
-
function place(location = "") {
|
|
2687
|
-
const url = meetingUrl(location);
|
|
2688
|
-
const rest = (url ? location.replace(url, "") : location).replace(/^[\s/|,·-]+|[\s/|,·-]+$/g, "");
|
|
2689
|
-
return rest || (url ? "video call" : "");
|
|
2690
|
-
}
|
|
2691
2750
|
var span = (m) => {
|
|
2692
2751
|
const h = Math.floor(m / 60);
|
|
2693
2752
|
return h ? m % 60 ? `${h}h ${m % 60}m` : `${h}h` : `${m}m`;
|
|
@@ -2938,6 +2997,18 @@ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
|
2938
2997
|
var GUTTER = 6;
|
|
2939
2998
|
var ALL_DAY_MAX = 3;
|
|
2940
2999
|
var COMPACT_BELOW = 18;
|
|
3000
|
+
function abutting(placed) {
|
|
3001
|
+
const span2 = (p) => [p.col / p.cols, (p.col + 1) / p.cols];
|
|
3002
|
+
return new Set(
|
|
3003
|
+
placed.filter((p) => {
|
|
3004
|
+
const [a, b] = span2(p);
|
|
3005
|
+
return placed.some((q) => {
|
|
3006
|
+
const [c, d] = span2(q);
|
|
3007
|
+
return q !== p && q.start === p.end && a < d && c < b;
|
|
3008
|
+
});
|
|
3009
|
+
})
|
|
3010
|
+
);
|
|
3011
|
+
}
|
|
2941
3012
|
function slots(events, day) {
|
|
2942
3013
|
return packColumns(
|
|
2943
3014
|
layoutDay(events, day).map((p) => {
|
|
@@ -2951,7 +3022,8 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
|
|
|
2951
3022
|
const cols = useMemo2(
|
|
2952
3023
|
() => days.map((day) => {
|
|
2953
3024
|
const on = eventsOnDay(events, day);
|
|
2954
|
-
|
|
3025
|
+
const placed = slots(events, day);
|
|
3026
|
+
return { day, count: on.length, allDay: on.filter((e) => e.allDay), placed, joined: abutting(placed) };
|
|
2955
3027
|
}),
|
|
2956
3028
|
[events, days]
|
|
2957
3029
|
);
|
|
@@ -2963,21 +3035,21 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
|
|
|
2963
3035
|
const focus = sel ? sel.start / 60 : today ? now.getHours() : 8;
|
|
2964
3036
|
const [top, scrollBy] = useScroll(sel ? focus : Math.max(focus - 1, 0), focus, gridH, 24);
|
|
2965
3037
|
const bar = /* @__PURE__ */ jsx5(Text5, { color: C.muted, children: "\u2502" });
|
|
2966
|
-
const row = (key3, gutter, cells) => /* @__PURE__ */ jsxs5(Box5, { width, height: 1, overflow: "hidden", children: [
|
|
3038
|
+
const row = (key3, gutter, cells, sep = bar) => /* @__PURE__ */ jsxs5(Box5, { width, height: 1, overflow: "hidden", children: [
|
|
2967
3039
|
gutter,
|
|
2968
3040
|
cols.map((c) => /* @__PURE__ */ jsxs5(Box5, { flexShrink: 0, children: [
|
|
2969
|
-
|
|
3041
|
+
sep,
|
|
2970
3042
|
/* @__PURE__ */ jsx5(Box5, { width: colW, flexShrink: 0, overflow: "hidden", children: cells(c) })
|
|
2971
3043
|
] }, c.day.getTime()))
|
|
2972
3044
|
] }, key3);
|
|
2973
3045
|
const gutterText = (text2) => /* @__PURE__ */ jsx5(Box5, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text5, { color: C.muted, children: text2 }) });
|
|
2974
|
-
const hourCells = (placed, day, h, nowLine) => {
|
|
3046
|
+
const hourCells = ({ placed, day, joined }, h, nowLine) => {
|
|
2975
3047
|
const out = [];
|
|
2976
3048
|
let pos = 0;
|
|
2977
3049
|
const gap = (to) => {
|
|
2978
3050
|
if (to <= pos) return;
|
|
2979
3051
|
out.push(
|
|
2980
|
-
/* @__PURE__ */ jsx5(Box5, { width: to - pos, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text5, { color: C.now, children: (nowLine ? "\u2500" : " ").repeat(to - pos) }) }, `g${pos}`)
|
|
3052
|
+
/* @__PURE__ */ jsx5(Box5, { width: to - pos, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text5, { color: C.now, dimColor: nowLine === "other", children: (nowLine ? "\u2500" : " ").repeat(to - pos) }) }, `g${pos}`)
|
|
2981
3053
|
);
|
|
2982
3054
|
pos = to;
|
|
2983
3055
|
};
|
|
@@ -3008,6 +3080,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
|
|
|
3008
3080
|
bold: selected || h === first,
|
|
3009
3081
|
italic: e.myStatus === "needsAction",
|
|
3010
3082
|
strikethrough: e.myStatus === "declined",
|
|
3083
|
+
underline: h === p.end / 60 - 1 && joined.has(p),
|
|
3011
3084
|
children: (" " + text2).slice(0, inner).padEnd(inner)
|
|
3012
3085
|
}
|
|
3013
3086
|
) }, k)
|
|
@@ -3052,8 +3125,13 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
|
|
|
3052
3125
|
/* @__PURE__ */ jsx5(Clickable, { flexDirection: "column", width, onWheel: (dir) => scrollBy(dir), children: Array.from({ length: Math.min(gridH, 24) }, (_, i) => {
|
|
3053
3126
|
const h = top + i;
|
|
3054
3127
|
const current = today && h === now.getHours();
|
|
3055
|
-
const gutter = /* @__PURE__ */ jsx5(Box5, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text5, { color: current ? C.now : C.muted, bold: current, children: current ? format9(now, "HH:mm") : `${String(h).padStart(2, "0")}:00` }) });
|
|
3056
|
-
return row(
|
|
3128
|
+
const gutter = /* @__PURE__ */ jsx5(Box5, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text5, { color: current ? C.now : C.muted, bold: current, children: current ? `${format9(now, "HH:mm")}\u2500` : `${String(h).padStart(2, "0")}:00` }) });
|
|
3129
|
+
return row(
|
|
3130
|
+
`h${h}`,
|
|
3131
|
+
gutter,
|
|
3132
|
+
(c) => hourCells(c, h, current && (isSameDay6(c.day, now) ? "today" : "other")),
|
|
3133
|
+
current ? /* @__PURE__ */ jsx5(Text5, { color: C.now, children: "\u253C" }) : bar
|
|
3134
|
+
);
|
|
3057
3135
|
}) })
|
|
3058
3136
|
] });
|
|
3059
3137
|
}
|
|
@@ -3299,13 +3377,172 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3299
3377
|
}
|
|
3300
3378
|
|
|
3301
3379
|
// src/ui/screens/Accounts.tsx
|
|
3302
|
-
import { useEffect as
|
|
3303
|
-
import { Box as
|
|
3380
|
+
import { useEffect as useEffect5, useRef as useRef6, useState as useState7 } from "react";
|
|
3381
|
+
import { Box as Box10, Text as Text10, useStdout as useStdout2 } from "ink";
|
|
3304
3382
|
|
|
3305
3383
|
// src/ui/screens/AddCaldav.tsx
|
|
3306
|
-
import { useRef as
|
|
3384
|
+
import { useRef as useRef5, useState as useState6 } from "react";
|
|
3385
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
3386
|
+
|
|
3387
|
+
// src/ui/StatusLine.tsx
|
|
3388
|
+
import { useEffect as useEffect4, useState as useState5 } from "react";
|
|
3389
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
3390
|
+
|
|
3391
|
+
// src/ui/screens/Invites.tsx
|
|
3392
|
+
import { useMemo as useMemo5, useRef as useRef4, useState as useState4 } from "react";
|
|
3307
3393
|
import { Box as Box7, Text as Text7 } from "ink";
|
|
3394
|
+
import { addDays as addDays8, startOfDay as startOfDay6 } from "date-fns";
|
|
3308
3395
|
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3396
|
+
var INVITE_DAYS = 60;
|
|
3397
|
+
function useUpcoming(now) {
|
|
3398
|
+
const day = startOfDay6(now).getTime();
|
|
3399
|
+
const range = useMemo5(() => ({ start: new Date(day).toISOString(), end: addDays8(day, INVITE_DAYS).toISOString() }), [day]);
|
|
3400
|
+
return useEvents(range);
|
|
3401
|
+
}
|
|
3402
|
+
var REPLY = { y: "accepted", n: "declined", m: "tentative" };
|
|
3403
|
+
function Invites({ onClose }) {
|
|
3404
|
+
const api = useApi();
|
|
3405
|
+
const [now] = useState4(() => /* @__PURE__ */ new Date());
|
|
3406
|
+
const { events, loaded } = useUpcoming(now);
|
|
3407
|
+
const invites = useMemo5(() => pendingInvites(events, now), [events, now]);
|
|
3408
|
+
const { accounts } = useDirectory();
|
|
3409
|
+
const [idx, setIdx] = useState4(0);
|
|
3410
|
+
const [rows, setRows] = useState4({});
|
|
3411
|
+
const busy = useRef4(/* @__PURE__ */ new Set());
|
|
3412
|
+
const [error, setError] = useState4("");
|
|
3413
|
+
const sel = Math.min(idx, Math.max(invites.length - 1, 0));
|
|
3414
|
+
const reply = (e, status) => {
|
|
3415
|
+
const k = eventKey(e);
|
|
3416
|
+
if (busy.current.has(k)) return;
|
|
3417
|
+
busy.current.add(k);
|
|
3418
|
+
setError("");
|
|
3419
|
+
setRows((r) => ({ ...r, [k]: "sending\u2026" }));
|
|
3420
|
+
api.events.respond(e, status).then(
|
|
3421
|
+
() => setRows((r) => ({ ...r, [k]: "sent" })),
|
|
3422
|
+
(err) => {
|
|
3423
|
+
busy.current.delete(k);
|
|
3424
|
+
setRows(({ [k]: _, ...r }) => r);
|
|
3425
|
+
setError(errorText(err));
|
|
3426
|
+
}
|
|
3427
|
+
);
|
|
3428
|
+
};
|
|
3429
|
+
const move = (dir) => setIdx(Math.max(Math.min(sel + dir, invites.length - 1), 0));
|
|
3430
|
+
const replySelected = (status) => void (invites[sel] && reply(invites[sel], status));
|
|
3431
|
+
useKeys((input, key3) => {
|
|
3432
|
+
if (key3.escape || input === "q") return onClose();
|
|
3433
|
+
if (input === "j" || key3.downArrow) return move(1);
|
|
3434
|
+
if (input === "k" || key3.upArrow) return move(-1);
|
|
3435
|
+
if (Object.hasOwn(REPLY, input)) replySelected(REPLY[input]);
|
|
3436
|
+
});
|
|
3437
|
+
return /* @__PURE__ */ jsxs7(Clickable, { flexDirection: "column", borderStyle: "round", borderColor: C.muted, paddingX: 1, onWheel: move, children: [
|
|
3438
|
+
/* @__PURE__ */ jsx9(Text7, { bold: true, children: "Invitations" }),
|
|
3439
|
+
!loaded ? /* @__PURE__ */ jsx9(Text7, { color: C.muted, children: "Loading\u2026" }) : invites.length === 0 ? /* @__PURE__ */ jsx9(Text7, { color: C.muted, children: "No pending invites" }) : invites.map((e, i) => {
|
|
3440
|
+
const a = accounts.find((x) => x.id === e.accountId);
|
|
3441
|
+
const o = ownerLine(void 0, a?.label ?? e.accountId, a?.email);
|
|
3442
|
+
return /* @__PURE__ */ jsx9(Clickable, { height: 1, onClick: () => setIdx(i), children: /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", inverse: i === sel, children: [
|
|
3443
|
+
i === sel ? "\u25B8 " : " ",
|
|
3444
|
+
/* @__PURE__ */ jsx9(Text7, { bold: true, children: e.title || "Untitled" }),
|
|
3445
|
+
" ",
|
|
3446
|
+
/* @__PURE__ */ jsx9(Text7, { color: C.muted, children: formatWhen(e) }),
|
|
3447
|
+
" ",
|
|
3448
|
+
/* @__PURE__ */ jsx9(Text7, { color: ansiOf(a?.color ?? ""), children: "\u25CF" }),
|
|
3449
|
+
" ",
|
|
3450
|
+
o.label,
|
|
3451
|
+
o.email ? ` \xB7 ${o.email}` : "",
|
|
3452
|
+
rows[eventKey(e)] ? /* @__PURE__ */ jsxs7(Text7, { color: C.yellow, children: [
|
|
3453
|
+
" ",
|
|
3454
|
+
rows[eventKey(e)]
|
|
3455
|
+
] }) : null
|
|
3456
|
+
] }) }, eventKey(e));
|
|
3457
|
+
}),
|
|
3458
|
+
error ? /* @__PURE__ */ jsx9(Text7, { color: C.red, children: error }) : null,
|
|
3459
|
+
/* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexWrap: "wrap", children: [
|
|
3460
|
+
invites.length > 0 && /* @__PURE__ */ jsx9(Button, { k: "y", label: "accept", color: C.green, onPress: () => replySelected("accepted") }),
|
|
3461
|
+
invites.length > 0 && /* @__PURE__ */ jsx9(Button, { k: "n", label: "decline", color: C.red, onPress: () => replySelected("declined") }),
|
|
3462
|
+
invites.length > 0 && /* @__PURE__ */ jsx9(Button, { k: "m", label: "maybe", color: C.yellow, onPress: () => replySelected("tentative") }),
|
|
3463
|
+
/* @__PURE__ */ jsx9(Box7, { marginRight: 2, children: /* @__PURE__ */ jsx9(Text7, { color: C.muted, children: "j/k move" }) }),
|
|
3464
|
+
/* @__PURE__ */ jsx9(Button, { k: "esc", label: "close", onPress: onClose })
|
|
3465
|
+
] })
|
|
3466
|
+
] });
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
// src/ui/StatusLine.tsx
|
|
3470
|
+
import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3471
|
+
var DAY_MS = 24 * 36e5;
|
|
3472
|
+
var noop = () => {
|
|
3473
|
+
};
|
|
3474
|
+
var FRAMES = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
|
|
3475
|
+
function Spinner() {
|
|
3476
|
+
const [i, setI] = useState5(0);
|
|
3477
|
+
useEffect4(() => {
|
|
3478
|
+
const t = setInterval(() => setI((n) => (n + 1) % FRAMES.length), 100);
|
|
3479
|
+
return () => clearInterval(t);
|
|
3480
|
+
}, []);
|
|
3481
|
+
return /* @__PURE__ */ jsx10(Text8, { color: C.accent, children: FRAMES[i] });
|
|
3482
|
+
}
|
|
3483
|
+
function StatusLine({ now, message, update, width, onOpen, onInvites, onAccounts, onHelp, onNew, onSync, onQuit }) {
|
|
3484
|
+
const { events } = useUpcoming(now);
|
|
3485
|
+
const { accounts } = useDirectory();
|
|
3486
|
+
const failed = accounts.filter((a) => a.error);
|
|
3487
|
+
const syncing = accounts.filter((a) => a.syncing);
|
|
3488
|
+
const { current, next } = pickNowNext(events, now);
|
|
3489
|
+
const soon = next && Date.parse(next.start) - now.getTime() < DAY_MS ? next : void 0;
|
|
3490
|
+
const invites = pendingInvites(events, now).length;
|
|
3491
|
+
const parts = [];
|
|
3492
|
+
if (syncing.length) parts.push([/* @__PURE__ */ jsxs8(Fragment2, { children: [
|
|
3493
|
+
/* @__PURE__ */ jsx10(Spinner, {}),
|
|
3494
|
+
" ",
|
|
3495
|
+
/* @__PURE__ */ jsxs8(Text8, { color: C.muted, children: [
|
|
3496
|
+
"syncing ",
|
|
3497
|
+
syncing.map((a) => a.label).join(", ")
|
|
3498
|
+
] })
|
|
3499
|
+
] }), onAccounts]);
|
|
3500
|
+
for (const e of current) parts.push([/* @__PURE__ */ jsxs8(Fragment2, { children: [
|
|
3501
|
+
/* @__PURE__ */ jsx10(Text8, { color: C.green, bold: true, children: "now" }),
|
|
3502
|
+
" ",
|
|
3503
|
+
e.title || "Untitled"
|
|
3504
|
+
] }), onOpen && (() => onOpen(e))]);
|
|
3505
|
+
if (soon) parts.push([/* @__PURE__ */ jsxs8(Fragment2, { children: [
|
|
3506
|
+
/* @__PURE__ */ jsx10(Text8, { color: C.cyan, bold: true, children: "next" }),
|
|
3507
|
+
" ",
|
|
3508
|
+
soon.title || "Untitled",
|
|
3509
|
+
" ",
|
|
3510
|
+
/* @__PURE__ */ jsx10(Text8, { color: C.yellow, children: startsLabel(soon.start, now) })
|
|
3511
|
+
] }), onOpen && (() => onOpen(soon))]);
|
|
3512
|
+
if (invites) parts.push([/* @__PURE__ */ jsxs8(Text8, { color: C.yellow, children: [
|
|
3513
|
+
invites === 1 ? "1 invite" : `${invites} invites`,
|
|
3514
|
+
" (i)"
|
|
3515
|
+
] }), onInvites]);
|
|
3516
|
+
if (failed.length) parts.push([/* @__PURE__ */ jsxs8(Text8, { color: C.red, children: [
|
|
3517
|
+
"\u26A0 ",
|
|
3518
|
+
failed.map((a) => a.label).join(", "),
|
|
3519
|
+
" sync failed (s)"
|
|
3520
|
+
] }), onAccounts]);
|
|
3521
|
+
if (message) parts.push([message]);
|
|
3522
|
+
if (!parts.length) parts.push([/* @__PURE__ */ jsx10(Text8, { color: C.muted, children: "nothing on in the next 24h" })]);
|
|
3523
|
+
if (update) parts.push([/* @__PURE__ */ jsxs8(Text8, { color: C.yellow, children: [
|
|
3524
|
+
"\u2191 ",
|
|
3525
|
+
update,
|
|
3526
|
+
" available \xB7 npm i -g mysticals"
|
|
3527
|
+
] })]);
|
|
3528
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width, height: 2, children: [
|
|
3529
|
+
/* @__PURE__ */ jsx10(Box8, { width, height: 1, overflow: "hidden", children: parts.map(([p, onClick], i) => /* @__PURE__ */ jsx10(Clickable, { flexShrink: 0, onClick, children: /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
3530
|
+
i > 0 && /* @__PURE__ */ jsx10(Text8, { color: C.muted, children: " \xB7 " }),
|
|
3531
|
+
p
|
|
3532
|
+
] }) }, i)) }),
|
|
3533
|
+
/* @__PURE__ */ jsxs8(Box8, { width, height: 1, overflow: "hidden", children: [
|
|
3534
|
+
/* @__PURE__ */ jsx10(Button, { k: "n", label: "new", onPress: onNew ?? noop }),
|
|
3535
|
+
/* @__PURE__ */ jsx10(Button, { k: "r", label: "sync", onPress: onSync ?? noop }),
|
|
3536
|
+
/* @__PURE__ */ jsx10(Button, { k: "i", label: "invites", onPress: onInvites ?? noop }),
|
|
3537
|
+
/* @__PURE__ */ jsx10(Button, { k: "s", label: "accounts", onPress: onAccounts ?? noop }),
|
|
3538
|
+
/* @__PURE__ */ jsx10(Button, { k: "?", label: "help", onPress: onHelp ?? noop }),
|
|
3539
|
+
/* @__PURE__ */ jsx10(Button, { k: "q", label: "quit", onPress: onQuit ?? noop })
|
|
3540
|
+
] })
|
|
3541
|
+
] });
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
// src/ui/screens/AddCaldav.tsx
|
|
3545
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3309
3546
|
var PALETTE2 = ["#bd93f9", "#50fa7b", "#8be9fd", "#ff79c6", "#ffb86c", "#f1fa8c"];
|
|
3310
3547
|
var PRESETS2 = [
|
|
3311
3548
|
{ id: "privateemail", name: "Namecheap Private Email", url: "https://dav.privateemail.com/dav.php/" },
|
|
@@ -3336,8 +3573,8 @@ var NAMES = {
|
|
|
3336
3573
|
color: "Colour"
|
|
3337
3574
|
};
|
|
3338
3575
|
function useKeyState(init) {
|
|
3339
|
-
const ref =
|
|
3340
|
-
const [, bump] =
|
|
3576
|
+
const ref = useRef5(init);
|
|
3577
|
+
const [, bump] = useState6(0);
|
|
3341
3578
|
return [
|
|
3342
3579
|
() => ref.current,
|
|
3343
3580
|
(next) => {
|
|
@@ -3413,53 +3650,58 @@ function AddCaldav({ onBack, onDone }) {
|
|
|
3413
3650
|
const v = get();
|
|
3414
3651
|
const shown = (f) => {
|
|
3415
3652
|
if (f === "provider") return `\u2039 ${PRESETS2.find((p) => p.id === v.preset).name} \u203A`;
|
|
3416
|
-
if (f === "color") return /* @__PURE__ */
|
|
3653
|
+
if (f === "color") return /* @__PURE__ */ jsxs9(Text9, { color: ansiOf(v.color), children: [
|
|
3417
3654
|
"\u2039 \u25CF ",
|
|
3418
3655
|
v.color,
|
|
3419
3656
|
" \u203A"
|
|
3420
3657
|
] });
|
|
3421
3658
|
if (f === "password") return "\u2022".repeat(v.password.length);
|
|
3422
|
-
return v[f] || /* @__PURE__ */
|
|
3659
|
+
return v[f] || /* @__PURE__ */ jsx11(Text9, { color: C.muted, children: f === "serverUrl" ? "https://caldav.example.com/" : f === "username" ? "you@company.com" : "Work" });
|
|
3423
3660
|
};
|
|
3424
|
-
return /* @__PURE__ */
|
|
3425
|
-
/* @__PURE__ */
|
|
3426
|
-
/* @__PURE__ */
|
|
3427
|
-
FIELDS.map((f) => /* @__PURE__ */
|
|
3428
|
-
/* @__PURE__ */
|
|
3661
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
3662
|
+
/* @__PURE__ */ jsx11(Text9, { bold: true, children: "Add CalDAV account" }),
|
|
3663
|
+
/* @__PURE__ */ jsx11(Text9, { color: C.muted, children: "Each account is isolated: events and invitations are only sent from the account they belong to." }),
|
|
3664
|
+
FIELDS.map((f) => /* @__PURE__ */ jsx11(Clickable, { onClick: () => clickField(f), children: /* @__PURE__ */ jsxs9(Text9, { children: [
|
|
3665
|
+
/* @__PURE__ */ jsx11(Text9, { color: f === v.field ? C.cyan : void 0, children: `${f === v.field ? "\u203A" : " "} ${NAMES[f].padEnd(13)}` }),
|
|
3429
3666
|
shown(f),
|
|
3430
|
-
f === v.field && f !== "provider" && f !== "color" ? /* @__PURE__ */
|
|
3667
|
+
f === v.field && f !== "provider" && f !== "color" ? /* @__PURE__ */ jsx11(Text9, { color: C.cyan, children: "\u258F" }) : null
|
|
3431
3668
|
] }) }, f)),
|
|
3432
|
-
/* @__PURE__ */
|
|
3433
|
-
v.busy && /* @__PURE__ */
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3669
|
+
/* @__PURE__ */ jsx11(Text9, { color: C.muted, children: "Check your provider's docs if connection fails." }),
|
|
3670
|
+
v.busy && /* @__PURE__ */ jsxs9(Text9, { children: [
|
|
3671
|
+
/* @__PURE__ */ jsx11(Spinner, {}),
|
|
3672
|
+
" Signing in to ",
|
|
3673
|
+
URL.canParse(v.serverUrl.trim()) ? new URL(v.serverUrl.trim()).host : "the server",
|
|
3674
|
+
"\u2026"
|
|
3675
|
+
] }),
|
|
3676
|
+
v.error && /* @__PURE__ */ jsx11(Text9, { color: C.red, children: v.error }),
|
|
3677
|
+
/* @__PURE__ */ jsxs9(Box9, { flexWrap: "wrap", children: [
|
|
3678
|
+
/* @__PURE__ */ jsx11(Box9, { marginRight: 2, children: /* @__PURE__ */ jsx11(Text9, { color: C.muted, children: "tab/\u2191\u2193 field \u2190\u2192 change provider/colour" }) }),
|
|
3679
|
+
/* @__PURE__ */ jsx11(Button, { k: "enter", label: "add account", color: C.green, onPress: () => !get().busy && submit() }),
|
|
3680
|
+
/* @__PURE__ */ jsx11(Button, { k: "esc", label: "back", onPress: () => !get().busy && onBack() })
|
|
3439
3681
|
] })
|
|
3440
3682
|
] });
|
|
3441
3683
|
}
|
|
3442
3684
|
|
|
3443
3685
|
// src/ui/screens/Accounts.tsx
|
|
3444
|
-
import { Fragment as
|
|
3686
|
+
import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3445
3687
|
function Accounts({ onClose }) {
|
|
3446
3688
|
const api = useApi();
|
|
3447
3689
|
const { accounts, calendars, loaded, error: loadError } = useDirectory();
|
|
3448
3690
|
const [getCursor, setCursor] = useKeyState(0);
|
|
3449
3691
|
const [getMode, setMode] = useKeyState({ kind: "list" });
|
|
3450
3692
|
const mode = getMode();
|
|
3451
|
-
const [message, setMessage] =
|
|
3452
|
-
const [authUrl, setAuthUrl] =
|
|
3453
|
-
|
|
3693
|
+
const [message, setMessage] = useState7();
|
|
3694
|
+
const [authUrl, setAuthUrl] = useState7();
|
|
3695
|
+
useEffect5(() => api.onAuthUrl(setAuthUrl), [api]);
|
|
3454
3696
|
const { stdout } = useStdout2();
|
|
3455
3697
|
const copyAuthUrl = () => {
|
|
3456
3698
|
if (!authUrl) return;
|
|
3457
3699
|
stdout.write(`\x1B]52;c;${Buffer.from(authUrl).toString("base64")}\x07`);
|
|
3458
3700
|
say("Sign-in link sent to the clipboard (needs a terminal with OSC 52 clipboard support)");
|
|
3459
3701
|
};
|
|
3460
|
-
const googleSession =
|
|
3461
|
-
const alive2 =
|
|
3462
|
-
|
|
3702
|
+
const googleSession = useRef6(0);
|
|
3703
|
+
const alive2 = useRef6(true);
|
|
3704
|
+
useEffect5(() => () => void (alive2.current = false), []);
|
|
3463
3705
|
const rows = accounts.flatMap((account) => [
|
|
3464
3706
|
{ account },
|
|
3465
3707
|
...calendars.filter((c) => c.accountId === account.id).map((calendar) => ({ account, calendar }))
|
|
@@ -3559,7 +3801,7 @@ function Accounts({ onClose }) {
|
|
|
3559
3801
|
{ isActive: mode.kind !== "caldav" }
|
|
3560
3802
|
);
|
|
3561
3803
|
if (mode.kind === "caldav") {
|
|
3562
|
-
return /* @__PURE__ */
|
|
3804
|
+
return /* @__PURE__ */ jsx12(Box10, { flexDirection: "column", borderStyle: "round", borderColor: C.muted, paddingX: 1, children: /* @__PURE__ */ jsx12(
|
|
3563
3805
|
AddCaldav,
|
|
3564
3806
|
{
|
|
3565
3807
|
onBack: () => setMode({ kind: "list" }),
|
|
@@ -3570,80 +3812,85 @@ function Accounts({ onClose }) {
|
|
|
3570
3812
|
}
|
|
3571
3813
|
) });
|
|
3572
3814
|
}
|
|
3573
|
-
const button = (k, label, input = k) => /* @__PURE__ */
|
|
3815
|
+
const button = (k, label, input = k) => /* @__PURE__ */ jsx12(Button, { k, label, onPress: () => listing() && act(input) });
|
|
3574
3816
|
const row = rows[idx];
|
|
3575
|
-
return /* @__PURE__ */
|
|
3576
|
-
/* @__PURE__ */
|
|
3577
|
-
/* @__PURE__ */
|
|
3578
|
-
loaded && !accounts.length && /* @__PURE__ */
|
|
3817
|
+
return /* @__PURE__ */ jsxs10(Clickable, { flexDirection: "column", borderStyle: "round", borderColor: C.muted, paddingX: 1, onWheel: (dir) => listing() && move(dir), children: [
|
|
3818
|
+
/* @__PURE__ */ jsx12(Text10, { bold: true, children: "Accounts & calendars" }),
|
|
3819
|
+
/* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "Accounts sync automatically every 2 minutes." }),
|
|
3820
|
+
loaded && !accounts.length && /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "No accounts yet. Press g (Google) or a (CalDAV) to add one." }),
|
|
3579
3821
|
rows.map((r, i) => {
|
|
3580
3822
|
const sel = i === idx;
|
|
3581
|
-
const mark = /* @__PURE__ */
|
|
3823
|
+
const mark = /* @__PURE__ */ jsx12(Text10, { color: C.cyan, children: sel ? "\u203A " : " " });
|
|
3582
3824
|
if (r.calendar) {
|
|
3583
3825
|
const c = r.calendar;
|
|
3584
|
-
return /* @__PURE__ */
|
|
3826
|
+
return /* @__PURE__ */ jsx12(Clickable, { onClick: () => click(i), children: /* @__PURE__ */ jsxs10(Text10, { inverse: sel, children: [
|
|
3585
3827
|
mark,
|
|
3586
3828
|
" ",
|
|
3587
3829
|
c.visible === false ? "[ ]" : "[x]",
|
|
3588
3830
|
" ",
|
|
3589
|
-
/* @__PURE__ */
|
|
3831
|
+
/* @__PURE__ */ jsx12(Text10, { color: ansiOf(c.color), children: "\u25CF" }),
|
|
3590
3832
|
" ",
|
|
3591
3833
|
c.name,
|
|
3592
|
-
c.readOnly && /* @__PURE__ */
|
|
3834
|
+
c.readOnly && /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: " (read-only)" })
|
|
3593
3835
|
] }) }, `${c.accountId}/${c.id}`);
|
|
3594
3836
|
}
|
|
3595
3837
|
const a = r.account;
|
|
3596
3838
|
const renaming = mode.kind === "rename" && mode.account.id === a.id;
|
|
3597
|
-
return /* @__PURE__ */
|
|
3598
|
-
/* @__PURE__ */
|
|
3839
|
+
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
|
|
3840
|
+
/* @__PURE__ */ jsx12(Clickable, { onClick: () => click(i), children: /* @__PURE__ */ jsxs10(Text10, { inverse: sel && !renaming, children: [
|
|
3599
3841
|
mark,
|
|
3600
|
-
/* @__PURE__ */
|
|
3842
|
+
/* @__PURE__ */ jsx12(Text10, { color: ansiOf(a.color), children: "\u25CF" }),
|
|
3601
3843
|
" ",
|
|
3602
|
-
renaming ? /* @__PURE__ */
|
|
3844
|
+
renaming ? /* @__PURE__ */ jsxs10(Text10, { color: C.cyan, children: [
|
|
3603
3845
|
mode.value,
|
|
3604
3846
|
"\u258F"
|
|
3605
|
-
] }) : /* @__PURE__ */
|
|
3847
|
+
] }) : /* @__PURE__ */ jsx12(Text10, { bold: true, children: a.label }),
|
|
3606
3848
|
" ",
|
|
3607
|
-
/* @__PURE__ */
|
|
3849
|
+
/* @__PURE__ */ jsxs10(Text10, { color: C.muted, children: [
|
|
3608
3850
|
a.email,
|
|
3609
3851
|
" \xB7 ",
|
|
3610
3852
|
a.kind === "google" ? "Google" : "CalDAV"
|
|
3611
3853
|
] })
|
|
3612
3854
|
] }) }),
|
|
3613
|
-
a.
|
|
3855
|
+
a.syncing && !a.synced && /* @__PURE__ */ jsxs10(Text10, { color: C.muted, children: [
|
|
3856
|
+
" ",
|
|
3857
|
+
/* @__PURE__ */ jsx12(Spinner, {}),
|
|
3858
|
+
" Loading calendars\u2026"
|
|
3859
|
+
] }),
|
|
3860
|
+
a.error && /* @__PURE__ */ jsx12(Text10, { color: C.red, children: ` Last sync failed: ${a.error}` })
|
|
3614
3861
|
] }, a.id);
|
|
3615
3862
|
}),
|
|
3616
|
-
mode.kind === "confirm" && /* @__PURE__ */
|
|
3617
|
-
/* @__PURE__ */
|
|
3863
|
+
mode.kind === "confirm" && /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", marginTop: 1, children: [
|
|
3864
|
+
/* @__PURE__ */ jsxs10(Text10, { color: C.red, bold: true, children: [
|
|
3618
3865
|
"Remove ",
|
|
3619
3866
|
mode.account.label,
|
|
3620
3867
|
" (",
|
|
3621
3868
|
mode.account.email,
|
|
3622
3869
|
")? [y/N]"
|
|
3623
3870
|
] }),
|
|
3624
|
-
/* @__PURE__ */
|
|
3871
|
+
/* @__PURE__ */ jsxs10(Text10, { children: [
|
|
3625
3872
|
"Removes local data and credentials for ",
|
|
3626
3873
|
mode.account.email,
|
|
3627
3874
|
". Nothing is deleted on the server."
|
|
3628
3875
|
] }),
|
|
3629
|
-
/* @__PURE__ */
|
|
3630
|
-
/* @__PURE__ */
|
|
3631
|
-
/* @__PURE__ */
|
|
3876
|
+
/* @__PURE__ */ jsxs10(Box10, { children: [
|
|
3877
|
+
/* @__PURE__ */ jsx12(Button, { k: "y", label: "remove", color: C.red, onPress: () => confirmRemove(true) }),
|
|
3878
|
+
/* @__PURE__ */ jsx12(Button, { k: "n", label: "keep", onPress: () => confirmRemove(false) })
|
|
3632
3879
|
] })
|
|
3633
3880
|
] }),
|
|
3634
|
-
mode.kind === "google" && /* @__PURE__ */
|
|
3635
|
-
/* @__PURE__ */
|
|
3636
|
-
authUrl && /* @__PURE__ */
|
|
3637
|
-
/* @__PURE__ */
|
|
3638
|
-
/* @__PURE__ */
|
|
3639
|
-
/* @__PURE__ */
|
|
3881
|
+
mode.kind === "google" && /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
|
|
3882
|
+
/* @__PURE__ */ jsx12(Text10, { color: C.yellow, children: "Opening browser \u2014 finish sign-in there\u2026 (esc to stop waiting)" }),
|
|
3883
|
+
authUrl && /* @__PURE__ */ jsxs10(Fragment3, { children: [
|
|
3884
|
+
/* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "No browser? Open this link yourself (c copies it):" }),
|
|
3885
|
+
/* @__PURE__ */ jsx12(Text10, { children: authUrl }),
|
|
3886
|
+
/* @__PURE__ */ jsx12(Text10, { color: C.muted, children: `Over SSH, forward its port first: ssh -L ${loopbackPort(authUrl)}:127.0.0.1:${loopbackPort(authUrl)} <host>` })
|
|
3640
3887
|
] })
|
|
3641
3888
|
] }),
|
|
3642
|
-
mode.kind === "rename" && /* @__PURE__ */
|
|
3643
|
-
loadError && /* @__PURE__ */
|
|
3644
|
-
message && /* @__PURE__ */
|
|
3645
|
-
mode.kind === "list" && /* @__PURE__ */
|
|
3646
|
-
/* @__PURE__ */
|
|
3889
|
+
mode.kind === "rename" && /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "enter save esc cancel" }),
|
|
3890
|
+
loadError && /* @__PURE__ */ jsx12(Text10, { color: C.red, children: loadError }),
|
|
3891
|
+
message && /* @__PURE__ */ jsx12(Text10, { color: message.error ? C.red : C.green, children: message.text }),
|
|
3892
|
+
mode.kind === "list" && /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, flexWrap: "wrap", children: [
|
|
3893
|
+
/* @__PURE__ */ jsx12(Box10, { marginRight: 2, children: /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "j/k move" }) }),
|
|
3647
3894
|
row?.calendar && button("space", "show/hide", " "),
|
|
3648
3895
|
row && button("r", "sync"),
|
|
3649
3896
|
button("R", "sync all"),
|
|
@@ -3652,7 +3899,7 @@ function Accounts({ onClose }) {
|
|
|
3652
3899
|
row && button("x", "remove"),
|
|
3653
3900
|
button("g", "add Google"),
|
|
3654
3901
|
button("a", "add CalDAV"),
|
|
3655
|
-
/* @__PURE__ */
|
|
3902
|
+
/* @__PURE__ */ jsx12(Button, { k: "esc", label: "close", onPress: onClose })
|
|
3656
3903
|
] })
|
|
3657
3904
|
] });
|
|
3658
3905
|
}
|
|
@@ -3664,161 +3911,6 @@ var loopbackPort = (authUrl) => {
|
|
|
3664
3911
|
}
|
|
3665
3912
|
};
|
|
3666
3913
|
|
|
3667
|
-
// src/ui/screens/Invites.tsx
|
|
3668
|
-
import { useMemo as useMemo5, useRef as useRef6, useState as useState6 } from "react";
|
|
3669
|
-
import { Box as Box9, Text as Text9 } from "ink";
|
|
3670
|
-
import { addDays as addDays8, startOfDay as startOfDay6 } from "date-fns";
|
|
3671
|
-
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3672
|
-
var INVITE_DAYS = 60;
|
|
3673
|
-
function useUpcoming(now) {
|
|
3674
|
-
const day = startOfDay6(now).getTime();
|
|
3675
|
-
const range = useMemo5(() => ({ start: new Date(day).toISOString(), end: addDays8(day, INVITE_DAYS).toISOString() }), [day]);
|
|
3676
|
-
return useEvents(range);
|
|
3677
|
-
}
|
|
3678
|
-
var REPLY = { y: "accepted", n: "declined", m: "tentative" };
|
|
3679
|
-
function Invites({ onClose }) {
|
|
3680
|
-
const api = useApi();
|
|
3681
|
-
const [now] = useState6(() => /* @__PURE__ */ new Date());
|
|
3682
|
-
const { events, loaded } = useUpcoming(now);
|
|
3683
|
-
const invites = useMemo5(() => pendingInvites(events, now), [events, now]);
|
|
3684
|
-
const { accounts } = useDirectory();
|
|
3685
|
-
const [idx, setIdx] = useState6(0);
|
|
3686
|
-
const [rows, setRows] = useState6({});
|
|
3687
|
-
const busy = useRef6(/* @__PURE__ */ new Set());
|
|
3688
|
-
const [error, setError] = useState6("");
|
|
3689
|
-
const sel = Math.min(idx, Math.max(invites.length - 1, 0));
|
|
3690
|
-
const reply = (e, status) => {
|
|
3691
|
-
const k = eventKey(e);
|
|
3692
|
-
if (busy.current.has(k)) return;
|
|
3693
|
-
busy.current.add(k);
|
|
3694
|
-
setError("");
|
|
3695
|
-
setRows((r) => ({ ...r, [k]: "sending\u2026" }));
|
|
3696
|
-
api.events.respond(e, status).then(
|
|
3697
|
-
() => setRows((r) => ({ ...r, [k]: "sent" })),
|
|
3698
|
-
(err) => {
|
|
3699
|
-
busy.current.delete(k);
|
|
3700
|
-
setRows(({ [k]: _, ...r }) => r);
|
|
3701
|
-
setError(errorText(err));
|
|
3702
|
-
}
|
|
3703
|
-
);
|
|
3704
|
-
};
|
|
3705
|
-
const move = (dir) => setIdx(Math.max(Math.min(sel + dir, invites.length - 1), 0));
|
|
3706
|
-
const replySelected = (status) => void (invites[sel] && reply(invites[sel], status));
|
|
3707
|
-
useKeys((input, key3) => {
|
|
3708
|
-
if (key3.escape || input === "q") return onClose();
|
|
3709
|
-
if (input === "j" || key3.downArrow) return move(1);
|
|
3710
|
-
if (input === "k" || key3.upArrow) return move(-1);
|
|
3711
|
-
if (Object.hasOwn(REPLY, input)) replySelected(REPLY[input]);
|
|
3712
|
-
});
|
|
3713
|
-
return /* @__PURE__ */ jsxs9(Clickable, { flexDirection: "column", borderStyle: "round", borderColor: C.muted, paddingX: 1, onWheel: move, children: [
|
|
3714
|
-
/* @__PURE__ */ jsx11(Text9, { bold: true, children: "Invitations" }),
|
|
3715
|
-
!loaded ? /* @__PURE__ */ jsx11(Text9, { color: C.muted, children: "Loading\u2026" }) : invites.length === 0 ? /* @__PURE__ */ jsx11(Text9, { color: C.muted, children: "No pending invites" }) : invites.map((e, i) => {
|
|
3716
|
-
const a = accounts.find((x) => x.id === e.accountId);
|
|
3717
|
-
const o = ownerLine(void 0, a?.label ?? e.accountId, a?.email);
|
|
3718
|
-
return /* @__PURE__ */ jsx11(Clickable, { height: 1, onClick: () => setIdx(i), children: /* @__PURE__ */ jsxs9(Text9, { wrap: "truncate", inverse: i === sel, children: [
|
|
3719
|
-
i === sel ? "\u25B8 " : " ",
|
|
3720
|
-
/* @__PURE__ */ jsx11(Text9, { bold: true, children: e.title || "Untitled" }),
|
|
3721
|
-
" ",
|
|
3722
|
-
/* @__PURE__ */ jsx11(Text9, { color: C.muted, children: formatWhen(e) }),
|
|
3723
|
-
" ",
|
|
3724
|
-
/* @__PURE__ */ jsx11(Text9, { color: ansiOf(a?.color ?? ""), children: "\u25CF" }),
|
|
3725
|
-
" ",
|
|
3726
|
-
o.label,
|
|
3727
|
-
o.email ? ` \xB7 ${o.email}` : "",
|
|
3728
|
-
rows[eventKey(e)] ? /* @__PURE__ */ jsxs9(Text9, { color: C.yellow, children: [
|
|
3729
|
-
" ",
|
|
3730
|
-
rows[eventKey(e)]
|
|
3731
|
-
] }) : null
|
|
3732
|
-
] }) }, eventKey(e));
|
|
3733
|
-
}),
|
|
3734
|
-
error ? /* @__PURE__ */ jsx11(Text9, { color: C.red, children: error }) : null,
|
|
3735
|
-
/* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexWrap: "wrap", children: [
|
|
3736
|
-
invites.length > 0 && /* @__PURE__ */ jsx11(Button, { k: "y", label: "accept", color: C.green, onPress: () => replySelected("accepted") }),
|
|
3737
|
-
invites.length > 0 && /* @__PURE__ */ jsx11(Button, { k: "n", label: "decline", color: C.red, onPress: () => replySelected("declined") }),
|
|
3738
|
-
invites.length > 0 && /* @__PURE__ */ jsx11(Button, { k: "m", label: "maybe", color: C.yellow, onPress: () => replySelected("tentative") }),
|
|
3739
|
-
/* @__PURE__ */ jsx11(Box9, { marginRight: 2, children: /* @__PURE__ */ jsx11(Text9, { color: C.muted, children: "j/k move" }) }),
|
|
3740
|
-
/* @__PURE__ */ jsx11(Button, { k: "esc", label: "close", onPress: onClose })
|
|
3741
|
-
] })
|
|
3742
|
-
] });
|
|
3743
|
-
}
|
|
3744
|
-
|
|
3745
|
-
// src/ui/StatusLine.tsx
|
|
3746
|
-
import { useEffect as useEffect5, useState as useState7 } from "react";
|
|
3747
|
-
import { Box as Box10, Text as Text10 } from "ink";
|
|
3748
|
-
import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3749
|
-
var DAY_MS = 24 * 36e5;
|
|
3750
|
-
var noop = () => {
|
|
3751
|
-
};
|
|
3752
|
-
var FRAMES = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
|
|
3753
|
-
function Spinner() {
|
|
3754
|
-
const [i, setI] = useState7(0);
|
|
3755
|
-
useEffect5(() => {
|
|
3756
|
-
const t = setInterval(() => setI((n) => (n + 1) % FRAMES.length), 100);
|
|
3757
|
-
return () => clearInterval(t);
|
|
3758
|
-
}, []);
|
|
3759
|
-
return /* @__PURE__ */ jsx12(Text10, { color: C.accent, children: FRAMES[i] });
|
|
3760
|
-
}
|
|
3761
|
-
function StatusLine({ now, message, update, width, onOpen, onInvites, onAccounts, onHelp, onNew, onSync, onQuit }) {
|
|
3762
|
-
const { events } = useUpcoming(now);
|
|
3763
|
-
const { accounts } = useDirectory();
|
|
3764
|
-
const failed = accounts.filter((a) => a.error);
|
|
3765
|
-
const syncing = accounts.filter((a) => a.syncing);
|
|
3766
|
-
const { current, next } = pickNowNext(events, now);
|
|
3767
|
-
const soon = next && Date.parse(next.start) - now.getTime() < DAY_MS ? next : void 0;
|
|
3768
|
-
const invites = pendingInvites(events, now).length;
|
|
3769
|
-
const parts = [];
|
|
3770
|
-
if (syncing.length) parts.push([/* @__PURE__ */ jsxs10(Fragment3, { children: [
|
|
3771
|
-
/* @__PURE__ */ jsx12(Spinner, {}),
|
|
3772
|
-
" ",
|
|
3773
|
-
/* @__PURE__ */ jsxs10(Text10, { color: C.muted, children: [
|
|
3774
|
-
"syncing ",
|
|
3775
|
-
syncing.map((a) => a.label).join(", ")
|
|
3776
|
-
] })
|
|
3777
|
-
] }), onAccounts]);
|
|
3778
|
-
for (const e of current) parts.push([/* @__PURE__ */ jsxs10(Fragment3, { children: [
|
|
3779
|
-
/* @__PURE__ */ jsx12(Text10, { color: C.green, bold: true, children: "now" }),
|
|
3780
|
-
" ",
|
|
3781
|
-
e.title || "Untitled"
|
|
3782
|
-
] }), onOpen && (() => onOpen(e))]);
|
|
3783
|
-
if (soon) parts.push([/* @__PURE__ */ jsxs10(Fragment3, { children: [
|
|
3784
|
-
/* @__PURE__ */ jsx12(Text10, { color: C.cyan, bold: true, children: "next" }),
|
|
3785
|
-
" ",
|
|
3786
|
-
soon.title || "Untitled",
|
|
3787
|
-
" ",
|
|
3788
|
-
/* @__PURE__ */ jsx12(Text10, { color: C.yellow, children: startsLabel(soon.start, now) })
|
|
3789
|
-
] }), onOpen && (() => onOpen(soon))]);
|
|
3790
|
-
if (invites) parts.push([/* @__PURE__ */ jsxs10(Text10, { color: C.yellow, children: [
|
|
3791
|
-
invites === 1 ? "1 invite" : `${invites} invites`,
|
|
3792
|
-
" (i)"
|
|
3793
|
-
] }), onInvites]);
|
|
3794
|
-
if (failed.length) parts.push([/* @__PURE__ */ jsxs10(Text10, { color: C.red, children: [
|
|
3795
|
-
"\u26A0 ",
|
|
3796
|
-
failed.map((a) => a.label).join(", "),
|
|
3797
|
-
" sync failed (s)"
|
|
3798
|
-
] }), onAccounts]);
|
|
3799
|
-
if (message) parts.push([message]);
|
|
3800
|
-
if (!parts.length) parts.push([/* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "nothing on in the next 24h" })]);
|
|
3801
|
-
if (update) parts.push([/* @__PURE__ */ jsxs10(Text10, { color: C.yellow, children: [
|
|
3802
|
-
"\u2191 ",
|
|
3803
|
-
update,
|
|
3804
|
-
" available \xB7 npm i -g mysticals"
|
|
3805
|
-
] })]);
|
|
3806
|
-
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", width, height: 2, children: [
|
|
3807
|
-
/* @__PURE__ */ jsx12(Box10, { width, height: 1, overflow: "hidden", children: parts.map(([p, onClick], i) => /* @__PURE__ */ jsx12(Clickable, { flexShrink: 0, onClick, children: /* @__PURE__ */ jsxs10(Text10, { children: [
|
|
3808
|
-
i > 0 && /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: " \xB7 " }),
|
|
3809
|
-
p
|
|
3810
|
-
] }) }, i)) }),
|
|
3811
|
-
/* @__PURE__ */ jsxs10(Box10, { width, height: 1, overflow: "hidden", children: [
|
|
3812
|
-
/* @__PURE__ */ jsx12(Button, { k: "n", label: "new", onPress: onNew ?? noop }),
|
|
3813
|
-
/* @__PURE__ */ jsx12(Button, { k: "r", label: "sync", onPress: onSync ?? noop }),
|
|
3814
|
-
/* @__PURE__ */ jsx12(Button, { k: "i", label: "invites", onPress: onInvites ?? noop }),
|
|
3815
|
-
/* @__PURE__ */ jsx12(Button, { k: "s", label: "accounts", onPress: onAccounts ?? noop }),
|
|
3816
|
-
/* @__PURE__ */ jsx12(Button, { k: "?", label: "help", onPress: onHelp ?? noop }),
|
|
3817
|
-
/* @__PURE__ */ jsx12(Button, { k: "q", label: "quit", onPress: onQuit ?? noop })
|
|
3818
|
-
] })
|
|
3819
|
-
] });
|
|
3820
|
-
}
|
|
3821
|
-
|
|
3822
3914
|
// src/ui/App.tsx
|
|
3823
3915
|
import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3824
3916
|
var VIEWS = { agenda: Agenda, day: Day, "2day": TwoDay, week: Week, month: Month };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mysticals",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Isolated multi-account calendar for the terminal on macOS, Linux and Windows",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"calendar",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"tsup": "^8.5.1",
|
|
48
48
|
"typescript": "^5.9.3",
|
|
49
49
|
"vitest": "^3.2.7",
|
|
50
|
-
"@mysticals/core": "0.
|
|
50
|
+
"@mysticals/core": "0.2.0"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"dev": "tsup --watch",
|