@walavave/cc98-cli 0.2.5 → 0.2.6

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 (147) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +3 -1
  3. package/dist/tui/account-modal.js +3 -3
  4. package/dist/tui/account-modal.js.map +1 -1
  5. package/dist/tui/app-runtime/content/following.js +2 -2
  6. package/dist/tui/app-runtime/content/following.js.map +1 -1
  7. package/dist/tui/app-runtime/content/list.d.ts.map +1 -1
  8. package/dist/tui/app-runtime/content/list.js +10 -6
  9. package/dist/tui/app-runtime/content/list.js.map +1 -1
  10. package/dist/tui/app-runtime/content/navigation.d.ts.map +1 -1
  11. package/dist/tui/app-runtime/content/navigation.js +4 -1
  12. package/dist/tui/app-runtime/content/navigation.js.map +1 -1
  13. package/dist/tui/app-runtime/content/search.js +1 -1
  14. package/dist/tui/app-runtime/content/search.js.map +1 -1
  15. package/dist/tui/app-runtime/image-viewer.js +2 -2
  16. package/dist/tui/app-runtime/image-viewer.js.map +1 -1
  17. package/dist/tui/app-runtime/interactions.d.ts +1 -1
  18. package/dist/tui/app-runtime/interactions.d.ts.map +1 -1
  19. package/dist/tui/app-runtime/interactions.js +3 -3
  20. package/dist/tui/app-runtime/interactions.js.map +1 -1
  21. package/dist/tui/app-runtime/keyboard.d.ts.map +1 -1
  22. package/dist/tui/app-runtime/keyboard.js +24 -3
  23. package/dist/tui/app-runtime/keyboard.js.map +1 -1
  24. package/dist/tui/app-runtime/modals.d.ts +5 -1
  25. package/dist/tui/app-runtime/modals.d.ts.map +1 -1
  26. package/dist/tui/app-runtime/modals.js +8 -6
  27. package/dist/tui/app-runtime/modals.js.map +1 -1
  28. package/dist/tui/app-runtime/mouse.d.ts +1 -1
  29. package/dist/tui/app-runtime/mouse.d.ts.map +1 -1
  30. package/dist/tui/app-runtime/state.d.ts.map +1 -1
  31. package/dist/tui/app-runtime/state.js +4 -0
  32. package/dist/tui/app-runtime/state.js.map +1 -1
  33. package/dist/tui/app-runtime/topic.d.ts.map +1 -1
  34. package/dist/tui/app-runtime/topic.js +117 -4
  35. package/dist/tui/app-runtime/topic.js.map +1 -1
  36. package/dist/tui/app.d.ts.map +1 -1
  37. package/dist/tui/app.js +3 -2
  38. package/dist/tui/app.js.map +1 -1
  39. package/dist/tui/cached-client.d.ts +1 -0
  40. package/dist/tui/cached-client.d.ts.map +1 -1
  41. package/dist/tui/cached-client.js +7 -0
  42. package/dist/tui/cached-client.js.map +1 -1
  43. package/dist/tui/data/content.js +2 -2
  44. package/dist/tui/data/content.js.map +1 -1
  45. package/dist/tui/data/navigation-state.d.ts +1 -0
  46. package/dist/tui/data/navigation-state.d.ts.map +1 -1
  47. package/dist/tui/data/navigation-state.js +9 -4
  48. package/dist/tui/data/navigation-state.js.map +1 -1
  49. package/dist/tui/data/search.d.ts.map +1 -1
  50. package/dist/tui/data/search.js +5 -1
  51. package/dist/tui/data/search.js.map +1 -1
  52. package/dist/tui/data/topic.d.ts +1 -1
  53. package/dist/tui/data/topic.d.ts.map +1 -1
  54. package/dist/tui/data/topic.js +35 -11
  55. package/dist/tui/data/topic.js.map +1 -1
  56. package/dist/tui/data/view-items.d.ts +1 -0
  57. package/dist/tui/data/view-items.d.ts.map +1 -1
  58. package/dist/tui/data/view-items.js +86 -9
  59. package/dist/tui/data/view-items.js.map +1 -1
  60. package/dist/tui/data/view-loader.d.ts.map +1 -1
  61. package/dist/tui/data/view-loader.js +9 -4
  62. package/dist/tui/data/view-loader.js.map +1 -1
  63. package/dist/tui/media/downloads.d.ts.map +1 -0
  64. package/dist/tui/{downloads.js → media/downloads.js} +1 -1
  65. package/dist/tui/media/downloads.js.map +1 -0
  66. package/dist/tui/media/emotion-catalog.d.ts.map +1 -0
  67. package/dist/tui/{emotion-catalog.js → media/emotion-catalog.js} +1 -1
  68. package/dist/tui/media/emotion-catalog.js.map +1 -0
  69. package/dist/tui/media/emotion-preview.d.ts.map +1 -0
  70. package/dist/tui/media/emotion-preview.js.map +1 -0
  71. package/dist/tui/media/image-preview.d.ts.map +1 -0
  72. package/dist/tui/{image-preview.js → media/image-preview.js} +1 -1
  73. package/dist/tui/media/image-preview.js.map +1 -0
  74. package/dist/tui/media/ubb-renderer.d.ts.map +1 -0
  75. package/dist/tui/{ubb-renderer.js → media/ubb-renderer.js} +134 -6
  76. package/dist/tui/media/ubb-renderer.js.map +1 -0
  77. package/dist/tui/render-core/ansi.d.ts.map +1 -0
  78. package/dist/tui/render-core/ansi.js.map +1 -0
  79. package/dist/tui/render-core/canvas.d.ts.map +1 -0
  80. package/dist/tui/render-core/canvas.js.map +1 -0
  81. package/dist/tui/render-core/layout.d.ts.map +1 -0
  82. package/dist/tui/render-core/layout.js.map +1 -0
  83. package/dist/tui/render-core/terminal.d.ts.map +1 -0
  84. package/dist/tui/{terminal.js → render-core/terminal.js} +1 -1
  85. package/dist/tui/render-core/terminal.js.map +1 -0
  86. package/dist/tui/render-core/text.d.ts.map +1 -0
  87. package/dist/tui/render-core/text.js.map +1 -0
  88. package/dist/tui/render-core/theme.d.ts.map +1 -0
  89. package/dist/tui/render-core/theme.js.map +1 -0
  90. package/dist/tui/renderer/content.d.ts +14 -0
  91. package/dist/tui/renderer/content.d.ts.map +1 -0
  92. package/dist/tui/renderer/content.js +474 -0
  93. package/dist/tui/renderer/content.js.map +1 -0
  94. package/dist/tui/renderer/modals.d.ts +4 -0
  95. package/dist/tui/renderer/modals.d.ts.map +1 -0
  96. package/dist/tui/renderer/modals.js +343 -0
  97. package/dist/tui/renderer/modals.js.map +1 -0
  98. package/dist/tui/renderer.d.ts +3 -4
  99. package/dist/tui/renderer.d.ts.map +1 -1
  100. package/dist/tui/renderer.js +12 -827
  101. package/dist/tui/renderer.js.map +1 -1
  102. package/dist/tui/tui-model.d.ts +6 -1
  103. package/dist/tui/tui-model.d.ts.map +1 -1
  104. package/dist/tui/tui-model.js +1 -1
  105. package/dist/tui/tui-model.js.map +1 -1
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/package.json +1 -1
  109. package/dist/tui/ansi.d.ts.map +0 -1
  110. package/dist/tui/ansi.js.map +0 -1
  111. package/dist/tui/canvas.d.ts.map +0 -1
  112. package/dist/tui/canvas.js.map +0 -1
  113. package/dist/tui/downloads.d.ts.map +0 -1
  114. package/dist/tui/downloads.js.map +0 -1
  115. package/dist/tui/emotion-catalog.d.ts.map +0 -1
  116. package/dist/tui/emotion-catalog.js.map +0 -1
  117. package/dist/tui/emotion-preview.d.ts.map +0 -1
  118. package/dist/tui/emotion-preview.js.map +0 -1
  119. package/dist/tui/image-preview.d.ts.map +0 -1
  120. package/dist/tui/image-preview.js.map +0 -1
  121. package/dist/tui/layout.d.ts.map +0 -1
  122. package/dist/tui/layout.js.map +0 -1
  123. package/dist/tui/terminal.d.ts.map +0 -1
  124. package/dist/tui/terminal.js.map +0 -1
  125. package/dist/tui/text.d.ts.map +0 -1
  126. package/dist/tui/text.js.map +0 -1
  127. package/dist/tui/theme.d.ts.map +0 -1
  128. package/dist/tui/theme.js.map +0 -1
  129. package/dist/tui/ubb-renderer.d.ts.map +0 -1
  130. package/dist/tui/ubb-renderer.js.map +0 -1
  131. /package/dist/tui/{downloads.d.ts → media/downloads.d.ts} +0 -0
  132. /package/dist/tui/{emotion-catalog.d.ts → media/emotion-catalog.d.ts} +0 -0
  133. /package/dist/tui/{emotion-preview.d.ts → media/emotion-preview.d.ts} +0 -0
  134. /package/dist/tui/{emotion-preview.js → media/emotion-preview.js} +0 -0
  135. /package/dist/tui/{image-preview.d.ts → media/image-preview.d.ts} +0 -0
  136. /package/dist/tui/{ubb-renderer.d.ts → media/ubb-renderer.d.ts} +0 -0
  137. /package/dist/tui/{ansi.d.ts → render-core/ansi.d.ts} +0 -0
  138. /package/dist/tui/{ansi.js → render-core/ansi.js} +0 -0
  139. /package/dist/tui/{canvas.d.ts → render-core/canvas.d.ts} +0 -0
  140. /package/dist/tui/{canvas.js → render-core/canvas.js} +0 -0
  141. /package/dist/tui/{layout.d.ts → render-core/layout.d.ts} +0 -0
  142. /package/dist/tui/{layout.js → render-core/layout.js} +0 -0
  143. /package/dist/tui/{terminal.d.ts → render-core/terminal.d.ts} +0 -0
  144. /package/dist/tui/{text.d.ts → render-core/text.d.ts} +0 -0
  145. /package/dist/tui/{text.js → render-core/text.js} +0 -0
  146. /package/dist/tui/{theme.d.ts → render-core/theme.d.ts} +0 -0
  147. /package/dist/tui/{theme.js → render-core/theme.js} +0 -0
