@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.
- package/CHANGELOG.md +33 -1
- package/README.md +36 -4
- package/dist/api/client.d.ts +5 -2
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +14 -5
- package/dist/api/client.js.map +1 -1
- package/dist/api/endpoints.d.ts +4 -1
- package/dist/api/endpoints.d.ts.map +1 -1
- package/dist/api/endpoints.js +5 -2
- package/dist/api/endpoints.js.map +1 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -5
- package/dist/config.js.map +1 -1
- package/dist/tui/account-modal.js +3 -3
- package/dist/tui/account-modal.js.map +1 -1
- package/dist/tui/app-runtime/content/following.d.ts +4 -0
- package/dist/tui/app-runtime/content/following.d.ts.map +1 -0
- package/dist/tui/app-runtime/content/following.js +113 -0
- package/dist/tui/app-runtime/content/following.js.map +1 -0
- package/dist/tui/app-runtime/content/list.d.ts +1 -1
- package/dist/tui/app-runtime/content/list.d.ts.map +1 -1
- package/dist/tui/app-runtime/content/list.js +49 -9
- package/dist/tui/app-runtime/content/list.js.map +1 -1
- package/dist/tui/app-runtime/content/navigation.d.ts.map +1 -1
- package/dist/tui/app-runtime/content/navigation.js +8 -0
- package/dist/tui/app-runtime/content/navigation.js.map +1 -1
- package/dist/tui/app-runtime/content/search.d.ts.map +1 -1
- package/dist/tui/app-runtime/content/search.js +40 -4
- package/dist/tui/app-runtime/content/search.js.map +1 -1
- package/dist/tui/app-runtime/content.d.ts +1 -0
- package/dist/tui/app-runtime/content.d.ts.map +1 -1
- package/dist/tui/app-runtime/content.js +1 -0
- package/dist/tui/app-runtime/content.js.map +1 -1
- package/dist/tui/app-runtime/image-viewer.js +2 -2
- package/dist/tui/app-runtime/image-viewer.js.map +1 -1
- package/dist/tui/app-runtime/interactions.d.ts +1 -1
- package/dist/tui/app-runtime/interactions.d.ts.map +1 -1
- package/dist/tui/app-runtime/interactions.js +87 -2
- package/dist/tui/app-runtime/interactions.js.map +1 -1
- package/dist/tui/app-runtime/keyboard.d.ts.map +1 -1
- package/dist/tui/app-runtime/keyboard.js +25 -4
- package/dist/tui/app-runtime/keyboard.js.map +1 -1
- package/dist/tui/app-runtime/modals.d.ts +5 -1
- package/dist/tui/app-runtime/modals.d.ts.map +1 -1
- package/dist/tui/app-runtime/modals.js +15 -10
- package/dist/tui/app-runtime/modals.js.map +1 -1
- package/dist/tui/app-runtime/mouse.d.ts +1 -1
- package/dist/tui/app-runtime/mouse.d.ts.map +1 -1
- package/dist/tui/app-runtime/state.d.ts.map +1 -1
- package/dist/tui/app-runtime/state.js +5 -0
- package/dist/tui/app-runtime/state.js.map +1 -1
- package/dist/tui/app-runtime/topic.d.ts.map +1 -1
- package/dist/tui/app-runtime/topic.js +118 -5
- package/dist/tui/app-runtime/topic.js.map +1 -1
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +26 -3
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/cached-client.d.ts +7 -1
- package/dist/tui/cached-client.d.ts.map +1 -1
- package/dist/tui/cached-client.js +23 -1
- package/dist/tui/cached-client.js.map +1 -1
- package/dist/tui/data/content.d.ts +3 -1
- package/dist/tui/data/content.d.ts.map +1 -1
- package/dist/tui/data/content.js +110 -3
- package/dist/tui/data/content.js.map +1 -1
- package/dist/tui/data/feed-status.d.ts.map +1 -1
- package/dist/tui/data/feed-status.js +25 -5
- package/dist/tui/data/feed-status.js.map +1 -1
- package/dist/tui/data/following.d.ts +9 -0
- package/dist/tui/data/following.d.ts.map +1 -0
- package/dist/tui/data/following.js +117 -0
- package/dist/tui/data/following.js.map +1 -0
- package/dist/tui/data/items.d.ts +1 -0
- package/dist/tui/data/items.d.ts.map +1 -1
- package/dist/tui/data/items.js +20 -4
- package/dist/tui/data/items.js.map +1 -1
- package/dist/tui/data/navigation-state.d.ts +4 -2
- package/dist/tui/data/navigation-state.d.ts.map +1 -1
- package/dist/tui/data/navigation-state.js +26 -5
- package/dist/tui/data/navigation-state.js.map +1 -1
- package/dist/tui/data/search.d.ts +4 -1
- package/dist/tui/data/search.d.ts.map +1 -1
- package/dist/tui/data/search.js +126 -29
- package/dist/tui/data/search.js.map +1 -1
- package/dist/tui/data/topic.d.ts +2 -2
- package/dist/tui/data/topic.d.ts.map +1 -1
- package/dist/tui/data/topic.js +42 -8
- package/dist/tui/data/topic.js.map +1 -1
- package/dist/tui/data/view-items.d.ts +7 -2
- package/dist/tui/data/view-items.d.ts.map +1 -1
- package/dist/tui/data/view-items.js +276 -16
- package/dist/tui/data/view-items.js.map +1 -1
- package/dist/tui/data/view-loader.d.ts +2 -1
- package/dist/tui/data/view-loader.d.ts.map +1 -1
- package/dist/tui/data/view-loader.js +52 -36
- package/dist/tui/data/view-loader.js.map +1 -1
- package/dist/tui/keymap.d.ts +1 -1
- package/dist/tui/keymap.d.ts.map +1 -1
- package/dist/tui/keymap.js +2 -0
- package/dist/tui/keymap.js.map +1 -1
- package/dist/tui/media/downloads.d.ts.map +1 -0
- package/dist/tui/{downloads.js → media/downloads.js} +1 -1
- package/dist/tui/media/downloads.js.map +1 -0
- package/dist/tui/media/emotion-catalog.d.ts.map +1 -0
- package/dist/tui/{emotion-catalog.js → media/emotion-catalog.js} +1 -1
- package/dist/tui/media/emotion-catalog.js.map +1 -0
- package/dist/tui/media/emotion-preview.d.ts.map +1 -0
- package/dist/tui/media/emotion-preview.js.map +1 -0
- package/dist/tui/media/image-preview.d.ts.map +1 -0
- package/dist/tui/{image-preview.js → media/image-preview.js} +1 -1
- package/dist/tui/media/image-preview.js.map +1 -0
- package/dist/tui/media/ubb-renderer.d.ts.map +1 -0
- package/dist/tui/{ubb-renderer.js → media/ubb-renderer.js} +134 -6
- package/dist/tui/media/ubb-renderer.js.map +1 -0
- package/dist/tui/render-core/ansi.d.ts.map +1 -0
- package/dist/tui/render-core/ansi.js.map +1 -0
- package/dist/tui/render-core/canvas.d.ts.map +1 -0
- package/dist/tui/render-core/canvas.js.map +1 -0
- package/dist/tui/render-core/layout.d.ts.map +1 -0
- package/dist/tui/render-core/layout.js.map +1 -0
- package/dist/tui/{terminal.d.ts → render-core/terminal.d.ts} +1 -0
- package/dist/tui/render-core/terminal.d.ts.map +1 -0
- package/dist/tui/{terminal.js → render-core/terminal.js} +16 -7
- package/dist/tui/render-core/terminal.js.map +1 -0
- package/dist/tui/render-core/text.d.ts.map +1 -0
- package/dist/tui/render-core/text.js.map +1 -0
- package/dist/tui/{theme.d.ts → render-core/theme.d.ts} +1 -0
- package/dist/tui/render-core/theme.d.ts.map +1 -0
- package/dist/tui/{theme.js → render-core/theme.js} +2 -1
- package/dist/tui/render-core/theme.js.map +1 -0
- package/dist/tui/renderer/content.d.ts +14 -0
- package/dist/tui/renderer/content.d.ts.map +1 -0
- package/dist/tui/renderer/content.js +474 -0
- package/dist/tui/renderer/content.js.map +1 -0
- package/dist/tui/renderer/modals.d.ts +4 -0
- package/dist/tui/renderer/modals.d.ts.map +1 -0
- package/dist/tui/renderer/modals.js +343 -0
- package/dist/tui/renderer/modals.js.map +1 -0
- package/dist/tui/renderer.d.ts +3 -4
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +23 -685
- package/dist/tui/renderer.js.map +1 -1
- package/dist/tui/tui-model.d.ts +33 -4
- package/dist/tui/tui-model.d.ts.map +1 -1
- package/dist/tui/tui-model.js +35 -6
- package/dist/tui/tui-model.js.map +1 -1
- package/dist/version.d.ts +3 -3
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -2
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/dist/tui/ansi.d.ts.map +0 -1
- package/dist/tui/ansi.js.map +0 -1
- package/dist/tui/canvas.d.ts.map +0 -1
- package/dist/tui/canvas.js.map +0 -1
- package/dist/tui/downloads.d.ts.map +0 -1
- package/dist/tui/downloads.js.map +0 -1
- package/dist/tui/emotion-catalog.d.ts.map +0 -1
- package/dist/tui/emotion-catalog.js.map +0 -1
- package/dist/tui/emotion-preview.d.ts.map +0 -1
- package/dist/tui/emotion-preview.js.map +0 -1
- package/dist/tui/image-preview.d.ts.map +0 -1
- package/dist/tui/image-preview.js.map +0 -1
- package/dist/tui/layout.d.ts.map +0 -1
- package/dist/tui/layout.js.map +0 -1
- package/dist/tui/terminal.d.ts.map +0 -1
- package/dist/tui/terminal.js.map +0 -1
- package/dist/tui/text.d.ts.map +0 -1
- package/dist/tui/text.js.map +0 -1
- package/dist/tui/theme.d.ts.map +0 -1
- package/dist/tui/theme.js.map +0 -1
- package/dist/tui/ubb-renderer.d.ts.map +0 -1
- package/dist/tui/ubb-renderer.js.map +0 -1
- /package/dist/tui/{downloads.d.ts → media/downloads.d.ts} +0 -0
- /package/dist/tui/{emotion-catalog.d.ts → media/emotion-catalog.d.ts} +0 -0
- /package/dist/tui/{emotion-preview.d.ts → media/emotion-preview.d.ts} +0 -0
- /package/dist/tui/{emotion-preview.js → media/emotion-preview.js} +0 -0
- /package/dist/tui/{image-preview.d.ts → media/image-preview.d.ts} +0 -0
- /package/dist/tui/{ubb-renderer.d.ts → media/ubb-renderer.d.ts} +0 -0
- /package/dist/tui/{ansi.d.ts → render-core/ansi.d.ts} +0 -0
- /package/dist/tui/{ansi.js → render-core/ansi.js} +0 -0
- /package/dist/tui/{canvas.d.ts → render-core/canvas.d.ts} +0 -0
- /package/dist/tui/{canvas.js → render-core/canvas.js} +0 -0
- /package/dist/tui/{layout.d.ts → render-core/layout.d.ts} +0 -0
- /package/dist/tui/{layout.js → render-core/layout.js} +0 -0
- /package/dist/tui/{text.d.ts → render-core/text.d.ts} +0 -0
- /package/dist/tui/{text.js → render-core/text.js} +0 -0
package/dist/tui/renderer.js
CHANGED
|
@@ -1,64 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Canvas } from "./canvas.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
export
|
|
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
|
|
124
|
-
if (
|
|
125
|
-
return
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|