@xp266/dshtui 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.
- package/bin/dshtui.js +65 -0
- package/cordis.patch.yml +81 -0
- package/lib/contract/index.d.mts +2 -0
- package/lib/contract/index.mjs +5 -0
- package/lib/dialog.d.mts +156 -0
- package/lib/dialog.mjs +3 -0
- package/lib/index-DqnaSXqP.d.mts +823 -0
- package/lib/index.d.mts +17 -0
- package/lib/index.mjs +11404 -0
- package/lib/prod-react-Dzof4qJx.mjs +4 -0
- package/lib/surface-DdxzIgIY.mjs +3126 -0
- package/lib/vendor.d.mts +3 -0
- package/lib/vendor.mjs +4 -0
- package/package.json +128 -0
- package/src/apply-theme.ts +8 -0
- package/src/boot-log.ts +35 -0
- package/src/chat/blocks.ts +41 -0
- package/src/chat/bridge.ts +1076 -0
- package/src/chat/builtin-tool-views.ts +53 -0
- package/src/chat/chat-face.ts +16 -0
- package/src/chat/chat-nodes.ts +53 -0
- package/src/chat/efforts.ts +4 -0
- package/src/chat/interactions.ts +329 -0
- package/src/chat/models.ts +263 -0
- package/src/chat/partial-json.ts +125 -0
- package/src/chat/presets.ts +26 -0
- package/src/chat/question-view.ts +106 -0
- package/src/chat/retry-status.ts +33 -0
- package/src/chat/session-groups.ts +34 -0
- package/src/chat/session-list.ts +311 -0
- package/src/chat/store.ts +636 -0
- package/src/chat/todo-view.ts +66 -0
- package/src/chat/tool-view.ts +311 -0
- package/src/chat/tool-views.ts +114 -0
- package/src/contract/index.ts +704 -0
- package/src/contract/upstream.ts +165 -0
- package/src/core/caret-nonce.ts +19 -0
- package/src/core/composer-layout.ts +65 -0
- package/src/core/edit.ts +152 -0
- package/src/core/field-view.ts +52 -0
- package/src/core/fields.ts +185 -0
- package/src/core/metrics.ts +26 -0
- package/src/core/paste.ts +149 -0
- package/src/core/segments.ts +112 -0
- package/src/core/text.ts +290 -0
- package/src/dialog.ts +11 -0
- package/src/env.ts +38 -0
- package/src/harness-home.ts +22 -0
- package/src/hot-theme.ts +56 -0
- package/src/index.tsx +185 -0
- package/src/kernel/registry.ts +102 -0
- package/src/kernel/surface.ts +33 -0
- package/src/log.ts +249 -0
- package/src/model/message.ts +89 -0
- package/src/model/selection.ts +63 -0
- package/src/performance-guard.ts +28 -0
- package/src/runtime/prod-react.ts +10 -0
- package/src/terminal/background.ts +66 -0
- package/src/terminal/capabilities.ts +54 -0
- package/src/terminal/clipboard-backends.ts +69 -0
- package/src/terminal/clipboard.ts +161 -0
- package/src/terminal/cursor-shape.ts +13 -0
- package/src/terminal/glyphs.ts +44 -0
- package/src/terminal/mouse.ts +154 -0
- package/src/terminal/probe.ts +61 -0
- package/src/terminal/screen.ts +349 -0
- package/src/theme-settings.ts +31 -0
- package/src/theme.ts +319 -0
- package/src/ui/app.tsx +454 -0
- package/src/ui/chrome/caps.tsx +9 -0
- package/src/ui/chrome/hint-service.ts +87 -0
- package/src/ui/chrome/input-status.ts +27 -0
- package/src/ui/chrome/key-gate.tsx +29 -0
- package/src/ui/chrome/status-bar.tsx +147 -0
- package/src/ui/contributions.ts +47 -0
- package/src/ui/dialog/defaults-dialog.tsx +152 -0
- package/src/ui/dialog/dialog-item.tsx +38 -0
- package/src/ui/dialog/dialog.tsx +386 -0
- package/src/ui/dialog/effort-dialog.tsx +45 -0
- package/src/ui/dialog/geometry.ts +131 -0
- package/src/ui/dialog/items.ts +124 -0
- package/src/ui/dialog/list-dialog.tsx +63 -0
- package/src/ui/dialog/models-dialog.tsx +203 -0
- package/src/ui/dialog/presets-dialog.tsx +46 -0
- package/src/ui/dialog/providers-dialog.tsx +313 -0
- package/src/ui/dialog/sessions-dialog.tsx +163 -0
- package/src/ui/dialog/sizes.ts +7 -0
- package/src/ui/dialog/status-lines.ts +11 -0
- package/src/ui/dialog/todo-dialog.tsx +29 -0
- package/src/ui/dialog/use-dialog-input.ts +186 -0
- package/src/ui/extension-point.ts +188 -0
- package/src/ui/home-logo.ts +132 -0
- package/src/ui/hooks/use-async-action.ts +25 -0
- package/src/ui/hooks/use-async-list.ts +71 -0
- package/src/ui/hooks/use-caret.ts +24 -0
- package/src/ui/hooks/use-chat-events.ts +153 -0
- package/src/ui/hooks/use-mouse-selection.ts +352 -0
- package/src/ui/hooks/use-scroll.ts +49 -0
- package/src/ui/hooks/use-terminal-size.ts +20 -0
- package/src/ui/input/commands.ts +118 -0
- package/src/ui/input/composer-bus.ts +14 -0
- package/src/ui/input/composer-fields.ts +111 -0
- package/src/ui/input/composer-keys.ts +21 -0
- package/src/ui/input/composer-paste.ts +20 -0
- package/src/ui/input/input-bar.tsx +279 -0
- package/src/ui/input/use-composer.ts +495 -0
- package/src/ui/key-arbiter.ts +37 -0
- package/src/ui/keymap.ts +15 -0
- package/src/ui/layout-service.ts +77 -0
- package/src/ui/message/layout.ts +763 -0
- package/src/ui/message/md/block.ts +247 -0
- package/src/ui/message/md/engine.ts +430 -0
- package/src/ui/message/md/extensions.ts +58 -0
- package/src/ui/message/md/highlight.ts +626 -0
- package/src/ui/message/md/index.ts +14 -0
- package/src/ui/message/md/inline.ts +97 -0
- package/src/ui/message/md/palette.ts +87 -0
- package/src/ui/message/message-list.tsx +69 -0
- package/src/ui/message/message-row.tsx +137 -0
- package/src/ui/message/message-views.ts +16 -0
- package/src/ui/message/renderers.ts +39 -0
- package/src/ui/message/tool-diff.ts +188 -0
- package/src/ui/message/warmup.ts +41 -0
- package/src/ui/message/wave.ts +98 -0
- package/src/ui/overlay.ts +22 -0
- package/src/ui/panels/approval-panel.tsx +193 -0
- package/src/ui/panels/exports.ts +3 -0
- package/src/ui/panels/question-model.ts +424 -0
- package/src/ui/panels/question-panel.tsx +154 -0
- package/src/ui/panels/surface.tsx +106 -0
- package/src/ui/panels-builtin.tsx +86 -0
- package/src/ui/pointer/builtins.ts +281 -0
- package/src/ui/pointer/registry.ts +25 -0
- package/src/ui/region.tsx +31 -0
- package/src/ui/selection/service.ts +92 -0
- package/src/ui/selection-registry.ts +88 -0
- package/src/ui/selection.tsx +148 -0
- package/src/ui/spinner-tick.tsx +24 -0
- package/src/ui/use-available-windows.ts +31 -0
- package/src/ui/widgets/actions.tsx +84 -0
- package/src/ui/widgets/button.tsx +57 -0
- package/src/ui/widgets/checkbox.tsx +58 -0
- package/src/ui/widgets/header.tsx +36 -0
- package/src/ui/widgets/index.ts +8 -0
- package/src/ui/widgets/input.tsx +76 -0
- package/src/ui/widgets/registry.ts +32 -0
- package/src/ui/widgets/search.tsx +59 -0
- package/src/ui/widgets/select.tsx +97 -0
- package/src/ui/widgets/static.tsx +37 -0
- package/src/ui/widgets/types.ts +1 -0
- package/src/ui/window-services-bridge.ts +71 -0
- package/src/ui/window-services.ts +20 -0
- package/src/ui/windows/models-window.tsx +15 -0
- package/src/ui/windows/providers-window.tsx +14 -0
- package/src/ui/windows/service-window.tsx +18 -0
- package/src/ui/windows/sessions-window.tsx +25 -0
- package/src/ui/windows-builtin.tsx +94 -0
- package/src/ui/windows.ts +18 -0
- package/src/vendor.ts +12 -0
|
@@ -0,0 +1,3126 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { Box, Text, useCursor, useInput, usePaste, useStdout } from "ink";
|
|
6
|
+
import { createContext, createElement, memo, useCallback, useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import stringWidth from "string-width";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
//#region src/harness-home.ts
|
|
10
|
+
const DSH_HOME_DIR_NAME = ".dsh";
|
|
11
|
+
const DSH_HOME_ENV = "DSH_HOME";
|
|
12
|
+
function expandHomePath(path) {
|
|
13
|
+
if (path === "~") return homedir();
|
|
14
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return join(homedir(), path.slice(2));
|
|
15
|
+
return path;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the DeepSeek Harness home the same way the harness does:
|
|
19
|
+
* `$DSH_HOME` when set (blank counts as unset), otherwise `~/.dsh`.
|
|
20
|
+
* Mirrors @deepseek-ai/dsh-home-paths without depending on it.
|
|
21
|
+
*/
|
|
22
|
+
function resolveDshHome(env = process.env) {
|
|
23
|
+
const fromEnv = env[DSH_HOME_ENV];
|
|
24
|
+
const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(homedir(), DSH_HOME_DIR_NAME);
|
|
25
|
+
return resolve(expandHomePath(selected));
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/log.ts
|
|
29
|
+
const LOG_LEVELS = [
|
|
30
|
+
"debug",
|
|
31
|
+
"info",
|
|
32
|
+
"warn",
|
|
33
|
+
"error"
|
|
34
|
+
];
|
|
35
|
+
const state = {
|
|
36
|
+
stderrEnabled: false,
|
|
37
|
+
fileEnabled: false,
|
|
38
|
+
minLevel: "info",
|
|
39
|
+
maxFileBytes: 5242880
|
|
40
|
+
};
|
|
41
|
+
const sinks = /* @__PURE__ */ new Set();
|
|
42
|
+
let fileSink;
|
|
43
|
+
function levelRank(level) {
|
|
44
|
+
return LOG_LEVELS.indexOf(level);
|
|
45
|
+
}
|
|
46
|
+
function fieldsText(fields) {
|
|
47
|
+
return fields === void 0 ? "" : ` ${JSON.stringify(fields)}`;
|
|
48
|
+
}
|
|
49
|
+
function formatLine(entry) {
|
|
50
|
+
return `${(/* @__PURE__ */ new Date()).toISOString()} [${entry.level}] ${entry.tag} ${entry.message}${fieldsText(entry.fields)}\n`;
|
|
51
|
+
}
|
|
52
|
+
function formatCrash(report) {
|
|
53
|
+
return [
|
|
54
|
+
`=== dshtui crash: ${report.kind} ===`,
|
|
55
|
+
`time: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
56
|
+
`node: ${process.version}`,
|
|
57
|
+
`message: ${report.message}${fieldsText(report.fields)}`,
|
|
58
|
+
report.stack === void 0 ? "" : `stack:\n${report.stack}`,
|
|
59
|
+
report.cause === void 0 ? "" : `cause: ${report.cause}`,
|
|
60
|
+
""
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
63
|
+
function writeStderr(line) {
|
|
64
|
+
if (!state.stderrEnabled) return;
|
|
65
|
+
try {
|
|
66
|
+
process.stderr.write(line);
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
function resolveLogFile(dir) {
|
|
70
|
+
return join(dir ?? join(resolveDshHome(), "logs"), "dshtui.log");
|
|
71
|
+
}
|
|
72
|
+
function rotateIfNeeded(logFile) {
|
|
73
|
+
let size;
|
|
74
|
+
try {
|
|
75
|
+
size = statSync(logFile).size;
|
|
76
|
+
} catch {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (size < state.maxFileBytes) return;
|
|
80
|
+
try {
|
|
81
|
+
unlinkSync(`${logFile}.1`);
|
|
82
|
+
} catch {}
|
|
83
|
+
try {
|
|
84
|
+
renameSync(logFile, `${logFile}.1`);
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
function fileSinkFor(logFile) {
|
|
88
|
+
return {
|
|
89
|
+
line(entry) {
|
|
90
|
+
if (!state.fileEnabled) return;
|
|
91
|
+
try {
|
|
92
|
+
rotateIfNeeded(logFile);
|
|
93
|
+
try {
|
|
94
|
+
chmodSync(logFile, 384);
|
|
95
|
+
} catch {}
|
|
96
|
+
appendFileSync(logFile, formatLine(entry), { mode: 384 });
|
|
97
|
+
} catch {}
|
|
98
|
+
if (entry.level === "warn" || entry.level === "error") writeStderr(`[dshtui] ${entry.message}${fieldsText(entry.fields)}\n`);
|
|
99
|
+
},
|
|
100
|
+
crash(report) {
|
|
101
|
+
if (!state.fileEnabled) return;
|
|
102
|
+
const line = formatCrash(report);
|
|
103
|
+
try {
|
|
104
|
+
rotateIfNeeded(logFile);
|
|
105
|
+
try {
|
|
106
|
+
chmodSync(logFile, 384);
|
|
107
|
+
} catch {}
|
|
108
|
+
appendFileSync(logFile, line, { mode: 384 });
|
|
109
|
+
} catch {}
|
|
110
|
+
writeStderr(line);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function removeFileSink() {
|
|
115
|
+
if (fileSink === void 0) return;
|
|
116
|
+
sinks.delete(fileSink);
|
|
117
|
+
fileSink = void 0;
|
|
118
|
+
}
|
|
119
|
+
function configureLogs(options = {}) {
|
|
120
|
+
if (options.stderr !== void 0) state.stderrEnabled = options.stderr;
|
|
121
|
+
if (options.level !== void 0) state.minLevel = options.level;
|
|
122
|
+
if (options.maxFileBytes !== void 0) state.maxFileBytes = Math.max(1, options.maxFileBytes);
|
|
123
|
+
state.fileEnabled = options.file ?? state.fileEnabled;
|
|
124
|
+
removeFileSink();
|
|
125
|
+
if (state.fileEnabled) {
|
|
126
|
+
const logFile = resolveLogFile(options.dir);
|
|
127
|
+
try {
|
|
128
|
+
mkdirSync(dirname(logFile), {
|
|
129
|
+
recursive: true,
|
|
130
|
+
mode: 448
|
|
131
|
+
});
|
|
132
|
+
fileSink = fileSinkFor(logFile);
|
|
133
|
+
sinks.add(fileSink);
|
|
134
|
+
} catch {
|
|
135
|
+
state.fileEnabled = false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function dispatch(entry) {
|
|
140
|
+
if (levelRank(entry.level) < levelRank(state.minLevel)) return;
|
|
141
|
+
for (const sink of sinks) try {
|
|
142
|
+
sink.line(entry);
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
function dispatchCrash(report) {
|
|
146
|
+
for (const sink of sinks) try {
|
|
147
|
+
sink.crash(report);
|
|
148
|
+
} catch {}
|
|
149
|
+
}
|
|
150
|
+
function log(level, tag, message, fields) {
|
|
151
|
+
dispatch({
|
|
152
|
+
level,
|
|
153
|
+
tag,
|
|
154
|
+
message,
|
|
155
|
+
fields
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function warn(tag, message, fields) {
|
|
159
|
+
log("warn", tag, message, fields);
|
|
160
|
+
}
|
|
161
|
+
function error(tag, message, fields) {
|
|
162
|
+
log("error", tag, message, fields);
|
|
163
|
+
}
|
|
164
|
+
function stringifyError(cause) {
|
|
165
|
+
if (cause instanceof Error) return cause.message;
|
|
166
|
+
return String(cause);
|
|
167
|
+
}
|
|
168
|
+
function writeCrashReport(report) {
|
|
169
|
+
dispatchCrash(report);
|
|
170
|
+
}
|
|
171
|
+
function installCrashHandlers(options = {}) {
|
|
172
|
+
const onUncaught = (cause) => {
|
|
173
|
+
writeCrashReport({
|
|
174
|
+
kind: "uncaughtException",
|
|
175
|
+
message: stringifyError(cause),
|
|
176
|
+
stack: cause instanceof Error ? cause.stack : void 0
|
|
177
|
+
});
|
|
178
|
+
if (options.exit === true) process.exit(1);
|
|
179
|
+
};
|
|
180
|
+
const onRejected = (cause) => {
|
|
181
|
+
writeCrashReport({
|
|
182
|
+
kind: "unhandledRejection",
|
|
183
|
+
message: stringifyError(cause),
|
|
184
|
+
stack: cause instanceof Error ? cause.stack : void 0
|
|
185
|
+
});
|
|
186
|
+
if (options.exit === true) process.exit(1);
|
|
187
|
+
};
|
|
188
|
+
process.on("uncaughtException", onUncaught);
|
|
189
|
+
process.on("unhandledRejection", onRejected);
|
|
190
|
+
return () => {
|
|
191
|
+
process.off("uncaughtException", onUncaught);
|
|
192
|
+
process.off("unhandledRejection", onRejected);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/env.ts
|
|
197
|
+
/**
|
|
198
|
+
Every dshtui environment variable, parsed once at import.
|
|
199
|
+
*/
|
|
200
|
+
function parseLogLevel(value) {
|
|
201
|
+
return LOG_LEVELS.includes(value ?? "") ? value : void 0;
|
|
202
|
+
}
|
|
203
|
+
function parseBytes(value) {
|
|
204
|
+
if (value === void 0 || value.trim() === "") return void 0;
|
|
205
|
+
const parsed = Number(value);
|
|
206
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
207
|
+
}
|
|
208
|
+
const env = {
|
|
209
|
+
/** Force color level: 0 none, 1 ansi16, 2 ansi256, 3 truecolor. */
|
|
210
|
+
color: process.env.DSH_TUI_COLOR,
|
|
211
|
+
/** Render every glyph as plain ASCII. */
|
|
212
|
+
ascii: process.env.DSH_TUI_ASCII === "1",
|
|
213
|
+
/** Force background mode: 'dark' or 'light'. */
|
|
214
|
+
background: process.env.DSH_TUI_BG,
|
|
215
|
+
/** Reload theme.ts on every save (path relative to cwd, default src/theme.ts). */
|
|
216
|
+
hotTheme: process.env.DSH_TUI_HOT_THEME === "1",
|
|
217
|
+
themePath: process.env.DSH_TUI_THEME_PATH,
|
|
218
|
+
/** Mirror warnings and errors to stderr; only the exact value '1' enables it. */
|
|
219
|
+
debug: process.env.DSH_TUI_DEBUG === "1",
|
|
220
|
+
/** Write diagnostics under the harness home logs directory (default: on). */
|
|
221
|
+
logFile: process.env.DSH_TUI_LOG_FILE !== "0",
|
|
222
|
+
/** Override the harness home base directory used for logs. */
|
|
223
|
+
logDir: process.env.DSH_TUI_LOG_DIR,
|
|
224
|
+
/** Minimum level written to the log file: debug | info | warn | error. */
|
|
225
|
+
logLevel: parseLogLevel(process.env.DSH_TUI_LOG_LEVEL),
|
|
226
|
+
/** Rotation cap for the log file in bytes. */
|
|
227
|
+
logMaxBytes: parseBytes(process.env.DSH_TUI_LOG_MAX_BYTES),
|
|
228
|
+
/** Exit the host process on an uncaught crash (default: record only). */
|
|
229
|
+
crashExit: process.env.DSH_TUI_CRASH_EXIT === "1"
|
|
230
|
+
};
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/terminal/capabilities.ts
|
|
233
|
+
function parseForcedColorLevel(value) {
|
|
234
|
+
if (value === "0" || value === "1" || value === "2" || value === "3") return Number(value);
|
|
235
|
+
if (value === "truecolor") return 3;
|
|
236
|
+
if (value === "none" || value === "false") return 0;
|
|
237
|
+
}
|
|
238
|
+
function detectColorLevel() {
|
|
239
|
+
const forced = parseForcedColorLevel(env.color);
|
|
240
|
+
if (forced !== void 0) return forced;
|
|
241
|
+
if (process.env.NO_COLOR !== void 0) return 0;
|
|
242
|
+
const forceColor = process.env.FORCE_COLOR;
|
|
243
|
+
if (forceColor !== void 0) {
|
|
244
|
+
if (forceColor === "false" || forceColor === "0") return 0;
|
|
245
|
+
if (forceColor === "true" || forceColor === "") return 1;
|
|
246
|
+
const parsed = Number(forceColor);
|
|
247
|
+
if (parsed === 1 || parsed === 2 || parsed === 3) return parsed;
|
|
248
|
+
}
|
|
249
|
+
const term = process.env.TERM ?? "";
|
|
250
|
+
if (term === "dumb") return 0;
|
|
251
|
+
const colorterm = process.env.COLORTERM ?? "";
|
|
252
|
+
if (colorterm === "truecolor" || colorterm === "24bit") return 3;
|
|
253
|
+
if (term.includes("truecolor")) return 3;
|
|
254
|
+
if (process.env.WT_SESSION !== void 0) return 3;
|
|
255
|
+
if (term.includes("256color")) return 2;
|
|
256
|
+
if (term !== "" || process.env.TERM_PROGRAM !== void 0) return 1;
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
function detectUnicode() {
|
|
260
|
+
if (env.ascii) return false;
|
|
261
|
+
if ((process.env.TERM ?? "") === "dumb") return false;
|
|
262
|
+
const locale = process.env.LC_ALL ?? process.env.LANG ?? "";
|
|
263
|
+
if (locale === "" || /utf-?8/i.test(locale)) return true;
|
|
264
|
+
const base = locale.split(".")[0] ?? locale;
|
|
265
|
+
if (base === "C" || base === "POSIX") return false;
|
|
266
|
+
return !locale.includes(".");
|
|
267
|
+
}
|
|
268
|
+
let colorLevel = detectColorLevel();
|
|
269
|
+
function setColorLevel(level) {
|
|
270
|
+
colorLevel = level;
|
|
271
|
+
chalk.level = level;
|
|
272
|
+
}
|
|
273
|
+
chalk.level = colorLevel;
|
|
274
|
+
const unicode = detectUnicode();
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/kernel/registry.ts
|
|
277
|
+
/**
|
|
278
|
+
* Keyed contribution registry with layered override semantics: registering
|
|
279
|
+
* an existing key displaces the previous layer, and disposing a layer
|
|
280
|
+
* restores the nearest live layer below it, so an overriding plugin that
|
|
281
|
+
* unmounts hands the slot back to whatever it replaced.
|
|
282
|
+
*
|
|
283
|
+
* `get(key)` resolves to the live layer with the lowest order (the one that
|
|
284
|
+
* would win a dispatch loop), not merely the most recently registered one, so
|
|
285
|
+
* single-key lookups agree with the iteration order plugins observe.
|
|
286
|
+
*/
|
|
287
|
+
function keyedRegistry(compareKeys = (a, b) => a < b ? -1 : a > b ? 1 : 0) {
|
|
288
|
+
const map = /* @__PURE__ */ new Map();
|
|
289
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
290
|
+
function notify() {
|
|
291
|
+
for (const listener of [...listeners]) try {
|
|
292
|
+
listener();
|
|
293
|
+
} catch {}
|
|
294
|
+
}
|
|
295
|
+
function winningLayer(key) {
|
|
296
|
+
let best;
|
|
297
|
+
let layer = map.get(key);
|
|
298
|
+
while (layer !== void 0) {
|
|
299
|
+
if (!layer.dead && (best === void 0 || layer.order < best.order)) best = layer;
|
|
300
|
+
layer = layer.prev;
|
|
301
|
+
}
|
|
302
|
+
return best;
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
register(key, value, options) {
|
|
306
|
+
const prev = map.get(key);
|
|
307
|
+
const layer = {
|
|
308
|
+
key,
|
|
309
|
+
order: options?.order ?? 100,
|
|
310
|
+
value,
|
|
311
|
+
prev,
|
|
312
|
+
dead: false
|
|
313
|
+
};
|
|
314
|
+
map.set(key, layer);
|
|
315
|
+
notify();
|
|
316
|
+
return () => {
|
|
317
|
+
if (layer.dead) return;
|
|
318
|
+
layer.dead = true;
|
|
319
|
+
if (map.get(key) !== layer) return;
|
|
320
|
+
let restore = layer.prev;
|
|
321
|
+
while (restore !== void 0 && restore.dead) restore = restore.prev;
|
|
322
|
+
if (restore === void 0) map.delete(key);
|
|
323
|
+
else map.set(key, restore);
|
|
324
|
+
notify();
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
entries() {
|
|
328
|
+
return [...map.values()].map((layer) => ({
|
|
329
|
+
key: layer.key,
|
|
330
|
+
order: layer.order,
|
|
331
|
+
value: layer.value
|
|
332
|
+
})).sort((a, b) => a.order - b.order || compareKeys(a.key, b.key));
|
|
333
|
+
},
|
|
334
|
+
values() {
|
|
335
|
+
return this.entries().map((entry) => entry.value);
|
|
336
|
+
},
|
|
337
|
+
get(key) {
|
|
338
|
+
return winningLayer(key)?.value;
|
|
339
|
+
},
|
|
340
|
+
has(key) {
|
|
341
|
+
return map.has(key);
|
|
342
|
+
},
|
|
343
|
+
subscribe(listener) {
|
|
344
|
+
listeners.add(listener);
|
|
345
|
+
return () => {
|
|
346
|
+
listeners.delete(listener);
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/theme.ts
|
|
353
|
+
const paletteContributions = keyedRegistry();
|
|
354
|
+
function registerPalette(contribution) {
|
|
355
|
+
const dispose = paletteContributions.register(contribution.id, contribution, { order: contribution.order });
|
|
356
|
+
materialize(currentMode);
|
|
357
|
+
return () => {
|
|
358
|
+
dispose();
|
|
359
|
+
materialize(currentMode);
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function subscribePalettes(listener) {
|
|
363
|
+
return paletteContributions.subscribe(listener);
|
|
364
|
+
}
|
|
365
|
+
const DARK_LADDER = {
|
|
366
|
+
base: "#0d0d0d",
|
|
367
|
+
sunken: "#1a1a1a",
|
|
368
|
+
surface: "#262626",
|
|
369
|
+
raised: "#333333",
|
|
370
|
+
line: "#686868",
|
|
371
|
+
text: "#9a9a9a",
|
|
372
|
+
ink: "#cccccc"
|
|
373
|
+
};
|
|
374
|
+
const LIGHT_LADDER = {
|
|
375
|
+
base: "#ffffff",
|
|
376
|
+
sunken: "#f2f2f2",
|
|
377
|
+
surface: "#e6e6e6",
|
|
378
|
+
raised: "#d9d9d9",
|
|
379
|
+
line: "#999999",
|
|
380
|
+
text: "#595959",
|
|
381
|
+
ink: "#1a1a1a"
|
|
382
|
+
};
|
|
383
|
+
const DARK_HUES = {
|
|
384
|
+
accent: "#ffae00",
|
|
385
|
+
info: "#4da0e8",
|
|
386
|
+
success: "#4caf50",
|
|
387
|
+
error: "#ff6753",
|
|
388
|
+
added: "#5d9e50",
|
|
389
|
+
removed: "#d25044"
|
|
390
|
+
};
|
|
391
|
+
const LIGHT_HUES = {
|
|
392
|
+
accent: "#a86800",
|
|
393
|
+
info: "#2f6fd0",
|
|
394
|
+
success: "#2e8b3d",
|
|
395
|
+
error: "#d44a3a",
|
|
396
|
+
added: "#0a8a2c",
|
|
397
|
+
removed: "#c22318"
|
|
398
|
+
};
|
|
399
|
+
const DARK_CODE = {
|
|
400
|
+
comment: "#5d9e50",
|
|
401
|
+
string: "#d78f6e",
|
|
402
|
+
number: "#82c46e",
|
|
403
|
+
keyword: "#c883c8",
|
|
404
|
+
fn: "#d8d89e",
|
|
405
|
+
type: "#52a898",
|
|
406
|
+
variable: "#4d9fd6",
|
|
407
|
+
fallback: "#a3b56a"
|
|
408
|
+
};
|
|
409
|
+
const LIGHT_CODE = {
|
|
410
|
+
comment: "#4a8a3d",
|
|
411
|
+
string: "#a05f42",
|
|
412
|
+
number: "#3f9a63",
|
|
413
|
+
keyword: "#8f4a88",
|
|
414
|
+
fn: "#7a7a3d",
|
|
415
|
+
type: "#2a7a6e",
|
|
416
|
+
variable: "#3a76a8",
|
|
417
|
+
fallback: "#647a34"
|
|
418
|
+
};
|
|
419
|
+
/** Mix two hex colors channel-wise; t=0 keeps the hue, t=1 keeps the gray. */
|
|
420
|
+
function tint(hue, gray, t) {
|
|
421
|
+
const channel = (hex, at) => parseInt(hex.slice(at, at + 2), 16);
|
|
422
|
+
const mix = (at) => {
|
|
423
|
+
return Math.round(channel(hue, at) * (1 - t) + channel(gray, at) * t).toString(16).padStart(2, "0");
|
|
424
|
+
};
|
|
425
|
+
return `#${mix(1)}${mix(3)}${mix(5)}`;
|
|
426
|
+
}
|
|
427
|
+
const THINK_DESATURATE = .55;
|
|
428
|
+
function buildPalette(ladder, hues, code, mode) {
|
|
429
|
+
const think = (hue) => tint(hue, ladder.text, mode === "dark" ? THINK_DESATURATE : .35);
|
|
430
|
+
const thinkCode = Object.fromEntries(Object.entries(code).map(([key, hue]) => [key, think(hue)]));
|
|
431
|
+
return {
|
|
432
|
+
ink: ladder.ink,
|
|
433
|
+
userBubbleBackground: ladder.surface,
|
|
434
|
+
aiBubbleBackground: ladder.sunken,
|
|
435
|
+
permissionBackground: ladder.sunken,
|
|
436
|
+
workspaceWriteText: hues.info,
|
|
437
|
+
dangerFullAccessText: hues.accent,
|
|
438
|
+
readOnlyText: hues.success,
|
|
439
|
+
dialogBackground: ladder.base,
|
|
440
|
+
dialogInputBackground: ladder.raised,
|
|
441
|
+
dialogHintText: ladder.text,
|
|
442
|
+
carouselCurrentBg: ladder.raised,
|
|
443
|
+
carouselButtonBg: ladder.sunken,
|
|
444
|
+
carouselButtonPressedBg: ladder.surface,
|
|
445
|
+
carouselSelectedText: hues.accent,
|
|
446
|
+
sectionHeader: hues.accent,
|
|
447
|
+
panelQuestionText: ladder.ink,
|
|
448
|
+
panelKeyText: ladder.ink,
|
|
449
|
+
modelText: ladder.ink,
|
|
450
|
+
effortText: hues.accent,
|
|
451
|
+
statusSeparator: ladder.text,
|
|
452
|
+
presetText: ladder.text,
|
|
453
|
+
cwdText: ladder.text,
|
|
454
|
+
statsText: ladder.text,
|
|
455
|
+
errorText: hues.error,
|
|
456
|
+
success: hues.success,
|
|
457
|
+
warning: hues.accent,
|
|
458
|
+
specialFieldText: mode === "dark" ? "#1a1a1a" : "#3d2800",
|
|
459
|
+
specialFieldBackground: hues.accent,
|
|
460
|
+
toolLabel: mode === "dark" ? "#2fc0e0" : "#0092b8",
|
|
461
|
+
toolBodyText: ladder.text,
|
|
462
|
+
diffAdded: hues.added,
|
|
463
|
+
diffRemoved: hues.removed,
|
|
464
|
+
diffAddedBackground: mode === "dark" ? "#384751" : "#e1f1e5",
|
|
465
|
+
diffRemovedBackground: mode === "dark" ? "#4f312c" : "#f5e8e5",
|
|
466
|
+
scrollTrackBackground: ladder.sunken,
|
|
467
|
+
scrollThumbBackground: ladder.line,
|
|
468
|
+
selectionBg: "#0066ff",
|
|
469
|
+
selectionFg: "#cccccc",
|
|
470
|
+
mdLink: hues.info,
|
|
471
|
+
mdInlineCode: hues.added,
|
|
472
|
+
mdQuoteBar: ladder.line,
|
|
473
|
+
mdHr: ladder.line,
|
|
474
|
+
mdList: hues.accent,
|
|
475
|
+
mdTaskDone: hues.added,
|
|
476
|
+
mdTaskTodo: ladder.line,
|
|
477
|
+
mdH1: mode === "dark" ? "#e0b568" : "#8a6d2f",
|
|
478
|
+
mdH3: mode === "dark" ? "#64b5d6" : "#2f6fa8",
|
|
479
|
+
mdCodePlain: ladder.ink,
|
|
480
|
+
mdCodeFallback: code.fallback,
|
|
481
|
+
codeComment: code.comment,
|
|
482
|
+
codeString: code.string,
|
|
483
|
+
codeNumber: code.number,
|
|
484
|
+
codeKeyword: code.keyword,
|
|
485
|
+
codeFunction: code.fn,
|
|
486
|
+
codeType: code.type,
|
|
487
|
+
codeVariable: code.variable,
|
|
488
|
+
codeConstant: code.variable,
|
|
489
|
+
codeOperator: ladder.ink,
|
|
490
|
+
thinkLink: think(hues.info),
|
|
491
|
+
thinkInlineCode: think(hues.added),
|
|
492
|
+
thinkHr: tint(ladder.line, ladder.base, mode === "dark" ? .35 : .15),
|
|
493
|
+
thinkList: ladder.text,
|
|
494
|
+
thinkTaskDone: think(hues.added),
|
|
495
|
+
thinkTaskTodo: ladder.line,
|
|
496
|
+
thinkH1: ladder.text,
|
|
497
|
+
thinkH3: ladder.text,
|
|
498
|
+
thinkCodePlain: ladder.text,
|
|
499
|
+
thinkCodeFallback: thinkCode.fallback,
|
|
500
|
+
thinkCodeComment: thinkCode.comment,
|
|
501
|
+
thinkCodeString: thinkCode.string,
|
|
502
|
+
thinkCodeNumber: thinkCode.number,
|
|
503
|
+
thinkCodeKeyword: thinkCode.keyword,
|
|
504
|
+
thinkCodeFunction: thinkCode.fn,
|
|
505
|
+
thinkCodeType: thinkCode.type,
|
|
506
|
+
thinkCodeVariable: thinkCode.variable,
|
|
507
|
+
thinkCodeConstant: thinkCode.variable,
|
|
508
|
+
homeLogoTop: mode === "dark" ? "#2181ff" : "#0381ff",
|
|
509
|
+
homeLogoBottom: mode === "dark" ? "#3a9aa0" : "#97c4ff"
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
const darkPalette = buildPalette(DARK_LADDER, DARK_HUES, DARK_CODE, "dark");
|
|
513
|
+
const palettes = {
|
|
514
|
+
dark: darkPalette,
|
|
515
|
+
light: buildPalette(LIGHT_LADDER, LIGHT_HUES, LIGHT_CODE, "light")
|
|
516
|
+
};
|
|
517
|
+
const COLORS = { ...darkPalette };
|
|
518
|
+
let currentMode = "dark";
|
|
519
|
+
function themeMode() {
|
|
520
|
+
return currentMode;
|
|
521
|
+
}
|
|
522
|
+
function permissionModes() {
|
|
523
|
+
return {
|
|
524
|
+
"workspace-write": {
|
|
525
|
+
color: COLORS.permissionBackground,
|
|
526
|
+
textColor: COLORS.workspaceWriteText,
|
|
527
|
+
name: "Workspace Write"
|
|
528
|
+
},
|
|
529
|
+
"danger-full-access": {
|
|
530
|
+
color: COLORS.permissionBackground,
|
|
531
|
+
textColor: COLORS.dangerFullAccessText,
|
|
532
|
+
name: "Full access"
|
|
533
|
+
},
|
|
534
|
+
"read-only": {
|
|
535
|
+
color: COLORS.permissionBackground,
|
|
536
|
+
textColor: COLORS.readOnlyText,
|
|
537
|
+
name: "Read Only"
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function effectivePalette(mode) {
|
|
542
|
+
const merged = { ...palettes[mode] };
|
|
543
|
+
for (const contribution of paletteContributions.values()) {
|
|
544
|
+
if (contribution.mode !== void 0 && contribution.mode !== "both" && contribution.mode !== mode) continue;
|
|
545
|
+
Object.assign(merged, contribution.colors);
|
|
546
|
+
}
|
|
547
|
+
return merged;
|
|
548
|
+
}
|
|
549
|
+
function materialize(mode) {
|
|
550
|
+
Object.assign(COLORS, effectivePalette(mode));
|
|
551
|
+
Object.assign(PERMISSION_MODES, permissionModes());
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Read a palette color by key, including keys contributed by plugins that
|
|
555
|
+
* are not part of the built-in Theme type. Falls back to the raw key when
|
|
556
|
+
* nothing defines it, so custom semantic names remain visible.
|
|
557
|
+
*/
|
|
558
|
+
function paletteColor(key) {
|
|
559
|
+
return COLORS[key] ?? key;
|
|
560
|
+
}
|
|
561
|
+
function setThemeMode(mode) {
|
|
562
|
+
currentMode = mode;
|
|
563
|
+
materialize(mode);
|
|
564
|
+
}
|
|
565
|
+
function replacePalettes(next) {
|
|
566
|
+
Object.assign(palettes.dark, next.dark);
|
|
567
|
+
Object.assign(palettes.light, next.light);
|
|
568
|
+
materialize(currentMode);
|
|
569
|
+
}
|
|
570
|
+
const PERMISSION_MODES = permissionModes();
|
|
571
|
+
materialize(currentMode);
|
|
572
|
+
function permissionModeInfo(mode) {
|
|
573
|
+
return PERMISSION_MODES[mode] ?? {
|
|
574
|
+
color: COLORS.permissionBackground,
|
|
575
|
+
textColor: COLORS.workspaceWriteText,
|
|
576
|
+
name: mode
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
//#endregion
|
|
580
|
+
//#region src/core/fields.ts
|
|
581
|
+
const FIELD_CHAR_BASE = 57344;
|
|
582
|
+
const FIELD_CHAR_END = 63743;
|
|
583
|
+
const FIELD_CHAR_COUNT = 6400;
|
|
584
|
+
const slots = /* @__PURE__ */ new Map();
|
|
585
|
+
const kindDefs = /* @__PURE__ */ new Map();
|
|
586
|
+
const labelWidthCache = /* @__PURE__ */ new Map();
|
|
587
|
+
let cursor = 0;
|
|
588
|
+
function specialFieldStyle() {
|
|
589
|
+
return {
|
|
590
|
+
color: COLORS.specialFieldText,
|
|
591
|
+
background: COLORS.specialFieldBackground,
|
|
592
|
+
bold: true
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function registerFieldKind(def) {
|
|
596
|
+
kindDefs.set(def.kind, def);
|
|
597
|
+
return () => {
|
|
598
|
+
if (kindDefs.get(def.kind) === def) kindDefs.delete(def.kind);
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function fieldStyleOf(slot) {
|
|
602
|
+
const def = kindDefs.get(slot.kind);
|
|
603
|
+
if (def !== void 0) return def.style();
|
|
604
|
+
return specialFieldStyle();
|
|
605
|
+
}
|
|
606
|
+
function fieldExpandOf(kind) {
|
|
607
|
+
return kindDefs.get(kind)?.expand;
|
|
608
|
+
}
|
|
609
|
+
function specialFieldFactory() {
|
|
610
|
+
return {
|
|
611
|
+
create: (input) => allocateField(input.kind, input.label, input.owner ?? "composer"),
|
|
612
|
+
release: releaseField,
|
|
613
|
+
pin: pinField,
|
|
614
|
+
isFieldChar,
|
|
615
|
+
labelOf: (char) => fieldSlotOf(char)?.label
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function isFieldCode(code) {
|
|
619
|
+
return code >= FIELD_CHAR_BASE && code <= FIELD_CHAR_END;
|
|
620
|
+
}
|
|
621
|
+
function hasFieldChar(text) {
|
|
622
|
+
for (let i = 0; i < text.length; i++) if (isFieldCode(text.charCodeAt(i))) return true;
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
function isFieldChar(char) {
|
|
626
|
+
return char.length === 1 && isFieldCode(char.charCodeAt(0));
|
|
627
|
+
}
|
|
628
|
+
function fieldCodeWidth(code) {
|
|
629
|
+
const slot = slots.get(code - FIELD_CHAR_BASE);
|
|
630
|
+
if (slot === void 0) return 1;
|
|
631
|
+
return labelWidth(slot.label);
|
|
632
|
+
}
|
|
633
|
+
function labelWidth(label) {
|
|
634
|
+
const hit = labelWidthCache.get(label);
|
|
635
|
+
if (hit !== void 0) return hit;
|
|
636
|
+
const width = stringWidth(label);
|
|
637
|
+
if (labelWidthCache.size >= 4096) labelWidthCache.clear();
|
|
638
|
+
labelWidthCache.set(label, width);
|
|
639
|
+
return width;
|
|
640
|
+
}
|
|
641
|
+
function allocateField(kind, label, owner) {
|
|
642
|
+
for (let i = 0; i < FIELD_CHAR_COUNT; i++) {
|
|
643
|
+
const index = (cursor + i) % FIELD_CHAR_COUNT;
|
|
644
|
+
if (slots.has(index)) continue;
|
|
645
|
+
cursor = (index + 1) % FIELD_CHAR_COUNT;
|
|
646
|
+
slots.set(index, {
|
|
647
|
+
kind,
|
|
648
|
+
label,
|
|
649
|
+
owner,
|
|
650
|
+
pins: 0
|
|
651
|
+
});
|
|
652
|
+
return String.fromCharCode(FIELD_CHAR_BASE + index);
|
|
653
|
+
}
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
function fieldSlotOf(char) {
|
|
657
|
+
if (char.length !== 1) return void 0;
|
|
658
|
+
const code = char.charCodeAt(0);
|
|
659
|
+
if (!isFieldCode(code)) return void 0;
|
|
660
|
+
return slots.get(code - FIELD_CHAR_BASE);
|
|
661
|
+
}
|
|
662
|
+
function releaseField(char) {
|
|
663
|
+
if (char.length !== 1) return;
|
|
664
|
+
const index = char.charCodeAt(0) - FIELD_CHAR_BASE;
|
|
665
|
+
const slot = slots.get(index);
|
|
666
|
+
if (slot === void 0 || slot.pins > 0) return;
|
|
667
|
+
slots.delete(index);
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Hold a field slot against the automatic reclaim scans (composer edits and
|
|
671
|
+
* message-list rebuilds drop every slot whose char is absent from the live
|
|
672
|
+
* text). Callers that create chips outside the composer value — custom
|
|
673
|
+
* windows, overlays, background tasks — must pin them and call the returned
|
|
674
|
+
* disposer when done.
|
|
675
|
+
*/
|
|
676
|
+
function pinField(char) {
|
|
677
|
+
if (char.length !== 1) return void 0;
|
|
678
|
+
const slot = slots.get(char.charCodeAt(0) - FIELD_CHAR_BASE);
|
|
679
|
+
if (slot === void 0) return void 0;
|
|
680
|
+
slot.pins += 1;
|
|
681
|
+
return () => {
|
|
682
|
+
slot.pins -= 1;
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function hasFieldSlots(owner) {
|
|
686
|
+
for (const slot of slots.values()) if (slot.owner === owner) return true;
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
function releaseUnreferenced(owner, keepText) {
|
|
690
|
+
for (const [index, slot] of slots) {
|
|
691
|
+
if (slot.owner !== owner || slot.pins > 0) continue;
|
|
692
|
+
const char = String.fromCharCode(FIELD_CHAR_BASE + index);
|
|
693
|
+
if (!keepText.includes(char)) slots.delete(index);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function releaseFields(owner) {
|
|
697
|
+
for (const [index, slot] of slots) if (slot.owner === owner && slot.pins === 0) slots.delete(index);
|
|
698
|
+
}
|
|
699
|
+
function imageChipLabel(count) {
|
|
700
|
+
return `[${count} images]`;
|
|
701
|
+
}
|
|
702
|
+
function linesChipLabel(count) {
|
|
703
|
+
return `[${count} lines]`;
|
|
704
|
+
}
|
|
705
|
+
function charactersChipLabel(count) {
|
|
706
|
+
return `[${count} characters]`;
|
|
707
|
+
}
|
|
708
|
+
//#endregion
|
|
709
|
+
//#region src/core/text.ts
|
|
710
|
+
const WIDTH_CACHE_LIMIT = 8192;
|
|
711
|
+
function cachedWidth(cache, text, compute) {
|
|
712
|
+
const hit = cache.get(text);
|
|
713
|
+
if (hit !== void 0) return hit;
|
|
714
|
+
const width = compute();
|
|
715
|
+
if (cache.size >= WIDTH_CACHE_LIMIT) cache.clear();
|
|
716
|
+
cache.set(text, width);
|
|
717
|
+
return width;
|
|
718
|
+
}
|
|
719
|
+
const widthCache = /* @__PURE__ */ new Map();
|
|
720
|
+
function textWidth(text) {
|
|
721
|
+
if (isAsciiPrintable(text)) return text.length;
|
|
722
|
+
const hit = widthCache.get(text);
|
|
723
|
+
if (hit !== void 0) return hit;
|
|
724
|
+
let total = 0;
|
|
725
|
+
let run = "";
|
|
726
|
+
for (let i = 0; i < text.length; i++) {
|
|
727
|
+
const code = text.charCodeAt(i);
|
|
728
|
+
if (isFieldCode(code)) {
|
|
729
|
+
if (run !== "") {
|
|
730
|
+
total += stringWidth(run);
|
|
731
|
+
run = "";
|
|
732
|
+
}
|
|
733
|
+
total += fieldCodeWidth(code);
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
run += text[i];
|
|
737
|
+
}
|
|
738
|
+
if (run !== "") total += stringWidth(run);
|
|
739
|
+
if (widthCache.size >= WIDTH_CACHE_LIMIT) widthCache.clear();
|
|
740
|
+
widthCache.set(text, total);
|
|
741
|
+
return total;
|
|
742
|
+
}
|
|
743
|
+
function isAsciiPrintable(text) {
|
|
744
|
+
for (let i = 0; i < text.length; i++) {
|
|
745
|
+
const code = text.charCodeAt(i);
|
|
746
|
+
if (code < 32 || code > 126) return false;
|
|
747
|
+
}
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
const charWidthCache = /* @__PURE__ */ new Map();
|
|
751
|
+
const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
752
|
+
function segmentGraphemes(text) {
|
|
753
|
+
return graphemeSegmenter.segment(text);
|
|
754
|
+
}
|
|
755
|
+
function charWidth(cluster) {
|
|
756
|
+
if (cluster.length === 1) {
|
|
757
|
+
const code = cluster.charCodeAt(0);
|
|
758
|
+
if (code >= 32 && code <= 126) return 1;
|
|
759
|
+
if (isFieldCode(code)) return fieldCodeWidth(code);
|
|
760
|
+
}
|
|
761
|
+
return cachedWidth(charWidthCache, cluster, () => stringWidth(cluster));
|
|
762
|
+
}
|
|
763
|
+
const NO_START_CHARS = /* @__PURE__ */ new Set("!%,.:;?)]}'\"。,、:;!?)]}》〉」』】〕〗〙〛…—~·%°′″‰");
|
|
764
|
+
const NO_END_CHARS = /* @__PURE__ */ new Set("([{'\"$#@`([{《〈「『【〔〖$¥");
|
|
765
|
+
const BREAK_DELIM_CHARS = /* @__PURE__ */ new Set("-/\\,.;:!?)]}");
|
|
766
|
+
function matchesAny(cluster, set) {
|
|
767
|
+
for (const ch of cluster) if (set.has(ch)) return true;
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
const ASCII_NO_START = /* @__PURE__ */ new Set();
|
|
771
|
+
for (const ch of "!%,.:;?)]}'\"") ASCII_NO_START.add(ch.charCodeAt(0));
|
|
772
|
+
const ASCII_NO_END = /* @__PURE__ */ new Set();
|
|
773
|
+
for (const ch of "([{'\"$#@`") ASCII_NO_END.add(ch.charCodeAt(0));
|
|
774
|
+
function canBreakBefore(prev, cur) {
|
|
775
|
+
if (prev.length === 1 && cur.length === 1) {
|
|
776
|
+
const pc = prev.charCodeAt(0);
|
|
777
|
+
const cc = cur.charCodeAt(0);
|
|
778
|
+
if (pc >= 32 && pc < 127 && cc >= 32 && cc < 127) {
|
|
779
|
+
if (ASCII_NO_START.has(cc) || ASCII_NO_END.has(pc)) return false;
|
|
780
|
+
if (pc === 32 || BREAK_DELIM_CHARS.has(prev)) return true;
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (matchesAny(cur, NO_START_CHARS) || matchesAny(prev, NO_END_CHARS)) return false;
|
|
785
|
+
if (charWidth(prev) >= 2 || charWidth(cur) >= 2) return true;
|
|
786
|
+
if (prev === " " || prev === " ") return true;
|
|
787
|
+
if (BREAK_DELIM_CHARS.has(prev)) return true;
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
function computeWrapStarts(clusters, width) {
|
|
791
|
+
const total = clusters.length;
|
|
792
|
+
const starts = [0];
|
|
793
|
+
let start = 0;
|
|
794
|
+
let lineW = 0;
|
|
795
|
+
let lastBreak = -1;
|
|
796
|
+
for (let i = 0; i < total; i++) {
|
|
797
|
+
const w = charWidth(clusters[i]);
|
|
798
|
+
if (lineW + w > width && i > start) {
|
|
799
|
+
let j;
|
|
800
|
+
if (lastBreak > start) j = lastBreak;
|
|
801
|
+
else {
|
|
802
|
+
j = i;
|
|
803
|
+
while (j > start + 1 && matchesAny(clusters[j], NO_START_CHARS)) j--;
|
|
804
|
+
while (j > start + 1 && matchesAny(clusters[j - 1], NO_END_CHARS)) j--;
|
|
805
|
+
}
|
|
806
|
+
starts.push(j);
|
|
807
|
+
lineW = 0;
|
|
808
|
+
for (let k = j; k < i; k++) lineW += charWidth(clusters[k]);
|
|
809
|
+
start = j;
|
|
810
|
+
lastBreak = -1;
|
|
811
|
+
}
|
|
812
|
+
const next = i + 1;
|
|
813
|
+
if (next < total && canBreakBefore(clusters[i], clusters[next])) lastBreak = next;
|
|
814
|
+
lineW += w;
|
|
815
|
+
}
|
|
816
|
+
return starts;
|
|
817
|
+
}
|
|
818
|
+
function pushWrapped(line, width, out) {
|
|
819
|
+
if (line.length === 0) {
|
|
820
|
+
out.push("");
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const clusters = [];
|
|
824
|
+
for (const { segment } of segmentGraphemes(line)) clusters.push(segment);
|
|
825
|
+
const starts = computeWrapStarts(clusters, width);
|
|
826
|
+
for (let r = 0; r < starts.length; r++) {
|
|
827
|
+
const from = starts[r];
|
|
828
|
+
const to = r + 1 < starts.length ? starts[r + 1] : clusters.length;
|
|
829
|
+
out.push(clusters.slice(from, to).join(""));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Single normalization applied before every wrap pass. Terminal rendering
|
|
834
|
+
* expands tabs and a lone carriage return rewinds the cursor, so both would
|
|
835
|
+
* make the visually rendered line wider than the width computation assumed
|
|
836
|
+
* and every later row would land on the wrong column. Both are replaced
|
|
837
|
+
* with spaces / newlines here so wrap math and rendering always agree.
|
|
838
|
+
*/
|
|
839
|
+
function normalizeWrapText(text) {
|
|
840
|
+
if (!text.includes("\r") && !text.includes(" ")) return text;
|
|
841
|
+
return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ");
|
|
842
|
+
}
|
|
843
|
+
function wrapLines(text, width) {
|
|
844
|
+
if (width <= 0) return [""];
|
|
845
|
+
const cacheKey = `${width}\u0000${text}`;
|
|
846
|
+
const hit = wrapCache.get(cacheKey);
|
|
847
|
+
if (hit !== void 0) return hit.lines;
|
|
848
|
+
const lines = [];
|
|
849
|
+
for (const rawLine of normalizeWrapText(text).split("\n")) pushWrapped(rawLine, width, lines);
|
|
850
|
+
evictWrapCacheIfNeeded();
|
|
851
|
+
wrapCache.set(cacheKey, {
|
|
852
|
+
lines,
|
|
853
|
+
bytes: text.length
|
|
854
|
+
});
|
|
855
|
+
return lines;
|
|
856
|
+
}
|
|
857
|
+
const WRAP_CACHE_MAX_BYTES = 4194304;
|
|
858
|
+
const wrapCache = /* @__PURE__ */ new Map();
|
|
859
|
+
let wrapCacheBytes = 0;
|
|
860
|
+
function evictWrapCacheIfNeeded() {
|
|
861
|
+
while (wrapCache.size > 0 && wrapCacheBytes >= WRAP_CACHE_MAX_BYTES) {
|
|
862
|
+
const oldest = wrapCache.keys().next();
|
|
863
|
+
if (oldest.done) return;
|
|
864
|
+
const entry = wrapCache.get(oldest.value);
|
|
865
|
+
if (entry !== void 0) wrapCacheBytes -= entry.bytes;
|
|
866
|
+
wrapCache.delete(oldest.value);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function pushWrappedOffsets(line, width, base, out) {
|
|
870
|
+
if (line.length === 0) {
|
|
871
|
+
out.push({
|
|
872
|
+
start: base,
|
|
873
|
+
end: base
|
|
874
|
+
});
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const clusters = [];
|
|
878
|
+
const offsets = [];
|
|
879
|
+
for (const { segment, index } of segmentGraphemes(line)) {
|
|
880
|
+
clusters.push(segment);
|
|
881
|
+
offsets.push(index);
|
|
882
|
+
}
|
|
883
|
+
const starts = computeWrapStarts(clusters, width);
|
|
884
|
+
for (let r = 0; r < starts.length; r++) {
|
|
885
|
+
const from = starts[r];
|
|
886
|
+
const to = r + 1 < starts.length ? offsets[starts[r + 1]] : line.length;
|
|
887
|
+
out.push({
|
|
888
|
+
start: base + offsets[from],
|
|
889
|
+
end: base + to
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function lineBreaks(text, width) {
|
|
894
|
+
const breaks = [];
|
|
895
|
+
let start = 0;
|
|
896
|
+
for (const rawLine of text.split("\n")) {
|
|
897
|
+
pushWrappedOffsets(rawLine, width, start, breaks);
|
|
898
|
+
start += rawLine.length + 1;
|
|
899
|
+
}
|
|
900
|
+
return breaks;
|
|
901
|
+
}
|
|
902
|
+
function locToPoint(text, width, index) {
|
|
903
|
+
const breaks = lineBreaks(text, width);
|
|
904
|
+
for (let row = 0; row < breaks.length; row++) {
|
|
905
|
+
const { start, end } = breaks[row];
|
|
906
|
+
if (index < end || index === end && row < breaks.length - 1 && breaks[row + 1].start > end) return {
|
|
907
|
+
row,
|
|
908
|
+
col: textWidth(text.slice(start, index))
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
const last = breaks[breaks.length - 1];
|
|
912
|
+
return {
|
|
913
|
+
row: breaks.length - 1,
|
|
914
|
+
col: textWidth(text.slice(last.start, last.end))
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
function colToCharIndex(line, col) {
|
|
918
|
+
let width = 0;
|
|
919
|
+
for (const { segment, index } of segmentGraphemes(line)) {
|
|
920
|
+
if (width >= col) return index;
|
|
921
|
+
width += charWidth(segment);
|
|
922
|
+
}
|
|
923
|
+
return line.length;
|
|
924
|
+
}
|
|
925
|
+
function caretScrollStart(text, caret, width) {
|
|
926
|
+
const clamped = Math.min(Math.max(0, caret), text.length);
|
|
927
|
+
const caretCol = textWidth(text.slice(0, clamped));
|
|
928
|
+
if (caretCol <= width) return 0;
|
|
929
|
+
const minPrefix = caretCol - width;
|
|
930
|
+
let prefix = 0;
|
|
931
|
+
for (const { segment, index } of segmentGraphemes(text)) {
|
|
932
|
+
if (index >= clamped) break;
|
|
933
|
+
if (prefix >= minPrefix) return index;
|
|
934
|
+
prefix += charWidth(segment);
|
|
935
|
+
}
|
|
936
|
+
return clamped;
|
|
937
|
+
}
|
|
938
|
+
function padToWidth(text, width) {
|
|
939
|
+
const used = textWidth(text);
|
|
940
|
+
return text + " ".repeat(Math.max(0, width - used));
|
|
941
|
+
}
|
|
942
|
+
function truncate(text, width) {
|
|
943
|
+
let used = 0;
|
|
944
|
+
for (const { segment, index } of segmentGraphemes(text)) {
|
|
945
|
+
const w = charWidth(segment);
|
|
946
|
+
if (used + w > width) return text.slice(0, index);
|
|
947
|
+
used += w;
|
|
948
|
+
}
|
|
949
|
+
return text;
|
|
950
|
+
}
|
|
951
|
+
function errorText(cause) {
|
|
952
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
953
|
+
}
|
|
954
|
+
//#endregion
|
|
955
|
+
//#region src/model/selection.ts
|
|
956
|
+
function selectedRange(sel, row) {
|
|
957
|
+
const top = Math.min(sel.anchorRow, sel.focusRow);
|
|
958
|
+
const bottom = Math.max(sel.anchorRow, sel.focusRow);
|
|
959
|
+
if (row < top || row > bottom) return null;
|
|
960
|
+
if (sel.anchorRow === sel.focusRow) return {
|
|
961
|
+
start: Math.min(sel.anchorCol, sel.focusCol),
|
|
962
|
+
end: Math.max(sel.anchorCol, sel.focusCol)
|
|
963
|
+
};
|
|
964
|
+
const upward = sel.focusRow < sel.anchorRow;
|
|
965
|
+
if (row === sel.anchorRow) return upward ? {
|
|
966
|
+
start: 0,
|
|
967
|
+
end: sel.anchorCol
|
|
968
|
+
} : {
|
|
969
|
+
start: sel.anchorCol,
|
|
970
|
+
end: Infinity
|
|
971
|
+
};
|
|
972
|
+
if (row === sel.focusRow) return upward ? {
|
|
973
|
+
start: sel.focusCol,
|
|
974
|
+
end: Infinity
|
|
975
|
+
} : {
|
|
976
|
+
start: 0,
|
|
977
|
+
end: sel.focusCol
|
|
978
|
+
};
|
|
979
|
+
return {
|
|
980
|
+
start: 0,
|
|
981
|
+
end: Infinity
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function toScreenSelection(selection, scrollTop, messageHeight) {
|
|
985
|
+
if (selection === null) return null;
|
|
986
|
+
if (!selection.inMessage) return selection;
|
|
987
|
+
const rawAnchor = selection.anchorRow - scrollTop;
|
|
988
|
+
const rawFocus = selection.focusRow - scrollTop;
|
|
989
|
+
const visible = (row) => row >= 0 && row < messageHeight;
|
|
990
|
+
if (!visible(rawAnchor) && !visible(rawFocus)) return null;
|
|
991
|
+
const clampRow = (row) => row < 0 ? 0 : row >= messageHeight ? messageHeight - 1 : row;
|
|
992
|
+
const snapCol = (raw, col) => raw < 0 ? 0 : raw >= messageHeight ? Infinity : col;
|
|
993
|
+
return {
|
|
994
|
+
anchorRow: clampRow(rawAnchor),
|
|
995
|
+
anchorCol: snapCol(rawAnchor, selection.anchorCol),
|
|
996
|
+
focusRow: clampRow(rawFocus),
|
|
997
|
+
focusCol: snapCol(rawFocus, selection.focusCol),
|
|
998
|
+
inMessage: true
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function clampFocusRow(inMessage, eventY, scrollTop, messageHeight, rows, dialogOpen) {
|
|
1002
|
+
if (inMessage) return Math.min(eventY + scrollTop, scrollTop + messageHeight - 1);
|
|
1003
|
+
if (dialogOpen) return Math.max(0, Math.min(eventY, rows - 1));
|
|
1004
|
+
return Math.max(messageHeight, eventY);
|
|
1005
|
+
}
|
|
1006
|
+
//#endregion
|
|
1007
|
+
//#region src/ui/selection-registry.ts
|
|
1008
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1009
|
+
function registerRowPiece(id, y, piece) {
|
|
1010
|
+
let pieces = rows.get(y);
|
|
1011
|
+
if (piece === null) {
|
|
1012
|
+
if (pieces !== void 0) {
|
|
1013
|
+
pieces.delete(id);
|
|
1014
|
+
if (pieces.size === 0) rows.delete(y);
|
|
1015
|
+
}
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (pieces === void 0) {
|
|
1019
|
+
pieces = /* @__PURE__ */ new Map();
|
|
1020
|
+
rows.set(y, pieces);
|
|
1021
|
+
}
|
|
1022
|
+
pieces.set(id, piece);
|
|
1023
|
+
}
|
|
1024
|
+
function rowPieces(y) {
|
|
1025
|
+
const pieces = rows.get(y);
|
|
1026
|
+
if (pieces === void 0) return [];
|
|
1027
|
+
return [...pieces.values()].sort((a, b) => a.col - b.col);
|
|
1028
|
+
}
|
|
1029
|
+
function visiblePieces(y) {
|
|
1030
|
+
const pieces = rowPieces(y);
|
|
1031
|
+
const chrome = pieces.filter((piece) => piece.layer !== "message");
|
|
1032
|
+
return chrome.length > 0 ? chrome : pieces;
|
|
1033
|
+
}
|
|
1034
|
+
function envelopeOf(selection) {
|
|
1035
|
+
return {
|
|
1036
|
+
start: Math.min(selection.anchorCol, selection.focusCol),
|
|
1037
|
+
end: Math.max(selection.anchorCol, selection.focusCol)
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function envelopeOverlaps(selection, col, width) {
|
|
1041
|
+
const env = envelopeOf(selection);
|
|
1042
|
+
return env.end > col && env.start < col + width;
|
|
1043
|
+
}
|
|
1044
|
+
function sliceByColumns(text, startCol, endCol) {
|
|
1045
|
+
const startIndex = colToCharIndex(text, Math.max(0, startCol));
|
|
1046
|
+
const endIndex = colToCharIndex(text, Math.max(0, endCol));
|
|
1047
|
+
return text.slice(startIndex, endIndex);
|
|
1048
|
+
}
|
|
1049
|
+
function chromeSelectionText(selection) {
|
|
1050
|
+
const top = Math.min(selection.anchorRow, selection.focusRow);
|
|
1051
|
+
const bottom = Math.max(selection.anchorRow, selection.focusRow);
|
|
1052
|
+
const env = envelopeOf(selection);
|
|
1053
|
+
const lines = [];
|
|
1054
|
+
for (let row = top; row <= bottom; row++) {
|
|
1055
|
+
const range = selectedRange(selection, row);
|
|
1056
|
+
if (range === null) continue;
|
|
1057
|
+
let line = "";
|
|
1058
|
+
let prevEnd = -1;
|
|
1059
|
+
for (const piece of visiblePieces(row)) {
|
|
1060
|
+
const width = textWidth(piece.text);
|
|
1061
|
+
if (!(env.end > piece.col && env.start < piece.col + width)) continue;
|
|
1062
|
+
const start = Math.max(range.start, piece.col);
|
|
1063
|
+
const end = Math.min(range.end, piece.col + width);
|
|
1064
|
+
if (start >= end) continue;
|
|
1065
|
+
const text = sliceByColumns(piece.text, start - piece.col, end - piece.col);
|
|
1066
|
+
if (text === "") continue;
|
|
1067
|
+
if (line !== "") line += prevEnd === piece.col ? "" : " ";
|
|
1068
|
+
line += text;
|
|
1069
|
+
prevEnd = piece.col + width;
|
|
1070
|
+
}
|
|
1071
|
+
lines.push(line);
|
|
1072
|
+
}
|
|
1073
|
+
return lines.join("\n").replace(/[ \t]+$/gm, "").replace(/\n+$/, "");
|
|
1074
|
+
}
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/core/segments.ts
|
|
1077
|
+
function wrapSegments(segments, width) {
|
|
1078
|
+
if (width <= 0) return [[]];
|
|
1079
|
+
const rows = [];
|
|
1080
|
+
let clusters = [];
|
|
1081
|
+
let styles = [];
|
|
1082
|
+
const flushLine = () => {
|
|
1083
|
+
const starts = computeWrapStarts(clusters, width);
|
|
1084
|
+
for (let r = 0; r < starts.length; r++) {
|
|
1085
|
+
const from = starts[r];
|
|
1086
|
+
const to = r + 1 < starts.length ? starts[r + 1] : clusters.length;
|
|
1087
|
+
if (from >= to) {
|
|
1088
|
+
rows.push([]);
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
const row = [];
|
|
1092
|
+
let runText = "";
|
|
1093
|
+
let runStyle = styles[from];
|
|
1094
|
+
for (let k = from; k < to; k++) {
|
|
1095
|
+
const style = styles[k];
|
|
1096
|
+
if (k > from && !sameStyle(runStyle, style)) {
|
|
1097
|
+
row.push({
|
|
1098
|
+
text: runText,
|
|
1099
|
+
style: runStyle
|
|
1100
|
+
});
|
|
1101
|
+
runText = "";
|
|
1102
|
+
runStyle = style;
|
|
1103
|
+
}
|
|
1104
|
+
runText += clusters[k];
|
|
1105
|
+
}
|
|
1106
|
+
row.push({
|
|
1107
|
+
text: runText,
|
|
1108
|
+
style: runStyle
|
|
1109
|
+
});
|
|
1110
|
+
rows.push(row);
|
|
1111
|
+
}
|
|
1112
|
+
clusters = [];
|
|
1113
|
+
styles = [];
|
|
1114
|
+
};
|
|
1115
|
+
for (const seg of segments) {
|
|
1116
|
+
const parts = normalizeWrapText(seg.text).split("\n");
|
|
1117
|
+
for (let p = 0; p < parts.length; p++) {
|
|
1118
|
+
if (p > 0) flushLine();
|
|
1119
|
+
appendClusters(parts[p], seg.style, clusters, styles);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
flushLine();
|
|
1123
|
+
return rows;
|
|
1124
|
+
}
|
|
1125
|
+
function sameStyle(a, b) {
|
|
1126
|
+
return a.color === b.color && a.background === b.background && a.bold === b.bold && a.italic === b.italic && a.strike === b.strike && a.underline === b.underline;
|
|
1127
|
+
}
|
|
1128
|
+
function appendClusters(text, style, clusters, styles) {
|
|
1129
|
+
let ascii = true;
|
|
1130
|
+
for (let i = 0; i < text.length; i++) {
|
|
1131
|
+
const code = text.charCodeAt(i);
|
|
1132
|
+
if (code < 32 || code > 126) {
|
|
1133
|
+
ascii = false;
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (ascii) {
|
|
1138
|
+
for (let i = 0; i < text.length; i++) {
|
|
1139
|
+
clusters.push(text[i]);
|
|
1140
|
+
styles.push(style);
|
|
1141
|
+
}
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
for (const { segment } of segmentGraphemes(text)) {
|
|
1145
|
+
clusters.push(segment);
|
|
1146
|
+
styles.push(style);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function mergeRuns(segments) {
|
|
1150
|
+
if (segments.length === 0) return segments;
|
|
1151
|
+
const out = [];
|
|
1152
|
+
for (const seg of segments) {
|
|
1153
|
+
const last = out[out.length - 1];
|
|
1154
|
+
if (last !== void 0 && sameStyle(last.style, seg.style)) last.text += seg.text;
|
|
1155
|
+
else out.push({
|
|
1156
|
+
text: seg.text,
|
|
1157
|
+
style: seg.style
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
return out;
|
|
1161
|
+
}
|
|
1162
|
+
function segmentsKey(segments) {
|
|
1163
|
+
let key = "";
|
|
1164
|
+
for (const seg of segments) {
|
|
1165
|
+
const flags = `${seg.style.bold ? "b" : ""}${seg.style.italic ? "i" : ""}${seg.style.strike ? "s" : ""}${seg.style.underline ? "u" : ""}${seg.style.background ? "g" : ""}`;
|
|
1166
|
+
key += `${seg.text.length}:${seg.text}\x1f${seg.style.color ?? ""}\x1e${seg.style.background ?? ""}\x1e${flags}\x1d`;
|
|
1167
|
+
}
|
|
1168
|
+
return key;
|
|
1169
|
+
}
|
|
1170
|
+
const glyphs = {
|
|
1171
|
+
spinnerFrames: unicode ? [
|
|
1172
|
+
"⠋",
|
|
1173
|
+
"⠙",
|
|
1174
|
+
"⠹",
|
|
1175
|
+
"⠸",
|
|
1176
|
+
"⠼",
|
|
1177
|
+
"⠴",
|
|
1178
|
+
"⠦",
|
|
1179
|
+
"⠧",
|
|
1180
|
+
"⠇",
|
|
1181
|
+
"⠏"
|
|
1182
|
+
] : [
|
|
1183
|
+
"|",
|
|
1184
|
+
"/",
|
|
1185
|
+
"-",
|
|
1186
|
+
"\\"
|
|
1187
|
+
],
|
|
1188
|
+
bullets: unicode ? [
|
|
1189
|
+
"•",
|
|
1190
|
+
"◦",
|
|
1191
|
+
"▪"
|
|
1192
|
+
] : [
|
|
1193
|
+
"*",
|
|
1194
|
+
"o",
|
|
1195
|
+
"-"
|
|
1196
|
+
],
|
|
1197
|
+
horizontal: unicode ? "─" : "-",
|
|
1198
|
+
tableVertical: unicode ? "│" : "|",
|
|
1199
|
+
tableBorders: {
|
|
1200
|
+
top: unicode ? [
|
|
1201
|
+
"┌",
|
|
1202
|
+
"┬",
|
|
1203
|
+
"┐"
|
|
1204
|
+
] : [
|
|
1205
|
+
"+",
|
|
1206
|
+
"-",
|
|
1207
|
+
"+"
|
|
1208
|
+
],
|
|
1209
|
+
middle: unicode ? [
|
|
1210
|
+
"├",
|
|
1211
|
+
"┼",
|
|
1212
|
+
"┤"
|
|
1213
|
+
] : [
|
|
1214
|
+
"+",
|
|
1215
|
+
"-",
|
|
1216
|
+
"+"
|
|
1217
|
+
],
|
|
1218
|
+
bottom: unicode ? [
|
|
1219
|
+
"└",
|
|
1220
|
+
"┴",
|
|
1221
|
+
"┘"
|
|
1222
|
+
] : [
|
|
1223
|
+
"+",
|
|
1224
|
+
"-",
|
|
1225
|
+
"+"
|
|
1226
|
+
]
|
|
1227
|
+
},
|
|
1228
|
+
quoteBar: unicode ? "▌" : "|",
|
|
1229
|
+
taskChecked: unicode ? "☑" : "[x]",
|
|
1230
|
+
taskUnchecked: unicode ? "☐" : "[ ]",
|
|
1231
|
+
headerExpanded: unicode ? "↓" : "v",
|
|
1232
|
+
headerCollapsed: "-",
|
|
1233
|
+
treeBranch: unicode ? "└" : "+",
|
|
1234
|
+
separator: unicode ? "·" : "-",
|
|
1235
|
+
tokenArrow: unicode ? "→" : "->",
|
|
1236
|
+
ellipsis: unicode ? "…" : "...",
|
|
1237
|
+
focusMarker: unicode ? "❯" : ">",
|
|
1238
|
+
tick: unicode ? "✓" : "x",
|
|
1239
|
+
checkboxOn: unicode ? "[✓]" : "[x]",
|
|
1240
|
+
checkboxOff: "[ ]",
|
|
1241
|
+
pageFlip: unicode ? "⇄" : "<->",
|
|
1242
|
+
wrapArrows: unicode ? "⇅" : "^v",
|
|
1243
|
+
carouselLeft: unicode ? "◀" : "<",
|
|
1244
|
+
carouselRight: unicode ? "▶" : ">",
|
|
1245
|
+
todoPending: "[ ]",
|
|
1246
|
+
todoInProgress: unicode ? "[●]" : "[o]",
|
|
1247
|
+
todoDone: unicode ? "[√]" : "[x]",
|
|
1248
|
+
halfBlockCaps: unicode,
|
|
1249
|
+
blockCapTop: "▄",
|
|
1250
|
+
blockCapBottom: "▀"
|
|
1251
|
+
};
|
|
1252
|
+
function inputFrameTop(rows, realRows) {
|
|
1253
|
+
return rows - 4 - realRows;
|
|
1254
|
+
}
|
|
1255
|
+
function inputStatusRow(rows) {
|
|
1256
|
+
return rows - 4 + 1;
|
|
1257
|
+
}
|
|
1258
|
+
function hintBlockTop(rows, inputHeight, visibleCount) {
|
|
1259
|
+
return Math.max(0, rows - inputHeight - 1 - visibleCount);
|
|
1260
|
+
}
|
|
1261
|
+
//#endregion
|
|
1262
|
+
//#region src/ui/layout-service.ts
|
|
1263
|
+
function inputContentTop(rows, inputHeight) {
|
|
1264
|
+
return rows - inputHeight;
|
|
1265
|
+
}
|
|
1266
|
+
/** Inclusive bottom row of the input's editable content area. */
|
|
1267
|
+
function inputContentBottom(rows, inputHeight) {
|
|
1268
|
+
return inputContentTop(rows, inputHeight) + inputHeight - 4;
|
|
1269
|
+
}
|
|
1270
|
+
function inputContentContains(y, rows, inputHeight) {
|
|
1271
|
+
return y >= inputContentTop(rows, inputHeight) && y <= inputContentBottom(rows, inputHeight);
|
|
1272
|
+
}
|
|
1273
|
+
/** Inclusive row span of the active interaction panel (message bottom .. above the status line). */
|
|
1274
|
+
function panelSpan(geometry) {
|
|
1275
|
+
return {
|
|
1276
|
+
top: geometry.messageHeight,
|
|
1277
|
+
bottom: geometry.rows - 2
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
function panelContains(y, geometry) {
|
|
1281
|
+
const span = panelSpan(geometry);
|
|
1282
|
+
return y >= span.top && y <= span.bottom;
|
|
1283
|
+
}
|
|
1284
|
+
/** Anchor row where a panel of `bodyRows` lines must start to sit above the status line. */
|
|
1285
|
+
function panelAnchorRow(rows, bodyRows) {
|
|
1286
|
+
return rows - 2 - bodyRows;
|
|
1287
|
+
}
|
|
1288
|
+
function hintSpan(rows, inputHeight, visibleCount, dialogOpen) {
|
|
1289
|
+
if (dialogOpen || visibleCount <= 0) return null;
|
|
1290
|
+
return {
|
|
1291
|
+
top: hintBlockTop(rows, inputHeight, visibleCount),
|
|
1292
|
+
bottom: rows - inputHeight - 1 - 1
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function scrollbarColumn(columns) {
|
|
1296
|
+
return columns - 3;
|
|
1297
|
+
}
|
|
1298
|
+
function messageBackgroundWidth(width) {
|
|
1299
|
+
return width - 2 - 3 - 1;
|
|
1300
|
+
}
|
|
1301
|
+
const RegionContext = createContext({
|
|
1302
|
+
x: 0,
|
|
1303
|
+
y: 0
|
|
1304
|
+
});
|
|
1305
|
+
function useOrigin() {
|
|
1306
|
+
return useContext(RegionContext);
|
|
1307
|
+
}
|
|
1308
|
+
function translate(parent, x, y) {
|
|
1309
|
+
return {
|
|
1310
|
+
x: parent.x + x,
|
|
1311
|
+
y: parent.y + y
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
function Region({ x = 0, y = 0, children }) {
|
|
1315
|
+
const parent = useOrigin();
|
|
1316
|
+
const value = useMemo(() => translate(parent, x, y), [
|
|
1317
|
+
parent.x,
|
|
1318
|
+
parent.y,
|
|
1319
|
+
x,
|
|
1320
|
+
y
|
|
1321
|
+
]);
|
|
1322
|
+
return /* @__PURE__ */ jsx(RegionContext.Provider, {
|
|
1323
|
+
value,
|
|
1324
|
+
children
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
//#endregion
|
|
1328
|
+
//#region src/ui/selection.tsx
|
|
1329
|
+
const SelectionContext = createContext(null);
|
|
1330
|
+
const SelectableContent = memo(function SelectableContent({ content, segments, color, bold, inverse, backgroundColor, sliceStart, sliceEnd }) {
|
|
1331
|
+
if (sliceStart >= 0 && sliceEnd > sliceStart) {
|
|
1332
|
+
const highlight = /* @__PURE__ */ jsx(Text, {
|
|
1333
|
+
backgroundColor: COLORS.selectionBg,
|
|
1334
|
+
color: COLORS.selectionFg,
|
|
1335
|
+
children: content.slice(sliceStart, sliceEnd)
|
|
1336
|
+
});
|
|
1337
|
+
if (segments !== void 0) return /* @__PURE__ */ jsxs(Text, {
|
|
1338
|
+
backgroundColor,
|
|
1339
|
+
children: [
|
|
1340
|
+
renderSegments(segments, 0, sliceStart, color ?? COLORS.ink),
|
|
1341
|
+
highlight,
|
|
1342
|
+
renderSegments(segments, sliceEnd, content.length, color ?? COLORS.ink)
|
|
1343
|
+
]
|
|
1344
|
+
});
|
|
1345
|
+
return /* @__PURE__ */ jsxs(Text, {
|
|
1346
|
+
backgroundColor,
|
|
1347
|
+
inverse,
|
|
1348
|
+
color: color ?? COLORS.ink,
|
|
1349
|
+
bold,
|
|
1350
|
+
children: [
|
|
1351
|
+
content.slice(0, sliceStart),
|
|
1352
|
+
highlight,
|
|
1353
|
+
content.slice(sliceEnd)
|
|
1354
|
+
]
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
if (segments !== void 0) return /* @__PURE__ */ jsx(Text, {
|
|
1358
|
+
backgroundColor,
|
|
1359
|
+
children: renderSegments(segments, 0, content.length, color ?? COLORS.ink)
|
|
1360
|
+
});
|
|
1361
|
+
return /* @__PURE__ */ jsx(Text, {
|
|
1362
|
+
backgroundColor,
|
|
1363
|
+
inverse,
|
|
1364
|
+
color: color ?? COLORS.ink,
|
|
1365
|
+
bold,
|
|
1366
|
+
children: content
|
|
1367
|
+
});
|
|
1368
|
+
}, (prev, next) => prev.content === next.content && prev.segments === next.segments && prev.color === next.color && prev.bold === next.bold && prev.inverse === next.inverse && prev.backgroundColor === next.backgroundColor && prev.sliceStart === next.sliceStart && prev.sliceEnd === next.sliceEnd);
|
|
1369
|
+
const SelectableText = function SelectableText({ y, col, text, segments, color, bold = false, inverse = false, backgroundColor, messageLayer = false, flow = false }) {
|
|
1370
|
+
const selection = useContext(SelectionContext);
|
|
1371
|
+
const origin = useOrigin();
|
|
1372
|
+
const absY = origin.y + y;
|
|
1373
|
+
const absCol = origin.x + col;
|
|
1374
|
+
const content = text ?? segments?.map((segment) => segment.text).join("") ?? "";
|
|
1375
|
+
const pieceId = useRef({});
|
|
1376
|
+
useLayoutEffect(() => {
|
|
1377
|
+
registerRowPiece(pieceId.current, absY, {
|
|
1378
|
+
col: absCol,
|
|
1379
|
+
text: content,
|
|
1380
|
+
...messageLayer ? { layer: "message" } : {}
|
|
1381
|
+
});
|
|
1382
|
+
return () => registerRowPiece(pieceId.current, absY, null);
|
|
1383
|
+
}, [
|
|
1384
|
+
absY,
|
|
1385
|
+
absCol,
|
|
1386
|
+
content,
|
|
1387
|
+
messageLayer
|
|
1388
|
+
]);
|
|
1389
|
+
let sliceStart = -1;
|
|
1390
|
+
let sliceEnd = -1;
|
|
1391
|
+
if (selection !== null) {
|
|
1392
|
+
const range = selectedRange(selection, absY);
|
|
1393
|
+
if (range !== null && (flow || envelopeOverlaps(selection, absCol, textWidth(content)))) {
|
|
1394
|
+
const lineWidth = textWidth(content);
|
|
1395
|
+
const start = Math.max(range.start, absCol);
|
|
1396
|
+
const end = Math.min(range.end, absCol + lineWidth);
|
|
1397
|
+
if (start < end) {
|
|
1398
|
+
sliceStart = colToCharIndex(content, start - absCol);
|
|
1399
|
+
sliceEnd = colToCharIndex(content, end - absCol);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return /* @__PURE__ */ jsx(SelectableContent, {
|
|
1404
|
+
content,
|
|
1405
|
+
segments,
|
|
1406
|
+
color,
|
|
1407
|
+
bold,
|
|
1408
|
+
inverse,
|
|
1409
|
+
backgroundColor,
|
|
1410
|
+
sliceStart,
|
|
1411
|
+
sliceEnd
|
|
1412
|
+
});
|
|
1413
|
+
};
|
|
1414
|
+
function renderSegments(segments, start, end, baseColor) {
|
|
1415
|
+
const out = [];
|
|
1416
|
+
let offset = 0;
|
|
1417
|
+
for (const segment of segments) {
|
|
1418
|
+
const segmentStart = offset;
|
|
1419
|
+
const segmentEnd = offset + segment.text.length;
|
|
1420
|
+
offset = segmentEnd;
|
|
1421
|
+
if (segmentEnd <= start || segmentStart >= end) continue;
|
|
1422
|
+
const from = Math.max(0, start - segmentStart);
|
|
1423
|
+
const to = Math.min(segment.text.length, end - segmentStart);
|
|
1424
|
+
if (from >= to) continue;
|
|
1425
|
+
out.push(/* @__PURE__ */ jsx(Text, {
|
|
1426
|
+
color: segment.style.color ?? baseColor,
|
|
1427
|
+
bold: segment.style.bold,
|
|
1428
|
+
italic: segment.style.italic,
|
|
1429
|
+
underline: segment.style.underline,
|
|
1430
|
+
strikethrough: segment.style.strike,
|
|
1431
|
+
backgroundColor: segment.style.background,
|
|
1432
|
+
children: segment.text.slice(from, to)
|
|
1433
|
+
}, out.length));
|
|
1434
|
+
}
|
|
1435
|
+
return out;
|
|
1436
|
+
}
|
|
1437
|
+
//#endregion
|
|
1438
|
+
//#region src/terminal/mouse.ts
|
|
1439
|
+
const MOUSE_ENABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000h\x1B[?1002h\x1B[?1006h";
|
|
1440
|
+
const MOUSE_DISABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000l\x1B[?1002l\x1B[?1006l";
|
|
1441
|
+
function writeMouseEnable() {
|
|
1442
|
+
process.stdout.write(MOUSE_ENABLE_SEQUENCES);
|
|
1443
|
+
}
|
|
1444
|
+
function writeMouseDisable() {
|
|
1445
|
+
process.stdout.write(MOUSE_DISABLE_SEQUENCES);
|
|
1446
|
+
}
|
|
1447
|
+
const MAX_BUFFER = 64;
|
|
1448
|
+
const SGR_PREFIX = "\x1B[<";
|
|
1449
|
+
function partialPrefixLength(buffer) {
|
|
1450
|
+
for (let keep = Math.min(2, buffer.length); keep > 0; keep--) if (SGR_PREFIX.startsWith(buffer.slice(-keep))) return keep;
|
|
1451
|
+
return 0;
|
|
1452
|
+
}
|
|
1453
|
+
function createMouseParser(onEvent) {
|
|
1454
|
+
let buffer = "";
|
|
1455
|
+
const pressedButtons = /* @__PURE__ */ new Set();
|
|
1456
|
+
function parseSequenceAt(str, offset) {
|
|
1457
|
+
if (!str.startsWith("\x1B[<", offset)) return null;
|
|
1458
|
+
let index = offset + 3;
|
|
1459
|
+
const values = [];
|
|
1460
|
+
let current = 0;
|
|
1461
|
+
let hasDigit = false;
|
|
1462
|
+
let terminator = "";
|
|
1463
|
+
for (; index < str.length; index++) {
|
|
1464
|
+
const ch = str[index];
|
|
1465
|
+
if (ch >= "0" && ch <= "9") {
|
|
1466
|
+
current = current * 10 + (ch.charCodeAt(0) - 48);
|
|
1467
|
+
hasDigit = true;
|
|
1468
|
+
} else if (ch === ";" && hasDigit) {
|
|
1469
|
+
values.push(current);
|
|
1470
|
+
current = 0;
|
|
1471
|
+
hasDigit = false;
|
|
1472
|
+
} else if (ch === "M" || ch === "m") {
|
|
1473
|
+
if (!hasDigit) return null;
|
|
1474
|
+
values.push(current);
|
|
1475
|
+
terminator = ch;
|
|
1476
|
+
break;
|
|
1477
|
+
} else return null;
|
|
1478
|
+
}
|
|
1479
|
+
if (terminator === "") return null;
|
|
1480
|
+
if (values.length !== 3) return null;
|
|
1481
|
+
return {
|
|
1482
|
+
event: decodeSgrEvent(values[0], values[1], values[2], terminator, pressedButtons),
|
|
1483
|
+
consumed: index - offset + 1
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
return { feed(chunk) {
|
|
1487
|
+
buffer += chunk;
|
|
1488
|
+
while (true) {
|
|
1489
|
+
const start = buffer.indexOf("\x1B[<");
|
|
1490
|
+
if (start === -1) {
|
|
1491
|
+
const keep = partialPrefixLength(buffer);
|
|
1492
|
+
buffer = keep === 0 ? "" : buffer.slice(-keep);
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
if (start > 0) {
|
|
1496
|
+
buffer = buffer.slice(start);
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
if (buffer.length > MAX_BUFFER) {
|
|
1500
|
+
buffer = "";
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
const parsed = parseSequenceAt(buffer, 0);
|
|
1504
|
+
if (parsed === null) return;
|
|
1505
|
+
const { event, consumed } = parsed;
|
|
1506
|
+
buffer = buffer.slice(consumed);
|
|
1507
|
+
onEvent(event);
|
|
1508
|
+
}
|
|
1509
|
+
} };
|
|
1510
|
+
}
|
|
1511
|
+
function decodeSgrEvent(rawButtonCode, wireX, wireY, pressRelease, pressed) {
|
|
1512
|
+
const button = rawButtonCode & 3;
|
|
1513
|
+
const isScroll = (rawButtonCode & 64) !== 0;
|
|
1514
|
+
const isMotion = (rawButtonCode & 32) !== 0;
|
|
1515
|
+
const modifiers = {
|
|
1516
|
+
shift: (rawButtonCode & 4) !== 0,
|
|
1517
|
+
alt: (rawButtonCode & 8) !== 0,
|
|
1518
|
+
ctrl: (rawButtonCode & 16) !== 0
|
|
1519
|
+
};
|
|
1520
|
+
let type;
|
|
1521
|
+
let scrollDirection;
|
|
1522
|
+
if (isScroll) {
|
|
1523
|
+
type = "scroll";
|
|
1524
|
+
scrollDirection = button === 0 ? "up" : "down";
|
|
1525
|
+
} else if (isMotion) type = pressed.size > 0 ? "drag" : "move";
|
|
1526
|
+
else if (pressRelease === "M") {
|
|
1527
|
+
type = "down";
|
|
1528
|
+
if (button !== 3) pressed.add(button);
|
|
1529
|
+
} else {
|
|
1530
|
+
type = "up";
|
|
1531
|
+
if (button === 3) pressed.clear();
|
|
1532
|
+
else pressed.delete(button);
|
|
1533
|
+
}
|
|
1534
|
+
return {
|
|
1535
|
+
type,
|
|
1536
|
+
button: button === 3 ? 0 : button,
|
|
1537
|
+
x: wireX - 1,
|
|
1538
|
+
y: wireY - 1,
|
|
1539
|
+
modifiers,
|
|
1540
|
+
...scrollDirection === void 0 ? {} : { scrollDirection }
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
function isMouseResidue(input) {
|
|
1544
|
+
return input.includes("\x1B") || /^\[<\d+(;\d+)*[Mm]$/.test(input);
|
|
1545
|
+
}
|
|
1546
|
+
function createMouseController(onEvent) {
|
|
1547
|
+
const parser = createMouseParser(onEvent);
|
|
1548
|
+
const onData = (chunk) => parser.feed(chunk.toString("latin1"));
|
|
1549
|
+
return {
|
|
1550
|
+
enable() {
|
|
1551
|
+
writeMouseEnable();
|
|
1552
|
+
process.stdin.on("data", onData);
|
|
1553
|
+
},
|
|
1554
|
+
disable() {
|
|
1555
|
+
writeMouseDisable();
|
|
1556
|
+
process.stdin.off("data", onData);
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
//#endregion
|
|
1561
|
+
//#region src/ui/key-arbiter.ts
|
|
1562
|
+
/**
|
|
1563
|
+
* Key-event suppression for ink's broadcast-based useInput.
|
|
1564
|
+
*
|
|
1565
|
+
* ink delivers every stdin chunk to every useInput subscriber, and delivery
|
|
1566
|
+
* order follows subscription order: KeymapGate (chrome key contributions,
|
|
1567
|
+
* mounted as the first child) -> message list -> composer -> panels ->
|
|
1568
|
+
* dialog -> the App shell. A participant that must win over the later ones
|
|
1569
|
+
* marks the current event consumed; later participants skip it.
|
|
1570
|
+
*
|
|
1571
|
+
* The consumed flag is scoped to one stdin chunk. KeymapGate bumps the
|
|
1572
|
+
* generation from its own stdin data listener, so even several synchronous
|
|
1573
|
+
* writes (as tests do) each get a fresh generation and no flag leaks across
|
|
1574
|
+
* events.
|
|
1575
|
+
*/
|
|
1576
|
+
let generation = 0;
|
|
1577
|
+
let consumedAtGeneration = -1;
|
|
1578
|
+
/** Start a new key-event generation; called once per stdin chunk. */
|
|
1579
|
+
function beginKeyEvent() {
|
|
1580
|
+
generation += 1;
|
|
1581
|
+
}
|
|
1582
|
+
/** Chrome key contributions consumed the event; later participants must skip it. */
|
|
1583
|
+
function markKeymapConsumed() {
|
|
1584
|
+
consumedAtGeneration = generation;
|
|
1585
|
+
}
|
|
1586
|
+
/** True when the chrome key chain consumed the current key event. */
|
|
1587
|
+
function isKeyConsumed() {
|
|
1588
|
+
return consumedAtGeneration === generation;
|
|
1589
|
+
}
|
|
1590
|
+
//#endregion
|
|
1591
|
+
//#region src/ui/hooks/use-caret.ts
|
|
1592
|
+
/**
|
|
1593
|
+
* Ink 7.1.1 rests the hardware cursor one row above where frames without a
|
|
1594
|
+
* trailing newline end; this layout fills the viewport, so request one row
|
|
1595
|
+
* lower.
|
|
1596
|
+
*/
|
|
1597
|
+
function useCaret() {
|
|
1598
|
+
const { setCursorPosition } = useCursor();
|
|
1599
|
+
return { setCursorPosition: useCallback((position) => {
|
|
1600
|
+
if (position === void 0) {
|
|
1601
|
+
setCursorPosition(void 0);
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
setCursorPosition({
|
|
1605
|
+
x: position.x,
|
|
1606
|
+
y: position.y + 1
|
|
1607
|
+
});
|
|
1608
|
+
}, [setCursorPosition]) };
|
|
1609
|
+
}
|
|
1610
|
+
//#endregion
|
|
1611
|
+
//#region src/core/caret-nonce.ts
|
|
1612
|
+
/**
|
|
1613
|
+
* Ink 7.1.1 places the caret one row high on the rewrite path; every caret
|
|
1614
|
+
* move changes these frame bytes so the diff renderer stays on the rewrite
|
|
1615
|
+
* path that useCaret() compensates for. The variant span exceeds any
|
|
1616
|
+
* terminal width, so no frame follows the misplaced cursor-only path.
|
|
1617
|
+
*/
|
|
1618
|
+
const NONCE_SPAN = 240;
|
|
1619
|
+
function caretNonceColor(cursor) {
|
|
1620
|
+
return `ansi256(${16 + cursor % NONCE_SPAN})`;
|
|
1621
|
+
}
|
|
1622
|
+
function caretNonceText(cursor) {
|
|
1623
|
+
return Math.floor(cursor / NONCE_SPAN) % 2 === 0 ? " " : "\xA0";
|
|
1624
|
+
}
|
|
1625
|
+
function caretNonceBold(cursor) {
|
|
1626
|
+
return Math.floor(cursor / 480) % 2 === 0;
|
|
1627
|
+
}
|
|
1628
|
+
//#endregion
|
|
1629
|
+
//#region src/terminal/cursor-shape.ts
|
|
1630
|
+
const SEQUENCES = {
|
|
1631
|
+
beam: "\x1B[1 q",
|
|
1632
|
+
block: "\x1B[2 q",
|
|
1633
|
+
reset: "\x1B[0 q",
|
|
1634
|
+
hide: "\x1B[?25l",
|
|
1635
|
+
show: "\x1B[?25h"
|
|
1636
|
+
};
|
|
1637
|
+
function writeCursorShape(shape) {
|
|
1638
|
+
process.stdout.write(SEQUENCES[shape]);
|
|
1639
|
+
}
|
|
1640
|
+
//#endregion
|
|
1641
|
+
//#region src/ui/chrome/caps.tsx
|
|
1642
|
+
function CapText({ background, top, width }) {
|
|
1643
|
+
if (glyphs.halfBlockCaps) return /* @__PURE__ */ jsx(Text, {
|
|
1644
|
+
color: background,
|
|
1645
|
+
children: (top ? glyphs.blockCapTop : glyphs.blockCapBottom).repeat(width)
|
|
1646
|
+
});
|
|
1647
|
+
return /* @__PURE__ */ jsx(Text, {
|
|
1648
|
+
backgroundColor: background,
|
|
1649
|
+
children: " ".repeat(width)
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
//#endregion
|
|
1653
|
+
//#region src/core/edit.ts
|
|
1654
|
+
function clamp(state, cursor) {
|
|
1655
|
+
return {
|
|
1656
|
+
value: state.value,
|
|
1657
|
+
cursor: Math.max(0, Math.min(cursor, state.value.length))
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
function spanAtOrBefore(value, cursor) {
|
|
1661
|
+
for (const segment of segmentGraphemes(value)) {
|
|
1662
|
+
const end = segment.index + segment.segment.length;
|
|
1663
|
+
if (end >= cursor) return {
|
|
1664
|
+
start: segment.index,
|
|
1665
|
+
end
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
function spanAtOrAfter(value, cursor) {
|
|
1670
|
+
for (const segment of segmentGraphemes(value)) if (segment.index >= cursor) return {
|
|
1671
|
+
start: segment.index,
|
|
1672
|
+
end: segment.index + segment.segment.length
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
function editInsert(state, text) {
|
|
1676
|
+
if (text === "") return clamp(state, state.cursor);
|
|
1677
|
+
const cursor = Math.max(0, Math.min(state.cursor, state.value.length));
|
|
1678
|
+
return {
|
|
1679
|
+
value: state.value.slice(0, cursor) + text + state.value.slice(cursor),
|
|
1680
|
+
cursor: cursor + text.length
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
function editBackspace(state) {
|
|
1684
|
+
if (state.cursor <= 0) return null;
|
|
1685
|
+
const span = spanAtOrBefore(state.value, state.cursor);
|
|
1686
|
+
if (span === void 0) return null;
|
|
1687
|
+
return {
|
|
1688
|
+
value: state.value.slice(0, span.start) + state.value.slice(span.end),
|
|
1689
|
+
cursor: span.start
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
function editDelete(state) {
|
|
1693
|
+
if (state.cursor >= state.value.length) return null;
|
|
1694
|
+
const span = spanAtOrAfter(state.value, state.cursor);
|
|
1695
|
+
if (span === void 0) return null;
|
|
1696
|
+
return {
|
|
1697
|
+
value: state.value.slice(0, state.cursor) + state.value.slice(span.end),
|
|
1698
|
+
cursor: state.cursor
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
function editCursorLeft(state) {
|
|
1702
|
+
if (state.cursor <= 0) return state;
|
|
1703
|
+
const span = spanAtOrBefore(state.value, state.cursor);
|
|
1704
|
+
return clamp(state, span === void 0 ? state.cursor - 1 : span.start);
|
|
1705
|
+
}
|
|
1706
|
+
function editCursorRight(state) {
|
|
1707
|
+
if (state.cursor >= state.value.length) return state;
|
|
1708
|
+
const span = spanAtOrAfter(state.value, state.cursor);
|
|
1709
|
+
return clamp(state, span === void 0 ? state.cursor + 1 : span.end);
|
|
1710
|
+
}
|
|
1711
|
+
const SEPARATOR_RANGES = [
|
|
1712
|
+
[8208, 8231],
|
|
1713
|
+
[12288, 12351],
|
|
1714
|
+
[65281, 65295],
|
|
1715
|
+
[65306, 65312],
|
|
1716
|
+
[65339, 65344],
|
|
1717
|
+
[65371, 65381],
|
|
1718
|
+
[65504, 65508]
|
|
1719
|
+
];
|
|
1720
|
+
function isAsciiWordChar(code) {
|
|
1721
|
+
return code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 95;
|
|
1722
|
+
}
|
|
1723
|
+
function isLineBreakAt(value, index) {
|
|
1724
|
+
const code = value.charCodeAt(index);
|
|
1725
|
+
return code === 10 || code === 13;
|
|
1726
|
+
}
|
|
1727
|
+
function isSeparatorAt(value, index) {
|
|
1728
|
+
const code = value.charCodeAt(index);
|
|
1729
|
+
if (code === 10 || code === 13) return true;
|
|
1730
|
+
if (isAsciiWordChar(code)) return false;
|
|
1731
|
+
if (code >= 128) {
|
|
1732
|
+
for (const [start, end] of SEPARATOR_RANGES) if (code >= start && code <= end) return true;
|
|
1733
|
+
return false;
|
|
1734
|
+
}
|
|
1735
|
+
if (code === 32) return true;
|
|
1736
|
+
return code >= 33 && code <= 126;
|
|
1737
|
+
}
|
|
1738
|
+
function wordLeftBoundary(value, cursor) {
|
|
1739
|
+
let i = cursor - 1;
|
|
1740
|
+
while (i >= 0 && !isLineBreakAt(value, i) && isSeparatorAt(value, i)) i--;
|
|
1741
|
+
while (i >= 0 && !isSeparatorAt(value, i)) i--;
|
|
1742
|
+
return i + 1;
|
|
1743
|
+
}
|
|
1744
|
+
function wordRightBoundary(value, cursor) {
|
|
1745
|
+
if (cursor >= value.length) return cursor;
|
|
1746
|
+
if (!isSeparatorAt(value, cursor)) {
|
|
1747
|
+
let i = cursor;
|
|
1748
|
+
while (i < value.length && !isSeparatorAt(value, i)) i++;
|
|
1749
|
+
return i;
|
|
1750
|
+
}
|
|
1751
|
+
let i = cursor;
|
|
1752
|
+
while (i < value.length && !isLineBreakAt(value, i) && isSeparatorAt(value, i)) i++;
|
|
1753
|
+
while (i < value.length && !isSeparatorAt(value, i)) i++;
|
|
1754
|
+
return i;
|
|
1755
|
+
}
|
|
1756
|
+
function editCursorWordLeft(state) {
|
|
1757
|
+
const boundary = wordLeftBoundary(state.value, state.cursor);
|
|
1758
|
+
return boundary >= state.cursor ? state : {
|
|
1759
|
+
value: state.value,
|
|
1760
|
+
cursor: boundary
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
function editCursorWordRight(state) {
|
|
1764
|
+
const boundary = wordRightBoundary(state.value, state.cursor);
|
|
1765
|
+
return boundary <= state.cursor ? state : {
|
|
1766
|
+
value: state.value,
|
|
1767
|
+
cursor: boundary
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
function editDeleteWordLeft(state) {
|
|
1771
|
+
const boundary = wordLeftBoundary(state.value, state.cursor);
|
|
1772
|
+
if (boundary >= state.cursor) return null;
|
|
1773
|
+
return {
|
|
1774
|
+
value: state.value.slice(0, boundary) + state.value.slice(state.cursor),
|
|
1775
|
+
cursor: boundary
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
function editDeleteWordRight(state) {
|
|
1779
|
+
const boundary = wordRightBoundary(state.value, state.cursor);
|
|
1780
|
+
if (boundary <= state.cursor) return null;
|
|
1781
|
+
return {
|
|
1782
|
+
value: state.value.slice(0, state.cursor) + state.value.slice(boundary),
|
|
1783
|
+
cursor: state.cursor
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
//#endregion
|
|
1787
|
+
//#region src/ui/widgets/registry.ts
|
|
1788
|
+
const widgets = keyedRegistry();
|
|
1789
|
+
function fallbackWidget(type) {
|
|
1790
|
+
return {
|
|
1791
|
+
height: () => 1,
|
|
1792
|
+
paintWidth: () => 24,
|
|
1793
|
+
render: ({ item }) => {
|
|
1794
|
+
const label = item.label;
|
|
1795
|
+
const text = typeof label === "string" && label !== "" ? `${type}: ${label}` : `${type}: (unknown item)`;
|
|
1796
|
+
return createElement(Text, { dimColor: true }, text);
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
function registerWidget(type, def, options) {
|
|
1801
|
+
return widgets.register(type, def, options);
|
|
1802
|
+
}
|
|
1803
|
+
function widgetOf(type) {
|
|
1804
|
+
return widgets.get(type) ?? fallbackWidget(type);
|
|
1805
|
+
}
|
|
1806
|
+
//#endregion
|
|
1807
|
+
//#region src/ui/dialog/items.ts
|
|
1808
|
+
function isSelectableRow(row) {
|
|
1809
|
+
if (row === void 0) return false;
|
|
1810
|
+
return row.items.some((item) => widgetOf(item.type).selectable === true);
|
|
1811
|
+
}
|
|
1812
|
+
function selectableSpan(row) {
|
|
1813
|
+
if (row === void 0) return 0;
|
|
1814
|
+
let stops = 0;
|
|
1815
|
+
for (const item of row.items) stops += widgetOf(item.type).stops?.(item) ?? 1;
|
|
1816
|
+
return Math.max(0, stops - 1);
|
|
1817
|
+
}
|
|
1818
|
+
function snapRow(rows, row) {
|
|
1819
|
+
const count = rows.length;
|
|
1820
|
+
if (count === 0) return 0;
|
|
1821
|
+
const index = Math.min(Math.max(row, 0), count - 1);
|
|
1822
|
+
if (isSelectableRow(rows[index])) return index;
|
|
1823
|
+
for (let down = index + 1; down < count; down++) if (isSelectableRow(rows[down])) return down;
|
|
1824
|
+
for (let up = index - 1; up >= 0; up--) if (isSelectableRow(rows[up])) return up;
|
|
1825
|
+
return index;
|
|
1826
|
+
}
|
|
1827
|
+
function stepRow(rows, from, delta) {
|
|
1828
|
+
let row = from + delta;
|
|
1829
|
+
while (row >= 0 && row < rows.length && !isSelectableRow(rows[row])) row += delta;
|
|
1830
|
+
return row >= 0 && row < rows.length ? row : from;
|
|
1831
|
+
}
|
|
1832
|
+
function focusedItem(row, col) {
|
|
1833
|
+
if (row === void 0) return void 0;
|
|
1834
|
+
const only = row.items.length === 1 ? row.items[0] : void 0;
|
|
1835
|
+
if (only !== void 0 && widgetOf(only.type).fullRowFocus === true) return only;
|
|
1836
|
+
return row.items[col];
|
|
1837
|
+
}
|
|
1838
|
+
function moveFocus(rows, focus, key) {
|
|
1839
|
+
switch (key) {
|
|
1840
|
+
case "up": return {
|
|
1841
|
+
row: stepRow(rows, focus.row, -1),
|
|
1842
|
+
col: 0
|
|
1843
|
+
};
|
|
1844
|
+
case "down": return {
|
|
1845
|
+
row: stepRow(rows, focus.row, 1),
|
|
1846
|
+
col: 0
|
|
1847
|
+
};
|
|
1848
|
+
case "left": return {
|
|
1849
|
+
row: focus.row,
|
|
1850
|
+
col: Math.max(0, focus.col - 1)
|
|
1851
|
+
};
|
|
1852
|
+
case "right": {
|
|
1853
|
+
const max = selectableSpan(rows[focus.row]);
|
|
1854
|
+
return {
|
|
1855
|
+
row: focus.row,
|
|
1856
|
+
col: Math.max(0, Math.min(max, focus.col + 1))
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function clampFocus(rows, focus, minRow, maxRow) {
|
|
1862
|
+
const row = snapRow(rows, Math.min(Math.max(focus.row, minRow), maxRow));
|
|
1863
|
+
return {
|
|
1864
|
+
row,
|
|
1865
|
+
col: Math.min(focus.col, selectableSpan(rows[row]))
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
function asTextItem(item) {
|
|
1869
|
+
return widgetOf(item.type).editable === true ? item : null;
|
|
1870
|
+
}
|
|
1871
|
+
function filterRowsWithHeaders(rows, query, searchRight) {
|
|
1872
|
+
const q = query.trim().toLowerCase();
|
|
1873
|
+
if (q === "") return rows;
|
|
1874
|
+
const matches = (item) => {
|
|
1875
|
+
const texts = widgetOf(item.type).searchTexts?.(item, searchRight);
|
|
1876
|
+
if (texts === void 0) return false;
|
|
1877
|
+
return texts.some((text) => text.toLowerCase().includes(q));
|
|
1878
|
+
};
|
|
1879
|
+
const out = [];
|
|
1880
|
+
let pendingHeader;
|
|
1881
|
+
for (const row of rows) {
|
|
1882
|
+
if (!isSelectableRow(row)) {
|
|
1883
|
+
pendingHeader = row;
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
if (!row.items.some(matches)) continue;
|
|
1887
|
+
if (pendingHeader !== void 0) {
|
|
1888
|
+
out.push(pendingHeader);
|
|
1889
|
+
pendingHeader = void 0;
|
|
1890
|
+
}
|
|
1891
|
+
out.push(row);
|
|
1892
|
+
}
|
|
1893
|
+
const first = out[0];
|
|
1894
|
+
if (first !== void 0 && first.items.length === 1 && first.items[0]?.type === "header") out[0] = { items: [{
|
|
1895
|
+
...first.items[0],
|
|
1896
|
+
leadingBlank: false
|
|
1897
|
+
}] };
|
|
1898
|
+
return out;
|
|
1899
|
+
}
|
|
1900
|
+
const SELECT_BLOCK_WIDTH_RATIO = .6;
|
|
1901
|
+
const ACTION_EDGE_RATIO = .2;
|
|
1902
|
+
const FOOTER_MAX_LINES_PER_ENTRY = 2;
|
|
1903
|
+
function wrapFooter(footer, width) {
|
|
1904
|
+
if (footer === void 0) return [];
|
|
1905
|
+
const rows = [];
|
|
1906
|
+
for (const line of footer) {
|
|
1907
|
+
if (line.text === "") continue;
|
|
1908
|
+
const wrapped = wrapLines(line.text, width);
|
|
1909
|
+
const capped = wrapped.slice(0, FOOTER_MAX_LINES_PER_ENTRY);
|
|
1910
|
+
if (wrapped.length > FOOTER_MAX_LINES_PER_ENTRY) capped[1] = truncate(capped[1] ?? "", Math.max(1, width - glyphs.ellipsis.length)) + glyphs.ellipsis;
|
|
1911
|
+
for (const text of capped) rows.push({
|
|
1912
|
+
text,
|
|
1913
|
+
color: line.color
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
return rows;
|
|
1917
|
+
}
|
|
1918
|
+
function wrapStatusLines(lines, width) {
|
|
1919
|
+
if (lines === void 0) return [];
|
|
1920
|
+
const rows = [];
|
|
1921
|
+
for (const line of lines) {
|
|
1922
|
+
if (line.text === "") continue;
|
|
1923
|
+
for (const text of wrapLines(line.text, width)) rows.push({
|
|
1924
|
+
text,
|
|
1925
|
+
color: line.color
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
return rows;
|
|
1929
|
+
}
|
|
1930
|
+
function rowHeight(row, width) {
|
|
1931
|
+
let height = 1;
|
|
1932
|
+
for (const item of row.items) {
|
|
1933
|
+
const itemHeight = widgetOf(item.type).height(item, width);
|
|
1934
|
+
if (itemHeight > height) height = itemHeight;
|
|
1935
|
+
}
|
|
1936
|
+
return height;
|
|
1937
|
+
}
|
|
1938
|
+
function rowTopOffset(rows, rowIndex, width) {
|
|
1939
|
+
let offset = 0;
|
|
1940
|
+
for (let i = 0; i < rowIndex; i++) offset += rowHeight(rows[i], width);
|
|
1941
|
+
return offset;
|
|
1942
|
+
}
|
|
1943
|
+
function rowBlockSpan(rows, rowIndex, width) {
|
|
1944
|
+
const top = rowTopOffset(rows, rowIndex, width);
|
|
1945
|
+
let blockTop = top;
|
|
1946
|
+
let bottom = top + rowHeight(rows[rowIndex] ?? { items: [] }, width) - 1;
|
|
1947
|
+
for (let i = rowIndex - 1; i >= 0 && !isSelectableRow(rows[i]); i--) blockTop -= rowHeight(rows[i], width);
|
|
1948
|
+
for (let i = rowIndex + 1; i < rows.length && !isSelectableRow(rows[i]); i++) bottom += rowHeight(rows[i], width);
|
|
1949
|
+
return {
|
|
1950
|
+
top: blockTop,
|
|
1951
|
+
bottom
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function adjustScroll(rows, focus, scrollTop, contentHeight, width) {
|
|
1955
|
+
const { top, bottom } = rowBlockSpan(rows, focus.row, width);
|
|
1956
|
+
if (top < scrollTop) return top;
|
|
1957
|
+
if (bottom >= scrollTop + contentHeight) return Math.max(0, bottom - contentHeight + 1);
|
|
1958
|
+
return scrollTop;
|
|
1959
|
+
}
|
|
1960
|
+
function listCenterScroll(rows, focusRow, viewportHeight, width, direction) {
|
|
1961
|
+
const total = rows.reduce((sum, row) => sum + rowHeight(row, width), 0);
|
|
1962
|
+
const maxScroll = Math.max(0, total - viewportHeight);
|
|
1963
|
+
const anchor = direction === "down" ? Math.floor(viewportHeight / 2) : Math.floor((viewportHeight - 1) / 2);
|
|
1964
|
+
const top = rowTopOffset(rows, focusRow, width);
|
|
1965
|
+
return Math.max(0, Math.min(top - anchor, maxScroll));
|
|
1966
|
+
}
|
|
1967
|
+
function hitRowIndex(y, top, titleLines, rows, scrollTop = 0, width = Number.POSITIVE_INFINITY) {
|
|
1968
|
+
const localY = y - top - 1 - titleLines + scrollTop;
|
|
1969
|
+
let offset = 0;
|
|
1970
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1971
|
+
const height = rowHeight(rows[i], width);
|
|
1972
|
+
if (localY >= offset && localY < offset + height) return i;
|
|
1973
|
+
offset += height;
|
|
1974
|
+
}
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
function selectBlock(contentWidth, value) {
|
|
1978
|
+
const blockWidth = Math.max(3, Math.floor(contentWidth * SELECT_BLOCK_WIDTH_RATIO));
|
|
1979
|
+
const innerWidth = Math.max(1, blockWidth - 6);
|
|
1980
|
+
const text = truncate(value || "(none)", innerWidth);
|
|
1981
|
+
const padding = innerWidth - textWidth(text);
|
|
1982
|
+
const fullPadding = blockWidth - textWidth(text);
|
|
1983
|
+
return {
|
|
1984
|
+
blockWidth,
|
|
1985
|
+
blockStart: contentWidth - blockWidth,
|
|
1986
|
+
text,
|
|
1987
|
+
leftPad: Math.floor(padding / 2),
|
|
1988
|
+
fullLeftPad: Math.floor(fullPadding / 2),
|
|
1989
|
+
cursorX: 3 + Math.floor(padding / 2) + textWidth(text)
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
function actionPositions(contentWidth, confirmLabel, cancelLabel) {
|
|
1993
|
+
const edge = Math.floor(contentWidth * ACTION_EDGE_RATIO);
|
|
1994
|
+
const confirmX = Math.min(edge, Math.max(0, contentWidth - textWidth(confirmLabel)));
|
|
1995
|
+
return {
|
|
1996
|
+
confirmX,
|
|
1997
|
+
cancelX: Math.max(confirmX + textWidth(confirmLabel), contentWidth - edge - textWidth(cancelLabel))
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
//#endregion
|
|
2001
|
+
//#region src/ui/widgets/header.tsx
|
|
2002
|
+
registerWidget("header", {
|
|
2003
|
+
selectable: false,
|
|
2004
|
+
height(item) {
|
|
2005
|
+
return item.leadingBlank === true ? 2 : 1;
|
|
2006
|
+
},
|
|
2007
|
+
paintWidth(item) {
|
|
2008
|
+
return textWidth(item.label);
|
|
2009
|
+
},
|
|
2010
|
+
onEnter() {
|
|
2011
|
+
return true;
|
|
2012
|
+
},
|
|
2013
|
+
render({ item, y, x, clip }) {
|
|
2014
|
+
const lead = item.leadingBlank === true && clip === 0 ? 1 : 0;
|
|
2015
|
+
const label = /* @__PURE__ */ jsx(SelectableText, {
|
|
2016
|
+
y: y + lead,
|
|
2017
|
+
col: x,
|
|
2018
|
+
text: item.label,
|
|
2019
|
+
color: COLORS.sectionHeader,
|
|
2020
|
+
bold: true
|
|
2021
|
+
});
|
|
2022
|
+
if (lead === 0) return label;
|
|
2023
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
2024
|
+
flexDirection: "column",
|
|
2025
|
+
children: [/* @__PURE__ */ jsx(Box, {
|
|
2026
|
+
height: 1,
|
|
2027
|
+
children: /* @__PURE__ */ jsx(Text, { children: " " })
|
|
2028
|
+
}), label]
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
}, { order: 500 });
|
|
2032
|
+
//#endregion
|
|
2033
|
+
//#region src/ui/widgets/actions.tsx
|
|
2034
|
+
function paddedLabel(label) {
|
|
2035
|
+
return ` ${label} `;
|
|
2036
|
+
}
|
|
2037
|
+
function hitActionZone(item, width, localX) {
|
|
2038
|
+
const positions = actionPositions(width, paddedLabel(item.confirmLabel), paddedLabel(item.cancelLabel));
|
|
2039
|
+
if (localX >= positions.confirmX && localX < positions.confirmX + textWidth(paddedLabel(item.confirmLabel))) return "confirm";
|
|
2040
|
+
if (localX >= positions.cancelX && localX < positions.cancelX + textWidth(paddedLabel(item.cancelLabel))) return "cancel";
|
|
2041
|
+
return null;
|
|
2042
|
+
}
|
|
2043
|
+
registerWidget("actions", {
|
|
2044
|
+
selectable: true,
|
|
2045
|
+
fullRowFocus: true,
|
|
2046
|
+
stops: () => 2,
|
|
2047
|
+
height() {
|
|
2048
|
+
return 1;
|
|
2049
|
+
},
|
|
2050
|
+
paintWidth() {
|
|
2051
|
+
return 0;
|
|
2052
|
+
},
|
|
2053
|
+
render({ item, focused, width, y, x, subCol }) {
|
|
2054
|
+
const confirmLabel = paddedLabel(item.confirmLabel);
|
|
2055
|
+
const cancelLabel = paddedLabel(item.cancelLabel);
|
|
2056
|
+
const positions = actionPositions(width, confirmLabel, cancelLabel);
|
|
2057
|
+
const mid = positions.cancelX - (positions.confirmX + textWidth(confirmLabel));
|
|
2058
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
2059
|
+
flexDirection: "row",
|
|
2060
|
+
children: [
|
|
2061
|
+
/* @__PURE__ */ jsx(Text, { children: " ".repeat(positions.confirmX) }),
|
|
2062
|
+
/* @__PURE__ */ jsx(SelectableText, {
|
|
2063
|
+
y,
|
|
2064
|
+
col: x + positions.confirmX,
|
|
2065
|
+
text: confirmLabel,
|
|
2066
|
+
inverse: focused && subCol === 0
|
|
2067
|
+
}),
|
|
2068
|
+
/* @__PURE__ */ jsx(Text, { children: " ".repeat(Math.max(0, mid)) }),
|
|
2069
|
+
/* @__PURE__ */ jsx(SelectableText, {
|
|
2070
|
+
y,
|
|
2071
|
+
col: x + positions.cancelX,
|
|
2072
|
+
text: cancelLabel,
|
|
2073
|
+
inverse: focused && subCol === 1
|
|
2074
|
+
})
|
|
2075
|
+
]
|
|
2076
|
+
});
|
|
2077
|
+
},
|
|
2078
|
+
onLeftRight(item, direction, api) {
|
|
2079
|
+
api.navigate(direction === 1 ? "right" : "left");
|
|
2080
|
+
return true;
|
|
2081
|
+
},
|
|
2082
|
+
onEnter(item, api) {
|
|
2083
|
+
if (api.subCol === 1) item.onCancel();
|
|
2084
|
+
else item.onConfirm();
|
|
2085
|
+
return true;
|
|
2086
|
+
},
|
|
2087
|
+
onClick(item, hit, actions) {
|
|
2088
|
+
if (hit.localY !== 0) {
|
|
2089
|
+
actions.focus(0);
|
|
2090
|
+
return true;
|
|
2091
|
+
}
|
|
2092
|
+
const zone = hitActionZone(item, hit.width, hit.localX);
|
|
2093
|
+
if (zone === "confirm") {
|
|
2094
|
+
actions.focus(0);
|
|
2095
|
+
item.onConfirm();
|
|
2096
|
+
return true;
|
|
2097
|
+
}
|
|
2098
|
+
if (zone === "cancel") {
|
|
2099
|
+
actions.focus(1);
|
|
2100
|
+
item.onCancel();
|
|
2101
|
+
return true;
|
|
2102
|
+
}
|
|
2103
|
+
actions.focus(0);
|
|
2104
|
+
return true;
|
|
2105
|
+
}
|
|
2106
|
+
}, { order: 500 });
|
|
2107
|
+
//#endregion
|
|
2108
|
+
//#region src/ui/dialog/sizes.ts
|
|
2109
|
+
const DIALOG_MAX_HEIGHT = .6;
|
|
2110
|
+
//#endregion
|
|
2111
|
+
//#region src/ui/widgets/input.tsx
|
|
2112
|
+
function scrollStart(lines, caretRow) {
|
|
2113
|
+
return Math.min(Math.max(caretRow - 2, 0), Math.max(0, lines - 3));
|
|
2114
|
+
}
|
|
2115
|
+
registerWidget("input", {
|
|
2116
|
+
selectable: true,
|
|
2117
|
+
editable: true,
|
|
2118
|
+
height(item, width) {
|
|
2119
|
+
return 2 + Math.min(wrapLines(item.value, width).length, 3);
|
|
2120
|
+
},
|
|
2121
|
+
paintWidth(item) {
|
|
2122
|
+
return textWidth(item.label);
|
|
2123
|
+
},
|
|
2124
|
+
caret(item, cursor, width) {
|
|
2125
|
+
const point = locToPoint(item.value, width, cursor);
|
|
2126
|
+
const lines = wrapLines(item.value, width).length;
|
|
2127
|
+
return {
|
|
2128
|
+
dy: 1 + point.row - scrollStart(lines, point.row),
|
|
2129
|
+
dx: point.col
|
|
2130
|
+
};
|
|
2131
|
+
},
|
|
2132
|
+
render({ item, focused, cursor, width, y, x, clip }) {
|
|
2133
|
+
const lines = wrapLines(item.value, width);
|
|
2134
|
+
const shown = Math.min(lines.length, 3);
|
|
2135
|
+
const point = cursor === void 0 ? null : locToPoint(item.value, width, cursor);
|
|
2136
|
+
const first = point === null ? 0 : scrollStart(lines.length, point.row);
|
|
2137
|
+
const parts = [];
|
|
2138
|
+
if (clip === 0) parts.push(/* @__PURE__ */ jsx(Box, {
|
|
2139
|
+
height: 1,
|
|
2140
|
+
children: /* @__PURE__ */ jsx(SelectableText, {
|
|
2141
|
+
y,
|
|
2142
|
+
col: x,
|
|
2143
|
+
text: item.label,
|
|
2144
|
+
color: focused ? void 0 : COLORS.dialogHintText
|
|
2145
|
+
})
|
|
2146
|
+
}, "label"));
|
|
2147
|
+
for (let index = 0; index < shown; index++) {
|
|
2148
|
+
if (1 + index < clip) continue;
|
|
2149
|
+
const currentY = y + 1 + index - clip;
|
|
2150
|
+
parts.push(/* @__PURE__ */ jsx(Box, {
|
|
2151
|
+
width,
|
|
2152
|
+
height: 1,
|
|
2153
|
+
backgroundColor: COLORS.dialogInputBackground,
|
|
2154
|
+
children: /* @__PURE__ */ jsx(SelectableText, {
|
|
2155
|
+
y: currentY,
|
|
2156
|
+
col: x,
|
|
2157
|
+
text: lines[first + index]
|
|
2158
|
+
})
|
|
2159
|
+
}, index));
|
|
2160
|
+
}
|
|
2161
|
+
parts.push(/* @__PURE__ */ jsx(Box, {
|
|
2162
|
+
height: 1,
|
|
2163
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
2164
|
+
color: focused && cursor !== void 0 ? caretNonceColor(cursor) : void 0,
|
|
2165
|
+
bold: focused && cursor !== void 0 ? caretNonceBold(cursor) : void 0,
|
|
2166
|
+
children: focused && cursor !== void 0 ? caretNonceText(cursor) : " "
|
|
2167
|
+
})
|
|
2168
|
+
}, "pad"));
|
|
2169
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
2170
|
+
flexDirection: "column",
|
|
2171
|
+
children: parts
|
|
2172
|
+
});
|
|
2173
|
+
},
|
|
2174
|
+
onLeftRight(item, direction, api) {
|
|
2175
|
+
if (direction === -1 && api.cursor > 0) api.setCursor(api.cursor - 1);
|
|
2176
|
+
if (direction === 1 && api.cursor < item.value.length) api.setCursor(api.cursor + 1);
|
|
2177
|
+
return true;
|
|
2178
|
+
},
|
|
2179
|
+
onEnter(item) {
|
|
2180
|
+
if (item.onEnter !== void 0) {
|
|
2181
|
+
item.onEnter();
|
|
2182
|
+
return true;
|
|
2183
|
+
}
|
|
2184
|
+
return false;
|
|
2185
|
+
}
|
|
2186
|
+
}, { order: 500 });
|
|
2187
|
+
//#endregion
|
|
2188
|
+
//#region src/ui/widgets/select.tsx
|
|
2189
|
+
function carouselHit(item, width, localX) {
|
|
2190
|
+
if (item.options.length <= 1) return null;
|
|
2191
|
+
const block = selectBlock(width, item.value);
|
|
2192
|
+
if (localX >= block.blockStart && localX < block.blockStart + 3) return -1;
|
|
2193
|
+
if (localX >= block.blockStart + block.blockWidth - 3 && localX < block.blockStart + block.blockWidth) return 1;
|
|
2194
|
+
return null;
|
|
2195
|
+
}
|
|
2196
|
+
function paintSelect(paint) {
|
|
2197
|
+
const { item, width, y, x } = paint;
|
|
2198
|
+
const block = selectBlock(width, item.value);
|
|
2199
|
+
const hasArrows = item.options.length > 1;
|
|
2200
|
+
const pad = hasArrows ? block.leftPad : block.fullLeftPad;
|
|
2201
|
+
const inner = block.blockWidth - (hasArrows ? 6 : 0);
|
|
2202
|
+
const valueCol = x + block.blockStart + (hasArrows ? 3 : 0);
|
|
2203
|
+
const valueText = " ".repeat(pad) + block.text + " ".repeat(Math.max(0, inner - pad - textWidth(block.text)));
|
|
2204
|
+
const carousel = /* @__PURE__ */ jsxs(Box, {
|
|
2205
|
+
width,
|
|
2206
|
+
justifyContent: "space-between",
|
|
2207
|
+
children: [/* @__PURE__ */ jsx(SelectableText, {
|
|
2208
|
+
y,
|
|
2209
|
+
col: x,
|
|
2210
|
+
text: `${item.label}:`
|
|
2211
|
+
}), /* @__PURE__ */ jsxs(Box, {
|
|
2212
|
+
width: block.blockWidth,
|
|
2213
|
+
flexDirection: "row",
|
|
2214
|
+
children: [
|
|
2215
|
+
hasArrows && /* @__PURE__ */ jsx(Text, {
|
|
2216
|
+
color: COLORS.ink,
|
|
2217
|
+
backgroundColor: paint.pressed === "left" ? COLORS.carouselButtonPressedBg : COLORS.carouselButtonBg,
|
|
2218
|
+
children: ` ${glyphs.carouselLeft} `
|
|
2219
|
+
}),
|
|
2220
|
+
/* @__PURE__ */ jsx(SelectableText, {
|
|
2221
|
+
y,
|
|
2222
|
+
col: valueCol,
|
|
2223
|
+
text: valueText,
|
|
2224
|
+
color: paint.focused ? COLORS.carouselSelectedText : COLORS.ink,
|
|
2225
|
+
backgroundColor: COLORS.carouselCurrentBg
|
|
2226
|
+
}),
|
|
2227
|
+
hasArrows && /* @__PURE__ */ jsx(Text, {
|
|
2228
|
+
color: COLORS.ink,
|
|
2229
|
+
backgroundColor: paint.pressed === "right" ? COLORS.carouselButtonPressedBg : COLORS.carouselButtonBg,
|
|
2230
|
+
children: ` ${glyphs.carouselRight} `
|
|
2231
|
+
})
|
|
2232
|
+
]
|
|
2233
|
+
})]
|
|
2234
|
+
});
|
|
2235
|
+
if (!item.spaced) return carousel;
|
|
2236
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
2237
|
+
flexDirection: "column",
|
|
2238
|
+
children: [paint.clip === 0 && carousel, /* @__PURE__ */ jsx(Box, {
|
|
2239
|
+
height: 1,
|
|
2240
|
+
children: /* @__PURE__ */ jsx(Text, { children: " " })
|
|
2241
|
+
})]
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
function cycle(item, direction) {
|
|
2245
|
+
const index = Math.max(0, item.options.indexOf(item.value));
|
|
2246
|
+
const length = item.options.length;
|
|
2247
|
+
const next = item.options[(index + direction + length) % length];
|
|
2248
|
+
if (next !== void 0) item.onChange(next);
|
|
2249
|
+
}
|
|
2250
|
+
registerWidget("select", {
|
|
2251
|
+
selectable: true,
|
|
2252
|
+
height(item) {
|
|
2253
|
+
return item.spaced === true ? 2 : 1;
|
|
2254
|
+
},
|
|
2255
|
+
paintWidth(item) {
|
|
2256
|
+
return textWidth(item.label);
|
|
2257
|
+
},
|
|
2258
|
+
render: paintSelect,
|
|
2259
|
+
onLeftRight(item, direction) {
|
|
2260
|
+
if (item.options.length <= 1) return false;
|
|
2261
|
+
cycle(item, direction);
|
|
2262
|
+
return true;
|
|
2263
|
+
},
|
|
2264
|
+
onEnter(item, api) {
|
|
2265
|
+
if (item.onEnter !== void 0) {
|
|
2266
|
+
item.onEnter();
|
|
2267
|
+
return true;
|
|
2268
|
+
}
|
|
2269
|
+
api.navigate("down");
|
|
2270
|
+
return true;
|
|
2271
|
+
},
|
|
2272
|
+
onClick(item, hit, actions) {
|
|
2273
|
+
if (hit.localY !== 0) return false;
|
|
2274
|
+
const direction = carouselHit(item, hit.width, hit.localX);
|
|
2275
|
+
if (direction === null) return false;
|
|
2276
|
+
cycle(item, direction);
|
|
2277
|
+
actions.flash(direction === -1 ? "left" : "right");
|
|
2278
|
+
return true;
|
|
2279
|
+
}
|
|
2280
|
+
}, { order: 500 });
|
|
2281
|
+
//#endregion
|
|
2282
|
+
//#region src/ui/widgets/button.tsx
|
|
2283
|
+
function paintButton(item, paint) {
|
|
2284
|
+
const { width, y, x, focused } = paint;
|
|
2285
|
+
if (item.right !== void 0) {
|
|
2286
|
+
const right = truncate(item.right, Math.floor(width / 2));
|
|
2287
|
+
const leftWidth = Math.max(1, width - textWidth(right));
|
|
2288
|
+
const label = padToWidth(truncate(item.label, leftWidth), leftWidth);
|
|
2289
|
+
if (focused) return /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(SelectableText, {
|
|
2290
|
+
y,
|
|
2291
|
+
col: x,
|
|
2292
|
+
text: label,
|
|
2293
|
+
inverse: true,
|
|
2294
|
+
color: item.rightColor
|
|
2295
|
+
}), /* @__PURE__ */ jsx(SelectableText, {
|
|
2296
|
+
y,
|
|
2297
|
+
col: x + leftWidth,
|
|
2298
|
+
text: right,
|
|
2299
|
+
inverse: true,
|
|
2300
|
+
color: item.rightColor
|
|
2301
|
+
})] });
|
|
2302
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
2303
|
+
width,
|
|
2304
|
+
justifyContent: "space-between",
|
|
2305
|
+
children: [/* @__PURE__ */ jsx(SelectableText, {
|
|
2306
|
+
y,
|
|
2307
|
+
col: x,
|
|
2308
|
+
text: item.label
|
|
2309
|
+
}), /* @__PURE__ */ jsx(SelectableText, {
|
|
2310
|
+
y,
|
|
2311
|
+
col: x + width - textWidth(right),
|
|
2312
|
+
text: right,
|
|
2313
|
+
color: item.rightColor ?? COLORS.dialogHintText
|
|
2314
|
+
})]
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
if (focused) return /* @__PURE__ */ jsx(SelectableText, {
|
|
2318
|
+
y,
|
|
2319
|
+
col: x,
|
|
2320
|
+
text: padToWidth(truncate(item.label, width), width),
|
|
2321
|
+
inverse: true
|
|
2322
|
+
});
|
|
2323
|
+
return /* @__PURE__ */ jsx(SelectableText, {
|
|
2324
|
+
y,
|
|
2325
|
+
col: x,
|
|
2326
|
+
text: item.label
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
registerWidget("button", {
|
|
2330
|
+
selectable: true,
|
|
2331
|
+
height() {
|
|
2332
|
+
return 1;
|
|
2333
|
+
},
|
|
2334
|
+
paintWidth(item) {
|
|
2335
|
+
return textWidth(item.label + (item.right ?? ""));
|
|
2336
|
+
},
|
|
2337
|
+
searchTexts(item, searchRight) {
|
|
2338
|
+
return searchRight && item.right !== void 0 ? [item.label, item.right] : [item.label];
|
|
2339
|
+
},
|
|
2340
|
+
render(paint) {
|
|
2341
|
+
return paintButton(paint.item, paint);
|
|
2342
|
+
},
|
|
2343
|
+
onEnter(item) {
|
|
2344
|
+
item.onPress();
|
|
2345
|
+
return true;
|
|
2346
|
+
},
|
|
2347
|
+
activate(item) {
|
|
2348
|
+
item.onPress();
|
|
2349
|
+
}
|
|
2350
|
+
}, { order: 500 });
|
|
2351
|
+
//#endregion
|
|
2352
|
+
//#region src/ui/widgets/checkbox.tsx
|
|
2353
|
+
function paintCheckbox(item, paint) {
|
|
2354
|
+
const { width, y, x, focused } = paint;
|
|
2355
|
+
const label = padToWidth(truncate(item.label, width - 2), width - 2);
|
|
2356
|
+
const check = item.checked ? ` ${glyphs.tick}` : " ";
|
|
2357
|
+
if (focused) return /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(SelectableText, {
|
|
2358
|
+
y,
|
|
2359
|
+
col: x,
|
|
2360
|
+
text: label,
|
|
2361
|
+
inverse: true
|
|
2362
|
+
}), /* @__PURE__ */ jsx(SelectableText, {
|
|
2363
|
+
y,
|
|
2364
|
+
col: x + width - 2,
|
|
2365
|
+
text: check,
|
|
2366
|
+
inverse: true
|
|
2367
|
+
})] });
|
|
2368
|
+
return /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(SelectableText, {
|
|
2369
|
+
y,
|
|
2370
|
+
col: x,
|
|
2371
|
+
text: label
|
|
2372
|
+
}), item.checked && /* @__PURE__ */ jsx(SelectableText, {
|
|
2373
|
+
y,
|
|
2374
|
+
col: x + width - 2,
|
|
2375
|
+
text: ` ${glyphs.tick}`,
|
|
2376
|
+
color: COLORS.success
|
|
2377
|
+
})] });
|
|
2378
|
+
}
|
|
2379
|
+
registerWidget("checkbox", {
|
|
2380
|
+
selectable: true,
|
|
2381
|
+
height() {
|
|
2382
|
+
return 1;
|
|
2383
|
+
},
|
|
2384
|
+
paintWidth(item) {
|
|
2385
|
+
return textWidth(item.label);
|
|
2386
|
+
},
|
|
2387
|
+
searchTexts(item) {
|
|
2388
|
+
return [item.label];
|
|
2389
|
+
},
|
|
2390
|
+
render(paint) {
|
|
2391
|
+
return paintCheckbox(paint.item, paint);
|
|
2392
|
+
},
|
|
2393
|
+
onEnter(item) {
|
|
2394
|
+
item.onConfirm();
|
|
2395
|
+
return true;
|
|
2396
|
+
},
|
|
2397
|
+
onSpace(item) {
|
|
2398
|
+
item.onToggle();
|
|
2399
|
+
},
|
|
2400
|
+
activate(item) {
|
|
2401
|
+
item.onConfirm();
|
|
2402
|
+
}
|
|
2403
|
+
}, { order: 500 });
|
|
2404
|
+
//#endregion
|
|
2405
|
+
//#region src/ui/widgets/search.tsx
|
|
2406
|
+
function searchTextOf(item) {
|
|
2407
|
+
return item.value === "" ? "Search" : item.value;
|
|
2408
|
+
}
|
|
2409
|
+
registerWidget("search", {
|
|
2410
|
+
selectable: true,
|
|
2411
|
+
editable: true,
|
|
2412
|
+
height() {
|
|
2413
|
+
return 2;
|
|
2414
|
+
},
|
|
2415
|
+
paintWidth(item) {
|
|
2416
|
+
return textWidth(searchTextOf(item));
|
|
2417
|
+
},
|
|
2418
|
+
onLeftRight(item, direction, api) {
|
|
2419
|
+
const next = Math.max(0, Math.min(item.value.length, api.cursor + direction));
|
|
2420
|
+
if (next === api.cursor) return false;
|
|
2421
|
+
api.setCursor(next);
|
|
2422
|
+
return true;
|
|
2423
|
+
},
|
|
2424
|
+
render({ item, cursor, width, y, x, clip }) {
|
|
2425
|
+
const isEmpty = item.value === "";
|
|
2426
|
+
const start = caretScrollStart(item.value, cursor ?? item.value.length, width);
|
|
2427
|
+
const text = isEmpty ? "Search" : truncate(item.value.slice(start), width);
|
|
2428
|
+
if (clip !== 0) return /* @__PURE__ */ jsx(Box, {
|
|
2429
|
+
flexDirection: "column",
|
|
2430
|
+
children: /* @__PURE__ */ jsx(Box, {
|
|
2431
|
+
height: 1,
|
|
2432
|
+
children: /* @__PURE__ */ jsx(Text, { children: " " })
|
|
2433
|
+
})
|
|
2434
|
+
});
|
|
2435
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
2436
|
+
flexDirection: "column",
|
|
2437
|
+
children: [/* @__PURE__ */ jsx(Box, {
|
|
2438
|
+
width,
|
|
2439
|
+
backgroundColor: COLORS.dialogInputBackground,
|
|
2440
|
+
children: /* @__PURE__ */ jsx(SelectableText, {
|
|
2441
|
+
y,
|
|
2442
|
+
col: x,
|
|
2443
|
+
text: padToWidth(text, width),
|
|
2444
|
+
color: isEmpty ? COLORS.dialogHintText : void 0
|
|
2445
|
+
})
|
|
2446
|
+
}), /* @__PURE__ */ jsx(Box, {
|
|
2447
|
+
height: 1,
|
|
2448
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
2449
|
+
color: cursor !== void 0 ? caretNonceColor(cursor) : void 0,
|
|
2450
|
+
bold: cursor !== void 0 ? caretNonceBold(cursor) : void 0,
|
|
2451
|
+
children: cursor !== void 0 ? caretNonceText(cursor) : " "
|
|
2452
|
+
})
|
|
2453
|
+
})]
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
}, { order: 500 });
|
|
2457
|
+
//#endregion
|
|
2458
|
+
//#region src/ui/widgets/static.tsx
|
|
2459
|
+
registerWidget("static", {
|
|
2460
|
+
selectable: false,
|
|
2461
|
+
height(item, width) {
|
|
2462
|
+
return Math.max(1, wrapLines(item.label, Math.max(4, width)).length);
|
|
2463
|
+
},
|
|
2464
|
+
paintWidth(item) {
|
|
2465
|
+
return textWidth(item.label);
|
|
2466
|
+
},
|
|
2467
|
+
render({ item, width, y, x }) {
|
|
2468
|
+
const lines = item.label === "" ? [""] : wrapLines(item.label, Math.max(4, width));
|
|
2469
|
+
if (lines.length === 1) return /* @__PURE__ */ jsx(SelectableText, {
|
|
2470
|
+
y,
|
|
2471
|
+
col: x,
|
|
2472
|
+
text: item.label === "" ? " " : item.label,
|
|
2473
|
+
color: item.label === "" ? void 0 : COLORS.toolBodyText
|
|
2474
|
+
});
|
|
2475
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
2476
|
+
flexDirection: "column",
|
|
2477
|
+
children: lines.map((line, index) => /* @__PURE__ */ jsx(SelectableText, {
|
|
2478
|
+
y: y + index,
|
|
2479
|
+
col: x,
|
|
2480
|
+
text: line === "" ? " " : line,
|
|
2481
|
+
color: line === "" ? void 0 : COLORS.toolBodyText
|
|
2482
|
+
}, index))
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
}, { order: 500 });
|
|
2486
|
+
//#endregion
|
|
2487
|
+
//#region src/ui/dialog/dialog-item.tsx
|
|
2488
|
+
function renderRow(row, focused, contentWidth, baseY, left, pressed = null, subCol = 0, clip = 0, cursor) {
|
|
2489
|
+
let col = left;
|
|
2490
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
2491
|
+
flexDirection: "row",
|
|
2492
|
+
children: row.items.map((item, index) => {
|
|
2493
|
+
const def = widgetOf(item.type);
|
|
2494
|
+
const next = def.render({
|
|
2495
|
+
item,
|
|
2496
|
+
focused,
|
|
2497
|
+
width: contentWidth,
|
|
2498
|
+
y: baseY,
|
|
2499
|
+
x: col,
|
|
2500
|
+
pressed,
|
|
2501
|
+
subCol,
|
|
2502
|
+
clip,
|
|
2503
|
+
cursor: focused ? cursor : void 0
|
|
2504
|
+
});
|
|
2505
|
+
col += def.paintWidth(item) + 1;
|
|
2506
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
2507
|
+
flexDirection: "row",
|
|
2508
|
+
children: [index > 0 && /* @__PURE__ */ jsx(Text, { children: " " }), next]
|
|
2509
|
+
}, index);
|
|
2510
|
+
})
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
//#endregion
|
|
2514
|
+
//#region src/ui/dialog/use-dialog-input.ts
|
|
2515
|
+
function applyNavigation(live, direction, search, setters, centerScroll = false) {
|
|
2516
|
+
const next = moveFocus(live.rows, live.focus, direction);
|
|
2517
|
+
const clamped = clampFocus(live.rows, next, live.focusMinRow, live.focusMaxRow);
|
|
2518
|
+
setters.setFocus(clamped);
|
|
2519
|
+
const contentRow = Math.max(0, clamped.row - (search ? 1 : 0));
|
|
2520
|
+
setters.setScrollTop(centerScroll && (direction === "up" || direction === "down") ? listCenterScroll(live.contentRows, contentRow, live.viewportHeight, live.contentWidth, direction) : adjustScroll(live.contentRows, {
|
|
2521
|
+
row: contentRow,
|
|
2522
|
+
col: clamped.col
|
|
2523
|
+
}, live.scrollTop, live.viewportHeight, live.contentWidth));
|
|
2524
|
+
}
|
|
2525
|
+
function arrowFromRaw(input) {
|
|
2526
|
+
const match = /^(?:\[|O)(?:1;)?\d*(?::\d+)?([ABCD])$/.exec(input);
|
|
2527
|
+
if (match === null) return null;
|
|
2528
|
+
const letter = match[1];
|
|
2529
|
+
return letter === "A" ? "up" : letter === "B" ? "down" : letter === "C" ? "right" : "left";
|
|
2530
|
+
}
|
|
2531
|
+
function useDialogInput(options) {
|
|
2532
|
+
const { live, search, closeGuarded, onClose, onCtrlA, onCtrlD, onCtrlE, onActivity, requestSearch, setFocus, setScrollTop, setCursor } = options;
|
|
2533
|
+
const setters = {
|
|
2534
|
+
setFocus,
|
|
2535
|
+
setScrollTop
|
|
2536
|
+
};
|
|
2537
|
+
const centerScroll = options.centerScroll === true;
|
|
2538
|
+
usePaste((text) => {
|
|
2539
|
+
onActivity?.();
|
|
2540
|
+
const state = live.current;
|
|
2541
|
+
const current = focusedItem(state.rows[state.focus.row], state.focus.col);
|
|
2542
|
+
const textItem = current === void 0 ? null : asTextItem(current);
|
|
2543
|
+
if (textItem === null) return;
|
|
2544
|
+
const normalized = text.replace(/\r\n?/g, "\n");
|
|
2545
|
+
if (normalized === "") return;
|
|
2546
|
+
const next = editInsert({
|
|
2547
|
+
value: textItem.value,
|
|
2548
|
+
cursor: state.cursor
|
|
2549
|
+
}, normalized);
|
|
2550
|
+
textItem.onChange(next.value);
|
|
2551
|
+
state.value = next.value;
|
|
2552
|
+
state.cursor = next.cursor;
|
|
2553
|
+
setCursor(next.cursor);
|
|
2554
|
+
});
|
|
2555
|
+
useInput((input, key) => {
|
|
2556
|
+
if (isKeyConsumed()) return;
|
|
2557
|
+
const state = live.current;
|
|
2558
|
+
const liveCurrent = focusedItem(state.rows[state.focus.row], state.focus.col);
|
|
2559
|
+
const rawArrow = arrowFromRaw(input);
|
|
2560
|
+
const isUp = key.upArrow || rawArrow === "up";
|
|
2561
|
+
const isDown = key.downArrow || rawArrow === "down";
|
|
2562
|
+
const isLeft = key.leftArrow || rawArrow === "left";
|
|
2563
|
+
const isRight = key.rightArrow || rawArrow === "right";
|
|
2564
|
+
if (key.ctrl && input === "a" && onCtrlA !== void 0 && onCtrlA(liveCurrent)) return;
|
|
2565
|
+
if (key.ctrl && input === "d" && onCtrlD !== void 0 && onCtrlD(liveCurrent)) return;
|
|
2566
|
+
if (key.ctrl && input === "e" && onCtrlE !== void 0 && onCtrlE(liveCurrent)) return;
|
|
2567
|
+
onActivity?.();
|
|
2568
|
+
if (key.escape || key.ctrl && input === "c") {
|
|
2569
|
+
if (!(key.ctrl && closeGuarded)) onClose();
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
const def = liveCurrent === void 0 ? void 0 : widgetOf(liveCurrent.type);
|
|
2573
|
+
const keyApi = {
|
|
2574
|
+
cursor: state.cursor,
|
|
2575
|
+
subCol: state.focus.col,
|
|
2576
|
+
setCursor(next) {
|
|
2577
|
+
state.cursor = next;
|
|
2578
|
+
setCursor(next);
|
|
2579
|
+
},
|
|
2580
|
+
navigate(direction) {
|
|
2581
|
+
applyNavigation(state, direction, search, setters, centerScroll);
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
if ((isLeft || isRight) && liveCurrent !== void 0 && def !== void 0) {
|
|
2585
|
+
if (def.onLeftRight?.(liveCurrent, isRight ? 1 : -1, keyApi) === true) return;
|
|
2586
|
+
}
|
|
2587
|
+
if (isUp || isDown) {
|
|
2588
|
+
applyNavigation(state, isUp ? "up" : "down", search, setters, centerScroll);
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
if (key.return) {
|
|
2592
|
+
if (liveCurrent === void 0 || def === void 0) return;
|
|
2593
|
+
if (!(def.onEnter?.(liveCurrent, keyApi) ?? false)) applyNavigation(state, "down", search, setters, centerScroll);
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
if (input === " " && liveCurrent !== void 0 && def?.onSpace !== void 0) {
|
|
2597
|
+
def.onSpace(liveCurrent);
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
const editingFormInput = liveCurrent !== void 0 && liveCurrent.type !== "search" && widgetOf(liveCurrent.type).editable === true;
|
|
2601
|
+
if (search && !editingFormInput) {
|
|
2602
|
+
const v = state.searchValue;
|
|
2603
|
+
const caret = state.focus.row === 0 ? Math.max(0, Math.min(state.cursor, v.length)) : v.length;
|
|
2604
|
+
if (isLeft || isRight) {
|
|
2605
|
+
const next = Math.max(0, Math.min(v.length, caret + (isRight ? 1 : -1)));
|
|
2606
|
+
if (next !== caret) {
|
|
2607
|
+
state.cursor = next;
|
|
2608
|
+
setCursor(next);
|
|
2609
|
+
}
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
const applyEdit = (next) => {
|
|
2613
|
+
if (next === null) return false;
|
|
2614
|
+
requestSearch(next.value);
|
|
2615
|
+
state.cursor = next.cursor;
|
|
2616
|
+
setCursor(next.cursor);
|
|
2617
|
+
return true;
|
|
2618
|
+
};
|
|
2619
|
+
if (key.backspace) {
|
|
2620
|
+
applyEdit(editBackspace({
|
|
2621
|
+
value: v,
|
|
2622
|
+
cursor: caret
|
|
2623
|
+
}));
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
if (key.delete) {
|
|
2627
|
+
applyEdit(editDelete({
|
|
2628
|
+
value: v,
|
|
2629
|
+
cursor: caret
|
|
2630
|
+
}));
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
if (input && !key.ctrl && !key.meta && !isMouseResidue(input)) {
|
|
2634
|
+
applyEdit(editInsert({
|
|
2635
|
+
value: v,
|
|
2636
|
+
cursor: caret
|
|
2637
|
+
}, input));
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
if (def?.editable === true && liveCurrent !== void 0) {
|
|
2642
|
+
const textItem = asTextItem(liveCurrent);
|
|
2643
|
+
if (textItem === null) return;
|
|
2644
|
+
const apply = (next, trackCursor) => {
|
|
2645
|
+
if (next === null) return false;
|
|
2646
|
+
state.value = next.value;
|
|
2647
|
+
textItem.onChange(next.value);
|
|
2648
|
+
if (trackCursor) {
|
|
2649
|
+
state.cursor = next.cursor;
|
|
2650
|
+
setCursor(next.cursor);
|
|
2651
|
+
}
|
|
2652
|
+
return true;
|
|
2653
|
+
};
|
|
2654
|
+
if (key.backspace) apply(editBackspace({
|
|
2655
|
+
value: state.value,
|
|
2656
|
+
cursor: state.cursor
|
|
2657
|
+
}), true);
|
|
2658
|
+
else if (key.delete) apply(editDelete({
|
|
2659
|
+
value: state.value,
|
|
2660
|
+
cursor: state.cursor
|
|
2661
|
+
}), false);
|
|
2662
|
+
else if (input && !key.ctrl && !key.meta && !isMouseResidue(input)) apply(editInsert({
|
|
2663
|
+
value: state.value,
|
|
2664
|
+
cursor: state.cursor
|
|
2665
|
+
}, input), true);
|
|
2666
|
+
}
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
//#endregion
|
|
2670
|
+
//#region src/ui/dialog/dialog.tsx
|
|
2671
|
+
const CloseGuardContext = createContext(false);
|
|
2672
|
+
function Dialog({ width, maxHeight, title, rows, footer, errors, onClose, search = false, searchRight = false, centerScroll = false, onCtrlA, onCtrlD, onCtrlE, onActivity, ref }) {
|
|
2673
|
+
const { stdout } = useStdout();
|
|
2674
|
+
const { setCursorPosition } = useCaret();
|
|
2675
|
+
const closeGuarded = useContext(CloseGuardContext);
|
|
2676
|
+
const columns = stdout?.columns ?? 80;
|
|
2677
|
+
const totalRows = stdout?.rows ?? 24;
|
|
2678
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
2679
|
+
const [cursor, setCursor] = useState(0);
|
|
2680
|
+
const cursorSyncRef = useRef({
|
|
2681
|
+
row: -1,
|
|
2682
|
+
col: -1,
|
|
2683
|
+
type: void 0
|
|
2684
|
+
});
|
|
2685
|
+
const [searchValue, setSearchValue] = useState("");
|
|
2686
|
+
const [carouselPress, setCarouselPress] = useState(null);
|
|
2687
|
+
const carouselPressTimer = useRef(void 0);
|
|
2688
|
+
const [errorScrollTop, setErrorScrollTop] = useState(0);
|
|
2689
|
+
const handleSearch = (value) => {
|
|
2690
|
+
setSearchValue(value);
|
|
2691
|
+
setScrollTop(0);
|
|
2692
|
+
};
|
|
2693
|
+
const searchRow = { items: [{
|
|
2694
|
+
type: "search",
|
|
2695
|
+
value: searchValue,
|
|
2696
|
+
onChange: handleSearch
|
|
2697
|
+
}] };
|
|
2698
|
+
const filteredRows = search ? filterRowsWithHeaders(rows, searchValue, searchRight) : rows;
|
|
2699
|
+
const displayRows = search ? [searchRow, ...filteredRows] : rows;
|
|
2700
|
+
const contentRows = search ? filteredRows : rows;
|
|
2701
|
+
const [focus, setFocus] = useState(() => ({
|
|
2702
|
+
row: snapRow(search ? [searchRow, ...rows] : rows, search ? 1 : 0),
|
|
2703
|
+
col: 0
|
|
2704
|
+
}));
|
|
2705
|
+
const widthCells = width <= 1 ? Math.floor(columns * width) : width;
|
|
2706
|
+
const windowWidth = Math.min(Math.max(1, widthCells), columns);
|
|
2707
|
+
const contentWidth = Math.max(1, windowWidth - 4);
|
|
2708
|
+
const fixedHeight = search ? rowHeight(searchRow, contentWidth) : 0;
|
|
2709
|
+
const focusMinRow = search ? 1 : 0;
|
|
2710
|
+
const focusMaxRow = Math.max(0, displayRows.length - 1);
|
|
2711
|
+
useEffect(() => {
|
|
2712
|
+
setFocus((current) => {
|
|
2713
|
+
const next = clampFocus(displayRows, current, focusMinRow, focusMaxRow);
|
|
2714
|
+
return next.row === current.row && next.col === current.col ? current : next;
|
|
2715
|
+
});
|
|
2716
|
+
}, [
|
|
2717
|
+
displayRows,
|
|
2718
|
+
focusMinRow,
|
|
2719
|
+
focusMaxRow
|
|
2720
|
+
]);
|
|
2721
|
+
const errorKey = (errors ?? []).map((line) => line.text).join("\n");
|
|
2722
|
+
useEffect(() => {
|
|
2723
|
+
setErrorScrollTop(0);
|
|
2724
|
+
}, [errorKey]);
|
|
2725
|
+
const titleLines = title === void 0 ? 0 : 2;
|
|
2726
|
+
const footerRows = wrapFooter(footer, contentWidth);
|
|
2727
|
+
const extraHeight = footerRows.length > 0 ? 1 + footerRows.length : 0;
|
|
2728
|
+
const desired = displayRows.reduce((sum, row) => sum + rowHeight(row, contentWidth), 0) + titleLines + extraHeight + 2;
|
|
2729
|
+
const maxRows = Math.max(1, maxHeight <= 1 ? Math.floor(totalRows * maxHeight) : maxHeight);
|
|
2730
|
+
const windowHeight = Math.min(desired, maxRows, totalRows);
|
|
2731
|
+
const contentHeight = Math.max(1, windowHeight - 2 - titleLines - extraHeight);
|
|
2732
|
+
const viewportHeight = Math.max(1, contentHeight - fixedHeight);
|
|
2733
|
+
const top = Math.max(0, Math.floor((totalRows - windowHeight) / 2));
|
|
2734
|
+
const left = Math.max(0, Math.floor((columns - windowWidth) / 2));
|
|
2735
|
+
const frameLeft = 2;
|
|
2736
|
+
const errorRows = wrapStatusLines(errors, contentWidth);
|
|
2737
|
+
const availableBelow = Math.max(0, totalRows - (top + windowHeight));
|
|
2738
|
+
const errorVisible = Math.max(0, Math.min(3, errorRows.length, availableBelow - 1));
|
|
2739
|
+
const maxErrorScroll = Math.max(0, errorRows.length - errorVisible);
|
|
2740
|
+
const errorScroll = Math.min(errorScrollTop, maxErrorScroll);
|
|
2741
|
+
const headerBottom = 1 + titleLines;
|
|
2742
|
+
const contentTop = headerBottom + fixedHeight;
|
|
2743
|
+
const safeFocus = {
|
|
2744
|
+
row: Math.min(Math.max(focus.row, 0), focusMaxRow),
|
|
2745
|
+
col: Math.min(Math.max(focus.col, 0), selectableSpan(displayRows[focus.row]))
|
|
2746
|
+
};
|
|
2747
|
+
const contentRowsHeight = contentRows.reduce((sum, row) => sum + rowHeight(row, contentWidth), 0);
|
|
2748
|
+
const maxScroll = Math.max(0, contentRowsHeight - viewportHeight);
|
|
2749
|
+
const scroll = adjustScroll(contentRows, {
|
|
2750
|
+
row: Math.max(0, safeFocus.row - (search ? 1 : 0)),
|
|
2751
|
+
col: 0
|
|
2752
|
+
}, Math.min(scrollTop, maxScroll), viewportHeight, contentWidth);
|
|
2753
|
+
const live = useRef({
|
|
2754
|
+
rows: displayRows,
|
|
2755
|
+
contentRows,
|
|
2756
|
+
focus,
|
|
2757
|
+
scrollTop: scroll,
|
|
2758
|
+
cursor,
|
|
2759
|
+
value: "",
|
|
2760
|
+
searchValue,
|
|
2761
|
+
viewportHeight,
|
|
2762
|
+
focusMinRow,
|
|
2763
|
+
focusMaxRow,
|
|
2764
|
+
contentWidth,
|
|
2765
|
+
top,
|
|
2766
|
+
windowHeight
|
|
2767
|
+
});
|
|
2768
|
+
live.current.rows = displayRows;
|
|
2769
|
+
live.current.contentRows = contentRows;
|
|
2770
|
+
live.current.focus = focus;
|
|
2771
|
+
live.current.scrollTop = scroll;
|
|
2772
|
+
live.current.searchValue = searchValue;
|
|
2773
|
+
live.current.viewportHeight = viewportHeight;
|
|
2774
|
+
live.current.focusMinRow = focusMinRow;
|
|
2775
|
+
live.current.focusMaxRow = focusMaxRow;
|
|
2776
|
+
live.current.contentWidth = contentWidth;
|
|
2777
|
+
live.current.top = top;
|
|
2778
|
+
live.current.windowHeight = windowHeight;
|
|
2779
|
+
const errorLive = useRef({
|
|
2780
|
+
top: 0,
|
|
2781
|
+
height: 0,
|
|
2782
|
+
maxScroll: 0
|
|
2783
|
+
});
|
|
2784
|
+
errorLive.current = {
|
|
2785
|
+
top: top + windowHeight,
|
|
2786
|
+
height: errorVisible + 1,
|
|
2787
|
+
maxScroll: maxErrorScroll
|
|
2788
|
+
};
|
|
2789
|
+
const current = focusedItem(displayRows[safeFocus.row], safeFocus.col);
|
|
2790
|
+
const currentText = current === void 0 ? null : asTextItem(current);
|
|
2791
|
+
live.current.value = currentText?.value ?? "";
|
|
2792
|
+
const effectiveCursor = cursorSyncRef.current.row === safeFocus.row && cursorSyncRef.current.col === safeFocus.col && cursorSyncRef.current.type === current?.type || currentText === null || currentText.type === "search" ? cursor : currentText.value.length;
|
|
2793
|
+
live.current.cursor = effectiveCursor;
|
|
2794
|
+
useEffect(() => {
|
|
2795
|
+
cursorSyncRef.current = {
|
|
2796
|
+
row: safeFocus.row,
|
|
2797
|
+
col: safeFocus.col,
|
|
2798
|
+
type: current?.type
|
|
2799
|
+
};
|
|
2800
|
+
if (currentText !== null && currentText.type !== "search") setCursor(currentText.value.length);
|
|
2801
|
+
}, [
|
|
2802
|
+
current?.type,
|
|
2803
|
+
safeFocus.row,
|
|
2804
|
+
safeFocus.col,
|
|
2805
|
+
displayRows.length
|
|
2806
|
+
]);
|
|
2807
|
+
const editingFormInput = current !== void 0 && current.type !== "search" && asTextItem(current) !== null;
|
|
2808
|
+
if (search && !editingFormInput) {
|
|
2809
|
+
const caret = Math.min(Math.max(0, cursor), searchValue.length);
|
|
2810
|
+
const start = caretScrollStart(searchValue, caret, contentWidth);
|
|
2811
|
+
setCursorPosition({
|
|
2812
|
+
x: left + frameLeft + textWidth(searchValue.slice(start, caret)),
|
|
2813
|
+
y: top + headerBottom
|
|
2814
|
+
});
|
|
2815
|
+
} else {
|
|
2816
|
+
const caretOffset = current === void 0 ? void 0 : widgetOf(current.type).caret?.(current, effectiveCursor, contentWidth);
|
|
2817
|
+
if (caretOffset === void 0) setCursorPosition(void 0);
|
|
2818
|
+
else {
|
|
2819
|
+
const rowOffset = rowTopOffset(contentRows, Math.max(0, safeFocus.row - (search ? 1 : 0)), contentWidth);
|
|
2820
|
+
setCursorPosition({
|
|
2821
|
+
x: left + frameLeft + caretOffset.dx,
|
|
2822
|
+
y: top + contentTop + rowOffset + caretOffset.dy - scroll
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
useEffect(() => {
|
|
2827
|
+
if (search && !editingFormInput) {
|
|
2828
|
+
writeCursorShape("beam");
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
writeCursorShape(current !== void 0 && widgetOf(current.type).caret !== void 0 ? "beam" : "block");
|
|
2832
|
+
}, [
|
|
2833
|
+
current?.type,
|
|
2834
|
+
safeFocus.row,
|
|
2835
|
+
safeFocus.col
|
|
2836
|
+
]);
|
|
2837
|
+
useEffect(() => {
|
|
2838
|
+
return () => {
|
|
2839
|
+
writeCursorShape("reset");
|
|
2840
|
+
clearTimeout(carouselPressTimer.current);
|
|
2841
|
+
};
|
|
2842
|
+
}, []);
|
|
2843
|
+
useDialogInput({
|
|
2844
|
+
live,
|
|
2845
|
+
search,
|
|
2846
|
+
closeGuarded,
|
|
2847
|
+
centerScroll,
|
|
2848
|
+
onClose,
|
|
2849
|
+
onCtrlA,
|
|
2850
|
+
onCtrlD,
|
|
2851
|
+
onCtrlE,
|
|
2852
|
+
onActivity,
|
|
2853
|
+
requestSearch: handleSearch,
|
|
2854
|
+
setFocus,
|
|
2855
|
+
setScrollTop,
|
|
2856
|
+
setCursor
|
|
2857
|
+
});
|
|
2858
|
+
const flashCarousel = (row, side) => {
|
|
2859
|
+
setCarouselPress({
|
|
2860
|
+
row,
|
|
2861
|
+
side
|
|
2862
|
+
});
|
|
2863
|
+
clearTimeout(carouselPressTimer.current);
|
|
2864
|
+
carouselPressTimer.current = setTimeout(() => setCarouselPress(null), 120);
|
|
2865
|
+
};
|
|
2866
|
+
useImperativeHandle(ref, () => ({
|
|
2867
|
+
wheelAt(y, dir) {
|
|
2868
|
+
onActivity?.();
|
|
2869
|
+
if (y < live.current.top || y >= live.current.top + live.current.windowHeight) {
|
|
2870
|
+
const error = errorLive.current;
|
|
2871
|
+
if (error.height > 1 && y >= error.top && y < error.top + error.height) {
|
|
2872
|
+
setErrorScrollTop((current) => Math.max(0, Math.min(current + dir, error.maxScroll)));
|
|
2873
|
+
return true;
|
|
2874
|
+
}
|
|
2875
|
+
return false;
|
|
2876
|
+
}
|
|
2877
|
+
applyNavigation(live.current, dir === -1 ? "up" : "down", search, {
|
|
2878
|
+
setFocus,
|
|
2879
|
+
setScrollTop
|
|
2880
|
+
}, centerScroll);
|
|
2881
|
+
return true;
|
|
2882
|
+
},
|
|
2883
|
+
clickAt(y, x) {
|
|
2884
|
+
onActivity?.();
|
|
2885
|
+
if (y < top || y >= top + windowHeight || x < left || x >= left + windowWidth) return;
|
|
2886
|
+
if (search) {
|
|
2887
|
+
const searchTop = top + headerBottom;
|
|
2888
|
+
if (y >= searchTop && y < searchTop + fixedHeight) {
|
|
2889
|
+
const clickCaret = safeFocus.row === 0 ? Math.min(cursor, searchValue.length) : searchValue.length;
|
|
2890
|
+
const start = caretScrollStart(searchValue, clickCaret, contentWidth);
|
|
2891
|
+
const visible = truncate(searchValue.slice(start), contentWidth);
|
|
2892
|
+
const col = Math.max(0, Math.min(x - (left + frameLeft), textWidth(visible)));
|
|
2893
|
+
setFocus({
|
|
2894
|
+
row: 0,
|
|
2895
|
+
col: 0
|
|
2896
|
+
});
|
|
2897
|
+
setCursor(Math.min(searchValue.length, start + colToCharIndex(visible, col)));
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
const rowIndex = hitRowIndex(y, top + fixedHeight, titleLines, contentRows, scroll, contentWidth);
|
|
2902
|
+
if (rowIndex === null) return;
|
|
2903
|
+
const rowSpec = contentRows[rowIndex];
|
|
2904
|
+
if (!isSelectableRow(rowSpec)) return;
|
|
2905
|
+
const displayRow = Math.min(Math.max(rowIndex + (search ? 1 : 0), focusMinRow), focusMaxRow);
|
|
2906
|
+
const rowOffset = rowTopOffset(contentRows, rowIndex, contentWidth);
|
|
2907
|
+
const hit = {
|
|
2908
|
+
localX: x - (left + frameLeft),
|
|
2909
|
+
localY: y - (top + contentTop + rowOffset - scroll),
|
|
2910
|
+
width: contentWidth
|
|
2911
|
+
};
|
|
2912
|
+
const clickActions = {
|
|
2913
|
+
focus: (col) => setFocus({
|
|
2914
|
+
row: displayRow,
|
|
2915
|
+
col
|
|
2916
|
+
}),
|
|
2917
|
+
flash: (side) => flashCarousel(rowIndex, side)
|
|
2918
|
+
};
|
|
2919
|
+
for (const item of rowSpec?.items ?? []) if (widgetOf(item.type).onClick?.(item, hit, clickActions) === true) return;
|
|
2920
|
+
if (safeFocus.row === displayRow && rowSpec !== void 0) {
|
|
2921
|
+
const first = rowSpec.items[0];
|
|
2922
|
+
if (first !== void 0) {
|
|
2923
|
+
const activate = widgetOf(first.type).activate;
|
|
2924
|
+
if (activate !== void 0) {
|
|
2925
|
+
activate(first);
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
setFocus({
|
|
2931
|
+
row: displayRow,
|
|
2932
|
+
col: 0
|
|
2933
|
+
});
|
|
2934
|
+
const contentRow = Math.max(0, Math.min(rowIndex, contentRows.length - 1));
|
|
2935
|
+
setScrollTop(adjustScroll(contentRows, {
|
|
2936
|
+
row: contentRow,
|
|
2937
|
+
col: 0
|
|
2938
|
+
}, scroll, viewportHeight, contentWidth));
|
|
2939
|
+
}
|
|
2940
|
+
}));
|
|
2941
|
+
const visibleRows = [];
|
|
2942
|
+
let offset = 0;
|
|
2943
|
+
for (let i = 0; i < contentRows.length; i++) {
|
|
2944
|
+
const height = rowHeight(contentRows[i], contentWidth);
|
|
2945
|
+
if (offset + height <= scroll) {
|
|
2946
|
+
offset += height;
|
|
2947
|
+
continue;
|
|
2948
|
+
}
|
|
2949
|
+
if (offset >= scroll + viewportHeight) break;
|
|
2950
|
+
const clip = Math.max(0, scroll - offset);
|
|
2951
|
+
const rel = Math.max(0, offset - scroll);
|
|
2952
|
+
const baseY = contentTop + rel;
|
|
2953
|
+
const focused = safeFocus.row === i + (search ? 1 : 0);
|
|
2954
|
+
visibleRows.push(/* @__PURE__ */ jsx(Box, {
|
|
2955
|
+
position: "absolute",
|
|
2956
|
+
top: rel,
|
|
2957
|
+
left: 0,
|
|
2958
|
+
width: contentWidth,
|
|
2959
|
+
children: renderRow(contentRows[i], focused, contentWidth, baseY, frameLeft, carouselPress?.row === i ? carouselPress.side : null, focused ? safeFocus.col : 0, clip, focused ? effectiveCursor : void 0)
|
|
2960
|
+
}, `row-${i}`));
|
|
2961
|
+
offset += height;
|
|
2962
|
+
}
|
|
2963
|
+
return /* @__PURE__ */ jsxs(Region, {
|
|
2964
|
+
x: left,
|
|
2965
|
+
y: top,
|
|
2966
|
+
children: [/* @__PURE__ */ jsxs(Box, {
|
|
2967
|
+
position: "absolute",
|
|
2968
|
+
top,
|
|
2969
|
+
left,
|
|
2970
|
+
width: windowWidth,
|
|
2971
|
+
height: windowHeight,
|
|
2972
|
+
flexDirection: "column",
|
|
2973
|
+
backgroundColor: COLORS.dialogBackground,
|
|
2974
|
+
paddingLeft: 2,
|
|
2975
|
+
paddingRight: 2,
|
|
2976
|
+
paddingTop: 1,
|
|
2977
|
+
paddingBottom: 1,
|
|
2978
|
+
children: [
|
|
2979
|
+
title !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Box, {
|
|
2980
|
+
justifyContent: "center",
|
|
2981
|
+
children: /* @__PURE__ */ jsx(SelectableText, {
|
|
2982
|
+
y: 1,
|
|
2983
|
+
col: frameLeft + Math.max(0, Math.floor((contentWidth - textWidth(title)) / 2)),
|
|
2984
|
+
text: title
|
|
2985
|
+
})
|
|
2986
|
+
}), /* @__PURE__ */ jsx(Box, {
|
|
2987
|
+
height: 1,
|
|
2988
|
+
children: /* @__PURE__ */ jsx(Text, { children: " " })
|
|
2989
|
+
})] }),
|
|
2990
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
2991
|
+
flexDirection: "column",
|
|
2992
|
+
children: [search && /* @__PURE__ */ jsx(Box, {
|
|
2993
|
+
flexDirection: "column",
|
|
2994
|
+
children: renderRow(searchRow, safeFocus.row === 0, contentWidth, headerBottom, frameLeft, null, 0, 0, cursor)
|
|
2995
|
+
}), /* @__PURE__ */ jsx(Box, {
|
|
2996
|
+
flexDirection: "column",
|
|
2997
|
+
height: viewportHeight,
|
|
2998
|
+
overflow: "hidden",
|
|
2999
|
+
children: visibleRows
|
|
3000
|
+
})]
|
|
3001
|
+
}),
|
|
3002
|
+
footerRows.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Box, {
|
|
3003
|
+
height: 1,
|
|
3004
|
+
children: /* @__PURE__ */ jsx(Text, { children: " " })
|
|
3005
|
+
}), footerRows.map((line, index) => /* @__PURE__ */ jsx(SelectableText, {
|
|
3006
|
+
y: windowHeight - footerRows.length + index - 1,
|
|
3007
|
+
col: frameLeft,
|
|
3008
|
+
text: line.text,
|
|
3009
|
+
color: line.color
|
|
3010
|
+
}, index))] })
|
|
3011
|
+
]
|
|
3012
|
+
}), errorVisible > 0 && /* @__PURE__ */ jsx(Box, {
|
|
3013
|
+
position: "absolute",
|
|
3014
|
+
top: top + windowHeight,
|
|
3015
|
+
left,
|
|
3016
|
+
width: windowWidth,
|
|
3017
|
+
height: errorVisible + 1,
|
|
3018
|
+
flexDirection: "column",
|
|
3019
|
+
backgroundColor: COLORS.dialogBackground,
|
|
3020
|
+
paddingLeft: 2,
|
|
3021
|
+
paddingRight: 2,
|
|
3022
|
+
paddingBottom: 1,
|
|
3023
|
+
children: errorRows.slice(errorScroll, errorScroll + errorVisible).map((line, index) => /* @__PURE__ */ jsx(SelectableText, {
|
|
3024
|
+
y: windowHeight + index,
|
|
3025
|
+
col: frameLeft,
|
|
3026
|
+
text: line.text,
|
|
3027
|
+
color: line.color
|
|
3028
|
+
}, errorScroll + index))
|
|
3029
|
+
})]
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
3032
|
+
//#endregion
|
|
3033
|
+
//#region src/ui/panels/surface.tsx
|
|
3034
|
+
/**
|
|
3035
|
+
* Build a key-hint row: keys in the panel key color, action words in the
|
|
3036
|
+
* muted description color ("enter select esc close").
|
|
3037
|
+
*/
|
|
3038
|
+
function panelLegend(hints, options = {}) {
|
|
3039
|
+
const keyStyle = { color: options.keyColor ?? COLORS.panelKeyText };
|
|
3040
|
+
const descriptionStyle = { color: options.descriptionColor ?? COLORS.toolBodyText };
|
|
3041
|
+
const parts = [];
|
|
3042
|
+
if (options.prefix !== void 0 && options.prefix !== "") parts.push({
|
|
3043
|
+
text: options.prefix,
|
|
3044
|
+
style: descriptionStyle
|
|
3045
|
+
});
|
|
3046
|
+
for (const [index, hint] of hints.entries()) {
|
|
3047
|
+
if (index > 0 || parts.length > 0) parts.push({
|
|
3048
|
+
text: " ",
|
|
3049
|
+
style: descriptionStyle
|
|
3050
|
+
});
|
|
3051
|
+
parts.push({
|
|
3052
|
+
text: hint.key,
|
|
3053
|
+
style: keyStyle
|
|
3054
|
+
});
|
|
3055
|
+
parts.push({
|
|
3056
|
+
text: " ",
|
|
3057
|
+
style: descriptionStyle
|
|
3058
|
+
});
|
|
3059
|
+
parts.push({
|
|
3060
|
+
text: hint.description,
|
|
3061
|
+
style: descriptionStyle
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
return mergeRuns(parts);
|
|
3065
|
+
}
|
|
3066
|
+
/**
|
|
3067
|
+
* Shared panel chrome: a floating block anchored above the status line with
|
|
3068
|
+
* half-block caps, the panel background, and per-row text or interactive
|
|
3069
|
+
* content. Builtin panels and plugin panels built through dshtui/dialog
|
|
3070
|
+
* share this shell so every interaction panel looks the same.
|
|
3071
|
+
*
|
|
3072
|
+
* Selection pieces span the row text only, never the pill padding, so
|
|
3073
|
+
* dragging over blank panel area selects nothing (same rule as messages).
|
|
3074
|
+
*/
|
|
3075
|
+
function PanelSurface({ columns, rows, body, bodyStart, background, blockWidth }) {
|
|
3076
|
+
return /* @__PURE__ */ jsx(Region, { children: /* @__PURE__ */ jsxs(Box, {
|
|
3077
|
+
position: "absolute",
|
|
3078
|
+
top: 0,
|
|
3079
|
+
left: 0,
|
|
3080
|
+
width: columns,
|
|
3081
|
+
height: rows,
|
|
3082
|
+
children: [
|
|
3083
|
+
/* @__PURE__ */ jsx(Box, {
|
|
3084
|
+
position: "absolute",
|
|
3085
|
+
top: bodyStart - 1,
|
|
3086
|
+
left: 2,
|
|
3087
|
+
width: blockWidth,
|
|
3088
|
+
children: /* @__PURE__ */ jsx(CapText, {
|
|
3089
|
+
background,
|
|
3090
|
+
top: true,
|
|
3091
|
+
width: blockWidth
|
|
3092
|
+
})
|
|
3093
|
+
}),
|
|
3094
|
+
body.map((row, index) => {
|
|
3095
|
+
const isEmpty = row.segments.every((segment) => segment.text.trim() === "");
|
|
3096
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
3097
|
+
position: "absolute",
|
|
3098
|
+
top: bodyStart + index,
|
|
3099
|
+
left: 2,
|
|
3100
|
+
width: blockWidth,
|
|
3101
|
+
paddingLeft: 2,
|
|
3102
|
+
paddingRight: 2,
|
|
3103
|
+
backgroundColor: background,
|
|
3104
|
+
children: row.content !== void 0 ? row.content : isEmpty ? /* @__PURE__ */ jsx(Text, { children: " " }) : /* @__PURE__ */ jsx(SelectableText, {
|
|
3105
|
+
y: bodyStart + index,
|
|
3106
|
+
col: 4,
|
|
3107
|
+
segments: row.segments
|
|
3108
|
+
})
|
|
3109
|
+
}, `panel-row-${index}`);
|
|
3110
|
+
}),
|
|
3111
|
+
/* @__PURE__ */ jsx(Box, {
|
|
3112
|
+
position: "absolute",
|
|
3113
|
+
top: bodyStart + body.length,
|
|
3114
|
+
left: 2,
|
|
3115
|
+
width: blockWidth,
|
|
3116
|
+
children: /* @__PURE__ */ jsx(CapText, {
|
|
3117
|
+
background,
|
|
3118
|
+
top: false,
|
|
3119
|
+
width: blockWidth
|
|
3120
|
+
})
|
|
3121
|
+
})
|
|
3122
|
+
]
|
|
3123
|
+
}) });
|
|
3124
|
+
}
|
|
3125
|
+
//#endregion
|
|
3126
|
+
export { errorText as $, SelectableText as A, setColorLevel as At, inputFrameTop as B, caretNonceText as C, registerPalette as Ct, markKeymapConsumed as D, themeMode as Dt, isKeyConsumed as E, subscribePalettes as Et, messageBackgroundWidth as F, warn as Ft, wrapSegments as G, glyphs as H, panelAnchorRow as I, clampFocusRow as J, chromeSelectionText as K, panelContains as L, Region as M, configureLogs as Mt, hintSpan as N, error as Nt, createMouseController as O, keyedRegistry as Ot, inputContentContains as P, installCrashHandlers as Pt, colToCharIndex as Q, scrollbarColumn as R, caretNonceColor as S, permissionModeInfo as St, beginKeyEvent as T, setThemeMode as Tt, mergeRuns as U, inputStatusRow as V, segmentsKey as W, toScreenSelection as X, selectedRange as Y, charWidth as Z, editDeleteWordRight as _, releaseFields as _t, DIALOG_MAX_HEIGHT as a, wrapLines as at, writeCursorShape as b, COLORS as bt, asTextItem as c, fieldExpandOf as ct, editCursorLeft as d, hasFieldChar as dt, lineBreaks as et, editCursorRight as f, hasFieldSlots as ft, editDeleteWordLeft as g, registerFieldKind as gt, editDelete as h, linesChipLabel as ht, Dialog as i, truncate as it, SelectionContext as j, env as jt, isMouseResidue as k, colorLevel as kt, registerWidget as l, fieldSlotOf as lt, editCursorWordRight as m, isFieldChar as mt, panelLegend as n, segmentGraphemes as nt, rowHeight as o, allocateField as ot, editCursorWordLeft as p, imageChipLabel as pt, sliceByColumns as q, CloseGuardContext as r, textWidth as rt, wrapStatusLines as s, charactersChipLabel as st, PanelSurface as t, locToPoint as tt, editBackspace as u, fieldStyleOf as ut, editInsert as v, releaseUnreferenced as vt, useCaret as w, replacePalettes as wt, caretNonceBold as x, paletteColor as xt, CapText as y, specialFieldFactory as yt, hintBlockTop as z };
|