mysticals 0.1.1 → 0.2.1

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.
Files changed (2) hide show
  1. package/dist/cli.js +331 -239
  2. 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.1.1";
8
+ var version = "0.2.1";
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
- /** `quiet`: the change came from this app (own edit/RSVP), so don't notify about it. */
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
- await store.getProvider(cached2.accountId).deleteEvent(cached2, Scope.parse(scope));
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
- return { day, count: on.length, allDay: on.filter((e) => e.allDay), placed: slots(events, day) };
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
- bar,
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(`h${h}`, gutter, (c) => hourCells(c.placed, c.day, h, current && isSameDay6(c.day, now)));
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 useEffect4, useRef as useRef5, useState as useState5 } from "react";
3303
- import { Box as Box8, Text as Text8, useStdout as useStdout2 } from "ink";
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 useRef4, useState as useState4 } from "react";
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 = useRef4(init);
3340
- const [, bump] = useState4(0);
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__ */ jsxs7(Text7, { color: ansiOf(v.color), children: [
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__ */ jsx9(Text7, { color: C.muted, children: f === "serverUrl" ? "https://caldav.example.com/" : f === "username" ? "you@company.com" : "Work" });
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__ */ jsxs7(Box7, { flexDirection: "column", children: [
3425
- /* @__PURE__ */ jsx9(Text7, { bold: true, children: "Add CalDAV account" }),
3426
- /* @__PURE__ */ jsx9(Text7, { color: C.muted, children: "Each account is isolated: events and invitations are only sent from the account they belong to." }),
3427
- FIELDS.map((f) => /* @__PURE__ */ jsx9(Clickable, { onClick: () => clickField(f), children: /* @__PURE__ */ jsxs7(Text7, { children: [
3428
- /* @__PURE__ */ jsx9(Text7, { color: f === v.field ? C.cyan : void 0, children: `${f === v.field ? "\u203A" : " "} ${NAMES[f].padEnd(13)}` }),
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__ */ jsx9(Text7, { color: C.cyan, children: "\u258F" }) : null
3667
+ f === v.field && f !== "provider" && f !== "color" ? /* @__PURE__ */ jsx11(Text9, { color: C.cyan, children: "\u258F" }) : null
3431
3668
  ] }) }, f)),
3432
- /* @__PURE__ */ jsx9(Text7, { color: C.muted, children: "Check your provider's docs if connection fails." }),
3433
- v.busy && /* @__PURE__ */ jsx9(Text7, { color: C.yellow, children: "Verifying\u2026" }),
3434
- v.error && /* @__PURE__ */ jsx9(Text7, { color: C.red, children: v.error }),
3435
- /* @__PURE__ */ jsxs7(Box7, { flexWrap: "wrap", children: [
3436
- /* @__PURE__ */ jsx9(Box7, { marginRight: 2, children: /* @__PURE__ */ jsx9(Text7, { color: C.muted, children: "tab/\u2191\u2193 field \u2190\u2192 change provider/colour" }) }),
3437
- /* @__PURE__ */ jsx9(Button, { k: "enter", label: "add account", color: C.green, onPress: () => !get().busy && submit() }),
3438
- /* @__PURE__ */ jsx9(Button, { k: "esc", label: "back", onPress: () => !get().busy && onBack() })
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 Fragment2, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
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] = useState5();
3452
- const [authUrl, setAuthUrl] = useState5();
3453
- useEffect4(() => api.onAuthUrl(setAuthUrl), [api]);
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 = useRef5(0);
3461
- const alive2 = useRef5(true);
3462
- useEffect4(() => () => void (alive2.current = false), []);
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__ */ jsx10(Box8, { flexDirection: "column", borderStyle: "round", borderColor: C.muted, paddingX: 1, children: /* @__PURE__ */ jsx10(
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__ */ jsx10(Button, { k, label, onPress: () => listing() && act(input) });
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__ */ jsxs8(Clickable, { flexDirection: "column", borderStyle: "round", borderColor: C.muted, paddingX: 1, onWheel: (dir) => listing() && move(dir), children: [
3576
- /* @__PURE__ */ jsx10(Text8, { bold: true, children: "Accounts & calendars" }),
3577
- /* @__PURE__ */ jsx10(Text8, { color: C.muted, children: "Accounts sync automatically every 2 minutes." }),
3578
- loaded && !accounts.length && /* @__PURE__ */ jsx10(Text8, { color: C.muted, children: "No accounts yet. Press g (Google) or a (CalDAV) to add one." }),
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__ */ jsx10(Text8, { color: C.cyan, children: sel ? "\u203A " : " " });
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__ */ jsx10(Clickable, { onClick: () => click(i), children: /* @__PURE__ */ jsxs8(Text8, { inverse: sel, children: [
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__ */ jsx10(Text8, { color: ansiOf(c.color), children: "\u25CF" }),
3831
+ /* @__PURE__ */ jsx12(Text10, { color: ansiOf(c.color), children: "\u25CF" }),
3590
3832
  " ",
3591
3833
  c.name,
3592
- c.readOnly && /* @__PURE__ */ jsx10(Text8, { color: C.muted, children: " (read-only)" })
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__ */ jsxs8(Box8, { flexDirection: "column", children: [
3598
- /* @__PURE__ */ jsx10(Clickable, { onClick: () => click(i), children: /* @__PURE__ */ jsxs8(Text8, { inverse: sel && !renaming, children: [
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__ */ jsx10(Text8, { color: ansiOf(a.color), children: "\u25CF" }),
3842
+ /* @__PURE__ */ jsx12(Text10, { color: ansiOf(a.color), children: "\u25CF" }),
3601
3843
  " ",
3602
- renaming ? /* @__PURE__ */ jsxs8(Text8, { color: C.cyan, children: [
3844
+ renaming ? /* @__PURE__ */ jsxs10(Text10, { color: C.cyan, children: [
3603
3845
  mode.value,
3604
3846
  "\u258F"
3605
- ] }) : /* @__PURE__ */ jsx10(Text8, { bold: true, children: a.label }),
3847
+ ] }) : /* @__PURE__ */ jsx12(Text10, { bold: true, children: a.label }),
3606
3848
  " ",
3607
- /* @__PURE__ */ jsxs8(Text8, { color: C.muted, children: [
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.error && /* @__PURE__ */ jsx10(Text8, { color: C.red, children: ` Last sync failed: ${a.error}` })
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__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, children: [
3617
- /* @__PURE__ */ jsxs8(Text8, { color: C.red, bold: true, children: [
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__ */ jsxs8(Text8, { children: [
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__ */ jsxs8(Box8, { children: [
3630
- /* @__PURE__ */ jsx10(Button, { k: "y", label: "remove", color: C.red, onPress: () => confirmRemove(true) }),
3631
- /* @__PURE__ */ jsx10(Button, { k: "n", label: "keep", onPress: () => confirmRemove(false) })
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__ */ jsxs8(Box8, { flexDirection: "column", children: [
3635
- /* @__PURE__ */ jsx10(Text8, { color: C.yellow, children: "Opening browser \u2014 finish sign-in there\u2026 (esc to stop waiting)" }),
3636
- authUrl && /* @__PURE__ */ jsxs8(Fragment2, { children: [
3637
- /* @__PURE__ */ jsx10(Text8, { color: C.muted, children: "No browser? Open this link yourself (c copies it):" }),
3638
- /* @__PURE__ */ jsx10(Text8, { children: authUrl }),
3639
- /* @__PURE__ */ jsx10(Text8, { color: C.muted, children: `Over SSH, forward its port first: ssh -L ${loopbackPort(authUrl)}:127.0.0.1:${loopbackPort(authUrl)} <host>` })
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__ */ jsx10(Text8, { color: C.muted, children: "enter save esc cancel" }),
3643
- loadError && /* @__PURE__ */ jsx10(Text8, { color: C.red, children: loadError }),
3644
- message && /* @__PURE__ */ jsx10(Text8, { color: message.error ? C.red : C.green, children: message.text }),
3645
- mode.kind === "list" && /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexWrap: "wrap", children: [
3646
- /* @__PURE__ */ jsx10(Box8, { marginRight: 2, children: /* @__PURE__ */ jsx10(Text8, { color: C.muted, children: "j/k move" }) }),
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__ */ jsx10(Button, { k: "esc", label: "close", onPress: onClose })
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.1.1",
3
+ "version": "0.2.1",
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.1.1"
50
+ "@mysticals/core": "0.2.1"
51
51
  },
52
52
  "scripts": {
53
53
  "dev": "tsup --watch",