cc98-cli 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/storage/settings-store.d.ts +2 -0
  2. package/dist/storage/settings-store.d.ts.map +1 -1
  3. package/dist/storage/settings-store.js +9 -0
  4. package/dist/storage/settings-store.js.map +1 -1
  5. package/dist/tui/ansi.d.ts.map +1 -1
  6. package/dist/tui/ansi.js +5 -1
  7. package/dist/tui/ansi.js.map +1 -1
  8. package/dist/tui/app.js +1 -1
  9. package/dist/tui/app.js.map +1 -1
  10. package/dist/tui/components/content.d.ts +1 -0
  11. package/dist/tui/components/content.d.ts.map +1 -1
  12. package/dist/tui/components/content.js +21 -1
  13. package/dist/tui/components/content.js.map +1 -1
  14. package/dist/tui/components/utils.d.ts.map +1 -1
  15. package/dist/tui/components/utils.js +33 -12
  16. package/dist/tui/components/utils.js.map +1 -1
  17. package/dist/tui/controller.d.ts +19 -1
  18. package/dist/tui/controller.d.ts.map +1 -1
  19. package/dist/tui/controller.js +341 -20
  20. package/dist/tui/controller.js.map +1 -1
  21. package/dist/tui/emoji-art.d.ts +11 -0
  22. package/dist/tui/emoji-art.d.ts.map +1 -0
  23. package/dist/tui/emoji-art.js +371 -0
  24. package/dist/tui/emoji-art.js.map +1 -0
  25. package/dist/tui/emoji-renderer.d.ts +16 -0
  26. package/dist/tui/emoji-renderer.d.ts.map +1 -0
  27. package/dist/tui/emoji-renderer.js +110 -0
  28. package/dist/tui/emoji-renderer.js.map +1 -0
  29. package/dist/tui/image-renderer.d.ts +46 -0
  30. package/dist/tui/image-renderer.d.ts.map +1 -0
  31. package/dist/tui/image-renderer.js +259 -0
  32. package/dist/tui/image-renderer.js.map +1 -0
  33. package/dist/tui/keybindings.js +1 -1
  34. package/dist/tui/keybindings.js.map +1 -1
  35. package/dist/tui/navigation.d.ts.map +1 -1
  36. package/dist/tui/navigation.js +3 -0
  37. package/dist/tui/navigation.js.map +1 -1
  38. package/dist/tui/renderer.d.ts.map +1 -1
  39. package/dist/tui/renderer.js +145 -2
  40. package/dist/tui/renderer.js.map +1 -1
  41. package/dist/tui/state/types.d.ts +8 -0
  42. package/dist/tui/state/types.d.ts.map +1 -1
  43. package/dist/tui/terminal-capabilities.d.ts +24 -0
  44. package/dist/tui/terminal-capabilities.d.ts.map +1 -0
  45. package/dist/tui/terminal-capabilities.js +55 -0
  46. package/dist/tui/terminal-capabilities.js.map +1 -0
  47. package/dist/tui/terminal.js +1 -1
  48. package/dist/tui/terminal.js.map +1 -1
  49. package/dist/tui/topic-reader.d.ts.map +1 -1
  50. package/dist/tui/topic-reader.js +8 -4
  51. package/dist/tui/topic-reader.js.map +1 -1
  52. package/dist/tui/ubb-renderer.d.ts +5 -1
  53. package/dist/tui/ubb-renderer.d.ts.map +1 -1
  54. package/dist/tui/ubb-renderer.js +71 -25
  55. package/dist/tui/ubb-renderer.js.map +1 -1
  56. package/package.json +2 -1
@@ -1,8 +1,10 @@
1
1
  import { checkForUpdate } from "../update.js";
2
2
  import { appVersion } from "../version.js";
3
+ import { Cc98Client } from "../api/client.js";
3
4
  import { getImageCache } from "../storage/image-cache.js";
4
5
  import { SettingsStore } from "../storage/settings-store.js";
