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