laohuang 0.3.2 → 0.4.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/README.md +142 -16
- package/dist/agent.d.ts +166 -0
- package/dist/agent.js +858 -0
- package/dist/agent.js.map +1 -0
- package/dist/bash-runner.d.ts +79 -0
- package/dist/bash-runner.js +464 -0
- package/dist/bash-runner.js.map +1 -0
- package/dist/cancellation.d.ts +36 -0
- package/dist/cancellation.js +123 -0
- package/dist/cancellation.js.map +1 -0
- package/dist/cli.d.ts +117 -0
- package/dist/cli.js +1307 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +21 -0
- package/dist/client.js +17 -0
- package/dist/client.js.map +1 -0
- package/dist/commands.d.ts +123 -0
- package/dist/commands.js +660 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.js +183 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +12 -0
- package/dist/credentials.js +102 -0
- package/dist/credentials.js.map +1 -0
- package/dist/events.d.ts +288 -0
- package/dist/events.js +838 -0
- package/dist/events.js.map +1 -0
- package/dist/model-adapter.d.ts +138 -0
- package/dist/model-adapter.js +244 -0
- package/dist/model-adapter.js.map +1 -0
- package/dist/model-selection.d.ts +67 -0
- package/dist/model-selection.js +148 -0
- package/dist/model-selection.js.map +1 -0
- package/dist/model-stream.d.ts +128 -0
- package/dist/model-stream.js +582 -0
- package/dist/model-stream.js.map +1 -0
- package/dist/project-instructions.d.ts +99 -0
- package/dist/project-instructions.js +348 -0
- package/dist/project-instructions.js.map +1 -0
- package/dist/providers.d.ts +9 -0
- package/dist/providers.js +26 -0
- package/dist/providers.js.map +1 -0
- package/dist/routing.d.ts +170 -0
- package/dist/routing.js +669 -0
- package/dist/routing.js.map +1 -0
- package/dist/semantic-classifier.d.ts +66 -0
- package/dist/semantic-classifier.js +86 -0
- package/dist/semantic-classifier.js.map +1 -0
- package/dist/session.d.ts +162 -0
- package/dist/session.js +871 -0
- package/dist/session.js.map +1 -0
- package/dist/system-prompt.d.ts +16 -0
- package/dist/system-prompt.js +42 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/terminal/editor.d.ts +161 -0
- package/dist/terminal/editor.js +1060 -0
- package/dist/terminal/editor.js.map +1 -0
- package/dist/terminal/input.d.ts +61 -0
- package/dist/terminal/input.js +276 -0
- package/dist/terminal/input.js.map +1 -0
- package/dist/terminal/markdown.d.ts +20 -0
- package/dist/terminal/markdown.js +621 -0
- package/dist/terminal/markdown.js.map +1 -0
- package/dist/terminal/screen.d.ts +66 -0
- package/dist/terminal/screen.js +624 -0
- package/dist/terminal/screen.js.map +1 -0
- package/dist/terminal/theme.d.ts +23 -0
- package/dist/terminal/theme.js +101 -0
- package/dist/terminal/theme.js.map +1 -0
- package/dist/terminal/ui.d.ts +285 -0
- package/dist/terminal/ui.js +1815 -0
- package/dist/terminal/ui.js.map +1 -0
- package/dist/tools.d.ts +95 -0
- package/dist/tools.js +444 -0
- package/dist/tools.js.map +1 -0
- package/dist/ui-state.d.ts +51 -0
- package/dist/ui-state.js +194 -0
- package/dist/ui-state.js.map +1 -0
- package/dist/web.d.ts +56 -0
- package/dist/web.js +247 -0
- package/dist/web.js.map +1 -0
- package/package.json +24 -21
- package/bin/laohuang.js +0 -10
- package/lib/launcher.js +0 -133
- package/vendor/laohuangcode-0.3.2-py3-none-any.whl +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental renderer for the regular terminal screen, plus the cell-width
|
|
3
|
+
* string helpers its frames rely on.
|
|
4
|
+
*
|
|
5
|
+
* Port of terminal_screen.py: a differential renderer that keeps the
|
|
6
|
+
* terminal append-only (no alternate screen, no full clears except on
|
|
7
|
+
* resize), emitting one synchronized-output write per frame.
|
|
8
|
+
*
|
|
9
|
+
* This module is also the canonical owner of the visible-width,
|
|
10
|
+
* escape-sequence, and wcwidth helpers shared by terminal/markdown.ts and
|
|
11
|
+
* terminal/editor.ts.
|
|
12
|
+
*/
|
|
13
|
+
export interface TerminalSize {
|
|
14
|
+
columns: number;
|
|
15
|
+
rows: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ScreenFrame {
|
|
18
|
+
lines: readonly string[];
|
|
19
|
+
activeStart: number;
|
|
20
|
+
cursorRow: number;
|
|
21
|
+
cursorCol: number;
|
|
22
|
+
}
|
|
23
|
+
/** The minimal terminal surface the renderer writes to. */
|
|
24
|
+
export interface TerminalDriver {
|
|
25
|
+
write(data: string): void;
|
|
26
|
+
flush(): void;
|
|
27
|
+
getSize(): TerminalSize;
|
|
28
|
+
restore(): void;
|
|
29
|
+
}
|
|
30
|
+
/** In-memory terminal driver for renderer tests. */
|
|
31
|
+
export declare class MemoryTerminalDriver implements TerminalDriver {
|
|
32
|
+
#private;
|
|
33
|
+
flushes: number;
|
|
34
|
+
restored: boolean;
|
|
35
|
+
restoreCalls: number;
|
|
36
|
+
constructor(size: TerminalSize);
|
|
37
|
+
write(data: string): void;
|
|
38
|
+
flush(): void;
|
|
39
|
+
getSize(): TerminalSize;
|
|
40
|
+
resize(size: TerminalSize): void;
|
|
41
|
+
restore(): void;
|
|
42
|
+
writes(): string;
|
|
43
|
+
writeChunks(): string[];
|
|
44
|
+
clearWrites(): void;
|
|
45
|
+
}
|
|
46
|
+
/** Regular-terminal differential renderer. */
|
|
47
|
+
export declare class PiMainScreenRenderer {
|
|
48
|
+
#private;
|
|
49
|
+
constructor(terminal: TerminalDriver, env?: Record<string, string | undefined>);
|
|
50
|
+
render(frame: ScreenFrame): void;
|
|
51
|
+
close(): void;
|
|
52
|
+
}
|
|
53
|
+
/** Visible column width of a string, ignoring terminal control sequences. */
|
|
54
|
+
export declare function visibleWidth(text: string): number;
|
|
55
|
+
/** Truncate a styled ANSI string to a visible width, closing open SGR. */
|
|
56
|
+
export declare function truncateToWidth(text: string, width: number): string;
|
|
57
|
+
/** Word-independent hard wrap of a styled ANSI string to a visible width. */
|
|
58
|
+
export declare function wrapTextToWidth(text: string, width: number): string[];
|
|
59
|
+
/** Remove all terminal escape sequences from a string. */
|
|
60
|
+
export declare function stripTerminalControls(text: string): string;
|
|
61
|
+
/** Split a string into extended grapheme clusters. */
|
|
62
|
+
export declare function graphemeClusters(text: string): string[];
|
|
63
|
+
/** Visible cell width of one grapheme cluster (emoji/ZWJ sequences count 2). */
|
|
64
|
+
export declare function clusterWidth(cluster: string): number;
|
|
65
|
+
/** Terminal cell width of a single code point (wcwidth equivalent). */
|
|
66
|
+
export declare function charCellWidth(char: string): number;
|
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental renderer for the regular terminal screen, plus the cell-width
|
|
3
|
+
* string helpers its frames rely on.
|
|
4
|
+
*
|
|
5
|
+
* Port of terminal_screen.py: a differential renderer that keeps the
|
|
6
|
+
* terminal append-only (no alternate screen, no full clears except on
|
|
7
|
+
* resize), emitting one synchronized-output write per frame.
|
|
8
|
+
*
|
|
9
|
+
* This module is also the canonical owner of the visible-width,
|
|
10
|
+
* escape-sequence, and wcwidth helpers shared by terminal/markdown.ts and
|
|
11
|
+
* terminal/editor.ts.
|
|
12
|
+
*/
|
|
13
|
+
/** In-memory terminal driver for renderer tests. */
|
|
14
|
+
export class MemoryTerminalDriver {
|
|
15
|
+
#size;
|
|
16
|
+
#writes = [];
|
|
17
|
+
flushes = 0;
|
|
18
|
+
restored = false;
|
|
19
|
+
restoreCalls = 0;
|
|
20
|
+
constructor(size) {
|
|
21
|
+
this.#size = { ...size };
|
|
22
|
+
}
|
|
23
|
+
write(data) {
|
|
24
|
+
this.#writes.push(data);
|
|
25
|
+
}
|
|
26
|
+
flush() {
|
|
27
|
+
this.flushes += 1;
|
|
28
|
+
}
|
|
29
|
+
getSize() {
|
|
30
|
+
return { ...this.#size };
|
|
31
|
+
}
|
|
32
|
+
resize(size) {
|
|
33
|
+
this.#size = { ...size };
|
|
34
|
+
}
|
|
35
|
+
restore() {
|
|
36
|
+
this.restored = true;
|
|
37
|
+
this.restoreCalls += 1;
|
|
38
|
+
}
|
|
39
|
+
writes() {
|
|
40
|
+
return this.#writes.join("");
|
|
41
|
+
}
|
|
42
|
+
writeChunks() {
|
|
43
|
+
return [...this.#writes];
|
|
44
|
+
}
|
|
45
|
+
clearWrites() {
|
|
46
|
+
this.#writes = [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Regular-terminal differential renderer. */
|
|
50
|
+
export class PiMainScreenRenderer {
|
|
51
|
+
#terminal;
|
|
52
|
+
#previousLines = [];
|
|
53
|
+
#previousWidth = 0;
|
|
54
|
+
#previousHeight = 0;
|
|
55
|
+
#previousViewportTop = 0;
|
|
56
|
+
#cursorRow = 0;
|
|
57
|
+
#hardwareCursorRow = 0;
|
|
58
|
+
#maxLinesRendered = 0;
|
|
59
|
+
#clearOnShrink;
|
|
60
|
+
#closed = false;
|
|
61
|
+
constructor(terminal, env = process.env) {
|
|
62
|
+
this.#terminal = terminal;
|
|
63
|
+
this.#clearOnShrink = env.PI_CLEAR_ON_SHRINK === "1";
|
|
64
|
+
}
|
|
65
|
+
render(frame) {
|
|
66
|
+
if (this.#closed) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const size = this.#terminal.getSize();
|
|
70
|
+
const width = Math.max(1, size.columns);
|
|
71
|
+
const height = Math.max(1, size.rows);
|
|
72
|
+
const newLines = frame.lines;
|
|
73
|
+
PiMainScreenRenderer.#validateLines(newLines, width);
|
|
74
|
+
const widthChanged = this.#previousWidth !== 0 && this.#previousWidth !== width;
|
|
75
|
+
const heightChanged = this.#previousHeight !== 0 && this.#previousHeight !== height;
|
|
76
|
+
const previousBufferLength = this.#previousHeight > 0
|
|
77
|
+
? this.#previousViewportTop + this.#previousHeight
|
|
78
|
+
: height;
|
|
79
|
+
const prevViewportTop = heightChanged
|
|
80
|
+
? Math.max(0, previousBufferLength - height)
|
|
81
|
+
: this.#previousViewportTop;
|
|
82
|
+
let viewportTop = prevViewportTop;
|
|
83
|
+
let hardwareCursorRow = this.#hardwareCursorRow;
|
|
84
|
+
const computeLineDiff = (targetRow) => {
|
|
85
|
+
const currentScreenRow = hardwareCursorRow - prevViewportTop;
|
|
86
|
+
const targetScreenRow = targetRow - viewportTop;
|
|
87
|
+
return targetScreenRow - currentScreenRow;
|
|
88
|
+
};
|
|
89
|
+
const fullRender = (clear) => {
|
|
90
|
+
let buffer = "\x1b[?2026h";
|
|
91
|
+
if (clear) {
|
|
92
|
+
buffer += "\x1b[2J\x1b[H\x1b[3J";
|
|
93
|
+
}
|
|
94
|
+
newLines.forEach((line, index) => {
|
|
95
|
+
if (index > 0) {
|
|
96
|
+
buffer += "\r\n";
|
|
97
|
+
}
|
|
98
|
+
buffer += line;
|
|
99
|
+
});
|
|
100
|
+
buffer += "\x1b[?2026l";
|
|
101
|
+
this.#cursorRow = Math.max(0, newLines.length - 1);
|
|
102
|
+
this.#hardwareCursorRow = this.#cursorRow;
|
|
103
|
+
if (clear) {
|
|
104
|
+
this.#maxLinesRendered = newLines.length;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.#maxLinesRendered = Math.max(this.#maxLinesRendered, newLines.length);
|
|
108
|
+
}
|
|
109
|
+
const bufferLength = Math.max(height, newLines.length);
|
|
110
|
+
this.#previousViewportTop = Math.max(0, bufferLength - height);
|
|
111
|
+
buffer += this.#positionHardwareCursor(frame, width, newLines.length);
|
|
112
|
+
this.#commitState(newLines, width, height);
|
|
113
|
+
this.#terminal.write(buffer);
|
|
114
|
+
this.#terminal.flush();
|
|
115
|
+
};
|
|
116
|
+
if (this.#previousLines.length === 0 &&
|
|
117
|
+
!widthChanged &&
|
|
118
|
+
!heightChanged) {
|
|
119
|
+
fullRender(false);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (widthChanged || heightChanged) {
|
|
123
|
+
fullRender(true);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (this.#clearOnShrink && newLines.length < this.#maxLinesRendered) {
|
|
127
|
+
fullRender(true);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const span = PiMainScreenRenderer.#changedSpan(this.#previousLines, newLines);
|
|
131
|
+
if (span === null) {
|
|
132
|
+
const buffer = this.#positionHardwareCursor(frame, width, newLines.length);
|
|
133
|
+
this.#previousViewportTop = prevViewportTop;
|
|
134
|
+
this.#previousHeight = height;
|
|
135
|
+
if (buffer) {
|
|
136
|
+
this.#terminal.write(buffer);
|
|
137
|
+
}
|
|
138
|
+
this.#terminal.flush();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let [firstChanged, lastChanged] = span;
|
|
142
|
+
const appendedLines = newLines.length > this.#previousLines.length;
|
|
143
|
+
if (appendedLines && firstChanged === -1) {
|
|
144
|
+
firstChanged = this.#previousLines.length;
|
|
145
|
+
lastChanged = newLines.length - 1;
|
|
146
|
+
}
|
|
147
|
+
const appendStart = appendedLines &&
|
|
148
|
+
firstChanged === this.#previousLines.length &&
|
|
149
|
+
firstChanged > 0;
|
|
150
|
+
if (firstChanged >= newLines.length) {
|
|
151
|
+
let buffer = "\x1b[?2026h";
|
|
152
|
+
const targetRow = Math.max(0, newLines.length - 1);
|
|
153
|
+
if (targetRow < prevViewportTop) {
|
|
154
|
+
fullRender(true);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const lineDiff = computeLineDiff(targetRow);
|
|
158
|
+
if (lineDiff > 0) {
|
|
159
|
+
buffer += `\x1b[${lineDiff}B`;
|
|
160
|
+
}
|
|
161
|
+
else if (lineDiff < 0) {
|
|
162
|
+
buffer += `\x1b[${-lineDiff}A`;
|
|
163
|
+
}
|
|
164
|
+
buffer += "\r";
|
|
165
|
+
const extraLines = this.#previousLines.length - newLines.length;
|
|
166
|
+
if (extraLines > height) {
|
|
167
|
+
fullRender(true);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const clearStartOffset = newLines.length === 0 ? 0 : 1;
|
|
171
|
+
if (extraLines > 0 && clearStartOffset > 0) {
|
|
172
|
+
buffer += `\x1b[${clearStartOffset}B`;
|
|
173
|
+
}
|
|
174
|
+
for (let index = 0; index < extraLines; index += 1) {
|
|
175
|
+
buffer += "\r\x1b[2K";
|
|
176
|
+
if (index < extraLines - 1) {
|
|
177
|
+
buffer += "\x1b[1B";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const moveBack = Math.max(0, extraLines - 1 + clearStartOffset);
|
|
181
|
+
if (moveBack > 0) {
|
|
182
|
+
buffer += `\x1b[${moveBack}A`;
|
|
183
|
+
}
|
|
184
|
+
buffer += "\x1b[?2026l";
|
|
185
|
+
this.#cursorRow = targetRow;
|
|
186
|
+
this.#hardwareCursorRow = targetRow;
|
|
187
|
+
buffer += this.#positionHardwareCursor(frame, width, newLines.length);
|
|
188
|
+
this.#previousViewportTop = prevViewportTop;
|
|
189
|
+
this.#commitState(newLines, width, height);
|
|
190
|
+
this.#terminal.write(buffer);
|
|
191
|
+
this.#terminal.flush();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (firstChanged < prevViewportTop) {
|
|
195
|
+
fullRender(true);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
let buffer = "\x1b[?2026h";
|
|
199
|
+
const prevViewportBottom = prevViewportTop + height - 1;
|
|
200
|
+
const moveTargetRow = appendStart ? firstChanged - 1 : firstChanged;
|
|
201
|
+
if (moveTargetRow > prevViewportBottom) {
|
|
202
|
+
const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
|
|
203
|
+
const moveToBottom = height - 1 - currentScreenRow;
|
|
204
|
+
if (moveToBottom > 0) {
|
|
205
|
+
buffer += `\x1b[${moveToBottom}B`;
|
|
206
|
+
}
|
|
207
|
+
const scroll = moveTargetRow - prevViewportBottom;
|
|
208
|
+
buffer += "\r\n".repeat(scroll);
|
|
209
|
+
viewportTop += scroll;
|
|
210
|
+
hardwareCursorRow = moveTargetRow;
|
|
211
|
+
}
|
|
212
|
+
const lineDiff = computeLineDiff(moveTargetRow);
|
|
213
|
+
if (lineDiff > 0) {
|
|
214
|
+
buffer += `\x1b[${lineDiff}B`;
|
|
215
|
+
}
|
|
216
|
+
else if (lineDiff < 0) {
|
|
217
|
+
buffer += `\x1b[${-lineDiff}A`;
|
|
218
|
+
}
|
|
219
|
+
buffer += appendStart ? "\r\n" : "\r";
|
|
220
|
+
const renderEnd = Math.min(lastChanged, newLines.length - 1);
|
|
221
|
+
for (let index = firstChanged; index <= renderEnd; index += 1) {
|
|
222
|
+
if (index > firstChanged) {
|
|
223
|
+
buffer += "\r\n";
|
|
224
|
+
}
|
|
225
|
+
buffer += "\x1b[2K";
|
|
226
|
+
buffer += newLines[index];
|
|
227
|
+
}
|
|
228
|
+
let finalCursorRow = renderEnd;
|
|
229
|
+
if (this.#previousLines.length > newLines.length) {
|
|
230
|
+
if (renderEnd < newLines.length - 1) {
|
|
231
|
+
const moveDown = newLines.length - 1 - renderEnd;
|
|
232
|
+
buffer += `\x1b[${moveDown}B`;
|
|
233
|
+
finalCursorRow = newLines.length - 1;
|
|
234
|
+
}
|
|
235
|
+
const extraLines = this.#previousLines.length - newLines.length;
|
|
236
|
+
for (let index = newLines.length; index < this.#previousLines.length; index += 1) {
|
|
237
|
+
buffer += "\r\n\x1b[2K";
|
|
238
|
+
}
|
|
239
|
+
buffer += `\x1b[${extraLines}A`;
|
|
240
|
+
}
|
|
241
|
+
buffer += "\x1b[?2026l";
|
|
242
|
+
this.#cursorRow = Math.max(0, newLines.length - 1);
|
|
243
|
+
this.#hardwareCursorRow = finalCursorRow;
|
|
244
|
+
this.#maxLinesRendered = Math.max(this.#maxLinesRendered, newLines.length);
|
|
245
|
+
this.#previousViewportTop = Math.max(prevViewportTop, finalCursorRow - height + 1);
|
|
246
|
+
buffer += this.#positionHardwareCursor(frame, width, newLines.length);
|
|
247
|
+
this.#commitState(newLines, width, height);
|
|
248
|
+
this.#terminal.write(buffer);
|
|
249
|
+
this.#terminal.flush();
|
|
250
|
+
}
|
|
251
|
+
close() {
|
|
252
|
+
if (this.#closed) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this.#closed = true;
|
|
256
|
+
try {
|
|
257
|
+
this.#terminal.write("\x1b[?25h");
|
|
258
|
+
this.#terminal.flush();
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
this.#terminal.restore();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
static #changedSpan(previous, current) {
|
|
265
|
+
let first = -1;
|
|
266
|
+
let last = -1;
|
|
267
|
+
const limit = Math.max(previous.length, current.length);
|
|
268
|
+
for (let index = 0; index < limit; index += 1) {
|
|
269
|
+
const oldLine = index < previous.length ? previous[index] : "";
|
|
270
|
+
const newLine = index < current.length ? current[index] : "";
|
|
271
|
+
if (oldLine === newLine) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (first === -1) {
|
|
275
|
+
first = index;
|
|
276
|
+
}
|
|
277
|
+
last = index;
|
|
278
|
+
}
|
|
279
|
+
if (first === -1) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return [first, last];
|
|
283
|
+
}
|
|
284
|
+
#positionHardwareCursor(frame, width, totalLines) {
|
|
285
|
+
if (frame.lines.length === 0 || totalLines <= 0) {
|
|
286
|
+
return "";
|
|
287
|
+
}
|
|
288
|
+
const targetRow = Math.max(0, Math.min(frame.cursorRow, totalLines - 1));
|
|
289
|
+
const targetCol = Math.max(0, Math.min(frame.cursorCol, Math.max(width - 1, 0)));
|
|
290
|
+
const rowDelta = targetRow - this.#hardwareCursorRow;
|
|
291
|
+
let buffer = "";
|
|
292
|
+
if (rowDelta > 0) {
|
|
293
|
+
buffer += `\x1b[${rowDelta}B`;
|
|
294
|
+
}
|
|
295
|
+
else if (rowDelta < 0) {
|
|
296
|
+
buffer += `\x1b[${-rowDelta}A`;
|
|
297
|
+
}
|
|
298
|
+
buffer += `\x1b[${targetCol + 1}G`;
|
|
299
|
+
this.#hardwareCursorRow = targetRow;
|
|
300
|
+
return buffer;
|
|
301
|
+
}
|
|
302
|
+
#commitState(lines, width, height) {
|
|
303
|
+
this.#previousLines = [...lines];
|
|
304
|
+
this.#previousWidth = width;
|
|
305
|
+
this.#previousHeight = height;
|
|
306
|
+
}
|
|
307
|
+
static #validateLines(lines, width) {
|
|
308
|
+
lines.forEach((line, index) => {
|
|
309
|
+
const lineWidth = visibleWidth(line);
|
|
310
|
+
if (lineWidth > width) {
|
|
311
|
+
throw new Error(`rendered line ${index} exceeds terminal width (${lineWidth} > ${width})`);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Visible-width string helpers (ANSI-aware, grapheme-cluster based)
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
/** Visible column width of a string, ignoring terminal control sequences. */
|
|
320
|
+
export function visibleWidth(text) {
|
|
321
|
+
if (!text) {
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
const stripped = stripTerminalControls(text).replace(/\t/g, " ");
|
|
325
|
+
let width = 0;
|
|
326
|
+
for (const cluster of graphemeClusters(stripped)) {
|
|
327
|
+
width += clusterWidth(cluster);
|
|
328
|
+
}
|
|
329
|
+
return width;
|
|
330
|
+
}
|
|
331
|
+
/** Truncate a styled ANSI string to a visible width, closing open SGR. */
|
|
332
|
+
export function truncateToWidth(text, width) {
|
|
333
|
+
if (width <= 0 || !text) {
|
|
334
|
+
return "";
|
|
335
|
+
}
|
|
336
|
+
const result = [];
|
|
337
|
+
const sgrPrefix = [];
|
|
338
|
+
let used = 0;
|
|
339
|
+
let index = 0;
|
|
340
|
+
while (index < text.length) {
|
|
341
|
+
if (text[index] === "\x1b") {
|
|
342
|
+
const end = consumeEscapeSequence(text, index);
|
|
343
|
+
const sequence = text.slice(index, end);
|
|
344
|
+
if (isSgrSequence(sequence)) {
|
|
345
|
+
result.push(sequence);
|
|
346
|
+
updateSgrPrefix(sgrPrefix, sequence);
|
|
347
|
+
}
|
|
348
|
+
index = end;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const nextEscape = text.indexOf("\x1b", index);
|
|
352
|
+
const end = nextEscape === -1 ? text.length : nextEscape;
|
|
353
|
+
for (const cluster of graphemeClusters(text.slice(index, end))) {
|
|
354
|
+
const [renderedCluster, widthOfCluster] = displayCluster(cluster);
|
|
355
|
+
if (used + widthOfCluster > width) {
|
|
356
|
+
if (sgrPrefix.length > 0) {
|
|
357
|
+
result.push("\x1b[0m");
|
|
358
|
+
}
|
|
359
|
+
return result.join("");
|
|
360
|
+
}
|
|
361
|
+
result.push(renderedCluster);
|
|
362
|
+
used += widthOfCluster;
|
|
363
|
+
}
|
|
364
|
+
index = end;
|
|
365
|
+
}
|
|
366
|
+
if (sgrPrefix.length > 0) {
|
|
367
|
+
result.push("\x1b[0m");
|
|
368
|
+
}
|
|
369
|
+
return result.join("");
|
|
370
|
+
}
|
|
371
|
+
/** Word-independent hard wrap of a styled ANSI string to a visible width. */
|
|
372
|
+
export function wrapTextToWidth(text, width) {
|
|
373
|
+
const targetWidth = Math.max(1, width);
|
|
374
|
+
const rows = [];
|
|
375
|
+
const sourceLines = text.split(/\r\n|\n|\r/);
|
|
376
|
+
for (const sourceLine of sourceLines.length > 0 ? sourceLines : [""]) {
|
|
377
|
+
let rowParts = [];
|
|
378
|
+
let rowWidth = 0;
|
|
379
|
+
const sgrPrefix = [];
|
|
380
|
+
const finishRow = () => {
|
|
381
|
+
if (sgrPrefix.length > 0) {
|
|
382
|
+
rowParts.push("\x1b[0m");
|
|
383
|
+
}
|
|
384
|
+
rows.push(rowParts.join(""));
|
|
385
|
+
};
|
|
386
|
+
let index = 0;
|
|
387
|
+
while (index < sourceLine.length) {
|
|
388
|
+
if (sourceLine[index] === "\x1b") {
|
|
389
|
+
const end = consumeEscapeSequence(sourceLine, index);
|
|
390
|
+
const sequence = sourceLine.slice(index, end);
|
|
391
|
+
if (isSgrSequence(sequence)) {
|
|
392
|
+
rowParts.push(sequence);
|
|
393
|
+
updateSgrPrefix(sgrPrefix, sequence);
|
|
394
|
+
}
|
|
395
|
+
index = end;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const nextEscape = sourceLine.indexOf("\x1b", index);
|
|
399
|
+
const end = nextEscape === -1 ? sourceLine.length : nextEscape;
|
|
400
|
+
for (const cluster of graphemeClusters(sourceLine.slice(index, end))) {
|
|
401
|
+
const [renderedCluster, widthOfCluster] = displayCluster(cluster);
|
|
402
|
+
if (rowWidth > 0 && rowWidth + widthOfCluster > targetWidth) {
|
|
403
|
+
finishRow();
|
|
404
|
+
rowParts = [...sgrPrefix];
|
|
405
|
+
rowWidth = 0;
|
|
406
|
+
}
|
|
407
|
+
if (widthOfCluster > targetWidth) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
rowParts.push(renderedCluster);
|
|
411
|
+
rowWidth += widthOfCluster;
|
|
412
|
+
}
|
|
413
|
+
index = end;
|
|
414
|
+
}
|
|
415
|
+
finishRow();
|
|
416
|
+
}
|
|
417
|
+
return rows;
|
|
418
|
+
}
|
|
419
|
+
/** Remove all terminal escape sequences from a string. */
|
|
420
|
+
export function stripTerminalControls(text) {
|
|
421
|
+
let result = "";
|
|
422
|
+
let index = 0;
|
|
423
|
+
while (index < text.length) {
|
|
424
|
+
if (text[index] !== "\x1b") {
|
|
425
|
+
result += text[index];
|
|
426
|
+
index += 1;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
index = consumeEscapeSequence(text, index);
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
433
|
+
function consumeEscapeSequence(text, start) {
|
|
434
|
+
if (start + 1 >= text.length) {
|
|
435
|
+
return start + 1;
|
|
436
|
+
}
|
|
437
|
+
const marker = text[start + 1];
|
|
438
|
+
if (marker === "[") {
|
|
439
|
+
let index = start + 2;
|
|
440
|
+
while (index < text.length) {
|
|
441
|
+
const code = text.charCodeAt(index);
|
|
442
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
443
|
+
return index + 1;
|
|
444
|
+
}
|
|
445
|
+
index += 1;
|
|
446
|
+
}
|
|
447
|
+
return text.length;
|
|
448
|
+
}
|
|
449
|
+
if (marker === "]") {
|
|
450
|
+
return consumeStringSequence(text, start + 2, true);
|
|
451
|
+
}
|
|
452
|
+
if (marker === "P") {
|
|
453
|
+
return consumeStringSequence(text, start + 2, false);
|
|
454
|
+
}
|
|
455
|
+
if (marker === "_") {
|
|
456
|
+
return consumeStringSequence(text, start + 2, true);
|
|
457
|
+
}
|
|
458
|
+
return start + 2;
|
|
459
|
+
}
|
|
460
|
+
function consumeStringSequence(text, start, allowBel) {
|
|
461
|
+
let index = start;
|
|
462
|
+
while (index < text.length) {
|
|
463
|
+
if (allowBel && text[index] === "\x07") {
|
|
464
|
+
return index + 1;
|
|
465
|
+
}
|
|
466
|
+
if (text.slice(index, index + 2) === "\x1b\\") {
|
|
467
|
+
return index + 2;
|
|
468
|
+
}
|
|
469
|
+
index += 1;
|
|
470
|
+
}
|
|
471
|
+
return text.length;
|
|
472
|
+
}
|
|
473
|
+
function isSgrSequence(sequence) {
|
|
474
|
+
return sequence.startsWith("\x1b[") && sequence.endsWith("m");
|
|
475
|
+
}
|
|
476
|
+
function updateSgrPrefix(prefix, sequence) {
|
|
477
|
+
const params = sequence
|
|
478
|
+
.slice(2, -1)
|
|
479
|
+
.replace(/:/g, ";")
|
|
480
|
+
.split(";")
|
|
481
|
+
.filter((part) => part.length > 0);
|
|
482
|
+
if (params.length === 0 || params.every((param) => param === "0")) {
|
|
483
|
+
prefix.length = 0;
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (params.includes("0")) {
|
|
487
|
+
prefix.splice(0, prefix.length, sequence);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
prefix.push(sequence);
|
|
491
|
+
}
|
|
492
|
+
function displayCluster(cluster) {
|
|
493
|
+
if (cluster === "\t") {
|
|
494
|
+
return [" ", 3];
|
|
495
|
+
}
|
|
496
|
+
return [cluster, clusterWidth(cluster)];
|
|
497
|
+
}
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
// Grapheme clusters and display width
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
const graphemeSegmenter = new Intl.Segmenter("en", {
|
|
502
|
+
granularity: "grapheme",
|
|
503
|
+
});
|
|
504
|
+
/** Split a string into extended grapheme clusters. */
|
|
505
|
+
export function graphemeClusters(text) {
|
|
506
|
+
const clusters = [];
|
|
507
|
+
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
508
|
+
clusters.push(segment);
|
|
509
|
+
}
|
|
510
|
+
return clusters;
|
|
511
|
+
}
|
|
512
|
+
function isRegionalIndicator(codePoint) {
|
|
513
|
+
return codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff;
|
|
514
|
+
}
|
|
515
|
+
/** Visible cell width of one grapheme cluster (emoji/ZWJ sequences count 2). */
|
|
516
|
+
export function clusterWidth(cluster) {
|
|
517
|
+
if (!cluster) {
|
|
518
|
+
return 0;
|
|
519
|
+
}
|
|
520
|
+
if (cluster.includes("") ||
|
|
521
|
+
cluster.includes("️") ||
|
|
522
|
+
[...cluster].some((char) => isRegionalIndicator(char.codePointAt(0)))) {
|
|
523
|
+
return 2;
|
|
524
|
+
}
|
|
525
|
+
let width = 0;
|
|
526
|
+
for (const char of cluster) {
|
|
527
|
+
width += charCellWidth(char);
|
|
528
|
+
}
|
|
529
|
+
return Math.max(0, width);
|
|
530
|
+
}
|
|
531
|
+
/** Terminal cell width of a single code point (wcwidth equivalent). */
|
|
532
|
+
export function charCellWidth(char) {
|
|
533
|
+
const cp = char.codePointAt(0);
|
|
534
|
+
if (cp === 0x200b || (cp >= 0x200c && cp <= 0x200f)) {
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
if (cp < 0x20 || (cp >= 0x7f && cp < 0xa0)) {
|
|
538
|
+
return 0;
|
|
539
|
+
}
|
|
540
|
+
if (isZeroWidth(cp)) {
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
if (isWide(cp)) {
|
|
544
|
+
return 2;
|
|
545
|
+
}
|
|
546
|
+
return 1;
|
|
547
|
+
}
|
|
548
|
+
function isZeroWidth(cp) {
|
|
549
|
+
return ((cp >= 0x0300 && cp <= 0x036f) ||
|
|
550
|
+
(cp >= 0x0483 && cp <= 0x0489) ||
|
|
551
|
+
(cp >= 0x0591 && cp <= 0x05bd) ||
|
|
552
|
+
cp === 0x05bf ||
|
|
553
|
+
(cp >= 0x05c1 && cp <= 0x05c2) ||
|
|
554
|
+
(cp >= 0x05c4 && cp <= 0x05c5) ||
|
|
555
|
+
cp === 0x05c7 ||
|
|
556
|
+
(cp >= 0x0610 && cp <= 0x061a) ||
|
|
557
|
+
(cp >= 0x064b && cp <= 0x065f) ||
|
|
558
|
+
cp === 0x0670 ||
|
|
559
|
+
(cp >= 0x06d6 && cp <= 0x06dc) ||
|
|
560
|
+
(cp >= 0x0e31 && cp <= 0x0e3a) ||
|
|
561
|
+
(cp >= 0x0e47 && cp <= 0x0e4e) ||
|
|
562
|
+
(cp >= 0x1ab0 && cp <= 0x1aff) ||
|
|
563
|
+
(cp >= 0x1dc0 && cp <= 0x1dff) ||
|
|
564
|
+
cp === 0x200d ||
|
|
565
|
+
(cp >= 0x20d0 && cp <= 0x20ff) ||
|
|
566
|
+
(cp >= 0xfe00 && cp <= 0xfe0f) ||
|
|
567
|
+
(cp >= 0xfe20 && cp <= 0xfe2f) ||
|
|
568
|
+
(cp >= 0xe0100 && cp <= 0xe01ef));
|
|
569
|
+
}
|
|
570
|
+
function isWide(cp) {
|
|
571
|
+
return ((cp >= 0x1100 && cp <= 0x115f) ||
|
|
572
|
+
(cp >= 0x231a && cp <= 0x231b) ||
|
|
573
|
+
(cp >= 0x2329 && cp <= 0x232a) ||
|
|
574
|
+
(cp >= 0x23e9 && cp <= 0x23ec) ||
|
|
575
|
+
cp === 0x23f0 ||
|
|
576
|
+
cp === 0x23f3 ||
|
|
577
|
+
(cp >= 0x25fd && cp <= 0x25fe) ||
|
|
578
|
+
(cp >= 0x2614 && cp <= 0x2615) ||
|
|
579
|
+
(cp >= 0x2648 && cp <= 0x2653) ||
|
|
580
|
+
cp === 0x267f ||
|
|
581
|
+
cp === 0x2693 ||
|
|
582
|
+
cp === 0x26a1 ||
|
|
583
|
+
(cp >= 0x26aa && cp <= 0x26ab) ||
|
|
584
|
+
(cp >= 0x26bd && cp <= 0x26be) ||
|
|
585
|
+
(cp >= 0x26c4 && cp <= 0x26c5) ||
|
|
586
|
+
cp === 0x26ce ||
|
|
587
|
+
cp === 0x26d4 ||
|
|
588
|
+
cp === 0x26ea ||
|
|
589
|
+
(cp >= 0x26f2 && cp <= 0x26f3) ||
|
|
590
|
+
cp === 0x26f5 ||
|
|
591
|
+
(cp >= 0x26fa && cp <= 0x26fd) ||
|
|
592
|
+
cp === 0x2705 ||
|
|
593
|
+
(cp >= 0x270a && cp <= 0x270b) ||
|
|
594
|
+
cp === 0x2728 ||
|
|
595
|
+
cp === 0x274c ||
|
|
596
|
+
cp === 0x274e ||
|
|
597
|
+
(cp >= 0x2753 && cp <= 0x2755) ||
|
|
598
|
+
cp === 0x2757 ||
|
|
599
|
+
(cp >= 0x2795 && cp <= 0x2797) ||
|
|
600
|
+
cp === 0x27b0 ||
|
|
601
|
+
cp === 0x27bf ||
|
|
602
|
+
(cp >= 0x2b1b && cp <= 0x2b1c) ||
|
|
603
|
+
cp === 0x2b50 ||
|
|
604
|
+
cp === 0x2b55 ||
|
|
605
|
+
(cp >= 0x2e80 && cp <= 0x2fff) ||
|
|
606
|
+
(cp >= 0x3000 && cp <= 0x303e) ||
|
|
607
|
+
(cp >= 0x3041 && cp <= 0x33ff) ||
|
|
608
|
+
(cp >= 0x3400 && cp <= 0x4dbf) ||
|
|
609
|
+
(cp >= 0x4e00 && cp <= 0x9fff) ||
|
|
610
|
+
(cp >= 0xa000 && cp <= 0xa4cf) ||
|
|
611
|
+
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
612
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
613
|
+
(cp >= 0xfe10 && cp <= 0xfe19) ||
|
|
614
|
+
(cp >= 0xfe30 && cp <= 0xfe6f) ||
|
|
615
|
+
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
616
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
617
|
+
(cp >= 0x16fe0 && cp <= 0x16fe4) ||
|
|
618
|
+
(cp >= 0x1f300 && cp <= 0x1f64f) ||
|
|
619
|
+
(cp >= 0x1f900 && cp <= 0x1f9ff) ||
|
|
620
|
+
(cp >= 0x1fa70 && cp <= 0x1faff) ||
|
|
621
|
+
(cp >= 0x20000 && cp <= 0x2fffd) ||
|
|
622
|
+
(cp >= 0x30000 && cp <= 0x3fffd));
|
|
623
|
+
}
|
|
624
|
+
//# sourceMappingURL=screen.js.map
|