5
6
  import { getKeybindingManager } from "./keybindings.js";
7
+ import { EMOJI_CATEGORIES, getEmojiArt, renderCc98Logo, renderEmojiCode } from "./emoji-renderer.js";
6
8
  import { navItems, settingsItems } from "./navigation.js";
7
9
  import { getStatus } from "./state/store.js";
8
10
  import { asArray, asNumber, asObject, chatItem, chatMessageItems, flattenBoards, genericItem, historyItem, isAbortError, jsonPreviewLines, loadChatUserNames, mapLimit, noticeItem, overviewStats, topicItem, unreadStats, userItem } from "./helpers.js";
@@ -15,11 +17,13 @@ export class TuiController {
15
17
  close;
16
18
  nextSignal;
17
19
  abortCurrent;
20
+ webVpnOptions;
18
21
  loadVersion = 0;
19
22
  keybindings;
20
23
  settingsStore = new SettingsStore();
21
24
  updateChecked = false;
22
- constructor(state, client, tokenStore, render, close, nextSignal, abortCurrent) {
25
+ autoSigninChecked = false;
26
+ constructor(state, client, tokenStore, render, close, nextSignal, abortCurrent, webVpnOptions) {
23
27
  this.state = state;
24
28
  this.client = client;
25
29
  this.tokenStore = tokenStore;
@@ -27,10 +31,12 @@ export class TuiController {
27
31
  this.close = close;
28
32
  this.nextSignal = nextSignal;
29
33
  this.abortCurrent = abortCurrent;
34
+ this.webVpnOptions = webVpnOptions;
30
35
  this.keybindings = getKeybindingManager();
31
36
  }
32
37
  async load(force = false) {
33
38
  const version = ++this.loadVersion;
39
+ let shouldAutoSignin = false;
34
40
  const signal = this.nextSignal();
35
41
  const nav = navItems[this.state.navIndex] ?? navItems[0];
36
42
  this.state.viewTitle = nav.label;
@@ -58,6 +64,10 @@ export class TuiController {
58
64
  this.updateChecked = true;
59
65
  void this.checkUpdate();
60
66
  }
67
+ if (!this.autoSigninChecked) {
68
+ this.autoSigninChecked = true;
69
+ shouldAutoSignin = true;
70
+ }
61
71
  const next = await this.loadView(nav.id, force, signal);
62
72
  if (version !== this.loadVersion)
63
73
  return;
@@ -80,6 +90,9 @@ export class TuiController {
80
90
  if (version === this.loadVersion) {
81
91
  this.state.loading = false;
82
92
  this.render();
93
+ if (shouldAutoSignin) {
94
+ void this.runAutoSignin();
95
+ }
83
96
  }
84
97
  }
85
98
  }
@@ -420,11 +433,11 @@ export class TuiController {
420
433
  this.openMenu();
421
434
  }
422
435
  }
423
- // c:复制链接
436
+ // c:图片复制图片本体,链接复制 URL
424
437
  if (this.keybindings.matches(key, "topicCopyLink")) {
425
438
  const currentLine = this.getCurrentTopicLine();
426
439
  if (currentLine?.kind === "image" && currentLine.imageUrl) {
427
- void this.copyToClipboard(currentLine.imageUrl);
440
+ void this.copyImageToClipboard(currentLine.imageUrl);
428
441
  }
429
442
  else if (currentLine?.kind === "link" && currentLine.linkUrl) {
430
443
  void this.copyToClipboard(currentLine.linkUrl);
@@ -432,8 +445,9 @@ export class TuiController {
432
445
  }
433
446
  }
434
447
  handleSettingsKey(key) {
448
+ const itemCount = this.state.items.length || settingsItems.length;
435
449
  if (this.keybindings.matches(key, "moveDown")) {
436
- this.state.itemIndex = Math.min(settingsItems.length - 1, this.state.itemIndex + 1);
450
+ this.state.itemIndex = Math.min(itemCount - 1, this.state.itemIndex + 1);
437
451
  this.render();
438
452
  return;
439
453
  }
@@ -450,7 +464,7 @@ export class TuiController {
450
464
  return;
451
465
  }
452
466
  if (this.keybindings.matches(key, "confirm") || this.keybindings.matches(key, "moveRight")) {
453
- void this.activateSetting(settingsItems[this.state.itemIndex]);
467
+ void this.activateSetting(this.state.items[this.state.itemIndex] ?? settingsItems[this.state.itemIndex]);
454
468
  }
455
469
  }
456
470
  handleNavKey(key) {
@@ -705,11 +719,18 @@ export class TuiController {
705
719
  };
706
720
  }
707
721
  case "settings": {
708
- const cacheStats = await this.client.getCacheStats();
722
+ const [cacheStats, autoSignin] = await Promise.all([
723
+ this.client.getCacheStats(),
724
+ this.settingsStore.isAutoSigninEnabled()
725
+ ]);
709
726
  return {
710
727
  title: "设置",
711
- items: [...settingsItems],
712
- stats: [{ title: "缓存", detail: `${cacheStats.fileCacheEntries} 文件` }, { title: "版本", detail: `v${appVersion}` }],
728
+ items: this.renderSettingsItems(autoSignin),
729
+ stats: [
730
+ { title: "自动签到", detail: autoSignin ? "已开启" : "已关闭" },
731
+ { title: "缓存", detail: `${cacheStats.fileCacheEntries} 文件` },
732
+ { title: "版本", detail: `v${appVersion}` }
733
+ ],
713
734
  status: "设置:j/k 选择 Enter 执行 h 返回"
714
735
  };
715
736
  }
@@ -731,6 +752,14 @@ export class TuiController {
731
752
  void this.openCacheManager();
732
753
  return;
733
754
  }
755
+ if (selected.meta === "pixel-logo") {
756
+ this.openPixelLogo();
757
+ return;
758
+ }
759
+ if (selected.meta === "emoji-preview") {
760
+ this.openEmojiPreview();
761
+ return;
762
+ }
734
763
  if (selected.meta === "update") {
735
764
  void this.checkUpdate(true);
736
765
  return;
@@ -739,6 +768,10 @@ export class TuiController {
739
768
  void this.openAccountSwitcher();
740
769
  return;
741
770
  }
771
+ if (selected.meta === "auto-signin") {
772
+ void this.toggleAutoSignin();
773
+ return;
774
+ }
742
775
  if (selected.meta === "logout") {
743
776
  void this.confirmLogout();
744
777
  return;
@@ -746,6 +779,64 @@ export class TuiController {
746
779
  this.state.status = "功能开发中...";
747
780
  this.render();
748
781
  }
782
+ renderSettingsItems(autoSignin) {
783
+ return settingsItems.map((item) => {
784
+ if (item.meta !== "auto-signin") {
785
+ return { ...item };
786
+ }
787
+ return {
788
+ ...item,
789
+ title: `自动签到: ${autoSignin ? "开启" : "关闭"}`,
790
+ detail: autoSignin
791
+ ? "启动后为所有账号执行每日签到"
792
+ : "默认关闭,启动时不自动签到"
793
+ };
794
+ });
795
+ }
796
+ async toggleAutoSignin() {
797
+ const enabled = await this.settingsStore.isAutoSigninEnabled();
798
+ const next = !enabled;
799
+ await this.settingsStore.setAutoSigninEnabled(next);
800
+ this.state.items = this.renderSettingsItems(next);
801
+ this.state.stats = [
802
+ { title: "自动签到", detail: next ? "已开启" : "已关闭" },
803
+ ...this.state.stats.filter((item) => item.title !== "自动签到")
804
+ ];
805
+ this.state.status = next ? "已开启自动签到" : "已关闭自动签到";
806
+ this.render();
807
+ }
808
+ async runAutoSignin() {
809
+ const enabled = await this.settingsStore.isAutoSigninEnabled();
810
+ if (!enabled)
811
+ return;
812
+ const accounts = await this.tokenStore.listAccounts();
813
+ if (accounts.length === 0)
814
+ return;
815
+ let success = 0;
816
+ let failed = 0;
817
+ this.state.status = `自动签到: 0/${accounts.length}`;
818
+ this.render();
819
+ for (const account of accounts) {
820
+ try {
821
+ const tokenStore = this.tokenStore.withAccount(account.account);
822
+ const client = new Cc98Client({ tokenStore, webVpn: this.webVpnOptions });
823
+ if (this.webVpnOptions) {
824
+ await client.initWebVpn();
825
+ }
826
+ await client.signin();
827
+ success += 1;
828
+ }
829
+ catch {
830
+ failed += 1;
831
+ }
832
+ this.state.status = `自动签到: ${success + failed}/${accounts.length}`;
833
+ this.render();
834
+ }
835
+ this.state.status = failed > 0
836
+ ? `自动签到完成: ${success} 成功,${failed} 失败`
837
+ : `自动签到完成: ${success} 个账号`;
838
+ this.render();
839
+ }
749
840
  async activateContentItem(selected, signal) {
750
841
  if (selected.topicId !== undefined) {
751
842
  await this.openTopic(selected.topicId, false, signal);
@@ -769,6 +860,19 @@ export class TuiController {
769
860
  await this.switchAccount(accountName);
770
861
  return;
771
862
  }
863
+ if (selected.meta?.startsWith("emoji-category:")) {
864
+ this.openEmojiCategory(selected.meta.slice("emoji-category:".length));
865
+ return;
866
+ }
867
+ if (selected.meta?.startsWith("emoji-batch:")) {
868
+ this.state.status = "继续向下选择具体表情,Enter 放大预览";
869
+ this.render();
870
+ return;
871
+ }
872
+ if (selected.meta?.startsWith("emoji:")) {
873
+ this.openEmojiDetail(selected.meta.slice("emoji:".length));
874
+ return;
875
+ }
772
876
  if (selected.action?.startsWith("notices:")) {
773
877
  await this.openNoticeList(selected.action.split(":")[1], signal);
774
878
  return;
@@ -804,6 +908,10 @@ export class TuiController {
804
908
  }
805
909
  }
806
910
  this.render();
911
+ // Start background image preloading so preview/open/copy can reuse the local cache.
912
+ if (this.state.topic) {
913
+ void this.preloadTopicImages(this.state.topic);
914
+ }
807
915
  }
808
916
  async openBoard(boardId, title, force, signal, pushParent = true) {
809
917
  if (pushParent)
@@ -978,6 +1086,7 @@ export class TuiController {
978
1086
  const posts = asArray(await this.client.getTopicPosts(topic.topicId, topic.loaded, topic.size, false, signal));
979
1087
  appendTopicPosts(topic, posts);
980
1088
  this.state.status = "";
1089
+ void this.preloadTopicImages(topic);
981
1090
  }
982
1091
  catch (error) {
983
1092
  if (!isAbortError(error))
@@ -1257,6 +1366,54 @@ export class TuiController {
1257
1366
  }
1258
1367
  return undefined;
1259
1368
  }
1369
+ /**
1370
+ * Preload images for topic in background
1371
+ * Images are cached and trigger re-render when ready
1372
+ */
1373
+ async preloadTopicImages(topic) {
1374
+ const cache = getImageCache();
1375
+ const imagesToLoad = new Set();
1376
+ // Collect all unique image URLs from posts
1377
+ for (const post of topic.posts) {
1378
+ for (const imageUrl of post.images) {
1379
+ if (imageUrl && !topic.imageCache.has(imageUrl) && !topic.imageLoading.has(imageUrl)) {
1380
+ imagesToLoad.add(imageUrl);
1381
+ }
1382
+ }
1383
+ }
1384
+ if (imagesToLoad.size === 0)
1385
+ return;
1386
+ // Load images in parallel with concurrency limit
1387
+ const urls = Array.from(imagesToLoad);
1388
+ const concurrency = 3;
1389
+ for (let i = 0; i < urls.length; i += concurrency) {
1390
+ // Check if topic is still the same (user might have navigated away)
1391
+ if (this.state.topic !== topic)
1392
+ break;
1393
+ const batch = urls.slice(i, i + concurrency);
1394
+ let shouldRender = false;
1395
+ const promises = batch.map(async (url) => {
1396
+ topic.imageLoading.add(url);
1397
+ try {
1398
+ const localPath = await cache.getOrDownload(url);
1399
+ topic.imageErrors.delete(url);
1400
+ topic.imageCache.set(url, localPath);
1401
+ shouldRender = true;
1402
+ }
1403
+ catch (error) {
1404
+ topic.imageErrors.set(url, error instanceof Error ? error.message : "下载失败");
1405
+ shouldRender = true;
1406
+ }
1407
+ finally {
1408
+ topic.imageLoading.delete(url);
1409
+ }
1410
+ });
1411
+ await Promise.all(promises);
1412
+ if (shouldRender && this.state.topic === topic) {
1413
+ this.render();
1414
+ }
1415
+ }
1416
+ }
1260
1417
  async openImage(url) {
1261
1418
  this.state.status = "正在下载图片...";
1262
1419
  this.render();
@@ -1269,7 +1426,11 @@ export class TuiController {
1269
1426
  const { execFile } = await import("node:child_process");
1270
1427
  const platform = process.platform;
1271
1428
  const command = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
1272
- const args = platform === "win32" ? ["/c", "start", "", localPath] : [localPath];
1429
+ const args = platform === "win32"
1430
+ ? ["/c", "start", "", localPath]
1431
+ : platform === "darwin"
1432
+ ? ["-a", "Preview", localPath]
1433
+ : [localPath];
1273
1434
  execFile(command, args, (error) => {
1274
1435
  if (error) {
1275
1436
  this.state.status = `打开失败: ${error.message}`;
@@ -1282,6 +1443,58 @@ export class TuiController {
1282
1443
  this.render();
1283
1444
  }
1284
1445
  }
1446
+ async copyImageToClipboard(url) {
1447
+ this.state.status = "正在复制图片...";
1448
+ this.render();
1449
+ try {
1450
+ const cache = getImageCache();
1451
+ const localPath = await cache.getOrDownload(url);
1452
+ await this.copyImageFileToClipboard(localPath);
1453
+ this.state.status = "已复制图片到剪贴板";
1454
+ this.render();
1455
+ }
1456
+ catch (error) {
1457
+ this.state.status = error instanceof Error ? error.message : "复制图片失败";
1458
+ this.render();
1459
+ }
1460
+ }
1461
+ async copyImageFileToClipboard(localPath) {
1462
+ const platform = process.platform;
1463
+ if (platform === "darwin") {
1464
+ await this.copyImageFileToClipboardMac(localPath);
1465
+ return;
1466
+ }
1467
+ if (platform === "win32") {
1468
+ const script = [
1469
+ "Add-Type -AssemblyName System.Windows.Forms",
1470
+ "Add-Type -AssemblyName System.Drawing",
1471
+ "$image=[System.Drawing.Image]::FromFile($args[0])",
1472
+ "[System.Windows.Forms.Clipboard]::SetImage($image)",
1473
+ "$image.Dispose()"
1474
+ ].join("; ");
1475
+ await execFilePromise("powershell.exe", ["-NoProfile", "-Command", script, localPath]);
1476
+ return;
1477
+ }
1478
+ const mime = imageMimeType(localPath);
1479
+ await execFilePromise("xclip", ["-selection", "clipboard", "-t", mime, localPath]);
1480
+ }
1481
+ async copyImageFileToClipboardMac(localPath) {
1482
+ const { mkdtemp, rm } = await import("node:fs/promises");
1483
+ const { tmpdir } = await import("node:os");
1484
+ const { join } = await import("node:path");
1485
+ const dir = await mkdtemp(join(tmpdir(), "cc98-image-"));
1486
+ const tiffPath = join(dir, "clipboard.tiff");
1487
+ try {
1488
+ await execFilePromise("sips", ["-s", "format", "tiff", localPath, "--out", tiffPath]);
1489
+ await execFilePromise("osascript", [
1490
+ "-e",
1491
+ `set the clipboard to (read (POSIX file ${appleScriptString(tiffPath)}) as TIFF picture)`
1492
+ ]);
1493
+ }
1494
+ finally {
1495
+ await rm(dir, { recursive: true, force: true });
1496
+ }
1497
+ }
1285
1498
  async copyToClipboard(text) {
1286
1499
  try {
1287
1500
  const { spawn } = await import("node:child_process");
@@ -1345,6 +1558,81 @@ export class TuiController {
1345
1558
  this.state.infoLines = lines;
1346
1559
  this.render();
1347
1560
  }
1561
+ openPixelLogo() {
1562
+ this.state.modal = "info";
1563
+ this.state.infoTitle = "CC98 像素 Logo";
1564
+ this.state.infoLines = [
1565
+ ...renderCc98Logo().split("\n"),
1566
+ "",
1567
+ "来源: https://www.cc98.org/static/images/98LOGO.ico",
1568
+ "渲染: 24-bit ANSI 半块像素"
1569
+ ];
1570
+ this.render();
1571
+ }
1572
+ openEmojiPreview() {
1573
+ const items = EMOJI_CATEGORIES.map((category) => ({
1574
+ title: `${category.label} (${category.codes.length})`,
1575
+ meta: `emoji-category:${category.id}`,
1576
+ detail: `来源目录: Assets/Emoji/${category.source} · ${category.codes[0]} - ${category.codes.at(-1)}`
1577
+ }));
1578
+ this.openReadOnlyList("表情包预览", items, EMOJI_CATEGORIES.map((category) => ({
1579
+ title: category.label,
1580
+ detail: `${category.codes.length} 个`
1581
+ })));
1582
+ this.state.status = "表情包预览:j/k 选择分类 Enter 进入 h 返回";
1583
+ this.render();
1584
+ }
1585
+ openEmojiCategory(categoryId) {
1586
+ const category = EMOJI_CATEGORIES.find((item) => item.id === categoryId);
1587
+ if (!category) {
1588
+ this.state.status = "未找到表情分类";
1589
+ this.render();
1590
+ return;
1591
+ }
1592
+ const items = [];
1593
+ for (let index = 0; index < category.codes.length; index += 20) {
1594
+ const batch = category.codes.slice(index, index + 20);
1595
+ const batchNumber = Math.floor(index / 20) + 1;
1596
+ items.push({
1597
+ title: `${category.label} 第 ${batchNumber} 批`,
1598
+ meta: `emoji-batch:${category.id}:${batchNumber}`,
1599
+ detail: `${batch[0]} - ${batch.at(-1)} · ${batch.length} 个`
1600
+ });
1601
+ for (const code of batch) {
1602
+ const art = getEmojiArt(code);
1603
+ items.push({
1604
+ title: `[${code}]`,
1605
+ meta: `emoji:${code}`,
1606
+ detail: art ? `${category.label} · ${art.width}x${art.height}px` : category.label
1607
+ });
1608
+ }
1609
+ }
1610
+ this.openReadOnlyList(category.label, items, [
1611
+ { title: "分类", detail: category.label },
1612
+ { title: "数量", detail: `${category.codes.length} 个` },
1613
+ { title: "来源", detail: `Assets/Emoji/${category.source}` }
1614
+ ]);
1615
+ this.state.status = `${category.label}:j/k 选择 Enter 放大 h 返回分类`;
1616
+ this.render();
1617
+ }
1618
+ openEmojiDetail(code) {
1619
+ const art = getEmojiArt(code);
1620
+ const rendered = renderEmojiCode(code);
1621
+ if (!art || !rendered) {
1622
+ this.state.status = `未找到表情 [${code}]`;
1623
+ this.render();
1624
+ return;
1625
+ }
1626
+ this.state.modal = "info";
1627
+ this.state.infoTitle = `[${code}]`;
1628
+ this.state.infoLines = [
1629
+ ...rendered.split("\n"),
1630
+ "",
1631
+ `尺寸: ${art.width}x${art.height}px`,
1632
+ `颜色: ${art.palette.length}`
1633
+ ];
1634
+ this.render();
1635
+ }
1348
1636
  async openAccountSwitcher() {
1349
1637
  try {
1350
1638
  const accounts = await this.tokenStore.listAccounts();
@@ -1480,15 +1768,15 @@ export class TuiController {
1480
1768
  this.render();
1481
1769
  }
1482
1770
  snapshotParent() {
1483
- if (!this.state.parentList) {
1484
- this.state.parentList = {
1485
- title: this.state.viewTitle,
1486
- items: [...this.state.items],
1487
- stats: [...this.state.stats],
1488
- itemIndex: this.state.itemIndex,
1489
- status: this.state.status
1490
- };
1491
- }
1771
+ this.state.parentList = {
1772
+ title: this.state.viewTitle,
1773
+ items: [...this.state.items],
1774
+ stats: [...this.state.stats],
1775
+ itemIndex: this.state.itemIndex,
1776
+ scroll: this.state.scroll,
1777
+ status: this.state.status,
1778
+ parent: this.state.parentList
1779
+ };
1492
1780
  }
1493
1781
  restoreParentList() {
1494
1782
  const parent = this.state.parentList;
@@ -1498,14 +1786,15 @@ export class TuiController {
1498
1786
  this.state.items = parent.items;
1499
1787
  this.state.stats = parent.stats;
1500
1788
  this.state.itemIndex = parent.itemIndex;
1789
+ this.state.scroll = parent.scroll;
1501
1790
  this.state.status = parent.status;
1502
- this.state.parentList = undefined;
1791
+ this.state.parentList = parent.parent;
1503
1792
  this.state.currentBoard = undefined;
1504
1793
  this.state.currentChat = undefined;
1505
1794
  this.state.topic = undefined;
1506
1795
  this.state.mode = "list";
1507
1796
  this.state.focus = "content";
1508
- this.state.scroll = 0;
1797
+ this.render();
1509
1798
  }
1510
1799
  async checkUpdate(forceShow = false) {
1511
1800
  if (forceShow) {
@@ -1554,4 +1843,36 @@ export class TuiController {
1554
1843
  }
1555
1844
  }
1556
1845
  }
1846
+ async function execFilePromise(command, args) {
1847
+ const { execFile } = await import("node:child_process");
1848
+ await new Promise((resolve, reject) => {
1849
+ execFile(command, args, (error) => {
1850
+ if (error) {
1851
+ reject(error);
1852
+ }
1853
+ else {
1854
+ resolve();
1855
+ }
1856
+ });
1857
+ });
1858
+ }
1859
+ function appleScriptString(value) {
1860
+ return JSON.stringify(value);
1861
+ }
1862
+ function imageMimeType(path) {
1863
+ const lower = path.toLowerCase();
1864
+ if (lower.endsWith(".png"))
1865
+ return "image/png";
1866
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))
1867
+ return "image/jpeg";
1868
+ if (lower.endsWith(".gif"))
1869
+ return "image/gif";
1870
+ if (lower.endsWith(".webp"))
1871
+ return "image/webp";
1872
+ if (lower.endsWith(".bmp"))
1873
+ return "image/bmp";
1874
+ if (lower.endsWith(".svg"))
1875
+ return "image/svg+xml";
1876
+ return "image/png";
1877
+ }
1557
1878
  //# sourceMappingURL=controller.js.map