@@ -1,65 +1,12 @@
1
- import { drawAccountModal, drawConfirmModal, drawLoginModal } from "./account-modal.js";
2
- import { ansi } from "./ansi.js";
3
- import { Canvas } from "./canvas.js";
4
- import { emotionCategories, getEmotionPreview, getEmotionCategory } from "./emotion-catalog.js";
5
- import { imagePreviewRows } from "./image-preview.js";
6
- import { center, fill, length, pad, rect, split } from "./layout.js";
7
- import { blank, cellWidth, fit, truncate, wrapText } from "./text.js";
8
- import { ruleLine, selectedLine, styled, textStyle, theme } from "./theme.js";
9
- import { currentTopicLine, getStatus, navItems } from "./tui-model.js";
10
- export function getSidebarWidth(totalWidth, preferred) {
11
- const fallback = totalWidth < 56 ? 0 : totalWidth < 90 ? 14 : 18;
12
- if (preferred === undefined || preferred <= 0 || totalWidth < 56) {
13
- return fallback;
14
- }
15
- return Math.max(10, Math.min(preferred, Math.max(10, Math.floor(totalWidth * 0.35))));
16
- }
17
- export function getRenderedListItemIndexAtRow(state, width, height, rowIndex) {
18
- const contentRow = rowIndex - 2;
19
- if (contentRow < 0) {
20
- return undefined;
21
- }
22
- const wrapDetail = shouldWrapListDetail(state);
23
- const inlineDetail = shouldInlineListDetail(state);
24
- const contentHeight = Math.max(1, height - 2);
25
- const scroll = getListScroll(state, contentHeight, width, wrapDetail, inlineDetail);
26
- let usedRows = 0;
27
- for (let index = scroll; index < state.items.length; index += 1) {
28
- const itemHeight = getListItemHeight(state.items[index], width, wrapDetail, inlineDetail);
29
- if (usedRows + itemHeight > contentHeight) {
30
- break;
31
- }
32
- if (contentRow >= usedRows && contentRow < usedRows + itemHeight) {
33
- return index;
34
- }
35
- usedRows += itemHeight;
36
- }
37
- return undefined;
38
- }
39
- export function getRenderedSearchItemIndexAtRow(state, width, height, rowIndex) {
40
- const search = state.currentSearch;
41
- if (!search) {
42
- return undefined;
43
- }
44
- const contentRow = rowIndex - 3;
45
- if (contentRow < 0) {
46
- return undefined;
47
- }
48
- const contentHeight = Math.max(1, height - 3);
49
- const scroll = getListScroll(state, contentHeight, width, false, true);
50
- let usedRows = 0;
51
- for (let index = scroll; index < state.items.length; index += 1) {
52
- const itemHeight = getListItemHeight(state.items[index], width, false, true);
53
- if (usedRows + itemHeight > contentHeight) {
54
- break;
55
- }
56
- if (contentRow >= usedRows && contentRow < usedRows + itemHeight) {
57
- return index;
58
- }
59
- usedRows += itemHeight;
60
- }
61
- return undefined;
62
- }
1
+ import { ansi } from "./render-core/ansi.js";
2
+ import { Canvas } from "./render-core/canvas.js";
3
+ import { fill, length, pad, rect, split } from "./render-core/layout.js";
4
+ import { cellWidth, fit } from "./render-core/text.js";
5
+ import { selectedLine, styled, textStyle, theme } from "./render-core/theme.js";
6
+ import { navItems } from "./tui-model.js";
7
+ import { getSidebarWidth, getRenderedListItemIndexAtRow, getRenderedSearchItemIndexAtRow, drawMain, drawStatusBar } from "./renderer/content.js";
8
+ import { drawModalFrame } from "./renderer/modals.js";
9
+ export { getSidebarWidth, getRenderedListItemIndexAtRow, getRenderedSearchItemIndexAtRow };
63
10
  export function draw(state, size, config) {
64
11
  const width = Math.max(1, size.columns);
65
12
  const height = Math.max(1, size.rows);
@@ -121,27 +68,9 @@ export function draw(state, size, config) {
121
68
  canvas.junction(sidebarRuleArea.x, outer.y + outer.height - 1, theme.border.teeBottom);
122
69
  }
123
70
  canvas.drawLines(statusArea, [drawStatusBar(state, statusArea.width)]);
124
- const baseLines = canvas.toLines();
125
- if (state.modal === "help") {
126
- return { text: drawHelpModal(baseLines, width, height) };
127
- }
128
- if (state.modal === "account") {
129
- return { text: drawAccountModal(baseLines, state.accountModal, width, height) };
130
- }
131
- if (state.modal === "login") {
132
- return { text: drawLoginModal(baseLines, state.loginForm, width, height) };
133
- }
134
- if (state.modal === "confirm" && state.confirmDialog) {
135
- return { text: drawConfirmModal(baseLines, state.confirmDialog, width, height) };
136
- }
137
- if (state.modal === "image" && state.imageViewer) {
138
- return drawImageModal(baseLines, state, width, height);
139
- }
140
- if (state.modal === "compose" && state.composeDialog) {
141
- return { text: drawComposeModal(baseLines, state, width, height) };
142
- }
143
- if (state.modal === "emotion-picker" && state.composeDialog) {
144
- return drawEmotionPickerModal(baseLines, state, width, height);
71
+ const modalFrame = drawModalFrame(canvas.toLines(), state, width, height);
72
+ if (modalFrame) {
73
+ return modalFrame;
145
74
  }
146
75
  return { text: canvas.toString(), imageOverlays };
147
76
  }
@@ -190,748 +119,4 @@ function drawSidebar(state, width, height) {
190
119
  }
191
120
  return rows;
192
121
  }
