@emailthing/cli 0.0.0-alpha.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.
package/dist/main.js ADDED
@@ -0,0 +1,1082 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ import {
4
+ EmailThingCLI,
5
+ syncData
6
+ } from "./index-xp4bqj3r.js";
7
+ import {
8
+ getDB,
9
+ loadAuth,
10
+ saveAuth
11
+ } from "./config-cfsae2jm.js";
12
+ import {
13
+ __callDispose,
14
+ __require,
15
+ __toESM,
16
+ __using
17
+ } from "./index-afyvwmt5.js";
18
+
19
+ // src/utils/colors.ts
20
+ var colors = {
21
+ reset: "\x1B[0m" /* RESET */,
22
+ bright: "\x1B[1m" /* BRIGHT */,
23
+ dim: "\x1B[2m" /* DIM */,
24
+ fg: {
25
+ black: "\x1B[30m" /* BLACK */,
26
+ red: "\x1B[31m" /* RED */,
27
+ green: "\x1B[32m" /* GREEN */,
28
+ yellow: "\x1B[33m" /* YELLOW */,
29
+ blue: "\x1B[34m" /* BLUE */,
30
+ magenta: "\x1B[35m" /* MAGENTA */,
31
+ cyan: "\x1B[36m" /* CYAN */,
32
+ white: "\x1B[37m" /* WHITE */,
33
+ gray: "\x1B[90m" /* GRAY */
34
+ },
35
+ bg: {
36
+ black: "\x1B[40m" /* BLACK */,
37
+ red: "\x1B[41m" /* RED */,
38
+ green: "\x1B[42m" /* GREEN */,
39
+ yellow: "\x1B[43m" /* YELLOW */,
40
+ blue: "\x1B[44m" /* BLUE */,
41
+ magenta: "\x1B[45m" /* MAGENTA */,
42
+ cyan: "\x1B[46m" /* CYAN */,
43
+ white: "\x1B[47m" /* WHITE */
44
+ }
45
+ };
46
+
47
+ // src/ui/renderer.ts
48
+ var { stdout, stdin } = process;
49
+ var defaultKeys = [
50
+ "up" /* UP */,
51
+ "down" /* DOWN */,
52
+ "left" /* LEFT */,
53
+ "right" /* RIGHT */,
54
+ "pageup" /* PAGEUP */,
55
+ "pagedown" /* PAGEDOWN */,
56
+ "home" /* HOME */,
57
+ "end" /* END */,
58
+ "enter" /* ENTER */,
59
+ "escape" /* ESCAPE */,
60
+ "ctrl-c" /* CTRL_C */,
61
+ "backspace" /* BACKSPACE */,
62
+ "tab" /* TAB */,
63
+ "ctrl-up" /* CTRL_UP */,
64
+ "ctrl-down" /* CTRL_DOWN */
65
+ ];
66
+
67
+ class TerminalRenderer {
68
+ lastBuffer = [];
69
+ constructor() {
70
+ this.setupTerminal(), this.watchResize(() => this.lastBuffer = []);
71
+ }
72
+ setupTerminal() {
73
+ stdout.write("\x1B[?1049h" /* ALT_SCREEN_ON */), stdout.write("\x1B[?25l" /* HIDE_CURSOR */), stdout.write("\x1B[2J" /* CLEAR_SCREEN */), stdin.setRawMode?.(!0), stdin.resume();
74
+ }
75
+ cleanup() {
76
+ stdout.write("\x1B[?25l" /* HIDE_CURSOR */), stdout.write("\x1B[?1049l" /* ALT_SCREEN_OFF */), stdin.setRawMode?.(!1), this.watchListeners.forEach((fn) => this.unwatchResize(fn));
77
+ }
78
+ [Symbol.dispose]() {
79
+ this.cleanup();
80
+ }
81
+ watchListeners = [];
82
+ watchResize(fn) {
83
+ stdout?.on("resize", fn), this.watchListeners.push(fn);
84
+ }
85
+ unwatchResize(fn) {
86
+ stdout?.off("resize", fn), this.watchListeners = this.watchListeners.filter((f) => f !== fn);
87
+ }
88
+ render(buffer) {
89
+ let { lines, cursor } = buffer, height = stdout.rows || 24, width = stdout.columns || 80, paddedLines = [...lines];
90
+ while (paddedLines.length < height)
91
+ paddedLines.push("");
92
+ let output = "";
93
+ for (let i = 0;i < Math.min(paddedLines.length, height); i++) {
94
+ let line = paddedLines[i], lastLine = this.lastBuffer[i] ?? "";
95
+ if (line !== lastLine)
96
+ output += `\x1B[${i + 1};1H\x1B[2K${line}`;
97
+ }
98
+ if (cursor)
99
+ output += `\x1B[${cursor.row + 1};${cursor.col + 1}H\x1B[?25h`;
100
+ else
101
+ output += "\x1B[?25l" /* HIDE_CURSOR */;
102
+ if (output)
103
+ stdout.write(output);
104
+ this.lastBuffer = paddedLines.slice(0, height);
105
+ }
106
+ clear() {
107
+ stdout.write("\x1B[2J\x1B[H"), this.lastBuffer = [];
108
+ }
109
+ getSize() {
110
+ return { width: stdout.columns || 80, height: stdout.rows || 24 };
111
+ }
112
+ }
113
+ var mapKey = (key) => {
114
+ if (key === "\x1B[A")
115
+ return "up" /* UP */;
116
+ else if (key === "\x1B[B")
117
+ return "down" /* DOWN */;
118
+ else if (key === "\x1B[C")
119
+ return "right" /* RIGHT */;
120
+ else if (key === "\x1B[D")
121
+ return "left" /* LEFT */;
122
+ else if (key === "\x1B[1;5A")
123
+ return "ctrl-up" /* CTRL_UP */;
124
+ else if (key === "\x1B[1;5B")
125
+ return "ctrl-down" /* CTRL_DOWN */;
126
+ else if (key === "\x1B[5~")
127
+ return "pageup" /* PAGEUP */;
128
+ else if (key === "\x1B[6~")
129
+ return "pagedown" /* PAGEDOWN */;
130
+ else if (key === "\x1B[H" || key === "\x1B[1~")
131
+ return "home" /* HOME */;
132
+ else if (key === "\x1B[F" || key === "\x1B[4~")
133
+ return "end" /* END */;
134
+ else if (key === "\r")
135
+ return "enter" /* ENTER */;
136
+ else if (key === "\x1B")
137
+ return "escape" /* ESCAPE */;
138
+ else if (key === "\x03")
139
+ return "ctrl-c" /* CTRL_C */;
140
+ else if (key === "\x7F")
141
+ return "backspace" /* BACKSPACE */;
142
+ else if (key === "\x1B[3~")
143
+ return "delete" /* DELETE */;
144
+ else if (key === "\t")
145
+ return "tab" /* TAB */;
146
+ else if (key === "\x1B[Z")
147
+ return "backtab" /* BACKTAB */;
148
+ else
149
+ return key;
150
+ };
151
+ function readKeys() {
152
+ let queuedKeys = [], resolveNext = null, onData = (data) => {
153
+ let key = mapKey(data.toString());
154
+ if (resolveNext)
155
+ resolveNext(key), resolveNext = null;
156
+ else
157
+ queuedKeys.push(key);
158
+ };
159
+ return stdin.on("data", onData), {
160
+ [Symbol.asyncIterator]() {
161
+ return {
162
+ next: () => new Promise((resolve) => {
163
+ if (queuedKeys.length > 0)
164
+ resolve({ value: queuedKeys.shift(), done: !1 });
165
+ else
166
+ resolveNext = (key) => resolve({ value: key, done: !1 });
167
+ }),
168
+ return: () => {
169
+ return stdin.off("data", onData), Promise.resolve({ value: void 0, done: !0 });
170
+ }
171
+ };
172
+ }
173
+ };
174
+ }
175
+ function truncate(text, maxWidth) {
176
+ let visibleLength = Bun.stringWidth(text, { ambiguousIsNarrow: !1, countAnsiEscapeCodes: !1 });
177
+ if (visibleLength <= maxWidth)
178
+ return text + " ".repeat(maxWidth - visibleLength);
179
+ while (Bun.stringWidth(text, { ambiguousIsNarrow: !1, countAnsiEscapeCodes: !1 }) > maxWidth - 1)
180
+ text = text.slice(0, -1);
181
+ return text + "\u2026";
182
+ }
183
+ function formatDate(dateStr) {
184
+ let date = new Date(dateStr), diff = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
185
+ if (diff < 86400000 /* ONE_DAY_MS */)
186
+ return date.toLocaleTimeString("en-US", {
187
+ hour: "2-digit",
188
+ minute: "2-digit"
189
+ });
190
+ if (diff < 604800000 /* ONE_WEEK_MS */)
191
+ return date.toLocaleDateString("en-US", { weekday: "short" });
192
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
193
+ }
194
+
195
+ // src/ui/login.ts
196
+ async function loginScreen() {
197
+ let __stack = [];
198
+ try {
199
+ const renderer = __using(__stack, new TerminalRenderer, 0);
200
+ let state = {
201
+ username: "",
202
+ password: "",
203
+ focusedField: "username"
204
+ };
205
+ let renderLogin = () => {
206
+ let { width, height } = renderer.getSize(), lines = [], title = "EmailThing CLI - Login", centerX = Math.floor((width - 22) / 2), centerY = Math.floor(height / 2) - 5;
207
+ for (let i = 0;i < centerY; i++)
208
+ lines.push("");
209
+ lines.push(" ".repeat(centerX) + colors.bright + "EmailThing CLI - Login" + colors.reset), lines.push(""), lines.push("");
210
+ let labelWidth = 12, inputWidth = 30, totalWidth = labelWidth + inputWidth + 4, startX = Math.floor((width - totalWidth) / 2), usernameFocused = state.focusedField === "username", passwordFocused = state.focusedField === "password", submitFocused = state.focusedField === "submit", usernameColor = usernameFocused ? colors.fg.cyan : colors.fg.white, passwordColor = passwordFocused ? colors.fg.cyan : colors.fg.white, submitColor = submitFocused ? colors.bg.cyan + colors.fg.black : colors.fg.white, usernameDisplay = state.username.padEnd(inputWidth - 2), passwordDisplay = "\u2022".repeat(state.password.length).padEnd(inputWidth - 2), usernameLineIdx = lines.length;
211
+ lines.push(" ".repeat(startX) + usernameColor + "Username: " + colors.reset + (usernameFocused ? colors.bg.white + colors.fg.black : "") + "[" + usernameDisplay + "]" + colors.reset), lines.push("");
212
+ let passwordLineIdx = lines.length;
213
+ lines.push(" ".repeat(startX) + passwordColor + "Password: " + colors.reset + (passwordFocused ? colors.bg.white + colors.fg.black : "") + "[" + passwordDisplay + "]" + colors.reset), lines.push(""), lines.push("");
214
+ let submitBtn = " Login ", btnStartX = Math.floor((width - submitBtn.length) / 2);
215
+ lines.push(" ".repeat(btnStartX) + submitColor + submitBtn + colors.reset), lines.push(""), lines.push(""), lines.push(" ".repeat(Math.floor((width - 50) / 2)) + colors.dim + "Tab: Next field | Enter: Submit | Ctrl+C: Exit" + colors.reset);
216
+ let cursor = void 0;
217
+ if (usernameFocused)
218
+ cursor = {
219
+ row: usernameLineIdx,
220
+ col: startX + labelWidth + 1 + state.username.length
221
+ };
222
+ else if (passwordFocused)
223
+ cursor = {
224
+ row: passwordLineIdx,
225
+ col: startX + labelWidth + 1 + state.password.length
226
+ };
227
+ renderer.render({ lines, cursor });
228
+ };
229
+ try {
230
+ renderLogin(), renderer.watchResize(renderLogin);
231
+ for await (let key of readKeys()) {
232
+ if (key === "ctrl-c" /* CTRL_C */)
233
+ return renderer.cleanup(), null;
234
+ if (key === "tab" /* TAB */)
235
+ if (state.focusedField === "username")
236
+ state.focusedField = "password";
237
+ else if (state.focusedField === "password")
238
+ state.focusedField = "submit";
239
+ else
240
+ state.focusedField = "username";
241
+ if (key === "enter" /* ENTER */) {
242
+ if (state.focusedField === "submit" || state.username && state.password)
243
+ return { username: state.username, password: state.password };
244
+ if (state.focusedField === "username")
245
+ state.focusedField = "password";
246
+ else if (state.focusedField === "password")
247
+ state.focusedField = "submit";
248
+ }
249
+ if (key === "backspace" /* BACKSPACE */) {
250
+ if (state.focusedField === "username" && state.username.length > 0)
251
+ state.username = state.username.slice(0, -1);
252
+ else if (state.focusedField === "password" && state.password.length > 0)
253
+ state.password = state.password.slice(0, -1);
254
+ }
255
+ if (key.length >= 1 && key >= " " && key <= "~" && !defaultKeys.includes(key)) {
256
+ if (state.focusedField === "username")
257
+ state.username += key;
258
+ else if (state.focusedField === "password")
259
+ state.password += key;
260
+ }
261
+ renderLogin();
262
+ }
263
+ throw Error("Unexpected end of input");
264
+ } finally {
265
+ renderer.cleanup();
266
+ }
267
+ } catch (_catch) {
268
+ var _err = _catch, _hasErr = 1;
269
+ } finally {
270
+ __callDispose(__stack, _err, _hasErr);
271
+ }
272
+ }
273
+
274
+ // src/ui/email-list.ts
275
+ async function emailListScreen(db, mailboxId, syncCallback = null, restoreId, modifyEmail) {
276
+ let __stack = [];
277
+ try {
278
+ const renderer = __using(__stack, new TerminalRenderer, 0);
279
+ let loadEmails = () => {
280
+ return db.query("SELECT id, subject, from_addr, snippet, isRead, isStarred, createdAt, categoryId FROM emails WHERE mailboxId = ? AND isDeleted = false ORDER BY createdAt DESC").all(mailboxId).map((e) => ({
281
+ ...e,
282
+ isRead: e.isRead === 1,
283
+ isStarred: e.isStarred === 1
284
+ }));
285
+ };
286
+ let loadCategories = () => {
287
+ let cats = db.query("SELECT * FROM categories WHERE mailboxId = ? AND isDeleted = false").all(mailboxId);
288
+ return new Map(cats.map((c) => [c.id, c]));
289
+ };
290
+ let mailbox = db.query("SELECT * FROM mailboxes WHERE id = ? AND isDeleted = false").get(mailboxId);
291
+ let emails = loadEmails();
292
+ let isSyncing = !1;
293
+ let state = {
294
+ emails,
295
+ categories: loadCategories(),
296
+ selectedEmailId: emails.length > 0 ? restoreId && emails.find((e) => e.id === restoreId)?.id || emails[0].id : null,
297
+ scrollOffset: restoreId ? Math.max(0, Math.min(emails.findIndex((e) => e.id === restoreId) - 1, emails.length - renderer.getSize().height + 4)) : 0,
298
+ mailboxName: mailbox?.address || "Inbox",
299
+ syncDots: 0
300
+ };
301
+ if (syncCallback && !restoreId)
302
+ isSyncing = !0, syncCallback().then(() => {
303
+ isSyncing = !1, state.emails = loadEmails(), state.categories = loadCategories(), renderEmailList();
304
+ });
305
+ let renderEmailList = () => {
306
+ let { width, height } = renderer.getSize(), lines = [], header = `${colors.bright}${state.mailboxName}${colors.reset} (${state.emails.length} emails)`;
307
+ lines.push(header), lines.push("\u2500".repeat(width));
308
+ let listHeight = height - 4, needsScroll = state.emails.length > listHeight, visibleStart = needsScroll ? state.scrollOffset : 0, visibleEnd = Math.min(visibleStart + listHeight, state.emails.length), scrollbarHeight = listHeight, scrollbarThumbSize = Math.max(1, Math.floor(listHeight / state.emails.length * scrollbarHeight)), scrollbarThumbPosition = Math.floor(visibleStart / state.emails.length * scrollbarHeight);
309
+ if (state.emails.length === 0)
310
+ lines.push(""), lines.push(colors.dim + "No emails" + colors.reset);
311
+ else
312
+ for (let i = visibleStart;i < visibleEnd; i++) {
313
+ let email = state.emails[i], isSelected = email.id === state.selectedEmailId, from = truncate(email.from_addr.split("<")[0].trim() || email.from_addr, 20), subject = truncate(email.subject || "(no subject)", width - 37), date = formatDate(email.createdAt), icon = " ", dot = email.isStarred ? "\u272A" : "\u25CF";
314
+ if (email.categoryId) {
315
+ let category = state.categories.get(email.categoryId);
316
+ if (category?.color) {
317
+ let color = Bun.color(category.color, "ansi");
318
+ if (email.isRead)
319
+ icon = `${colors.dim}${color}${dot}${colors.reset}`;
320
+ else
321
+ icon = `${color}${dot}${colors.reset}`;
322
+ }
323
+ } else if (!email.isRead)
324
+ icon = email.isStarred ? `${"\x1B[33m" /* YELLOW */}${dot}` : dot;
325
+ else if (email.isStarred)
326
+ icon = `${colors.dim}${"\x1B[33m" /* YELLOW */}\u2605`;
327
+ let bg = isSelected ? colors.bg.blue : "", fg = email.isStarred ? email.isRead ? colors.fg.yellow + colors.dim : colors.bright + colors.fg.yellow : email.isRead ? colors.dim : colors.bright, reset = colors.reset, scrollbarChar = " ";
328
+ if (needsScroll) {
329
+ let lineIndex = i - visibleStart;
330
+ if (lineIndex >= scrollbarThumbPosition && lineIndex < scrollbarThumbPosition + scrollbarThumbSize)
331
+ scrollbarChar = colors.dim + "\u2588" + colors.reset;
332
+ else
333
+ scrollbarChar = colors.dim + "\u2502" + colors.reset;
334
+ }
335
+ let line = bg + " " + icon + fg + bg + " " + from.padEnd(20) + " " + subject.padEnd(width - 52) + " " + date.padStart(10) + reset + " " + scrollbarChar;
336
+ lines.push(line);
337
+ }
338
+ lines.push("\u2500".repeat(width));
339
+ let statusLine = "Enter: View | s: Star | u: Read/Unread | c: Compose | r: Refresh | m: Mailbox | q: Quit", syncIndicator = "";
340
+ if (isSyncing)
341
+ syncIndicator = " Syncing" + (".".repeat(state.syncDots + 1) + " ".repeat(3 - state.syncDots - 1));
342
+ let _truncate = (text, length) => text.length > length ? text.substring(0, length - 1) + "\u2026" : text.padEnd(length, " ");
343
+ lines.push(colors.dim + _truncate(statusLine, width - syncIndicator.length) + syncIndicator + colors.reset), renderer.render({ lines });
344
+ };
345
+ let animInterval = setInterval(() => {
346
+ if (isSyncing)
347
+ state.syncDots = (state.syncDots + 1) % 3;
348
+ renderEmailList();
349
+ }, 500);
350
+ try {
351
+ if (syncCallback && !restoreId)
352
+ isSyncing = !0, syncCallback().then(() => {
353
+ isSyncing = !1, state.emails = loadEmails(), state.categories = loadCategories(), renderEmailList();
354
+ });
355
+ renderEmailList(), renderer.watchResize(renderEmailList);
356
+ for await (let key of readKeys()) {
357
+ if (key === "q" || key === "ctrl-c" /* CTRL_C */)
358
+ return renderer.cleanup(), { action: "quit" };
359
+ if (key === "c")
360
+ return { action: "compose" };
361
+ if (key === "r") {
362
+ if (syncCallback && !isSyncing)
363
+ isSyncing = !0, syncCallback().then(() => {
364
+ isSyncing = !1, state.emails = loadEmails(), state.categories = loadCategories(), renderEmailList();
365
+ });
366
+ continue;
367
+ }
368
+ if (key === "m")
369
+ return { action: "switch" };
370
+ if (key === "s" && modifyEmail && state.emails.length > 0 && state.selectedEmailId) {
371
+ let idx = state.emails.findIndex((e) => e.id === state.selectedEmailId);
372
+ if (idx !== -1) {
373
+ let email = state.emails[idx], newStarred = !email.isStarred;
374
+ db.run("UPDATE emails SET isStarred = ? WHERE id = ?", [newStarred ? 1 : 0, email.id]), state.emails[idx].isStarred = newStarred, modifyEmail({ id: email.id, mailboxId, isStarred: newStarred });
375
+ }
376
+ }
377
+ if (key === "u" && modifyEmail && state.emails.length > 0 && state.selectedEmailId) {
378
+ let idx = state.emails.findIndex((e) => e.id === state.selectedEmailId);
379
+ if (idx !== -1) {
380
+ let email = state.emails[idx], newRead = !email.isRead;
381
+ db.run("UPDATE emails SET isRead = ? WHERE id = ?", [newRead ? 1 : 0, email.id]), state.emails[idx].isRead = newRead, modifyEmail({ id: email.id, mailboxId, isRead: newRead });
382
+ }
383
+ }
384
+ let FAST_SCROLL_AMOUNT = 5;
385
+ if (key === "up" /* UP */ && state.emails.length > 0 && state.selectedEmailId) {
386
+ let idx = state.emails.findIndex((e) => e.id === state.selectedEmailId);
387
+ if (idx > 0) {
388
+ state.selectedEmailId = state.emails[idx - 1].id;
389
+ let { height } = renderer.getSize(), listHeight = height - 4;
390
+ if (idx - 1 < state.scrollOffset + 1 && state.scrollOffset > 0)
391
+ state.scrollOffset = Math.max(0, idx - 1 - 1);
392
+ }
393
+ }
394
+ if (key === "ctrl-up" /* CTRL_UP */ && state.emails.length > 0 && state.selectedEmailId) {
395
+ let idx = state.emails.findIndex((e) => e.id === state.selectedEmailId);
396
+ if (idx > 0) {
397
+ let newIdx = Math.max(0, idx - FAST_SCROLL_AMOUNT);
398
+ state.selectedEmailId = state.emails[newIdx].id;
399
+ let { height } = renderer.getSize(), listHeight = height - 4;
400
+ state.scrollOffset = Math.max(0, newIdx - Math.floor(listHeight / 2));
401
+ }
402
+ }
403
+ if (key === "down" /* DOWN */ && state.emails.length > 0 && state.selectedEmailId) {
404
+ let idx = state.emails.findIndex((e) => e.id === state.selectedEmailId);
405
+ if (idx < state.emails.length - 1) {
406
+ state.selectedEmailId = state.emails[idx + 1].id;
407
+ let { height } = renderer.getSize(), listHeight = height - 4;
408
+ if (idx + 1 >= state.scrollOffset + listHeight - 1)
409
+ state.scrollOffset = Math.min(state.emails.length - listHeight, idx + 1 - listHeight + 2);
410
+ }
411
+ }
412
+ if (key === "ctrl-down" /* CTRL_DOWN */ && state.emails.length > 0 && state.selectedEmailId) {
413
+ let idx = state.emails.findIndex((e) => e.id === state.selectedEmailId);
414
+ if (idx < state.emails.length - 1) {
415
+ let newIdx = Math.min(state.emails.length - 1, idx + FAST_SCROLL_AMOUNT);
416
+ state.selectedEmailId = state.emails[newIdx].id;
417
+ let { height } = renderer.getSize(), listHeight = height - 4;
418
+ state.scrollOffset = Math.max(0, Math.min(state.emails.length - listHeight, newIdx - Math.floor(listHeight / 2)));
419
+ }
420
+ }
421
+ if (key === "pageup" /* PAGEUP */ && state.emails.length > 0 && state.selectedEmailId) {
422
+ let idx = state.emails.findIndex((e) => e.id === state.selectedEmailId);
423
+ if (idx > 0) {
424
+ let { height } = renderer.getSize(), listHeight = height - 4, newIdx = Math.max(0, idx - listHeight);
425
+ state.selectedEmailId = state.emails[newIdx].id, state.scrollOffset = Math.max(0, state.scrollOffset - listHeight);
426
+ }
427
+ }
428
+ if (key === "pagedown" /* PAGEDOWN */ && state.emails.length > 0 && state.selectedEmailId) {
429
+ let idx = state.emails.findIndex((e) => e.id === state.selectedEmailId);
430
+ if (idx < state.emails.length - 1) {
431
+ let { height } = renderer.getSize(), listHeight = height - 4, newIdx = Math.min(state.emails.length - 1, idx + listHeight);
432
+ state.selectedEmailId = state.emails[newIdx].id;
433
+ let maxScroll = Math.max(0, state.emails.length - listHeight);
434
+ state.scrollOffset = Math.min(maxScroll, state.scrollOffset + listHeight);
435
+ }
436
+ }
437
+ if (key === "home" /* HOME */ && state.emails.length > 0)
438
+ state.selectedEmailId = state.emails[0].id, state.scrollOffset = 0;
439
+ if (key === "end" /* END */ && state.emails.length > 0) {
440
+ state.selectedEmailId = state.emails[state.emails.length - 1].id;
441
+ let { height } = renderer.getSize(), listHeight = height - 4;
442
+ state.scrollOffset = Math.max(0, state.emails.length - listHeight);
443
+ }
444
+ if (key === "enter" /* ENTER */ && state.emails.length > 0 && state.selectedEmailId)
445
+ return {
446
+ action: "view",
447
+ emailId: state.selectedEmailId,
448
+ emailIds: state.emails.map((e) => e.id)
449
+ };
450
+ renderEmailList();
451
+ }
452
+ throw Error("Unexpected end of input");
453
+ } finally {
454
+ clearInterval(animInterval), renderer.cleanup();
455
+ }
456
+ } catch (_catch) {
457
+ var _err = _catch, _hasErr = 1;
458
+ } finally {
459
+ __callDispose(__stack, _err, _hasErr);
460
+ }
461
+ }
462
+
463
+ // src/ui/markdown-highlight.ts
464
+ function markdownHighlight(text) {
465
+ let dimWrapper = (text2) => colors.dim + text2 + colors.reset, linkAnsi = (text2, href) => `\x1B]8;;${href}\x1B\\${text2}\x1B]8;;\x1B\\`;
466
+ return Bun.markdown.render(text, {
467
+ heading: (children, { level }) => `${"\x1B[1m" /* BRIGHT */}${"#".repeat(level)} ${children.replaceAll("\x1B[0m" /* RESET */, "\x1B[0m" /* RESET */ + "\x1B[1m" /* BRIGHT */)}${"\x1B[0m" /* RESET */}
468
+
469
+ `,
470
+ paragraph: (children) => children + `
471
+
472
+ `,
473
+ strong: (children) => `${"\x1B[1m" /* BRIGHT */}${children}${"\x1B[0m" /* RESET */}`,
474
+ emphasis: (children) => `${"\x1B[3m" /* ITALIC */}${children}${"\x1B[0m" /* RESET */}`,
475
+ strikethrough: (children) => `${"\x1B[9m" /* STRIKETHROUGH */}${children}${"\x1B[0m" /* RESET */}`,
476
+ link: (children, { href, title }) => `${"\x1B[34m" /* BLUE */}${linkAnsi(children || "", href)}${"\x1B[0m" /* RESET */}`,
477
+ blockquote: (children) => {
478
+ return children.trimEnd().split(`
479
+ `).map((line) => colors.fg.blue + "> " + line.replaceAll("\x1B[0m" /* RESET */, "\x1B[0m" /* RESET */ + colors.fg.blue) + colors.reset).join(`
480
+ `) + `
481
+
482
+ `;
483
+ },
484
+ code: (children, meta) => dimWrapper("```" + (meta?.language || "") + `
485
+ `) + children.split(`
486
+ `).map((line) => colors.fg.cyan + line.replaceAll("\x1B[0m" /* RESET */, "\x1B[0m" /* RESET */ + colors.fg.cyan)).join(`
487
+ `) + colors.reset + dimWrapper("```\n\n"),
488
+ codespan: (children) => dimWrapper("`") + colors.fg.cyan + children + colors.reset + dimWrapper("`"),
489
+ hr: () => dimWrapper(`---
490
+
491
+ `),
492
+ image: (children, { src, title }) => `${"\x1B[34m" /* BLUE */}${linkAnsi((children || src) + ` ${colors.dim}(image)`, src)}${colors.reset}`,
493
+ list: (children, { ordered, start }) => children.split(`
494
+ `).slice(0, -1).map((line, idx) => {
495
+ if (line.startsWith("- ")) {
496
+ let bullet = ordered ? `${start + idx}.` : "\u2022";
497
+ return `${colors.dim}${bullet}${colors.reset} ${line.slice(2)}`;
498
+ } else
499
+ return (ordered ? " " : " ") + line;
500
+ }).join(`
501
+ `) + `
502
+
503
+ `,
504
+ listItem: (children, meta) => {
505
+ return `${meta?.checked === !0 ? "[x]" : meta?.checked === !1 ? "[ ]" : "-"} ${children}
506
+ `;
507
+ },
508
+ text: (children) => children,
509
+ html: (children) => colors.dim + children + colors.reset
510
+ }, {
511
+ autolinks: !0,
512
+ hardSoftBreaks: !0,
513
+ tables: !1
514
+ });
515
+ }
516
+
517
+ // src/ui/email-view.ts
518
+ async function emailViewScreen(db, mailboxId, emailId, modifyEmail) {
519
+ let __stack = [];
520
+ try {
521
+ const renderer = __using(__stack, new TerminalRenderer, 0);
522
+ let _rawEmail = db.query("SELECT * FROM emails WHERE id = ? AND mailboxId = ?").get(emailId, mailboxId);
523
+ let email = _rawEmail && {
524
+ ..._rawEmail,
525
+ isRead: _rawEmail.isRead === 1,
526
+ isStarred: _rawEmail.isStarred === 1
527
+ };
528
+ if (!email)
529
+ return renderer.cleanup(), "back";
530
+ if (!email.isRead)
531
+ db.run("UPDATE emails SET isRead = 1 WHERE id = ?", [emailId]);
532
+ let scrollOffset = 0;
533
+ let bodyText = email.body || email.html || "(empty)";
534
+ let bodyMarkdowned = markdownHighlight(bodyText);
535
+ let getBodyLines = (width) => Bun.wrapAnsi(bodyMarkdowned, width - 2, { hard: !0, ambiguousIsNarrow: !1, trim: !1 }).split(`
536
+ `);
537
+ let renderEmail = () => {
538
+ let { width, height } = renderer.getSize(), lines = [];
539
+ lines.push(colors.bright + (email.subject || "(no subject)") + colors.reset), lines.push(" ".repeat(width)), lines.push(colors.dim + "From: " + colors.reset + email.from_addr), lines.push(colors.dim + "To: " + colors.reset + email.to_addr), lines.push(colors.dim + "Date: " + colors.reset + new Date(email.createdAt).toLocaleString()), lines.push(colors.dim + "\u2500".repeat(Math.min(width, 60)) + colors.reset);
540
+ let bodyLines = getBodyLines(width), viewHeight = height - 8, visibleStart = scrollOffset, visibleEnd = Math.min(scrollOffset + viewHeight, bodyLines.length), showScrollbar = bodyLines.length > viewHeight || scrollOffset > 0, scrollbarHeight = viewHeight, scrollbarThumbSize = Math.max(1, Math.floor(viewHeight / bodyLines.length * scrollbarHeight)), scrollbarThumbPosition = Math.floor(scrollOffset / bodyLines.length * scrollbarHeight);
541
+ for (let i = visibleStart;i < visibleEnd; i++) {
542
+ let line = bodyLines[i] || "", lineIndex = i - visibleStart, scrollbarChar = " ";
543
+ if (showScrollbar)
544
+ if (lineIndex >= scrollbarThumbPosition && lineIndex < scrollbarThumbPosition + scrollbarThumbSize)
545
+ scrollbarChar += colors.reset + colors.dim + "\u2588" + colors.reset;
546
+ else
547
+ scrollbarChar += colors.reset + colors.dim + "\u2502" + colors.reset;
548
+ let plainLength = Bun.stringWidth(line, { ambiguousIsNarrow: !1, countAnsiEscapeCodes: !1 }), padding = " ".repeat(Math.max(0, width - 2 - plainLength));
549
+ lines.push(line + padding + scrollbarChar);
550
+ }
551
+ while (lines.length < height - 1)
552
+ lines.push(" ".repeat(width));
553
+ let starStatus = email.isStarred ? "Unstar" : "Star", readStatus = email.isRead ? "Mark Unread (u)" : "Mark Read (u)", statusBarText = `\u2191/\u2193: Scroll | \u2190/\u2192: Prev/Next | Enter\xD72: Browser | s: ${starStatus} | u: ${readStatus} | Esc: Back | q: Quit`, statusBarTruncated = Bun.stringWidth(statusBarText, { ambiguousIsNarrow: !1, countAnsiEscapeCodes: !1 }) > width ? statusBarText.substring(0, width - 1) + "\u2026" : statusBarText, statusBar = colors.dim + statusBarTruncated.replace("Esc: Back", colors.bright + colors.fg.cyan + "Esc: Back" + colors.reset + colors.dim);
554
+ colors.reset, lines.push(statusBar), renderer.render({ lines });
555
+ };
556
+ try {
557
+ renderEmail(), renderer.watchResize(renderEmail);
558
+ let DOUBLE_PRESS_THRESHOLD_MS = 500, lastEnterTime = 0;
559
+ for await (let key of readKeys()) {
560
+ if (key === "q" || key === "ctrl-c" /* CTRL_C */)
561
+ return renderer.cleanup(), "quit";
562
+ if (key === "escape" /* ESCAPE */ || key === "backspace" /* BACKSPACE */)
563
+ return "back";
564
+ if (key === "enter" /* ENTER */) {
565
+ let now = Date.now();
566
+ if (now - lastEnterTime < DOUBLE_PRESS_THRESHOLD_MS) {
567
+ let url = `https://emailthing.app/mail/${mailboxId}/${emailId}`, platform = process.platform, openCmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
568
+ try {
569
+ Bun.spawn([openCmd, url], { stdout: "inherit", stderr: "inherit" });
570
+ } catch (error) {}
571
+ lastEnterTime = 0;
572
+ } else
573
+ lastEnterTime = now;
574
+ }
575
+ if (key === "left" /* LEFT */)
576
+ return "prev";
577
+ if (key === "right" /* RIGHT */)
578
+ return "next";
579
+ if (key === "s" && modifyEmail) {
580
+ let newStarred = !email.isStarred;
581
+ email.isStarred = newStarred, db.run("UPDATE emails SET isStarred = ? WHERE id = ?", [newStarred ? 1 : 0, emailId]), modifyEmail({ id: emailId, mailboxId, isStarred: newStarred });
582
+ }
583
+ if (key === "u" && modifyEmail) {
584
+ let newRead = !email.isRead;
585
+ email.isRead = newRead, db.run("UPDATE emails SET isRead = ? WHERE id = ?", [newRead ? 1 : 0, emailId]), modifyEmail({ id: emailId, mailboxId, isRead: newRead });
586
+ }
587
+ let FAST_SCROLL_AMOUNT = 5;
588
+ if (key === "up" /* UP */ && scrollOffset > 0)
589
+ scrollOffset--;
590
+ if (key === "ctrl-up" /* CTRL_UP */ && scrollOffset > 0)
591
+ scrollOffset = Math.max(0, scrollOffset - FAST_SCROLL_AMOUNT);
592
+ if (key === "down" /* DOWN */) {
593
+ let { width, height } = renderer.getSize(), bodyLines = getBodyLines(width), maxScroll = Math.max(0, bodyLines.length - (height - 9));
594
+ if (scrollOffset < maxScroll)
595
+ scrollOffset++;
596
+ }
597
+ if (key === "ctrl-down" /* CTRL_DOWN */) {
598
+ let { width, height } = renderer.getSize(), bodyLines = getBodyLines(width), maxScroll = Math.max(0, bodyLines.length - (height - 9));
599
+ scrollOffset = Math.min(maxScroll, scrollOffset + FAST_SCROLL_AMOUNT);
600
+ }
601
+ if (key === "pageup" /* PAGEUP */) {
602
+ let { width, height } = renderer.getSize(), pageSize = height - 9;
603
+ scrollOffset = Math.max(0, scrollOffset - pageSize);
604
+ }
605
+ if (key === "pagedown" /* PAGEDOWN */) {
606
+ let { width, height } = renderer.getSize(), bodyLines = getBodyLines(width), pageSize = height - 9, maxScroll = Math.max(0, bodyLines.length - pageSize);
607
+ scrollOffset = Math.min(maxScroll, scrollOffset + pageSize);
608
+ }
609
+ if (key === "home" /* HOME */)
610
+ scrollOffset = 0;
611
+ if (key === "end" /* END */) {
612
+ let { width, height } = renderer.getSize(), bodyLines = getBodyLines(width);
613
+ scrollOffset = Math.max(0, bodyLines.length - (height - 9));
614
+ }
615
+ renderEmail();
616
+ }
617
+ throw Error("Unexpected end of input");
618
+ } finally {
619
+ renderer.cleanup();
620
+ }
621
+ } catch (_catch) {
622
+ var _err = _catch, _hasErr = 1;
623
+ } finally {
624
+ __callDispose(__stack, _err, _hasErr);
625
+ }
626
+ }
627
+
628
+ // src/ui/compose.ts
629
+ async function composeScreen() {
630
+ let __stack = [];
631
+ try {
632
+ const renderer = __using(__stack, new TerminalRenderer, 0);
633
+ let state = {
634
+ to: "",
635
+ subject: "",
636
+ body: "",
637
+ focusedField: "to",
638
+ bodyLines: [""],
639
+ bodyCursor: 0,
640
+ bodyCol: 0,
641
+ bodyScrollOffset: 0
642
+ };
643
+ let FAST_SCROLL_AMOUNT = 5;
644
+ let renderCompose = () => {
645
+ let { width, height } = renderer.getSize(), lines = [];
646
+ lines.push(colors.bright + "Compose Email" + colors.reset), lines.push("\u2500".repeat(width));
647
+ let prefixWidth = 2, scrollbarWidth = 2, bodyUsableWidth = width - prefixWidth - scrollbarWidth, visualLines = [];
648
+ for (let i = 0;i < state.bodyLines.length; ++i) {
649
+ let line = state.bodyLines[i] || "", wrapped = Bun.wrapAnsi(line, bodyUsableWidth, { hard: !0, ambiguousIsNarrow: !1, trim: !1 }).split(`
650
+ `), offset = 0;
651
+ for (let r = 0;r < wrapped.length; ++r) {
652
+ let visualText = wrapped[r], charsStart = offset, charsEnd = offset + visualText.length;
653
+ visualLines.push({ logicalLine: i, wrapRow: r, charsStart, charsEnd, visualText }), offset = charsEnd;
654
+ }
655
+ }
656
+ let toFocused = state.focusedField === "to", subjectFocused = state.focusedField === "subject", bodyFocused = state.focusedField === "body", sendFocused = state.focusedField === "send", cancelFocused = state.focusedField === "cancel", toColor = toFocused ? colors.fg.cyan : colors.fg.white, subjectColor = subjectFocused ? colors.fg.cyan : colors.fg.white, bodyColor = bodyFocused ? colors.fg.cyan : colors.fg.white, toLineIdx = lines.length;
657
+ lines.push(toColor + "To: " + colors.reset + (toFocused ? colors.bg.white + colors.fg.black : "") + state.to + colors.reset);
658
+ let subjectLineIdx = lines.length;
659
+ lines.push(subjectColor + "Subject: " + colors.reset + (subjectFocused ? colors.bg.white + colors.fg.black : "") + state.subject + colors.reset), lines.push("\u2500".repeat(width)), lines.push(bodyColor + "Body:" + colors.reset);
660
+ let bodyStartIdx = lines.length, bodyHeight = height - lines.length - 3, cursorVisualRow = 0, cursorVisualCol = 0;
661
+ for (let vi = 0;vi < visualLines.length; ++vi) {
662
+ let meta = visualLines[vi];
663
+ if (meta.logicalLine === state.bodyCursor && state.bodyCol >= meta.charsStart && state.bodyCol <= meta.charsEnd) {
664
+ cursorVisualRow = vi, cursorVisualCol = state.bodyCol - meta.charsStart;
665
+ break;
666
+ }
667
+ }
668
+ let maxScroll = Math.max(0, visualLines.length - bodyHeight), bodyScrollVisualOffset = Math.max(0, Math.min(cursorVisualRow - Math.floor(bodyHeight / 2), maxScroll)), showScrollbar = visualLines.length > bodyHeight || bodyScrollVisualOffset > 0, scrollbarHeight = bodyHeight, scrollbarThumbSize = Math.max(1, Math.floor(bodyHeight / visualLines.length * scrollbarHeight)), availableScrollSpace = scrollbarHeight - scrollbarThumbSize, scrollbarThumbPosition = maxScroll === 0 ? 0 : Math.round(bodyScrollVisualOffset / maxScroll * availableScrollSpace);
669
+ for (let i = 0;i < bodyHeight; i++) {
670
+ let visIdx = bodyScrollVisualOffset + i;
671
+ if (visIdx >= visualLines.length) {
672
+ lines.push(" ".repeat(width));
673
+ continue;
674
+ }
675
+ let meta = visualLines[visIdx], scrollbarChar = " ";
676
+ if (showScrollbar)
677
+ if (i >= scrollbarThumbPosition && i < scrollbarThumbPosition + scrollbarThumbSize)
678
+ scrollbarChar += colors.reset + colors.dim + "\u2588" + colors.reset;
679
+ else
680
+ scrollbarChar += colors.reset + colors.dim + "\u2502" + colors.reset;
681
+ let padded = meta.visualText + " ".repeat(bodyUsableWidth - Bun.stringWidth(meta.visualText)), prefix = meta.wrapRow === 0 && meta.logicalLine === state.bodyCursor ? "> " : " ";
682
+ lines.push(prefix + padded + scrollbarChar);
683
+ }
684
+ lines.push("\u2500".repeat(width));
685
+ let sendBtn = sendFocused ? colors.bg.green + colors.fg.black + " Send " + colors.reset : " Send ", cancelBtn = cancelFocused ? colors.bg.red + colors.fg.black + " Cancel " + colors.reset : " Cancel ";
686
+ lines.push(sendBtn + " " + cancelBtn), lines.push(colors.dim + "Tab: Next | Ctrl+C: Cancel" + colors.reset);
687
+ let cursor = void 0;
688
+ if (toFocused)
689
+ cursor = { row: toLineIdx, col: 4 + state.to.length };
690
+ else if (subjectFocused)
691
+ cursor = { row: subjectLineIdx, col: 9 + state.subject.length };
692
+ else if (bodyFocused)
693
+ cursor = {
694
+ row: bodyStartIdx + (cursorVisualRow - bodyScrollVisualOffset),
695
+ col: 2 + cursorVisualCol
696
+ };
697
+ renderer.render({ lines, cursor });
698
+ };
699
+ function ensureBodyCursorVisible() {
700
+ let { height } = renderer.getSize(), bodyHeight = height - 11;
701
+ if (state.bodyCursor < state.bodyScrollOffset)
702
+ state.bodyScrollOffset = state.bodyCursor;
703
+ else if (state.bodyCursor >= state.bodyScrollOffset + bodyHeight)
704
+ state.bodyScrollOffset = state.bodyCursor - bodyHeight + 1;
705
+ let maxScroll = Math.max(0, state.bodyLines.length - bodyHeight);
706
+ state.bodyScrollOffset = Math.max(0, Math.min(state.bodyScrollOffset, maxScroll));
707
+ }
708
+ try {
709
+ renderCompose(), renderer.watchResize(renderCompose);
710
+ for await (let key of readKeys()) {
711
+ if (key === "ctrl-c" /* CTRL_C */)
712
+ return renderer.cleanup(), null;
713
+ else if (key === "tab" /* TAB */) {
714
+ let fields = ["to", "subject", "body", "send", "cancel"], currentIndex = fields.indexOf(state.focusedField);
715
+ state.focusedField = fields[(currentIndex + 1) % fields.length];
716
+ } else if (key === "backtab" /* BACKTAB */) {
717
+ let fields = ["to", "subject", "body", "send", "cancel"], currentIndex = fields.indexOf(state.focusedField);
718
+ state.focusedField = fields[(currentIndex - 1 + fields.length) % fields.length];
719
+ } else if (key === "enter" /* ENTER */) {
720
+ if (state.focusedField === "send")
721
+ return state.body = state.bodyLines.join(`
722
+ `), {
723
+ to: state.to,
724
+ subject: state.subject,
725
+ body: state.body
726
+ };
727
+ else if (state.focusedField === "cancel")
728
+ return null;
729
+ else if (state.focusedField === "to")
730
+ state.focusedField = "subject";
731
+ else if (state.focusedField === "subject")
732
+ state.focusedField = "body";
733
+ else if (state.focusedField === "body") {
734
+ let line = state.bodyLines[state.bodyCursor] || "", left = line.slice(0, state.bodyCol), right = line.slice(state.bodyCol);
735
+ state.bodyLines[state.bodyCursor] = left, state.bodyLines.splice(state.bodyCursor + 1, 0, right), state.bodyCursor++, state.bodyCol = 0, ensureBodyCursorVisible();
736
+ }
737
+ } else if (key === "backspace" /* BACKSPACE */) {
738
+ if (state.focusedField === "to" && state.to.length > 0)
739
+ state.to = state.to.slice(0, -1);
740
+ else if (state.focusedField === "subject" && state.subject.length > 0)
741
+ state.subject = state.subject.slice(0, -1);
742
+ else if (state.focusedField === "body") {
743
+ let currentLine = state.bodyLines[state.bodyCursor] || "";
744
+ if (state.bodyCol > 0)
745
+ state.bodyLines[state.bodyCursor] = currentLine.slice(0, state.bodyCol - 1) + currentLine.slice(state.bodyCol), state.bodyCol--;
746
+ else if (state.bodyCursor > 0) {
747
+ let prev = state.bodyLines[state.bodyCursor - 1] || "";
748
+ state.bodyCol = prev.length, state.bodyLines[state.bodyCursor - 1] = prev + currentLine, state.bodyLines.splice(state.bodyCursor, 1), state.bodyCursor--, ensureBodyCursorVisible();
749
+ }
750
+ }
751
+ } else if (key === "delete" /* DELETE */ && state.focusedField === "body") {
752
+ let currentLine = state.bodyLines[state.bodyCursor] || "";
753
+ if (state.bodyCol < currentLine.length)
754
+ state.bodyLines[state.bodyCursor] = currentLine.slice(0, state.bodyCol) + currentLine.slice(state.bodyCol + 1);
755
+ else if (state.bodyCursor < state.bodyLines.length - 1) {
756
+ let next = state.bodyLines[state.bodyCursor + 1] || "";
757
+ state.bodyLines[state.bodyCursor] = currentLine + next, state.bodyLines.splice(state.bodyCursor + 1, 1);
758
+ }
759
+ } else if ((key === "pageup" /* PAGEUP */ || key === "ctrl-up" /* CTRL_UP */) && state.focusedField === "body") {
760
+ state.bodyCursor = Math.max(0, state.bodyCursor - FAST_SCROLL_AMOUNT);
761
+ let newLine = state.bodyLines[state.bodyCursor] || "";
762
+ state.bodyCol = Math.min(state.bodyCol, newLine.length), ensureBodyCursorVisible();
763
+ } else if ((key === "pagedown" /* PAGEDOWN */ || key === "ctrl-down" /* CTRL_DOWN */) && state.focusedField === "body") {
764
+ state.bodyCursor = Math.min(state.bodyLines.length - 1, state.bodyCursor + FAST_SCROLL_AMOUNT);
765
+ let newLine = state.bodyLines[state.bodyCursor] || "";
766
+ state.bodyCol = Math.min(state.bodyCol, newLine.length), ensureBodyCursorVisible();
767
+ } else if (key === "home" /* HOME */ && state.focusedField === "body")
768
+ state.bodyCursor = 0, state.bodyCol = 0, ensureBodyCursorVisible();
769
+ else if (key === "end" /* END */ && state.focusedField === "body")
770
+ state.bodyCursor = state.bodyLines.length - 1, state.bodyCol = (state.bodyLines[state.bodyCursor] || "").length, ensureBodyCursorVisible();
771
+ else if (key === "up" /* UP */ && state.focusedField === "body") {
772
+ let { width } = renderer.getSize(), prefixWidth = 2, scrollbarWidth = 2, bodyUsableWidth = width - 2 - 2, visualLines = [];
773
+ for (let i = 0;i < state.bodyLines.length; ++i) {
774
+ let line = state.bodyLines[i] || "", wrapped = Bun.wrapAnsi(line, bodyUsableWidth, { hard: !0, ambiguousIsNarrow: !1, trim: !1 }).split(`
775
+ `), offset = 0;
776
+ for (let r = 0;r < wrapped.length; ++r) {
777
+ let visualText = wrapped[r], charsStart = offset, charsEnd = offset + visualText.length;
778
+ visualLines.push({ logicalLine: i, wrapRow: r, charsStart, charsEnd, visualText }), offset = charsEnd;
779
+ }
780
+ }
781
+ let cursorVisualRow = 0, cursorVisualCol = 0;
782
+ for (let vi = 0;vi < visualLines.length; ++vi) {
783
+ let meta = visualLines[vi];
784
+ if (meta.logicalLine === state.bodyCursor && state.bodyCol >= meta.charsStart && state.bodyCol <= meta.charsEnd) {
785
+ cursorVisualRow = vi, cursorVisualCol = state.bodyCol - meta.charsStart;
786
+ break;
787
+ }
788
+ }
789
+ if (cursorVisualRow === 0)
790
+ state.bodyCursor = 0, state.bodyCol = 0, ensureBodyCursorVisible();
791
+ else {
792
+ let prevVisual = visualLines[cursorVisualRow - 1];
793
+ state.bodyCursor = prevVisual.logicalLine, state.bodyCol = Math.min(prevVisual.charsStart + cursorVisualCol, prevVisual.charsEnd), ensureBodyCursorVisible();
794
+ }
795
+ } else if (key === "down" /* DOWN */ && state.focusedField === "body") {
796
+ let { width } = renderer.getSize(), prefixWidth = 2, scrollbarWidth = 2, bodyUsableWidth = width - 2 - 2, visualLines = [];
797
+ for (let i = 0;i < state.bodyLines.length; ++i) {
798
+ let line = state.bodyLines[i] || "", wrapped = Bun.wrapAnsi(line, bodyUsableWidth, { hard: !0, ambiguousIsNarrow: !1, trim: !1 }).split(`
799
+ `), offset = 0;
800
+ for (let r = 0;r < wrapped.length; ++r) {
801
+ let visualText = wrapped[r], charsStart = offset, charsEnd = offset + visualText.length;
802
+ visualLines.push({ logicalLine: i, wrapRow: r, charsStart, charsEnd, visualText }), offset = charsEnd;
803
+ }
804
+ if (line.length === 0)
805
+ visualLines.push({ logicalLine: i, wrapRow: 0, charsStart: 0, charsEnd: 0, visualText: "" });
806
+ }
807
+ let cursorVisualRow = 0, cursorVisualCol = 0;
808
+ for (let vi = 0;vi < visualLines.length; ++vi) {
809
+ let meta = visualLines[vi];
810
+ if (meta.logicalLine === state.bodyCursor && state.bodyCol >= meta.charsStart && state.bodyCol <= meta.charsEnd) {
811
+ cursorVisualRow = vi, cursorVisualCol = state.bodyCol - meta.charsStart;
812
+ break;
813
+ }
814
+ }
815
+ if (state.bodyLines[state.bodyCursor].length === 0 && state.bodyCursor < state.bodyLines.length - 1)
816
+ state.bodyCursor++, state.bodyCol = 0, ensureBodyCursorVisible();
817
+ cursorVisualRow = -1, cursorVisualCol = 0;
818
+ for (let vi = 0;vi < visualLines.length; ++vi) {
819
+ let meta = visualLines[vi];
820
+ if (meta.logicalLine === state.bodyCursor && (meta.charsStart <= state.bodyCol && state.bodyCol <= meta.charsEnd || meta.charsStart === 0 && meta.charsEnd === 0)) {
821
+ cursorVisualRow = vi, cursorVisualCol = state.bodyCol - meta.charsStart;
822
+ break;
823
+ }
824
+ }
825
+ if (cursorVisualRow === -1)
826
+ cursorVisualRow = visualLines.findIndex((meta) => meta.logicalLine === state.bodyCursor && meta.wrapRow === 0), cursorVisualCol = 0;
827
+ if (cursorVisualRow === visualLines.length - 1 || visualLines[cursorVisualRow + 1] && visualLines[cursorVisualRow + 1].logicalLine !== state.bodyCursor) {
828
+ if (state.bodyCursor < state.bodyLines.length - 1)
829
+ state.bodyCursor++, state.bodyCol = 0;
830
+ else
831
+ state.bodyCursor = state.bodyLines.length - 1, state.bodyCol = (state.bodyLines[state.bodyCursor] || "").length;
832
+ ensureBodyCursorVisible();
833
+ } else {
834
+ let nextVisual = visualLines[cursorVisualRow + 1];
835
+ state.bodyCursor = nextVisual.logicalLine, state.bodyCol = Math.min(nextVisual.charsStart + cursorVisualCol, nextVisual.charsEnd), ensureBodyCursorVisible();
836
+ }
837
+ } else if (key === "left" /* LEFT */ && state.focusedField === "body") {
838
+ if (state.bodyCol > 0)
839
+ state.bodyCol--;
840
+ else if (state.bodyCursor > 0) {
841
+ state.bodyCursor--;
842
+ let newLine = state.bodyLines[state.bodyCursor] || "";
843
+ state.bodyCol = newLine.length, ensureBodyCursorVisible();
844
+ }
845
+ } else if (key === "right" /* RIGHT */ && state.focusedField === "body") {
846
+ let curLine = state.bodyLines[state.bodyCursor] || "";
847
+ if (state.bodyCol < curLine.length)
848
+ state.bodyCol++;
849
+ else if (state.bodyCursor < state.bodyLines.length - 1)
850
+ state.bodyCursor++, state.bodyCol = 0, ensureBodyCursorVisible();
851
+ } else if (key.length >= 1 && !defaultKeys.includes(key)) {
852
+ if (state.focusedField === "to")
853
+ state.to += key;
854
+ else if (state.focusedField === "subject")
855
+ state.subject += key;
856
+ else if (state.focusedField === "body") {
857
+ let line = state.bodyLines[state.bodyCursor] || "";
858
+ state.bodyLines[state.bodyCursor] = line.slice(0, state.bodyCol) + key + line.slice(state.bodyCol), state.bodyCol += key.length, ensureBodyCursorVisible();
859
+ }
860
+ }
861
+ renderCompose();
862
+ }
863
+ throw Error("Unexpected end of input");
864
+ } finally {
865
+ renderer.cleanup();
866
+ }
867
+ } catch (_catch) {
868
+ var _err = _catch, _hasErr = 1;
869
+ } finally {
870
+ __callDispose(__stack, _err, _hasErr);
871
+ }
872
+ }
873
+
874
+ // src/ui/mailbox-switcher.ts
875
+ async function mailboxSwitcher(db, currentMailboxId) {
876
+ let __stack = [];
877
+ try {
878
+ const renderer = __using(__stack, new TerminalRenderer, 0);
879
+ let mailboxes = db.query(`
880
+ SELECT mailboxId as id, mailbox_aliases.alias AS default_alias
881
+ FROM mailboxes
882
+ INNER JOIN mailbox_aliases
883
+ ON mailboxes.id = mailbox_aliases.mailboxId AND mailbox_aliases.\`default\` = true AND mailbox_aliases.isDeleted = false
884
+ WHERE mailboxes.isDeleted = false
885
+ `).all();
886
+ let options = [
887
+ ...mailboxes,
888
+ { id: "switch-user", default_alias: "(switch user)" }
889
+ ];
890
+ let selectedIndex = options.findIndex((m) => m.id === currentMailboxId);
891
+ if (selectedIndex === -1)
892
+ selectedIndex = 0;
893
+ let renderMailboxes = () => {
894
+ let { width, height } = renderer.getSize(), lines = [];
895
+ lines.push(colors.bright + "Switch Mailbox" + colors.reset), lines.push("\u2500".repeat(width));
896
+ for (let i = 0;i < options.length; i++) {
897
+ let mailbox = options[i], isSelected = i === selectedIndex, isCurrent = mailbox.id === currentMailboxId, isSwitchUser = mailbox.id === "switch-user", marker = !isSwitchUser && isCurrent ? "\u25CF " : " ", bg = isSelected ? colors.bg.blue : "", fg = !isSwitchUser && isCurrent ? colors.bright : "";
898
+ lines.push(bg + fg + marker + mailbox.default_alias + colors.reset);
899
+ }
900
+ while (lines.length < height - 2)
901
+ lines.push("");
902
+ lines.push("\u2500".repeat(width)), lines.push(colors.dim + "Enter: Select | Esc: Cancel | q: Quit" + colors.reset), renderer.render({ lines });
903
+ };
904
+ try {
905
+ renderMailboxes(), renderer.watchResize(renderMailboxes);
906
+ for await (let key of readKeys()) {
907
+ if (key === "escape" /* ESCAPE */ || key === "q" || key === "ctrl-c" /* CTRL_C */) {
908
+ if (key === "ctrl-c" /* CTRL_C */)
909
+ renderer.cleanup();
910
+ return null;
911
+ }
912
+ if (key === "up" /* UP */ && selectedIndex > 0)
913
+ selectedIndex--;
914
+ if (key === "down" /* DOWN */ && selectedIndex < options.length - 1)
915
+ selectedIndex++;
916
+ if (key === "enter" /* ENTER */)
917
+ return options[selectedIndex].id;
918
+ renderMailboxes();
919
+ }
920
+ throw Error("Unexpected end of input");
921
+ } finally {
922
+ renderer.cleanup();
923
+ }
924
+ } catch (_catch) {
925
+ var _err = _catch, _hasErr = 1;
926
+ } finally {
927
+ __callDispose(__stack, _err, _hasErr);
928
+ }
929
+ }
930
+
931
+ // src/main.ts
932
+ var usingProcess = (key, fn) => {
933
+ return process.on(key, fn), {
934
+ [Symbol.dispose]() {
935
+ process.off(key, fn);
936
+ },
937
+ getValue() {}
938
+ };
939
+ };
940
+ if (globalThis._cli_route_cache)
941
+ process.removeAllListeners("SIGINT"), process.stdin.removeAllListeners("data");
942
+ async function main() {
943
+ let __stack = [];
944
+ try {
945
+ let db = getDB();
946
+ let client = new EmailThingCLI;
947
+ let isExiting = !1;
948
+ const _ = __using(__stack, usingProcess("SIGINT", () => {
949
+ if (isExiting)
950
+ return;
951
+ isExiting = !0, process.stdout.write("\x1B[?25h" /* SHOW_CURSOR */ + "\x1B[?1049l" /* ALT_SCREEN_OFF */), db.close(), process.exit(0);
952
+ }), 0);
953
+ let auth = loadAuth(db);
954
+ console.log(`Welcome to EmailThing CLI!
955
+ `);
956
+ if (!auth) {
957
+ let credentials = await loginScreen();
958
+ if (!credentials)
959
+ console.log("Login cancelled"), process.exit(0);
960
+ try {
961
+ let loginData = await client.login(credentials.username, credentials.password);
962
+ saveAuth(db, loginData), console.log("Login successful!");
963
+ } catch (error) {
964
+ console.error("Login failed:", error), process.exit(1);
965
+ }
966
+ } else
967
+ client.setAuth(auth.token, auth.refreshToken, auth.tokenExpiresAt);
968
+ let mailboxes = db.query("SELECT * FROM mailboxes").all();
969
+ if (mailboxes.length === 0)
970
+ await syncData(client, db);
971
+ let mailboxesWithCounts = db.query(`
972
+ SELECT m.*, COUNT(e.id) as emailCount
973
+ FROM mailboxes m
974
+ LEFT JOIN emails e ON m.id = e.mailboxId
975
+ GROUP BY m.id
976
+ ORDER BY emailCount DESC
977
+ `).all();
978
+ if (!mailboxesWithCounts.length)
979
+ console.error("No mailboxes found"), process.exit(1);
980
+ let defaultMailbox = mailboxesWithCounts[0];
981
+ let currentMailboxId = defaultMailbox.id;
982
+ let backgroundSync = async () => {
983
+ try {
984
+ await syncData(client, db, !0);
985
+ } catch (error) {}
986
+ };
987
+ let modifyEmailFn = async (updates) => {
988
+ try {
989
+ await client.modifyEmail(updates);
990
+ } catch (error) {
991
+ console.error("Failed to modify email:", error);
992
+ }
993
+ };
994
+ if (mailboxes.length > 0)
995
+ backgroundSync();
996
+ let route = globalThis._cli_route_cache || { route: "list", mailboxId: currentMailboxId };
997
+ while (route.route !== "quit")
998
+ switch (globalThis._cli_route_cache = route, route.route) {
999
+ case "list": {
1000
+ let result = await emailListScreen(db, route.mailboxId, backgroundSync, route.restoreId, modifyEmailFn);
1001
+ if (result.action === "quit")
1002
+ route = { route: "quit" };
1003
+ else if (result.action === "switch")
1004
+ route = { route: "switch", mailboxId: route.mailboxId };
1005
+ else if (result.action === "compose")
1006
+ route = { route: "compose", mailboxId: route.mailboxId };
1007
+ else if (result.action === "view" && result.emailId)
1008
+ route = {
1009
+ route: "view",
1010
+ mailboxId: route.mailboxId,
1011
+ emailId: result.emailId,
1012
+ _emailListCache: result.emailIds || void 0
1013
+ };
1014
+ else
1015
+ route = { route: "quit" };
1016
+ break;
1017
+ }
1018
+ case "switch": {
1019
+ let newMailboxId = await mailboxSwitcher(db, route.mailboxId);
1020
+ if (newMailboxId === "switch-user") {
1021
+ let { clearAuth, resetDB } = await import("./config-cfsae2jm.js");
1022
+ return clearAuth(db), resetDB(db), main();
1023
+ } else if (newMailboxId)
1024
+ route = { route: "list", mailboxId: newMailboxId };
1025
+ else
1026
+ route = { route: "list", mailboxId: route.mailboxId };
1027
+ break;
1028
+ }
1029
+ case "compose": {
1030
+ let composeData = await composeScreen();
1031
+ if (composeData)
1032
+ try {
1033
+ console.log("Sending email..."), await client.sendDraft({
1034
+ draftId: crypto.randomUUID(),
1035
+ mailboxId: route.mailboxId,
1036
+ body: composeData.body,
1037
+ subject: composeData.subject,
1038
+ from: defaultMailbox.address,
1039
+ to: [{ address: composeData.to }]
1040
+ }), console.log("Email sent!"), await syncData(client, db);
1041
+ } catch (error) {
1042
+ console.error("Send failed:", error);
1043
+ }
1044
+ route = { route: "list", mailboxId: route.mailboxId };
1045
+ break;
1046
+ }
1047
+ case "view": {
1048
+ let emails = route._emailListCache || db.query("SELECT id FROM emails WHERE mailboxId = ? AND isDeleted = FALSE ORDER BY createdAt DESC").all(route.mailboxId).map((e) => e.id), currentEmailIndex = emails.indexOf(route.emailId);
1049
+ while (route.route === "view") {
1050
+ globalThis._cli_route_cache = route;
1051
+ let idx = currentEmailIndex, currentEmail = emails[idx], viewResult = await emailViewScreen(db, route.mailboxId, currentEmail, modifyEmailFn);
1052
+ if (viewResult === "quit")
1053
+ route = { route: "quit" };
1054
+ else if (viewResult === "back")
1055
+ route = { route: "list", mailboxId: route.mailboxId, restoreId: currentEmail };
1056
+ else if (viewResult === "next" && idx < emails.length - 1)
1057
+ route = { route: "view", mailboxId: route.mailboxId, emailId: emails[idx + 1], _emailListCache: emails }, currentEmailIndex = idx + 1;
1058
+ else if (viewResult === "prev" && idx > 0)
1059
+ route = { route: "view", mailboxId: route.mailboxId, emailId: emails[idx - 1], _emailListCache: emails }, currentEmailIndex = idx - 1;
1060
+ else
1061
+ route = { route: "list", mailboxId: route.mailboxId, restoreId: currentEmail };
1062
+ }
1063
+ break;
1064
+ }
1065
+ case "quit":
1066
+ default: {
1067
+ route = { route: "quit" };
1068
+ break;
1069
+ }
1070
+ }
1071
+ process.stdout.write(`${"\x1B[?25h" /* SHOW_CURSOR */}${"\x1B[?1049l" /* ALT_SCREEN_OFF */}`);
1072
+ db.close();
1073
+ process.exit(0);
1074
+ } catch (_catch) {
1075
+ var _err = _catch, _hasErr = 1;
1076
+ } finally {
1077
+ __callDispose(__stack, _err, _hasErr);
1078
+ }
1079
+ }
1080
+ main().catch((error) => {
1081
+ process.stdout.write(`${"\x1B[?25h" /* SHOW_CURSOR */}${"\x1B[?1049l" /* ALT_SCREEN_OFF */}`), console.error("Fatal error:", error), process.exit(1);
1082
+ });