@walavave/cc98-cli 0.1.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 (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/api/client.d.ts +57 -0
  4. package/dist/api/client.d.ts.map +1 -0
  5. package/dist/api/client.js +298 -0
  6. package/dist/api/client.js.map +1 -0
  7. package/dist/api/endpoints.d.ts +48 -0
  8. package/dist/api/endpoints.d.ts.map +1 -0
  9. package/dist/api/endpoints.js +53 -0
  10. package/dist/api/endpoints.js.map +1 -0
  11. package/dist/api/types.d.ts +35 -0
  12. package/dist/api/types.d.ts.map +1 -0
  13. package/dist/api/types.js +2 -0
  14. package/dist/api/types.js.map +1 -0
  15. package/dist/api/webvpn.d.ts +40 -0
  16. package/dist/api/webvpn.d.ts.map +1 -0
  17. package/dist/api/webvpn.js +259 -0
  18. package/dist/api/webvpn.js.map +1 -0
  19. package/dist/cli/commands/account.d.ts +2 -0
  20. package/dist/cli/commands/account.d.ts.map +1 -0
  21. package/dist/cli/commands/account.js +81 -0
  22. package/dist/cli/commands/account.js.map +1 -0
  23. package/dist/cli/commands/board.d.ts +2 -0
  24. package/dist/cli/commands/board.d.ts.map +1 -0
  25. package/dist/cli/commands/board.js +61 -0
  26. package/dist/cli/commands/board.js.map +1 -0
  27. package/dist/cli/commands/cache.d.ts +2 -0
  28. package/dist/cli/commands/cache.d.ts.map +1 -0
  29. package/dist/cli/commands/cache.js +68 -0
  30. package/dist/cli/commands/cache.js.map +1 -0
  31. package/dist/cli/commands/forum.d.ts +2 -0
  32. package/dist/cli/commands/forum.d.ts.map +1 -0
  33. package/dist/cli/commands/forum.js +38 -0
  34. package/dist/cli/commands/forum.js.map +1 -0
  35. package/dist/cli/commands/login.d.ts +2 -0
  36. package/dist/cli/commands/login.d.ts.map +1 -0
  37. package/dist/cli/commands/login.js +98 -0
  38. package/dist/cli/commands/login.js.map +1 -0
  39. package/dist/cli/commands/logout.d.ts +2 -0
  40. package/dist/cli/commands/logout.d.ts.map +1 -0
  41. package/dist/cli/commands/logout.js +20 -0
  42. package/dist/cli/commands/logout.js.map +1 -0
  43. package/dist/cli/commands/me.d.ts +2 -0
  44. package/dist/cli/commands/me.d.ts.map +1 -0
  45. package/dist/cli/commands/me.js +9 -0
  46. package/dist/cli/commands/me.js.map +1 -0
  47. package/dist/cli/commands/message.d.ts +2 -0
  48. package/dist/cli/commands/message.d.ts.map +1 -0
  49. package/dist/cli/commands/message.js +42 -0
  50. package/dist/cli/commands/message.js.map +1 -0
  51. package/dist/cli/commands/notice.d.ts +2 -0
  52. package/dist/cli/commands/notice.d.ts.map +1 -0
  53. package/dist/cli/commands/notice.js +30 -0
  54. package/dist/cli/commands/notice.js.map +1 -0
  55. package/dist/cli/commands/post.d.ts +2 -0
  56. package/dist/cli/commands/post.d.ts.map +1 -0
  57. package/dist/cli/commands/post.js +37 -0
  58. package/dist/cli/commands/post.js.map +1 -0
  59. package/dist/cli/commands/search.d.ts +2 -0
  60. package/dist/cli/commands/search.d.ts.map +1 -0
  61. package/dist/cli/commands/search.js +13 -0
  62. package/dist/cli/commands/search.js.map +1 -0
  63. package/dist/cli/commands/topic.d.ts +2 -0
  64. package/dist/cli/commands/topic.d.ts.map +1 -0
  65. package/dist/cli/commands/topic.js +196 -0
  66. package/dist/cli/commands/topic.js.map +1 -0
  67. package/dist/cli/commands/update.d.ts +2 -0
  68. package/dist/cli/commands/update.d.ts.map +1 -0
  69. package/dist/cli/commands/update.js +24 -0
  70. package/dist/cli/commands/update.js.map +1 -0
  71. package/dist/cli/commands/user.d.ts +2 -0
  72. package/dist/cli/commands/user.d.ts.map +1 -0
  73. package/dist/cli/commands/user.js +90 -0
  74. package/dist/cli/commands/user.js.map +1 -0
  75. package/dist/cli/commands/vpn.d.ts +2 -0
  76. package/dist/cli/commands/vpn.d.ts.map +1 -0
  77. package/dist/cli/commands/vpn.js +177 -0
  78. package/dist/cli/commands/vpn.js.map +1 -0
  79. package/dist/cli/context.d.ts +9 -0
  80. package/dist/cli/context.d.ts.map +1 -0
  81. package/dist/cli/context.js +26 -0
  82. package/dist/cli/context.js.map +1 -0
  83. package/dist/cli/options.d.ts +6 -0
  84. package/dist/cli/options.d.ts.map +1 -0
  85. package/dist/cli/options.js +22 -0
  86. package/dist/cli/options.js.map +1 -0
  87. package/dist/cli/parse.d.ts +15 -0
  88. package/dist/cli/parse.d.ts.map +1 -0
  89. package/dist/cli/parse.js +55 -0
  90. package/dist/cli/parse.js.map +1 -0
  91. package/dist/cli/prompt.d.ts +4 -0
  92. package/dist/cli/prompt.d.ts.map +1 -0
  93. package/dist/cli/prompt.js +53 -0
  94. package/dist/cli/prompt.js.map +1 -0
  95. package/dist/cli/router.d.ts +2 -0
  96. package/dist/cli/router.d.ts.map +1 -0
  97. package/dist/cli/router.js +90 -0
  98. package/dist/cli/router.js.map +1 -0
  99. package/dist/config.d.ts +10 -0
  100. package/dist/config.d.ts.map +1 -0
  101. package/dist/config.js +74 -0
  102. package/dist/config.js.map +1 -0
  103. package/dist/main.d.ts +3 -0
  104. package/dist/main.d.ts.map +1 -0
  105. package/dist/main.js +17 -0
  106. package/dist/main.js.map +1 -0
  107. package/dist/storage/cache-store.d.ts +62 -0
  108. package/dist/storage/cache-store.d.ts.map +1 -0
  109. package/dist/storage/cache-store.js +233 -0
  110. package/dist/storage/cache-store.js.map +1 -0
  111. package/dist/storage/paths.d.ts +5 -0
  112. package/dist/storage/paths.d.ts.map +1 -0
  113. package/dist/storage/paths.js +17 -0
  114. package/dist/storage/paths.js.map +1 -0
  115. package/dist/storage/token-store.d.ts +39 -0
  116. package/dist/storage/token-store.d.ts.map +1 -0
  117. package/dist/storage/token-store.js +177 -0
  118. package/dist/storage/token-store.js.map +1 -0
  119. package/dist/storage/vpn-store.d.ts +16 -0
  120. package/dist/storage/vpn-store.d.ts.map +1 -0
  121. package/dist/storage/vpn-store.js +72 -0
  122. package/dist/storage/vpn-store.js.map +1 -0
  123. package/dist/tui/account-modal.d.ts +31 -0
  124. package/dist/tui/account-modal.d.ts.map +1 -0
  125. package/dist/tui/account-modal.js +97 -0
  126. package/dist/tui/account-modal.js.map +1 -0
  127. package/dist/tui/ansi.d.ts +20 -0
  128. package/dist/tui/ansi.d.ts.map +1 -0
  129. package/dist/tui/ansi.js +28 -0
  130. package/dist/tui/ansi.js.map +1 -0
  131. package/dist/tui/app-data.d.ts +22 -0
  132. package/dist/tui/app-data.d.ts.map +1 -0
  133. package/dist/tui/app-data.js +828 -0
  134. package/dist/tui/app-data.js.map +1 -0
  135. package/dist/tui/app-runtime.d.ts +27 -0
  136. package/dist/tui/app-runtime.d.ts.map +1 -0
  137. package/dist/tui/app-runtime.js +874 -0
  138. package/dist/tui/app-runtime.js.map +1 -0
  139. package/dist/tui/app.d.ts +2 -0
  140. package/dist/tui/app.d.ts.map +1 -0
  141. package/dist/tui/app.js +172 -0
  142. package/dist/tui/app.js.map +1 -0
  143. package/dist/tui/cached-client.d.ts +41 -0
  144. package/dist/tui/cached-client.d.ts.map +1 -0
  145. package/dist/tui/cached-client.js +78 -0
  146. package/dist/tui/cached-client.js.map +1 -0
  147. package/dist/tui/canvas.d.ts +26 -0
  148. package/dist/tui/canvas.d.ts.map +1 -0
  149. package/dist/tui/canvas.js +204 -0
  150. package/dist/tui/canvas.js.map +1 -0
  151. package/dist/tui/downloads.d.ts +2 -0
  152. package/dist/tui/downloads.d.ts.map +1 -0
  153. package/dist/tui/downloads.js +81 -0
  154. package/dist/tui/downloads.js.map +1 -0
  155. package/dist/tui/image-preview.d.ts +19 -0
  156. package/dist/tui/image-preview.d.ts.map +1 -0
  157. package/dist/tui/image-preview.js +217 -0
  158. package/dist/tui/image-preview.js.map +1 -0
  159. package/dist/tui/interactions.d.ts +5 -0
  160. package/dist/tui/interactions.d.ts.map +1 -0
  161. package/dist/tui/interactions.js +25 -0
  162. package/dist/tui/interactions.js.map +1 -0
  163. package/dist/tui/keymap.d.ts +7 -0
  164. package/dist/tui/keymap.d.ts.map +1 -0
  165. package/dist/tui/keymap.js +262 -0
  166. package/dist/tui/keymap.js.map +1 -0
  167. package/dist/tui/layout.d.ts +29 -0
  168. package/dist/tui/layout.d.ts.map +1 -0
  169. package/dist/tui/layout.js +82 -0
  170. package/dist/tui/layout.js.map +1 -0
  171. package/dist/tui/renderer.d.ts +9 -0
  172. package/dist/tui/renderer.d.ts.map +1 -0
  173. package/dist/tui/renderer.js +375 -0
  174. package/dist/tui/renderer.js.map +1 -0
  175. package/dist/tui/terminal.d.ts +41 -0
  176. package/dist/tui/terminal.d.ts.map +1 -0
  177. package/dist/tui/terminal.js +162 -0
  178. package/dist/tui/terminal.js.map +1 -0
  179. package/dist/tui/text.d.ts +10 -0
  180. package/dist/tui/text.d.ts.map +1 -0
  181. package/dist/tui/text.js +158 -0
  182. package/dist/tui/text.js.map +1 -0
  183. package/dist/tui/theme.d.ts +56 -0
  184. package/dist/tui/theme.d.ts.map +1 -0
  185. package/dist/tui/theme.js +95 -0
  186. package/dist/tui/theme.js.map +1 -0
  187. package/dist/tui/tui-model.d.ts +126 -0
  188. package/dist/tui/tui-model.d.ts.map +1 -0
  189. package/dist/tui/tui-model.js +54 -0
  190. package/dist/tui/tui-model.js.map +1 -0
  191. package/dist/tui/ubb-renderer.d.ts +12 -0
  192. package/dist/tui/ubb-renderer.d.ts.map +1 -0
  193. package/dist/tui/ubb-renderer.js +196 -0
  194. package/dist/tui/ubb-renderer.js.map +1 -0
  195. package/dist/update.d.ts +17 -0
  196. package/dist/update.d.ts.map +1 -0
  197. package/dist/update.js +88 -0
  198. package/dist/update.js.map +1 -0
  199. package/dist/version.d.ts +6 -0
  200. package/dist/version.d.ts.map +1 -0
  201. package/dist/version.js +6 -0
  202. package/dist/version.js.map +1 -0
  203. package/docs/images/tui.png +0 -0
  204. package/docs/images/tui2.png +0 -0
  205. package/package.json +43 -0
@@ -0,0 +1,874 @@
1
+ import { checkForUpdate } from "../update.js";
2
+ import { createLoginForm, isPrintableInput, updateLoginField } from "./account-modal.js";
3
+ import { getDefaultAccountName, jumpRelativeTopicFloor, jumpToTopicFloor, loadNextChatPage, loadNextTopicPage, normalizeLoginMessage, openBoard, openChat, openTopic, refreshAccounts, restoreParentList } from "./app-data.js";
4
+ import { loadModalImagePreview, supportsImagePreview } from "./image-preview.js";
5
+ import { fill, length, pad, rect, split } from "./layout.js";
6
+ import { getSidebarWidth } from "./renderer.js";
7
+ import { downloadUrlToDownloads } from "./downloads.js";
8
+ import { currentTopicLine, currentTopicPost, getStatus, navItems, settingsItems } from "./tui-model.js";
9
+ export function createMouseHandler(context, handleScroll, clampSidebarWidth, getDividerColumn, getSize) {
10
+ let pendingScrollRender;
11
+ const scheduleScrollRender = () => {
12
+ if (pendingScrollRender) {
13
+ clearTimeout(pendingScrollRender);
14
+ }
15
+ pendingScrollRender = setTimeout(() => {
16
+ pendingScrollRender = undefined;
17
+ context.render();
18
+ }, 80);
19
+ };
20
+ return (event) => {
21
+ const { state, render } = context;
22
+ if (state.modal) {
23
+ if (event.kind === "up") {
24
+ state.draggingSidebarDivider = false;
25
+ }
26
+ return;
27
+ }
28
+ const size = getSize();
29
+ const dividerColumn = getDividerColumn();
30
+ const withinFrame = event.row >= 2 && event.row < size.rows - 1;
31
+ if (event.kind === "down" && event.button === "wheel-up") {
32
+ handleScroll(state, -3);
33
+ scheduleScrollRender();
34
+ return;
35
+ }
36
+ if (event.kind === "down" && event.button === "wheel-down") {
37
+ handleScroll(state, 3);
38
+ scheduleScrollRender();
39
+ return;
40
+ }
41
+ if (event.kind === "down" && event.button === "left" && withinFrame && event.column === dividerColumn) {
42
+ state.draggingSidebarDivider = true;
43
+ return;
44
+ }
45
+ if (event.kind === "drag" && state.draggingSidebarDivider) {
46
+ state.sidebarWidth = clampSidebarWidth(event.column - 1, size.columns);
47
+ render();
48
+ return;
49
+ }
50
+ if (event.kind === "up") {
51
+ state.draggingSidebarDivider = false;
52
+ if (event.button === "left") {
53
+ if (handleSidebarClick(context, event, size.columns, size.rows)) {
54
+ return;
55
+ }
56
+ if (handleContentClick(context, event, size.columns, size.rows)) {
57
+ return;
58
+ }
59
+ void handleTopicClick(context, event, size.columns, size.rows);
60
+ }
61
+ }
62
+ };
63
+ }
64
+ export function createKeyHandler(context) {
65
+ return (key) => {
66
+ const { keymap, state, close, render } = context;
67
+ const keyAction = keymap.feed(key);
68
+ if (key === "\u0003" || key === "q") {
69
+ close();
70
+ return;
71
+ }
72
+ if (key === "?") {
73
+ state.modal = state.modal === "help" ? null : "help";
74
+ render();
75
+ return;
76
+ }
77
+ if (state.modal === "help") {
78
+ if (key === "h" || key === "\x1b[D" || key === "\x1b" || key === "?" || key === "\r") {
79
+ state.modal = null;
80
+ render();
81
+ }
82
+ return;
83
+ }
84
+ if (state.modal === "account") {
85
+ handleAccountModal(context, key);
86
+ return;
87
+ }
88
+ if (state.modal === "login") {
89
+ handleLoginModal(context, key);
90
+ return;
91
+ }
92
+ if (state.modal === "confirm") {
93
+ handleConfirmModal(context, key);
94
+ return;
95
+ }
96
+ if (state.modal === "image") {
97
+ handleImageModal(context, key);
98
+ return;
99
+ }
100
+ if (state.mode === "topic") {
101
+ handleTopicMode(context, key, keyAction);
102
+ return;
103
+ }
104
+ if (state.mode === "settings") {
105
+ handleSettingsMode(context, key);
106
+ return;
107
+ }
108
+ if (state.focus === "nav") {
109
+ handleNavFocus(context, key);
110
+ return;
111
+ }
112
+ handleContentFocus(context, key);
113
+ };
114
+ }
115
+ function showNotification(state, message, durationMs = 3200) {
116
+ state.notification = {
117
+ message,
118
+ expiresAt: Date.now() + durationMs
119
+ };
120
+ }
121
+ function handleAccountModal(context, key) {
122
+ const { state, render, tokenStore, load } = context;
123
+ if (key === "j" || key === "\x1b[B") {
124
+ state.accountModal.selectedIndex = Math.min(state.accountModal.accounts.length, state.accountModal.selectedIndex + 1);
125
+ render();
126
+ return;
127
+ }
128
+ if (key === "k" || key === "\x1b[A") {
129
+ state.accountModal.selectedIndex = Math.max(0, state.accountModal.selectedIndex - 1);
130
+ render();
131
+ return;
132
+ }
133
+ if (key === "h" || key === "\x1b[D" || key === "\x1b") {
134
+ state.modal = null;
135
+ state.status = getStatus(state);
136
+ render();
137
+ return;
138
+ }
139
+ if (key === "\r" || key === "l" || key === "\x1b[C") {
140
+ if (state.accountModal.selectedIndex === state.accountModal.accounts.length) {
141
+ state.loginForm = createLoginForm();
142
+ state.modal = "login";
143
+ render();
144
+ return;
145
+ }
146
+ const selected = state.accountModal.accounts[state.accountModal.selectedIndex];
147
+ if (!selected) {
148
+ return;
149
+ }
150
+ state.status = `正在切换到 @${selected.account}...`;
151
+ state.modal = null;
152
+ render();
153
+ void tokenStore.useAccount(selected.account).then(() => {
154
+ state.account = selected.account;
155
+ showNotification(state, `已切换到 @${selected.account}`);
156
+ void load(true);
157
+ }).catch((error) => {
158
+ state.error = error instanceof Error ? error.message : String(error);
159
+ state.status = "账号切换失败";
160
+ render();
161
+ });
162
+ }
163
+ }
164
+ function handleLoginModal(context, key) {
165
+ const { state, render, rawClient, tokenStore, load } = context;
166
+ if (state.loginForm.submitting) {
167
+ return;
168
+ }
169
+ if (key === "\t" || key === "j" || key === "\x1b[B") {
170
+ state.loginForm.fieldIndex = (state.loginForm.fieldIndex + 1) % 3;
171
+ render();
172
+ return;
173
+ }
174
+ if (key === "k" || key === "\x1b[A") {
175
+ state.loginForm.fieldIndex = (state.loginForm.fieldIndex + 2) % 3;
176
+ render();
177
+ return;
178
+ }
179
+ if (key === "h" || key === "\x1b[D" || key === "\x1b") {
180
+ state.modal = "account";
181
+ state.loginForm.error = undefined;
182
+ state.status = getStatus(state);
183
+ render();
184
+ return;
185
+ }
186
+ if (key === "l" || key === "\x1b[C") {
187
+ if (state.loginForm.fieldIndex < 2) {
188
+ state.loginForm.fieldIndex += 1;
189
+ render();
190
+ }
191
+ return;
192
+ }
193
+ if (key === "\x7f") {
194
+ updateLoginField(state.loginForm, (value) => value.slice(0, -1));
195
+ render();
196
+ return;
197
+ }
198
+ if (key === "\r") {
199
+ if (state.loginForm.fieldIndex < 2) {
200
+ state.loginForm.fieldIndex += 1;
201
+ render();
202
+ return;
203
+ }
204
+ const username = state.loginForm.username.trim();
205
+ const password = state.loginForm.password;
206
+ if (!username || !password) {
207
+ state.loginForm.error = "用户名和密码不能为空";
208
+ render();
209
+ return;
210
+ }
211
+ state.loginForm.submitting = true;
212
+ state.loginForm.error = undefined;
213
+ state.status = `正在登录 ${username}...`;
214
+ render();
215
+ void rawClient.loginWithPassword(username, password).then(async (token) => {
216
+ const me = await rawClient.getMeWithAccessToken(token.accessToken);
217
+ const resolvedAccount = getDefaultAccountName(me, username);
218
+ await tokenStore.saveAccount(resolvedAccount, {
219
+ accessToken: token.accessToken,
220
+ refreshToken: token.refreshToken,
221
+ userId: typeof me.id === "number" ? me.id : undefined,
222
+ username,
223
+ displayName: typeof me.name === "string" ? me.name : undefined
224
+ });
225
+ state.account = resolvedAccount;
226
+ await refreshAccounts(state, tokenStore);
227
+ state.loginForm = createLoginForm();
228
+ state.modal = null;
229
+ showNotification(state, `已登录为 ${typeof me.name === "string" ? me.name : username}`);
230
+ await load(true);
231
+ }).catch((error) => {
232
+ state.loginForm.submitting = false;
233
+ state.loginForm.error = normalizeLoginMessage(error);
234
+ state.status = "登录失败";
235
+ render();
236
+ });
237
+ return;
238
+ }
239
+ if (isPrintableInput(key)) {
240
+ updateLoginField(state.loginForm, (value) => `${value}${key}`);
241
+ render();
242
+ }
243
+ }
244
+ function handleConfirmModal(context, key) {
245
+ const { state, render, client, tokenStore, load } = context;
246
+ if (!state.confirmDialog) {
247
+ state.modal = null;
248
+ render();
249
+ return;
250
+ }
251
+ if (key === "j" || key === "\x1b[B" || key === "\t") {
252
+ state.confirmDialog.selectedIndex = Math.min(1, state.confirmDialog.selectedIndex + 1);
253
+ render();
254
+ return;
255
+ }
256
+ if (key === "k" || key === "\x1b[A") {
257
+ state.confirmDialog.selectedIndex = Math.max(0, state.confirmDialog.selectedIndex - 1);
258
+ render();
259
+ return;
260
+ }
261
+ if (key === "h" || key === "\x1b[D" || key === "\x1b") {
262
+ state.modal = null;
263
+ state.confirmDialog = undefined;
264
+ state.status = getStatus(state);
265
+ render();
266
+ return;
267
+ }
268
+ if (key !== "\r") {
269
+ return;
270
+ }
271
+ if (state.confirmDialog.selectedIndex === 1) {
272
+ state.modal = null;
273
+ state.confirmDialog = undefined;
274
+ state.status = getStatus(state);
275
+ render();
276
+ return;
277
+ }
278
+ const action = state.confirmDialog.action;
279
+ state.modal = null;
280
+ if (action === "cache-cleanup") {
281
+ state.status = "正在清理缓存...";
282
+ render();
283
+ void client.clearCache().then(() => {
284
+ showNotification(state, "缓存已清理");
285
+ void load(true);
286
+ }).catch((error) => {
287
+ state.error = error instanceof Error ? error.message : String(error);
288
+ state.status = "缓存清理失败";
289
+ render();
290
+ }).finally(() => {
291
+ state.confirmDialog = undefined;
292
+ });
293
+ return;
294
+ }
295
+ const account = state.account;
296
+ state.status = account ? `正在退出 @${account}...` : "正在清除登录信息...";
297
+ render();
298
+ void (async () => {
299
+ if (account) {
300
+ await tokenStore.removeAccount(account);
301
+ }
302
+ else {
303
+ await tokenStore.clear();
304
+ }
305
+ state.account = await tokenStore.getCurrentAccountName();
306
+ await refreshAccounts(state, tokenStore);
307
+ showNotification(state, "已退出登录");
308
+ await load(true);
309
+ })().catch((error) => {
310
+ state.error = error instanceof Error ? error.message : String(error);
311
+ state.status = "退出登录失败";
312
+ render();
313
+ }).finally(() => {
314
+ state.confirmDialog = undefined;
315
+ });
316
+ }
317
+ function handleTopicMode(context, key, keyAction) {
318
+ const { state, render, client, config, nextSignal, abortCurrent } = context;
319
+ if (key === ":" && state.topic && !state.topic.floorInput) {
320
+ state.topic.floorInput = ":";
321
+ state.status = "跳转到楼层:输入数字后 Enter 确认 Esc 取消";
322
+ render();
323
+ return;
324
+ }
325
+ if (/^\d$/.test(key) && state.topic?.floorInput.startsWith(":")) {
326
+ if (state.topic.floorInput === ":" && key === "0") {
327
+ return;
328
+ }
329
+ state.topic.floorInput = `${state.topic.floorInput}${key}`.slice(0, 7);
330
+ state.status = `跳转到 ${state.topic.floorInput.slice(1)} 楼:Enter 确认 Esc 取消`;
331
+ render();
332
+ return;
333
+ }
334
+ if (key === "\x7f" && state.topic?.floorInput) {
335
+ state.topic.floorInput = state.topic.floorInput.slice(0, -1);
336
+ state.status = state.topic.floorInput
337
+ ? state.topic.floorInput === ":"
338
+ ? "跳转到楼层:输入数字后 Enter 确认 Esc 取消"
339
+ : `跳转到 ${state.topic.floorInput.slice(1)} 楼:Enter 确认 Esc 取消`
340
+ : getStatus(state);
341
+ render();
342
+ return;
343
+ }
344
+ if (key === "\r" && state.topic?.floorInput) {
345
+ const floor = Number(state.topic.floorInput.slice(1));
346
+ state.topic.floorInput = "";
347
+ if (Number.isInteger(floor) && floor > 0) {
348
+ void jumpToTopicFloor(client, state, floor, render, config, nextSignal());
349
+ }
350
+ else {
351
+ state.status = getStatus(state);
352
+ render();
353
+ }
354
+ return;
355
+ }
356
+ if ((key === "]" || key === "】" || keyAction === "topic.next-reply") && state.topic) {
357
+ jumpRelativeTopicFloor(state, 1);
358
+ state.status = getStatus(state);
359
+ render();
360
+ return;
361
+ }
362
+ if ((key === "[" || key === "【" || keyAction === "topic.previous-reply") && state.topic) {
363
+ jumpRelativeTopicFloor(state, -1);
364
+ state.status = getStatus(state);
365
+ render();
366
+ return;
367
+ }
368
+ if (key === "h" || key === "\x1b[D") {
369
+ abortCurrent();
370
+ leaveTopicMode(state);
371
+ render();
372
+ return;
373
+ }
374
+ if (key === "\x1b" && state.topic?.floorInput) {
375
+ state.topic.floorInput = "";
376
+ state.status = getStatus(state);
377
+ render();
378
+ return;
379
+ }
380
+ if (key === "j" || key === "\x1b[B") {
381
+ const maxScroll = Math.max(0, (state.topic?.lines.length ?? 0) - 1);
382
+ const wasAtEnd = state.scroll >= maxScroll;
383
+ state.scroll = Math.min(maxScroll, state.scroll + 1);
384
+ state.status = getStatus(state);
385
+ render();
386
+ if (wasAtEnd && state.topic?.hasMore && !state.loadingMore) {
387
+ void loadNextTopicPage(client, state, render, config, nextSignal(), true);
388
+ }
389
+ return;
390
+ }
391
+ if (key === "k" || key === "\x1b[A") {
392
+ state.scroll = Math.max(0, state.scroll - 1);
393
+ state.status = getStatus(state);
394
+ render();
395
+ return;
396
+ }
397
+ if (key === " ") {
398
+ void openTopicImageViewer(context);
399
+ return;
400
+ }
401
+ if (key === "n") {
402
+ void loadNextTopicPage(client, state, render, config, nextSignal());
403
+ return;
404
+ }
405
+ if (key === "\x1b[C") {
406
+ void stepTopicImageViewer(context, 1);
407
+ return;
408
+ }
409
+ if (key === "r") {
410
+ if (state.topic) {
411
+ void openTopic(client, state, state.topic.topicId, render, config, true, nextSignal());
412
+ }
413
+ return;
414
+ }
415
+ }
416
+ function handleImageModal(context, key) {
417
+ const { state, render } = context;
418
+ if (!state.imageViewer) {
419
+ state.modal = null;
420
+ render();
421
+ return;
422
+ }
423
+ if (key === " " || key === "\x1b" || key === "h" || key === "\r") {
424
+ state.modal = null;
425
+ state.status = getStatus(state);
426
+ render();
427
+ return;
428
+ }
429
+ if (key === "\x1b[C" || key === "l") {
430
+ void stepTopicImageViewer(context, 1);
431
+ return;
432
+ }
433
+ if (key === "\x1b[D" || key === "k") {
434
+ void stepTopicImageViewer(context, -1);
435
+ return;
436
+ }
437
+ }
438
+ async function openTopicImageViewer(context) {
439
+ const { state, render } = context;
440
+ const topic = state.topic;
441
+ if (!topic) {
442
+ return;
443
+ }
444
+ if (!supportsImagePreview()) {
445
+ state.status = "当前终端不支持图片大图预览";
446
+ render();
447
+ return;
448
+ }
449
+ const images = topic.posts.flatMap((post) => post.images);
450
+ if (images.length === 0) {
451
+ state.status = "当前帖子没有可预览的图片";
452
+ render();
453
+ return;
454
+ }
455
+ const currentLine = currentTopicLine(topic, state.scroll);
456
+ const currentPost = currentTopicPost(topic, state.scroll);
457
+ const targetUrl = currentLine?.imageUrl ?? currentPost?.images[0] ?? images[0];
458
+ const index = Math.max(0, images.findIndex((url) => url === targetUrl));
459
+ state.imageViewer = {
460
+ images,
461
+ index,
462
+ loading: true
463
+ };
464
+ state.modal = "image";
465
+ render();
466
+ await refreshTopicImageViewer(context, index);
467
+ }
468
+ async function stepTopicImageViewer(context, delta) {
469
+ const viewer = context.state.imageViewer;
470
+ if (!viewer || viewer.images.length === 0) {
471
+ return;
472
+ }
473
+ const nextIndex = Math.min(viewer.images.length - 1, Math.max(0, viewer.index + delta));
474
+ if (nextIndex === viewer.index && viewer.token) {
475
+ return;
476
+ }
477
+ viewer.index = nextIndex;
478
+ viewer.loading = true;
479
+ viewer.error = undefined;
480
+ viewer.token = undefined;
481
+ viewer.renderSize = undefined;
482
+ context.state.modal = "image";
483
+ context.render();
484
+ await refreshTopicImageViewer(context, nextIndex);
485
+ }
486
+ async function refreshTopicImageViewer(context, index) {
487
+ const { state, render } = context;
488
+ const viewer = state.imageViewer;
489
+ if (!viewer) {
490
+ return;
491
+ }
492
+ const terminalSize = process.stdout.isTTY
493
+ ? { columns: process.stdout.columns || 80, rows: process.stdout.rows || 24 }
494
+ : { columns: 80, rows: 24 };
495
+ const modalWidth = Math.max(24, Math.min(terminalSize.columns - 4, Math.floor(terminalSize.columns * 0.92)));
496
+ const modalHeight = Math.max(10, Math.min(terminalSize.rows - 2, Math.floor(terminalSize.rows * 0.9)));
497
+ const maxColumns = Math.max(1, modalWidth - 2);
498
+ const maxRows = Math.max(1, modalHeight - 2);
499
+ const url = viewer.images[index];
500
+ try {
501
+ const loadedImage = await loadModalImagePreview(url ?? "", maxColumns, maxRows);
502
+ if (!state.imageViewer || state.imageViewer.index !== index) {
503
+ return;
504
+ }
505
+ state.imageViewer.loading = false;
506
+ state.imageViewer.token = loadedImage?.token;
507
+ state.imageViewer.renderSize = loadedImage?.size;
508
+ state.imageViewer.error = loadedImage ? undefined : "当前终端无法显示这张图片";
509
+ }
510
+ catch (error) {
511
+ if (!state.imageViewer || state.imageViewer.index !== index) {
512
+ return;
513
+ }
514
+ state.imageViewer.loading = false;
515
+ state.imageViewer.token = undefined;
516
+ state.imageViewer.renderSize = undefined;
517
+ state.imageViewer.error = error instanceof Error ? error.message : "图片加载失败";
518
+ }
519
+ render();
520
+ }
521
+ function leaveTopicMode(state) {
522
+ state.mode = "list";
523
+ state.focus = "content";
524
+ state.viewTitle = state.currentBoard?.title ?? state.currentChat?.title ?? navItems[state.navIndex]?.label ?? state.viewTitle;
525
+ state.status = getStatus(state);
526
+ }
527
+ function enterContentMode(state, resetIndex = false) {
528
+ if (navItems[state.navIndex]?.id === "settings") {
529
+ state.mode = "settings";
530
+ }
531
+ state.focus = "content";
532
+ if (resetIndex) {
533
+ state.itemIndex = 0;
534
+ }
535
+ state.status = getStatus(state);
536
+ }
537
+ function leaveContentMode(state) {
538
+ if (state.parentList) {
539
+ restoreParentList(state);
540
+ return;
541
+ }
542
+ state.mode = "list";
543
+ state.focus = "nav";
544
+ state.status = getStatus(state);
545
+ }
546
+ function openSelectedItem(context) {
547
+ const { state, render, client, config, nextSignal } = context;
548
+ const selected = state.items[state.itemIndex];
549
+ if (selected?.topicId !== undefined) {
550
+ void openTopic(client, state, selected.topicId, render, config, false, nextSignal());
551
+ return true;
552
+ }
553
+ if (selected?.boardId !== undefined) {
554
+ void openBoard(client, state, selected.boardId, selected.title, render, false, nextSignal());
555
+ return true;
556
+ }
557
+ if (selected?.chatUserId !== undefined) {
558
+ void openChat(client, state, selected.chatUserId, selected.title, render, false, nextSignal());
559
+ return true;
560
+ }
561
+ return false;
562
+ }
563
+ async function handleTopicClick(context, event, columns, rows) {
564
+ const { state, render } = context;
565
+ if (state.mode !== "topic" || !state.topic || state.loading || state.error) {
566
+ return;
567
+ }
568
+ const mainArea = getMainAreaRect(columns, rows, context.config, state.sidebarWidth);
569
+ if (!withinRect(event.column, event.row, mainArea)) {
570
+ return;
571
+ }
572
+ const bodyRow = event.row - (mainArea.y + 1);
573
+ const bodyLineIndex = bodyRow - 3;
574
+ if (bodyLineIndex < 0) {
575
+ return;
576
+ }
577
+ const absoluteLine = state.scroll + bodyLineIndex;
578
+ const lineEntry = state.topic.posts
579
+ .flatMap((post) => post.lines)
580
+ .find((entry) => entry.line === absoluteLine);
581
+ const url = lineEntry?.linkUrl ?? lineEntry?.imageUrl;
582
+ if (!url) {
583
+ return;
584
+ }
585
+ state.status = `正在下载 ${shortUrl(url)}...`;
586
+ render();
587
+ try {
588
+ const savedPath = await downloadUrlToDownloads(url);
589
+ showNotification(state, `已下载到 ${savedPath}`);
590
+ }
591
+ catch (error) {
592
+ state.status = error instanceof Error ? error.message : "下载失败";
593
+ }
594
+ finally {
595
+ render();
596
+ }
597
+ }
598
+ function handleSidebarClick(context, event, columns, rows) {
599
+ const { state, render, load, abortCurrent } = context;
600
+ if (state.mode === "topic") {
601
+ return false;
602
+ }
603
+ const sidebarArea = getSidebarAreaRect(columns, rows, context.config, state.sidebarWidth);
604
+ if (sidebarArea.width <= 0 || !withinRect(event.column, event.row, sidebarArea)) {
605
+ return false;
606
+ }
607
+ const rowIndex = event.row - (sidebarArea.y + 1);
608
+ if (rowIndex < 0 || rowIndex >= navItems.length) {
609
+ return true;
610
+ }
611
+ const nextNavIndex = Math.max(0, Math.min(navItems.length - 1, rowIndex));
612
+ if (state.navIndex === nextNavIndex && state.focus === "nav" && state.mode !== "settings") {
613
+ return true;
614
+ }
615
+ abortCurrent();
616
+ state.navIndex = nextNavIndex;
617
+ state.focus = "nav";
618
+ if (state.mode === "settings") {
619
+ state.mode = "list";
620
+ }
621
+ state.status = getStatus(state);
622
+ render();
623
+ void load();
624
+ return true;
625
+ }
626
+ function handleContentClick(context, event, columns, rows) {
627
+ const { state, render } = context;
628
+ if (state.mode === "topic" || state.loading || state.error) {
629
+ return false;
630
+ }
631
+ const mainArea = getMainAreaRect(columns, rows, context.config, state.sidebarWidth);
632
+ if (!withinRect(event.column, event.row, mainArea)) {
633
+ return false;
634
+ }
635
+ if (navItems[state.navIndex]?.id === "settings" && state.mode !== "settings") {
636
+ state.mode = "settings";
637
+ }
638
+ if (state.mode === "settings") {
639
+ const rowIndex = event.row - (mainArea.y + 1) - 2;
640
+ if (rowIndex < 0 || rowIndex >= settingsItems.length) {
641
+ return true;
642
+ }
643
+ state.itemIndex = rowIndex;
644
+ enterContentMode(state);
645
+ render();
646
+ return true;
647
+ }
648
+ const rowIndex = event.row - (mainArea.y + 1) - 2;
649
+ if (rowIndex < 0) {
650
+ return true;
651
+ }
652
+ const itemHeight = 2;
653
+ if (rowIndex % itemHeight !== 0 && rowIndex % itemHeight !== 1) {
654
+ return true;
655
+ }
656
+ const visibleCapacity = Math.max(1, Math.floor(Math.max(1, mainArea.height - 2) / itemHeight));
657
+ const scroll = getContentListScroll(state, visibleCapacity);
658
+ const itemOffset = Math.floor(rowIndex / itemHeight);
659
+ const itemIndex = scroll + itemOffset;
660
+ if (itemIndex < 0 || itemIndex >= state.items.length) {
661
+ return true;
662
+ }
663
+ state.itemIndex = itemIndex;
664
+ enterContentMode(state);
665
+ render();
666
+ return true;
667
+ }
668
+ function getMainAreaRect(columns, rows, config, sidebarWidthOverride) {
669
+ const { mainArea } = getBodyColumnRects(columns, rows, config, sidebarWidthOverride);
670
+ return mainArea;
671
+ }
672
+ function getSidebarAreaRect(columns, rows, config, sidebarWidthOverride) {
673
+ const { sidebarArea } = getBodyColumnRects(columns, rows, config, sidebarWidthOverride);
674
+ return sidebarArea;
675
+ }
676
+ function getBodyColumnRects(columns, rows, config, sidebarWidthOverride) {
677
+ const width = Math.max(1, columns);
678
+ const height = Math.max(1, rows);
679
+ const outer = rect(width, Math.max(0, height - 1));
680
+ const root = pad(outer, 1);
681
+ const verticalLayout = config.hideTopChrome
682
+ ? [fill()]
683
+ : [length(1), length(1), length(1), length(1), fill()];
684
+ const areas = split(root, "vertical", verticalLayout);
685
+ const bodyArea = config.hideTopChrome ? areas[0] : areas[4];
686
+ const sidebarWidth = getSidebarWidth(width, sidebarWidthOverride);
687
+ const bodyColumns = split(bodyArea, "horizontal", [
688
+ length(sidebarWidth),
689
+ length(sidebarWidth > 0 ? 1 : 0),
690
+ fill()
691
+ ]);
692
+ return {
693
+ sidebarArea: bodyColumns[0],
694
+ mainArea: bodyColumns[2]
695
+ };
696
+ }
697
+ function getContentListScroll(state, visibleCapacity) {
698
+ const maxScroll = Math.max(0, state.items.length - visibleCapacity);
699
+ const current = Math.min(Math.max(0, state.scroll), maxScroll);
700
+ if (state.itemIndex < current) {
701
+ return state.itemIndex;
702
+ }
703
+ if (state.itemIndex >= current + visibleCapacity) {
704
+ return Math.min(maxScroll, state.itemIndex - visibleCapacity + 1);
705
+ }
706
+ return current;
707
+ }
708
+ function withinRect(column, row, area) {
709
+ const x = column - 1;
710
+ const y = row - 1;
711
+ return x >= area.x && x < area.x + area.width && y >= area.y && y < area.y + area.height;
712
+ }
713
+ function shortUrl(value) {
714
+ try {
715
+ const url = new URL(value);
716
+ const fileName = url.pathname.split("/").filter(Boolean).at(-1) ?? url.host;
717
+ return `${url.host}/${fileName}`;
718
+ }
719
+ catch {
720
+ return value;
721
+ }
722
+ }
723
+ function handleSettingsMode(context, key) {
724
+ const { state, render, tokenStore, load } = context;
725
+ if (key === "j" || key === "\x1b[B") {
726
+ state.itemIndex = Math.min(settingsItems.length - 1, state.itemIndex + 1);
727
+ render();
728
+ return;
729
+ }
730
+ if (key === "k" || key === "\x1b[A") {
731
+ state.itemIndex = Math.max(0, state.itemIndex - 1);
732
+ render();
733
+ return;
734
+ }
735
+ if (key === "h" || key === "\x1b[D") {
736
+ leaveContentMode(state);
737
+ render();
738
+ return;
739
+ }
740
+ if (key !== "l" && key !== "\x1b[C" && key !== "\r") {
741
+ return;
742
+ }
743
+ const selected = settingsItems[state.itemIndex];
744
+ if (selected?.meta === "help") {
745
+ state.modal = "help";
746
+ render();
747
+ return;
748
+ }
749
+ if (selected?.meta === "cache") {
750
+ state.confirmDialog = {
751
+ title: "缓存清理",
752
+ detail: "清理过期文件缓存,并清空当前会话中的内存缓存?",
753
+ confirmLabel: "确认清理",
754
+ cancelLabel: "取消",
755
+ selectedIndex: 1,
756
+ action: "cache-cleanup"
757
+ };
758
+ state.modal = "confirm";
759
+ render();
760
+ return;
761
+ }
762
+ if (selected?.meta === "logout") {
763
+ state.confirmDialog = {
764
+ title: "退出登录",
765
+ detail: state.account
766
+ ? `删除本地账号 @${state.account} 的登录信息?`
767
+ : "清除本地登录信息?",
768
+ confirmLabel: "确认退出",
769
+ cancelLabel: "取消",
770
+ selectedIndex: 1,
771
+ action: "logout"
772
+ };
773
+ state.modal = "confirm";
774
+ render();
775
+ return;
776
+ }
777
+ if (selected?.meta === "account") {
778
+ void refreshAccounts(state, tokenStore).then(() => {
779
+ if (state.accountModal.accounts.length === 0) {
780
+ state.loginForm = createLoginForm();
781
+ state.modal = "login";
782
+ }
783
+ else {
784
+ state.modal = "account";
785
+ }
786
+ render();
787
+ }).catch((error) => {
788
+ state.error = error instanceof Error ? error.message : String(error);
789
+ state.status = "读取账号列表失败";
790
+ render();
791
+ });
792
+ return;
793
+ }
794
+ if (selected?.meta === "update") {
795
+ state.status = "正在检查 GitHub Release...";
796
+ render();
797
+ void checkForUpdate().then((result) => {
798
+ showNotification(state, result.message);
799
+ render();
800
+ }).catch((error) => {
801
+ state.status = error instanceof Error ? error.message : "检查更新失败";
802
+ render();
803
+ });
804
+ return;
805
+ }
806
+ void load(true);
807
+ }
808
+ function handleNavFocus(context, key) {
809
+ const { state, render, load } = context;
810
+ if (key === "j" || key === "\x1b[B") {
811
+ state.navIndex = Math.min(navItems.length - 1, state.navIndex + 1);
812
+ void load();
813
+ return;
814
+ }
815
+ if (key === "k" || key === "\x1b[A") {
816
+ state.navIndex = Math.max(0, state.navIndex - 1);
817
+ void load();
818
+ return;
819
+ }
820
+ if (key === "l" || key === "\x1b[C" || key === "\r") {
821
+ if (!state.loading && state.items.length > 0) {
822
+ enterContentMode(state, key === "\r");
823
+ render();
824
+ }
825
+ return;
826
+ }
827
+ if (key === "r") {
828
+ void load(true);
829
+ }
830
+ }
831
+ function handleContentFocus(context, key) {
832
+ const { state, render, client, config, nextSignal, abortCurrent, load } = context;
833
+ if (key === "j" || key === "\x1b[B") {
834
+ state.itemIndex = Math.min(Math.max(0, state.items.length - 1), state.itemIndex + 1);
835
+ render();
836
+ return;
837
+ }
838
+ if (key === "k" || key === "\x1b[A") {
839
+ state.itemIndex = Math.max(0, state.itemIndex - 1);
840
+ render();
841
+ return;
842
+ }
843
+ if (key === "h" || key === "\x1b[D" || key === "\x1b") {
844
+ abortCurrent();
845
+ leaveContentMode(state);
846
+ render();
847
+ return;
848
+ }
849
+ if (key === "l" || key === "\x1b[C" || key === "\r") {
850
+ if (openSelectedItem(context)) {
851
+ return;
852
+ }
853
+ state.status = "当前条目不可进入";
854
+ render();
855
+ return;
856
+ }
857
+ if ((key === "n" || key === " ") && state.currentChat) {
858
+ void loadNextChatPage(client, state, render, nextSignal());
859
+ return;
860
+ }
861
+ if (key === "r") {
862
+ if (state.currentBoard) {
863
+ void openBoard(client, state, state.currentBoard.boardId, state.currentBoard.title, render, true, nextSignal(), false);
864
+ return;
865
+ }
866
+ if (state.currentChat) {
867
+ void openChat(client, state, state.currentChat.userId, state.currentChat.title, render, true, nextSignal(), false);
868
+ return;
869
+ }
870
+ void load(true);
871
+ return;
872
+ }
873
+ }
874
+ //# sourceMappingURL=app-runtime.js.map