193
- function drawMain(state, width, height) {
194
- if (state.mode === "topic") {
195
- return drawTopic(state, width, height);
196
- }
197
- if (state.currentFollowing) {
198
- return drawFollowing(state, width, height);
199
- }
200
- if (state.currentSearch) {
201
- return drawSearch(state, width, height);
202
- }
203
- if (state.loading) {
204
- return { rows: [
205
- textStyle.primaryBold(` ${state.viewTitle}`),
206
- fit(textStyle.muted(" 正在加载..."), width),
207
- ruleLine(Math.max(0, width - 1)),
208
- textStyle.muted(` ${"· ".repeat(Math.max(1, Math.floor((width - 2) / 2))).slice(0, width - 1)}`)
209
- ].concat(blank(height - 4, width)).slice(0, height), imageOverlays: [] };
210
- }
211
- if (state.error) {
212
- return { rows: [
213
- textStyle.primaryBold(` ${state.viewTitle}`),
214
- ruleLine(Math.max(0, width - 1)),
215
- textStyle.danger(" 请求失败"),
216
- fit(` ${state.error}`, width)
217
- ].concat(blank(height - 4, width)).slice(0, height), imageOverlays: [] };
218
- }
219
- const rows = [];
220
- rows.push(textStyle.primaryBold(` ${state.viewTitle}`));
221
- rows.push(ruleLine(Math.max(0, width - 1)));
222
- const contentHeight = Math.max(1, height - 2);
223
- const wrapDetail = shouldWrapListDetail(state);
224
- const inlineDetail = shouldInlineListDetail(state);
225
- const scroll = getListScroll(state, contentHeight, width, wrapDetail, inlineDetail);
226
- const visible = getVisibleItems(state.items, scroll, contentHeight, width, wrapDetail, inlineDetail);
227
- visible.forEach(({ item: itemValue, index }) => {
228
- const itemHeight = getListItemHeight(itemValue, width, wrapDetail, inlineDetail);
229
- if (rows.length + itemHeight > height) {
230
- return;
231
- }
232
- const active = index === state.itemIndex && (state.focus === "content" || state.mode === "settings");
233
- const marker = active ? theme.marker.selected : theme.marker.normal;
234
- const title = fit(` ${marker} ${renderListItemTitle(itemValue, inlineDetail)}`, width);
235
- const isUnread = Boolean(itemValue.unread || ((itemValue.unreadCount ?? 0) > 0));
236
- rows.push(renderListTitleRow(title, width, active, state.focus === "content" || state.mode === "settings", isUnread));
237
- if (itemValue.meta) {
238
- rows.push(fit(textStyle.muted(` ${itemValue.meta}`), width));
239
- }
240
- for (const detailLine of getListItemDetailLines(itemValue, width, wrapDetail, inlineDetail)) {
241
- rows.push(fit(textStyle.muted(` ${detailLine}`), width));
242
- }
243
- });
244
- if (visible.length === 0) {
245
- rows.push(textStyle.muted(" 暂无数据"));
246
- }
247
- const lastVisibleIndex = visible.at(-1)?.index ?? scroll - 1;
248
- if (lastVisibleIndex < state.items.length - 1 && rows.length < height) {
249
- rows.push(fit(textStyle.muted(` ↓ 还有 ${state.items.length - lastVisibleIndex - 1} 项`), width));
250
- }
251
- else if (state.currentFeed?.hasMore && rows.length < height) {
252
- rows.push(fit(textStyle.muted(" ↓ 到底自动继续加载,或按 n/Space"), width));
253
- }
254
- return { rows: rows.concat(blank(height - rows.length, width)).slice(0, height), imageOverlays: [] };
255
- }
256
- function listItemTitle(itemValue, inlineDetail) {
257
- if (!itemValue.detail || !inlineDetail) {
258
- return itemValue.title;
259
- }
260
- if ("topicId" in itemValue && itemValue.topicId !== undefined) {
261
- return itemValue.title;
262
- }
263
- return `${itemValue.title} ${truncate(itemValue.detail, 80)}`;
264
- }
265
- function renderListItemTitle(itemValue, inlineDetail) {
266
- const base = listItemTitle(itemValue, inlineDetail);
267
- if (!itemValue.unreadCount || itemValue.unreadCount <= 0) {
268
- return base;
269
- }
270
- return `${base} ${textStyle.noticeBold(`(${itemValue.unreadCount})`)}`;
271
- }
272
- function renderListTitleRow(title, width, active, focused, isUnread) {
273
- if (active) {
274
- if (isUnread) {
275
- return styled(fit(title, width), `${theme.color.selectedBg}${theme.color.notice}${ansi.bold}`);
276
- }
277
- return selectedLine(title, width, focused);
278
- }
279
- if (isUnread) {
280
- return textStyle.noticeBold(title);
281
- }
282
- return textStyle.muted(title);
283
- }
284
- function getListItemHeight(itemValue, width, wrapDetail, inlineDetail) {
285
- return 1 + (itemValue.meta ? 1 : 0) + getListItemDetailLines(itemValue, width, wrapDetail, inlineDetail).length;
286
- }
287
- function getVisibleItems(items, scroll, availableRows, width, wrapDetail, inlineDetail) {
288
- const visible = [];
289
- let usedRows = 0;
290
- for (let index = scroll; index < items.length; index += 1) {
291
- const item = items[index];
292
- const itemHeight = getListItemHeight(item, width, wrapDetail, inlineDetail);
293
- if (usedRows + itemHeight > availableRows) {
294
- break;
295
- }
296
- visible.push({ item, index });
297
- usedRows += itemHeight;
298
- }
299
- return visible;
300
- }
301
- function isListItemVisible(items, scroll, itemIndex, availableRows, width, wrapDetail, inlineDetail) {
302
- if (itemIndex < scroll) {
303
- return false;
304
- }
305
- let usedRows = 0;
306
- for (let index = scroll; index <= itemIndex && index < items.length; index += 1) {
307
- usedRows += getListItemHeight(items[index], width, wrapDetail, inlineDetail);
308
- if (usedRows > availableRows) {
309
- return false;
310
- }
311
- }
312
- return true;
313
- }
314
- function getListScroll(state, availableRows, width, wrapDetail, inlineDetail) {
315
- const maxScroll = Math.max(0, state.items.length - 1);
316
- const current = Math.min(Math.max(0, state.scroll), maxScroll);
317
- if (state.itemIndex < current) {
318
- return state.itemIndex;
319
- }
320
- if (!isListItemVisible(state.items, current, state.itemIndex, availableRows, width, wrapDetail, inlineDetail)) {
321
- let next = current;
322
- while (next < state.itemIndex && !isListItemVisible(state.items, next, state.itemIndex, availableRows, width, wrapDetail, inlineDetail)) {
323
- next += 1;
324
- }
325
- return next;
326
- }
327
- return current;
328
- }
329
- function drawSearch(state, width, height) {
330
- const search = state.currentSearch;
331
- if (!search) {
332
- return { rows: blank(height, width), imageOverlays: [] };
333
- }
334
- const rows = [];
335
- rows.push(drawSearchHeader(state.viewTitle, search, width));
336
- const inputText = search.draft || "";
337
- const placeholder = inputText ? "" : "输入关键词后按 Enter";
338
- const inputLabel = ` 搜索${searchTabLabel(search.kind)}> ${inputText || placeholder}`;
339
- if (search.focus === "input" && state.focus === "content") {
340
- rows.push(selectedLine(fit(inputLabel, width), width, true));
341
- }
342
- else if (inputText) {
343
- rows.push(textStyle.primary(fit(inputLabel, width)));
344
- }
345
- else {
346
- rows.push(textStyle.muted(fit(inputLabel, width)));
347
- }
348
- rows.push(ruleLine(Math.max(0, width - 1)));
349
- if (state.loading) {
350
- rows.push(fit(textStyle.muted(" 正在搜索..."), width));
351
- return { rows: rows.concat(blank(height - rows.length, width)).slice(0, height), imageOverlays: [] };
352
- }
353
- const contentHeight = Math.max(1, height - 3);
354
- const scroll = getListScroll(state, contentHeight, width, false, true);
355
- const visible = getVisibleItems(state.items, scroll, contentHeight, width, false, true);
356
- if (visible.length === 0) {
357
- rows.push(textStyle.muted(search.searched ? " 暂无搜索结果" : " 在输入框中输入关键词并按 Enter;上键切换搜索类型"));
358
- return { rows: rows.concat(blank(height - rows.length, width)).slice(0, height), imageOverlays: [] };
359
- }
360
- visible.forEach(({ item: itemValue, index }) => {
361
- const itemHeight = getListItemHeight(itemValue, width, false, true);
362
- if (rows.length + itemHeight > height) {
363
- return;
364
- }
365
- const active = index === state.itemIndex && state.focus === "content" && search.focus === "results";
366
- const marker = active ? theme.marker.selected : theme.marker.normal;
367
- const title = fit(` ${marker} ${renderListItemTitle(itemValue, true)}`, width);
368
- rows.push(active ? selectedLine(title, width, true) : textStyle.muted(title));
369
- if (itemValue.meta) {
370
- rows.push(fit(textStyle.muted(` ${itemValue.meta}`), width));
371
- }
372
- });
373
- const lastVisibleIndex = visible.at(-1)?.index ?? scroll - 1;
374
- if (lastVisibleIndex < state.items.length - 1 && rows.length < height) {
375
- rows.push(fit(textStyle.muted(` ↓ 还有 ${state.items.length - lastVisibleIndex - 1} 项`), width));
376
- }
377
- else if (search.hasMore && rows.length < height) {
378
- rows.push(fit(textStyle.muted(" ↓ 到底自动继续加载,或按 n/Space"), width));
379
- }
380
- return { rows: rows.concat(blank(height - rows.length, width)).slice(0, height), imageOverlays: [] };
381
- }
382
- function drawFollowing(state, width, height) {
383
- const following = state.currentFollowing;
384
- if (!following) {
385
- return { rows: blank(height, width), imageOverlays: [] };
386
- }
387
- const rows = [];
388
- rows.push(drawFollowingHeader(state.viewTitle, following, width));
389
- rows.push(ruleLine(Math.max(0, width - 1)));
390
- if (state.loading) {
391
- rows.push(fit(textStyle.muted(` 正在读取关注${followingTabLabel(following.kind)}...`), width));
392
- return { rows: rows.concat(blank(height - rows.length, width)).slice(0, height), imageOverlays: [] };
393
- }
394
- const contentHeight = Math.max(1, height - 2);
395
- const scroll = getListScroll(state, contentHeight, width, false, true);
396
- const visible = getVisibleItems(state.items, scroll, contentHeight, width, false, true);
397
- if (visible.length === 0) {
398
- rows.push(textStyle.muted(` 暂无关注${followingTabLabel(following.kind)}内容`));
399
- return { rows: rows.concat(blank(height - rows.length, width)).slice(0, height), imageOverlays: [] };
400
- }
401
- visible.forEach(({ item: itemValue, index }) => {
402
- const itemHeight = getListItemHeight(itemValue, width, false, true);
403
- if (rows.length + itemHeight > height) {
404
- return;
405
- }
406
- const active = index === state.itemIndex && state.focus === "content" && following.focus === "results";
407
- const marker = active ? theme.marker.selected : theme.marker.normal;
408
- const title = fit(` ${marker} ${renderListItemTitle(itemValue, true)}`, width);
409
- rows.push(active ? selectedLine(title, width, true) : textStyle.muted(title));
410
- if (itemValue.meta) {
411
- rows.push(fit(textStyle.muted(` ${itemValue.meta}`), width));
412
- }
413
- });
414
- const lastVisibleIndex = visible.at(-1)?.index ?? scroll - 1;
415
- if (lastVisibleIndex < state.items.length - 1 && rows.length < height) {
416
- rows.push(fit(textStyle.muted(` ↓ 还有 ${state.items.length - lastVisibleIndex - 1} 项`), width));
417
- }
418
- else if (following.hasMore && rows.length < height) {
419
- rows.push(fit(textStyle.muted(" ↓ 到底自动继续加载,或按 n/Space"), width));
420
- }
421
- return { rows: rows.concat(blank(height - rows.length, width)).slice(0, height), imageOverlays: [] };
422
- }
423
- function drawSearchHeader(title, search, width) {
424
- const titleText = ` ${title}`;
425
- const tabs = searchKinds(search).map((entry) => drawSearchTab(searchTabLabel(entry, search), entry === search.kind));
426
- const tabsText = tabs.join(" ");
427
- return fit(`${textStyle.primaryBold(titleText)} ${tabsText}`, width);
428
- }
429
- function drawFollowingHeader(title, following, width) {
430
- const titleText = ` ${title}`;
431
- const tabs = ["board", "user", "favorite"].map((entry) => drawSearchTab(followingTabLabel(entry), entry === following.kind));
432
- return fit(`${textStyle.primaryBold(titleText)} ${tabs.join(" ")}`, width);
433
- }
434
- function drawSearchTab(label, active) {
435
- const content = `[${label}]`;
436
- return active
437
- ? styled(content, `${theme.color.selectedBg}${theme.color.selectedFg}${ansi.bold}`)
438
- : styled(content, theme.color.textOnPrimary);
439
- }
440
- function searchKinds(search) {
441
- return search.board ? ["topic", "board", "user", "board-topic"] : ["topic", "board", "user"];
442
- }
443
- function followingTabLabel(kind) {
444
- switch (kind) {
445
- case "board":
446
- return "版面";
447
- case "user":
448
- return "用户";
449
- case "favorite":
450
- return "收藏";
451
- }
452
- }
453
- function searchTabLabel(kind, search) {
454
- switch (kind) {
455
- case "topic":
456
- return "主题";
457
- case "board":
458
- return "版面";
459
- case "user":
460
- return "用户";
461
- case "board-topic":
462
- return `版内:${search?.board?.title ?? ""}`;
463
- }
464
- }
465
- function getListItemDetailLines(itemValue, width, wrapDetail, inlineDetail) {
466
- if (!itemValue.detail || inlineDetail) {
467
- return [];
468
- }
469
- if ("topicId" in itemValue && itemValue.topicId !== undefined) {
470
- return [];
471
- }
472
- const detailWidth = Math.max(1, width - 2);
473
- if (!wrapDetail) {
474
- return [truncate(itemValue.detail, detailWidth)];
475
- }
476
- return wrapText(itemValue.detail, detailWidth);
477
- }
478
- function shouldWrapListDetail(state) {
479
- return Boolean(state.currentChat);
480
- }
481
- function shouldInlineListDetail(state) {
482
- return !shouldWrapListDetail(state);
483
- }
484
- function drawTopic(state, width, height) {
485
- if (state.loading && (!state.topic || state.topic.lines.length === 0)) {
486
- return { rows: [
487
- textStyle.primary(" 正在打开帖子..."),
488
- "",
489
- textStyle.muted(" 只加载第一页,不预取未读楼层。")
490
- ].concat(blank(height - 3, width)).slice(0, height), imageOverlays: [] };
491
- }
492
- if (state.error) {
493
- return { rows: [
494
- textStyle.danger(" 读取帖子失败"),
495
- fit(` ${state.error}`, width),
496
- "",
497
- textStyle.muted(" h/Esc 返回列表")
498
- ].concat(blank(height - 4, width)).slice(0, height), imageOverlays: [] };
499
- }
500
- const topic = state.topic;
501
- if (!topic) {
502
- return { rows: blank(height, width), imageOverlays: [] };
503
- }
504
- const rows = [];
505
- const imageOverlays = [];
506
- rows.push(fit(textStyle.primaryBold(` ${topic.title}`), width));
507
- rows.push(fit(textStyle.muted(` ${topic.meta}`), width));
508
- rows.push(ruleLine(Math.max(0, width - 1)));
509
- const viewport = Math.max(0, height - rows.length - 1);
510
- const maxScroll = Math.max(0, topic.lines.length - viewport);
511
- const visibleScroll = Math.min(state.scroll, maxScroll);
512
- const body = topic.lines.slice(visibleScroll, visibleScroll + viewport);
513
- for (let index = 0; index < body.length; index += 1) {
514
- const bodyLine = body[index] ?? "";
515
- const lineEntry = currentTopicLine(topic, visibleScroll + index);
516
- const imagePreview = lineEntry?.imagePreview;
517
- const previewHeight = Math.max(1, lineEntry?.imagePreviewRows ?? imagePreviewRows);
518
- const placeholderHeight = Math.max(1, lineEntry?.imageBlockRows ?? imagePlaceholderHeight(body, index));
519
- const imageFitsViewport = imagePreview !== undefined &&
520
- bodyLine.startsWith("[image ") &&
521
- previewHeight <= placeholderHeight &&
522
- index + placeholderHeight <= body.length;
523
- if (imageFitsViewport) {
524
- imageOverlays.push({ row: rows.length, token: imagePreview });
525
- rows.push(...Array.from({ length: placeholderHeight }, () => topicBodyLine("", width)));
526
- index += placeholderHeight - 1;
527
- }
528
- else if (bodyLine.startsWith("[image ")) {
529
- rows.push(topicBodyLine(bodyLine, width, textStyle.primarySoft));
530
- }
531
- else if (bodyLine.startsWith(theme.quote.prefix)) {
532
- rows.push(topicBodyLine(bodyLine, width, textStyle.muted));
533
- }
534
- else if (/^#\d+ /.test(bodyLine)) {
535
- rows.push(topicBodyLine(bodyLine, width, textStyle.ok));
536
- }
537
- else if (isTopicDivider(bodyLine)) {
538
- rows.push(topicBodyLine(bodyLine, width, textStyle.rule));
539
- }
540
- else {
541
- rows.push(topicBodyLine(bodyLine, width));
542
- }
543
- }
544
- const pageInfo = topic.hasMore
545
- ? `已载入 ${topic.loaded} 楼,n 下一页`
546
- : `已载入 ${topic.loaded} 楼,已到底`;
547
- rows.push(fit(textStyle.muted(`${pageInfo}${state.loadingMore ? " · 加载中" : ""}`), width));
548
- return {
549
- rows: rows.concat(blank(height - rows.length, width)).slice(0, height),
550
- imageOverlays
551
- };
552
- }
553
- function topicBodyLine(content, width, style) {
554
- const innerWidth = Math.max(0, width - 2);
555
- const padded = fit(content, innerWidth);
556
- return fit(` ${style ? style(padded) : padded} `, width);
557
- }
558
- function imagePlaceholderHeight(body, start) {
559
- let height = 1;
560
- for (let index = start + 1; index < body.length; index += 1) {
561
- const line = body[index] ?? "";
562
- if (line.startsWith("[image ") || line.trim() !== "") {
563
- break;
564
- }
565
- height += 1;
566
- }
567
- return height;
568
- }
569
- function isTopicDivider(content) {
570
- return content.length >= 8 && [...content].every((char) => char === theme.border.horizontal);
571
- }
572
- function drawStatusBar(state, width) {
573
- const notification = state.notification && state.notification.expiresAt > Date.now()
574
- ? state.notification.message
575
- : undefined;
576
- const left = state.focus === "nav" && !notification
577
- ? ""
578
- : notification ?? (state.status || getStatus(state));
579
- const right = getKeyHints(state);
580
- const padding = Math.max(1, width - cellWidth(left) - cellWidth(right) - 2);
581
- const leftText = notification || isNotificationStatus(left)
582
- ? textStyle.notice(` ${left}`)
583
- : textStyle.muted(` ${left}`);
584
- return fit(`${leftText}${textStyle.muted(`${" ".repeat(padding)}${right} `)}`, width);
585
- }
586
- function isNotificationStatus(status) {
587
- return status.startsWith("已") ||
588
- status.startsWith("发现新版本 ") ||
589
- status.startsWith("当前已是最新版本 ") ||
590
- status === "缓存已清理";
591
- }
592
- function getKeyHints(state) {
593
- const hints = ["j/k 楼层", "↑↓ 逐行", "h← 返回", "l→ 进入", "Enter 确认"];
594
- if (state.currentChat) {
595
- hints.push("c 私信", "n 更多");
596
- }
597
- if (state.currentUser) {
598
- hints.push("a 关注", "n 更多");
599
- }
600
- if (state.currentFeed) {
601
- hints.push("n 更多");
602
- }
603
- if (state.mode === "topic") {
604
- hints.push("c 评论", "a 赞", "s 踩", "d 收藏", "u 用户页");
605
- }
606
- hints.push("f 搜索", "r 刷新", "? 帮助", "q 退出");
607
- return hints.join(" ");
608
- }
609
- function drawHelpModal(baseLines, width, height) {
610
- const canvas = new Canvas(width, height);
611
- canvas.drawLines(rect(width, height), baseLines);
612
- const helpContent = [
613
- textStyle.primaryBold(" 快捷键帮助"),
614
- "",
615
- " 全局",
616
- " q 退出程序",
617
- " ? 打开/关闭帮助",
618
- " j/k 上下移动选择",
619
- " ↑/↓ 按行滚动或移动",
620
- " h/←/Esc 返回上一层/左栏",
621
- " l/→/Enter 进入/确认/执行",
622
- " r 刷新当前视图",
623
- " n/Space 加载更多",
624
- "",
625
- " 导航与列表",
626
- " f 跳到搜索输入框",
627
- " s 用户页中打开与对方私信",
628
- " a 用户页中关注/取关",
629
- " Tab 搜索/关注页切换焦点",
630
- " i 或 / 搜索结果中回到输入框",
631
- "",
632
- " 搜索页",
633
- " Tabs 焦点 h/l 或 ←/→ 切换类型",
634
- " Input 焦点 Enter 执行搜索",
635
- " Results Enter 打开条目",
636
- "",
637
- " 关注页",
638
- " Tabs 焦点 h/l 或 ←/→ 切换 关注/版面/用户/收藏",
639
- " Results Enter 打开条目",
640
- "",
641
- " 主题页",
642
- " j/k 按楼层跳转",
643
- " [/ ] 上一回复 / 下一回复",
644
- " Alt+↑/↓ 上一回复 / 下一回复",
645
- " :数字Enter 跳转到指定楼层",
646
- " c 默认打开评论框,可在 keymap 中改 compose.open",
647
- " a / s / d 赞 / 踩 / 收藏",
648
- " u 打开当前楼层作者用户页",
649
- " Space 打开图片预览",
650
- " ←/→ 切换预览图片",
651
- "",
652
- " 私信与评论框",
653
- " c 默认打开私信框,可在 keymap 中改 compose.open",
654
- " Enter 发送",
655
- " Shift+Enter 换行",
656
- " Ctrl+A 打开表情选择器",
657
- " Backspace 删除前一个字符",
658
- " Tab 插入两个空格",
659
- "",
660
- " 表情选择器",
661
- " ↑/↓/j/k 上下移动",
662
- " ←/→/h/l 切换分类或表情",
663
- " Enter 插入表情",
664
- " Esc 关闭选择器",
665
- "",
666
- " 账号与确认弹窗",
667
- " j/k 上下移动选择",
668
- " Tab 登录框切换字段",
669
- " Enter 选择 / 下一项 / 提交",
670
- " Esc 返回或取消",
671
- "",
672
- " 其它",
673
- " Space 主题页看图;列表页等同 n",
674
- "",
675
- " 按任意键关闭"
676
- ];
677
- const area = center(rect(width, height), 68, Math.min(height - 2, helpContent.length + 2));
678
- canvas.overlay(area, helpContent, { fill: theme.color.panelBg });
679
- return canvas.toString();
680
- }
681
- function drawImageModal(baseLines, state, width, height) {
682
- const viewer = state.imageViewer;
683
- if (!viewer) {
684
- return { text: baseLines.join("\n") };
685
- }
686
- const canvas = new Canvas(width, height);
687
- canvas.drawLines(rect(width, height), baseLines);
688
- const modalWidth = Math.max(24, Math.min(width - 4, Math.floor(width * 0.92)));
689
- const modalHeight = Math.max(10, Math.min(height - 2, Math.floor(height * 0.9)));
690
- const area = center(rect(width, height), modalWidth, modalHeight);
691
- const imageArea = pad(area, 1);
692
- const rows = Array.from({ length: imageArea.height }, (_, index) => {
693
- if (viewer.loading && index === Math.floor(imageArea.height / 2)) {
694
- return fit(textStyle.muted(" 正在加载大图..."), imageArea.width);
695
- }
696
- if (viewer.error && index === Math.floor(imageArea.height / 2)) {
697
- return fit(textStyle.danger(" 图片加载失败"), imageArea.width);
698
- }
699
- return " ".repeat(imageArea.width);
700
- });
701
- canvas.overlay(area, rows, { fill: theme.color.panelBg });
702
- const overlayColumns = Math.min(imageArea.width, Math.max(1, viewer.renderSize?.columns ?? imageArea.width));
703
- const overlayRows = Math.min(imageArea.height, Math.max(1, viewer.renderSize?.rows ?? imageArea.height));
704
- const overlayColumnOffset = Math.max(0, Math.floor((imageArea.width - overlayColumns) / 2));
705
- const overlayRowOffset = Math.max(0, Math.floor((imageArea.height - overlayRows) / 2));
706
- return {
707
- text: canvas.toString(),
708
- imageOverlays: viewer.token && imageArea.width > 0 && imageArea.height > 0
709
- ? [{
710
- row: imageArea.y + overlayRowOffset + 1,
711
- column: imageArea.x + overlayColumnOffset + 1,
712
- token: viewer.token
713
- }]
714
- : []
715
- };
716
- }
717
- function drawComposeModal(baseLines, state, width, height) {
718
- const compose = state.composeDialog;
719
- if (!compose) {
720
- return baseLines.join("\n");
721
- }
722
- const canvas = new Canvas(width, height);
723
- canvas.drawLines(rect(width, height), baseLines);
724
- const modalWidth = Math.min(Math.max(1, width - 2), Math.max(36, Math.floor(width * 0.72)));
725
- const modalHeight = Math.min(Math.max(1, height - 2), Math.max(10, Math.min(height - 6, 14)));
726
- const area = center(rect(width, height), modalWidth, modalHeight);
727
- const innerWidth = Math.max(1, area.width - 2);
728
- const draftHeight = Math.max(3, area.height - 5);
729
- const contentWidth = Math.max(1, innerWidth - 1);
730
- const draftView = buildComposeDraftView(compose.draftUnits, compose.cursorIndex, contentWidth, draftHeight);
731
- const rows = [
732
- fit(`${textStyle.primaryBold(compose.target.kind === "chat" ? " 发送私信" : " 发表评论")}${textStyle.muted(` ${compose.submitting ? "正在发送..." : "Enter 发送 Shift+Enter 换行 表情快捷键打开表情 Esc 取消"}`)}`, innerWidth),
733
- ruleLine(Math.max(0, innerWidth))
734
- ];
735
- for (let index = 0; index < draftHeight; index += 1) {
736
- const line = draftView.lines[index] ?? "";
737
- if (line) {
738
- rows.push(fit(` ${line}`, innerWidth));
739
- }
740
- else if (compose.draft.length === 0 && index === 0) {
741
- rows.push(textStyle.muted(fit(" 输入评论内容", innerWidth)));
742
- }
743
- else {
744
- rows.push(" ".repeat(innerWidth));
745
- }
746
- }
747
- canvas.overlay(area, rows, { fill: theme.color.panelBg });
748
- return canvas.toString();
749
- }
750
- function buildComposeDraftView(draftUnits, cursorIndex, width, viewportHeight) {
751
- const logicalLines = splitComposeUnitsByNewline(draftUnits);
752
- const visualLines = [];
753
- let offset = 0;
754
- let cursorRow = 0;
755
- logicalLines.forEach((logicalLine, logicalIndex) => {
756
- const wrapped = wrapComposeLine(logicalLine, width);
757
- let segmentOffset = 0;
758
- wrapped.forEach((segment) => {
759
- const segmentStart = segmentOffset;
760
- const segmentEnd = segmentStart + segment.length;
761
- const hasCursor = cursorIndex >= offset + segmentStart && cursorIndex <= offset + segmentEnd;
762
- if (hasCursor) {
763
- cursorRow = visualLines.length;
764
- }
765
- visualLines.push(renderComposeCursor(segment, hasCursor ? cursorIndex - offset - segmentStart : undefined));
766
- segmentOffset += segment.length;
767
- });
768
- offset += logicalLine.length;
769
- if (logicalIndex < logicalLines.length - 1) {
770
- if (cursorIndex === offset) {
771
- cursorRow = visualLines.length;
772
- }
773
- offset += 1;
774
- }
775
- });
776
- if (visualLines.length === 0) {
777
- visualLines.push(renderComposeCursor([], 0));
778
- cursorRow = 0;
779
- }
780
- else if (cursorIndex === draftUnits.length && draftUnits.at(-1) === "\n") {
781
- cursorRow = visualLines.length;
782
- visualLines.push(renderComposeCursor([], 0));
783
- }
784
- const start = Math.max(0, Math.min(cursorRow, Math.max(0, visualLines.length - viewportHeight)));
785
- const lines = visualLines.slice(start, start + viewportHeight);
786
- while (lines.length < viewportHeight) {
787
- lines.push("");
788
- }
789
- return { lines };
790
- }
791
- function splitComposeUnitsByNewline(units) {
792
- if (units.length === 0) {
793
- return [[]];
794
- }
795
- const lines = [[]];
796
- for (const unit of units) {
797
- if (unit === "\n") {
798
- lines.push([]);
799
- continue;
800
- }
801
- lines[lines.length - 1]?.push(unit);
802
- }
803
- return lines;
804
- }
805
- function wrapComposeLine(units, width) {
806
- if (units.length === 0) {
807
- return [[]];
808
- }
809
- const lines = [];
810
- let current = [];
811
- let currentWidth = 0;
812
- for (const unit of units) {
813
- const unitWidth = cellWidth(unit);
814
- if (currentWidth + unitWidth > width && current.length > 0) {
815
- lines.push(current);
816
- current = [unit];
817
- currentWidth = unitWidth;
818
- }
819
- else {
820
- current.push(unit);
821
- currentWidth += unitWidth;
822
- }
823
- }
824
- lines.push(current);
825
- return lines;
826
- }
827
- function renderComposeCursor(units, cursorColumn) {
828
- if (cursorColumn === undefined) {
829
- return units.join("");
830
- }
831
- const safeIndex = Math.max(0, Math.min(units.length, cursorColumn));
832
- const cursorStyle = `${theme.color.emotionSelectedBorder}`;
833
- const cursorGlyph = styled("|", cursorStyle);
834
- if (safeIndex === units.length) {
835
- return `${units.join("")}${cursorGlyph}`;
836
- }
837
- return `${units.slice(0, safeIndex).join("")}${cursorGlyph}${units.slice(safeIndex).join("")}`;
838
- }
839
- function drawEmotionPickerModal(baseLines, state, width, height) {
840
- const compose = state.composeDialog;
841
- if (!compose) {
842
- return { text: baseLines.join("\n") };
843
- }
844
- const composeLayer = drawComposeModal(baseLines, state, width, height).split("\n");
845
- const canvas = new Canvas(width, height);
846
- canvas.drawLines(rect(width, height), composeLayer);
847
- const modalWidth = Math.min(Math.max(1, width - 2), Math.max(56, Math.floor(width * 0.78)));
848
- const modalHeight = Math.min(Math.max(1, height - 2), Math.max(18, Math.floor(height * 0.72)));
849
- const area = center(rect(width, height), modalWidth, modalHeight);
850
- canvas.overlay(area, [], { fill: theme.color.panelBg });
851
- const inner = pad(area, 1);
852
- const cellWidthValue = 11;
853
- const cellHeight = 5;
854
- const sidebarWidth = Math.max(8, Math.min(12, Math.floor(inner.width * 0.2)));
855
- const gridArea = {
856
- x: inner.x + sidebarWidth + 1,
857
- y: inner.y,
858
- width: Math.max(1, inner.width - sidebarWidth - 1),
859
- height: inner.height
860
- };
861
- const columns = Math.max(1, Math.floor(gridArea.width / cellWidthValue));
862
- const previewColumns = Math.max(1, cellWidthValue - 2);
863
- const sidebarArea = {
864
- x: inner.x,
865
- y: inner.y,
866
- width: sidebarWidth,
867
- height: inner.height
868
- };
869
- const category = getEmotionCategory(compose.emotionCategoryIndex);
870
- const pageRows = Math.max(1, Math.floor(Math.max(1, gridArea.height - 1) / cellHeight));
871
- const pageSize = columns * pageRows;
872
- const start = Math.max(0, Math.floor(compose.emotionSelectedIndex / pageSize) * pageSize);
873
- const visible = category.entries.slice(start, start + pageSize);
874
- const imageOverlays = [];
875
- const sidebarRows = emotionCategories.map((item, index) => {
876
- const selected = index === compose.emotionCategoryIndex;
877
- const row = fit(` ${item.label}`, sidebarArea.width);
878
- if (selected) {
879
- return selectedLine(row, sidebarArea.width, true);
880
- }
881
- return textStyle.muted(row);
882
- });
883
- canvas.drawLines(sidebarArea, sidebarRows.concat(blank(Math.max(0, sidebarArea.height - sidebarRows.length), sidebarArea.width)));
884
- canvas.verticalRule({ x: inner.x + sidebarWidth, y: inner.y, width: 1, height: inner.height });
885
- const title = fit(textStyle.primaryBold(` ${category.label} · ${visible.length}/${category.entries.length}`), gridArea.width);
886
- canvas.drawLines({ x: gridArea.x, y: gridArea.y, width: gridArea.width, height: 1 }, [title]);
887
- visible.forEach((entry, index) => {
888
- const localIndex = start + index;
889
- const col = index % columns;
890
- const row = Math.floor(index / columns);
891
- const x = gridArea.x + col * cellWidthValue;
892
- const y = gridArea.y + 1 + row * cellHeight;
893
- if (x + cellWidthValue > gridArea.x + gridArea.width || y + cellHeight > gridArea.y + gridArea.height) {
894
- return;
895
- }
896
- const selected = localIndex === compose.emotionSelectedIndex;
897
- const borderStyle = selected ? theme.color.emotionSelectedBorder : theme.color.muted;
898
- const box = { x, y, width: cellWidthValue, height: cellHeight };
899
- canvas.frame(box);
900
- const preview = getEmotionPreview(entry, previewColumns);
901
- if (preview) {
902
- imageOverlays.push({
903
- row: y + 2,
904
- column: x + 2,
905
- token: preview.token
906
- });
907
- }
908
- else {
909
- canvas.drawLines({ x: x + 1, y: y + 1, width: cellWidthValue - 2, height: 2 }, [
910
- fit(selected ? textStyle.primarySoft(" 预览中") : textStyle.muted(" 预览中"), cellWidthValue - 2),
911
- " ".repeat(cellWidthValue - 2)
912
- ]);
913
- }
914
- if (selected) {
915
- tintBox(canvas, box, theme.color.emotionSelectedBorder);
916
- }
917
- else {
918
- tintBox(canvas, box, borderStyle);
919
- }
920
- });
921
- return {
922
- text: canvas.toString(),
923
- imageOverlays
924
- };
925
- }
926
- function tintBox(canvas, area, style) {
927
- canvas.drawLines({ x: area.x, y: area.y, width: area.width, height: 1 }, [textStyleWithStyle(`${theme.border.topLeft}${theme.border.horizontal.repeat(Math.max(0, area.width - 2))}${theme.border.topRight}`, style)]);
928
- for (let row = 1; row < area.height - 1; row += 1) {
929
- canvas.drawLines({ x: area.x, y: area.y + row, width: 1, height: 1 }, [textStyleWithStyle(theme.border.vertical, style)]);
930
- canvas.drawLines({ x: area.x + area.width - 1, y: area.y + row, width: 1, height: 1 }, [textStyleWithStyle(theme.border.vertical, style)]);
931
- }
932
- canvas.drawLines({ x: area.x, y: area.y + area.height - 1, width: area.width, height: 1 }, [textStyleWithStyle(`${theme.border.bottomLeft}${theme.border.horizontal.repeat(Math.max(0, area.width - 2))}${theme.border.bottomRight}`, style)]);
933
- }
934
- function textStyleWithStyle(content, style) {
935
- return `${style}${content}\x1b[0m`;
936
- }
937
122
  //# sourceMappingURL=renderer.js.map