@selesai/code 0.8.2 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +4 -2
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +18 -0
- package/dist/cli/config-selector.js +2 -2
- package/dist/cli/startup-ui.d.ts +1 -1
- package/dist/cli/startup-ui.js +2 -2
- package/dist/core/keybindings.d.ts +44 -4
- package/dist/core/llama/provider.js +22 -7
- package/dist/core/model-resolver.js +2 -0
- package/dist/core/model-runtime.d.ts +4 -2
- package/dist/core/model-runtime.js +23 -2
- package/dist/core/remote-catalog-provider.js +72 -67
- package/dist/core/settings-manager.d.ts +8 -0
- package/dist/core/settings-manager.js +18 -0
- package/dist/extensions/pi-powerline-footer/CHANGELOG.md +2 -1
- package/dist/extensions/pi-powerline-footer/index.ts +3 -272
- package/dist/extensions/pi-powerline-footer/package.json +4 -5
- package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -6
- package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +2 -2
- package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +1 -1
- package/dist/extensions/pi-powerline-footer/tsconfig.json +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/main.js +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +6 -2
- package/dist/modes/interactive/components/settings-selector.js +20 -0
- package/dist/modes/interactive/interactive-mode.d.ts +28 -0
- package/dist/modes/interactive/interactive-mode.js +214 -28
- package/dist/modes/interactive/theme/dark.json +1 -0
- package/dist/modes/interactive/theme/light.json +1 -0
- package/dist/modes/interactive/theme/theme-controller.d.ts +3 -0
- package/dist/modes/interactive/theme/theme-controller.js +10 -1
- package/dist/modes/interactive/theme/theme.d.ts +2 -2
- package/dist/modes/interactive/theme/theme.js +24 -6
- package/docs/keybindings.md +35 -7
- package/docs/settings.md +16 -14
- package/docs/usage.md +5 -0
- package/package.json +5 -5
- package/dist/extensions/pi-powerline-footer/fixed-editor/cluster.ts +0 -113
- package/dist/extensions/pi-powerline-footer/fixed-editor/terminal-split.ts +0 -1080
- package/dist/extensions/pi-powerline-footer/tests/fixed-editor.test.ts +0 -1452
- package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -49
|
@@ -1,1080 +0,0 @@
|
|
|
1
|
-
import { isKeyRelease, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import { matchesConfiguredShortcut } from "../shortcuts.ts";
|
|
3
|
-
import type { FixedEditorClusterRender } from "./cluster.ts";
|
|
4
|
-
|
|
5
|
-
export interface TerminalLike {
|
|
6
|
-
columns: number;
|
|
7
|
-
rows: number;
|
|
8
|
-
kittyProtocolActive?: boolean;
|
|
9
|
-
write(data: string): void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface KeyboardScrollShortcuts {
|
|
13
|
-
up: string;
|
|
14
|
-
down: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface TerminalSplitCompositorOptions {
|
|
18
|
-
tui: any;
|
|
19
|
-
terminal: TerminalLike;
|
|
20
|
-
renderCluster: (width: number, terminalRows: number) => FixedEditorClusterRender;
|
|
21
|
-
getShowHardwareCursor?: () => boolean;
|
|
22
|
-
mouseScroll?: boolean;
|
|
23
|
-
keyboardScrollShortcuts?: KeyboardScrollShortcuts;
|
|
24
|
-
shouldCaptureKeyboardScroll?: () => boolean;
|
|
25
|
-
onCopySelection?: (text: string) => void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface PatchedRenderable {
|
|
29
|
-
render(width: number): string[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface RenderPatch {
|
|
33
|
-
target: PatchedRenderable;
|
|
34
|
-
originalRender: (width: number) => string[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface RenderPassCluster {
|
|
38
|
-
width: number;
|
|
39
|
-
terminalRows: number;
|
|
40
|
-
cluster: FixedEditorClusterRender;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
type CompositeLineAt = (
|
|
44
|
-
baseLine: string,
|
|
45
|
-
overlayLine: string,
|
|
46
|
-
startCol: number,
|
|
47
|
-
overlayWidth: number,
|
|
48
|
-
totalWidth: number,
|
|
49
|
-
) => string;
|
|
50
|
-
|
|
51
|
-
interface SgrMousePacket {
|
|
52
|
-
code: number;
|
|
53
|
-
col: number;
|
|
54
|
-
row: number;
|
|
55
|
-
final: "M" | "m";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface SelectionPoint {
|
|
59
|
-
line: number;
|
|
60
|
-
col: number;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
type SelectionArea = "root" | "cluster";
|
|
64
|
-
|
|
65
|
-
interface SelectionLocation {
|
|
66
|
-
area: SelectionArea;
|
|
67
|
-
point: SelectionPoint;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
interface DisposeOptions {
|
|
71
|
-
resetExtendedKeyboardModes?: boolean;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
type ExtendedKeyboardMode = "kitty" | "modifyOtherKeys";
|
|
75
|
-
|
|
76
|
-
const CONTEXT_MENU_MOUSE_REPORTING_PAUSE_MS = 1200;
|
|
77
|
-
const CONTEXT_MENU_SELECTION_RESTORE_WINDOW_MS = 5000;
|
|
78
|
-
const CONTEXT_MENU_CLIPBOARD_RESTORE_INTERVAL_MS = 100;
|
|
79
|
-
const DOUBLE_CLICK_MS = 500;
|
|
80
|
-
const DEFAULT_KEYBOARD_SCROLL_SHORTCUTS: KeyboardScrollShortcuts = {
|
|
81
|
-
up: "super+up",
|
|
82
|
-
down: "super+down",
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
export function beginSynchronizedOutput(): string {
|
|
86
|
-
return "\x1b[?2026h";
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function endSynchronizedOutput(): string {
|
|
90
|
-
return "\x1b[?2026l";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function setScrollRegion(top: number, bottom: number): string {
|
|
94
|
-
return `\x1b[${top};${bottom}r`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function resetScrollRegion(): string {
|
|
98
|
-
return "\x1b[r";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function moveCursor(row: number, col: number): string {
|
|
102
|
-
return `\x1b[${row};${col}H`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function clearLine(): string {
|
|
106
|
-
return "\x1b[2K";
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function hideCursor(): string {
|
|
110
|
-
return "\x1b[?25l";
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function showCursor(): string {
|
|
114
|
-
return "\x1b[?25h";
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function enterAlternateScreen(): string {
|
|
118
|
-
return "\x1b[?1049h";
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function exitAlternateScreen(): string {
|
|
122
|
-
return "\x1b[?1049l";
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function enableAlternateScrollMode(): string {
|
|
126
|
-
return "\x1b[?1007h";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function disableAlternateScrollMode(): string {
|
|
130
|
-
return "\x1b[?1007l";
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function enableMouseReporting(): string {
|
|
134
|
-
return "\x1b[?1002h\x1b[?1006h";
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function disableMouseReporting(): string {
|
|
138
|
-
return "\x1b[?1006l\x1b[?1002l\x1b[?1000l";
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function enableExtendedKeyboardMode(mode: ExtendedKeyboardMode): string {
|
|
142
|
-
return mode === "kitty" ? "\x1b[>7u" : "\x1b[>4;2m";
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function disableExtendedKeyboardMode(mode: ExtendedKeyboardMode): string {
|
|
146
|
-
return mode === "kitty" ? "\x1b[<u" : "\x1b[>4;0m";
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function resetExtendedKeyboardModes(): string {
|
|
150
|
-
return "\x1b[<999u\x1b[>4;0m";
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function emergencyTerminalModeReset(): string {
|
|
154
|
-
return beginSynchronizedOutput()
|
|
155
|
-
+ resetScrollRegion()
|
|
156
|
-
+ disableMouseReporting()
|
|
157
|
-
+ enableAlternateScrollMode()
|
|
158
|
-
+ exitAlternateScreen()
|
|
159
|
-
+ resetExtendedKeyboardModes()
|
|
160
|
-
+ endSynchronizedOutput();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function parseKeyboardScrollDelta(data: string, shortcuts: KeyboardScrollShortcuts = DEFAULT_KEYBOARD_SCROLL_SHORTCUTS): number {
|
|
164
|
-
if (isKeyRelease(data)) return 0;
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
matchesConfiguredShortcut(data, shortcuts.up)
|
|
168
|
-
|| matchesKey(data, "pageUp")
|
|
169
|
-
|| matchesKey(data, "ctrl+shift+up")
|
|
170
|
-
|| /^\x1b\[(?:5;9(?::[12])?~|1;6(?::[12])?A|57421;9(?::[12])?u|57419;6(?::[12])?u)$/.test(data)
|
|
171
|
-
) return 10;
|
|
172
|
-
if (
|
|
173
|
-
matchesConfiguredShortcut(data, shortcuts.down)
|
|
174
|
-
|| matchesKey(data, "pageDown")
|
|
175
|
-
|| matchesKey(data, "ctrl+shift+down")
|
|
176
|
-
|| /^\x1b\[(?:6;9(?::[12])?~|1;6(?::[12])?B|57422;9(?::[12])?u|57420;6(?::[12])?u)$/.test(data)
|
|
177
|
-
) return -10;
|
|
178
|
-
return 0;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
|
|
182
|
-
const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
183
|
-
const packets: SgrMousePacket[] = [];
|
|
184
|
-
let offset = 0;
|
|
185
|
-
|
|
186
|
-
for (const match of data.matchAll(pattern)) {
|
|
187
|
-
if (match.index !== offset) return null;
|
|
188
|
-
offset = match.index + match[0].length;
|
|
189
|
-
packets.push({
|
|
190
|
-
code: Number(match[1]),
|
|
191
|
-
col: Number(match[2]),
|
|
192
|
-
row: Number(match[3]),
|
|
193
|
-
final: match[4] as "M" | "m",
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return packets.length > 0 && offset === data.length ? packets : null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function mouseBaseButton(code: number): number {
|
|
201
|
-
return code & ~(4 | 8 | 16 | 32);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function mouseScrollDelta(packet: SgrMousePacket): number {
|
|
205
|
-
if (packet.final !== "M") return 0;
|
|
206
|
-
const baseButton = mouseBaseButton(packet.code);
|
|
207
|
-
if (baseButton === 64) return 3;
|
|
208
|
-
if (baseButton === 65) return -3;
|
|
209
|
-
return 0;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function isLeftPress(packet: SgrMousePacket): boolean {
|
|
213
|
-
return packet.final === "M" && mouseBaseButton(packet.code) === 0 && (packet.code & 32) === 0;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function isLeftDrag(packet: SgrMousePacket): boolean {
|
|
217
|
-
return packet.final === "M" && mouseBaseButton(packet.code) === 0 && (packet.code & 32) !== 0;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function isRightPress(packet: SgrMousePacket): boolean {
|
|
221
|
-
return packet.final === "M" && mouseBaseButton(packet.code) === 2 && (packet.code & 32) === 0;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function isMouseRelease(packet: SgrMousePacket): boolean {
|
|
225
|
-
return packet.final === "m";
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function stripOscSequences(line: string): string {
|
|
229
|
-
return line.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "");
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function stripAnsi(line: string): string {
|
|
233
|
-
return stripOscSequences(line).replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
237
|
-
|
|
238
|
-
function sliceColumns(text: string, startCol: number, endCol: number): string {
|
|
239
|
-
let col = 0;
|
|
240
|
-
let result = "";
|
|
241
|
-
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
242
|
-
const width = Math.max(0, visibleWidth(segment));
|
|
243
|
-
if (col >= startCol && col < endCol) {
|
|
244
|
-
result += segment;
|
|
245
|
-
}
|
|
246
|
-
col += width;
|
|
247
|
-
}
|
|
248
|
-
return result;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function compareSelectionPoints(a: SelectionPoint, b: SelectionPoint): number {
|
|
252
|
-
return a.line === b.line ? a.col - b.col : a.line - b.line;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function descriptorForRows(terminal: TerminalLike): PropertyDescriptor | undefined {
|
|
256
|
-
let target: object | null = terminal;
|
|
257
|
-
while (target) {
|
|
258
|
-
const descriptor = Object.getOwnPropertyDescriptor(target, "rows");
|
|
259
|
-
if (descriptor) return descriptor;
|
|
260
|
-
target = Object.getPrototypeOf(target);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return undefined;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function readRows(terminal: TerminalLike, descriptor: PropertyDescriptor | undefined): number {
|
|
267
|
-
if (descriptor?.get) {
|
|
268
|
-
const value = descriptor.get.call(terminal);
|
|
269
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 24;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const value = Reflect.get(terminal, "rows");
|
|
273
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 24;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function sanitizeLine(line: string, width: number): string {
|
|
277
|
-
return visibleWidth(line) > width ? truncateToWidth(line, width, "", true) : line;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function sanitizeOverlayBaseLine(line: string, width: number): string {
|
|
281
|
-
return sanitizeLine(stripOscSequences(line), width);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function normalizeOverlayCompositionLine(line: string): string {
|
|
285
|
-
return line.includes("\t") ? line.replace(/\t/g, " ") : line;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
export function buildFixedClusterPaint(
|
|
289
|
-
cluster: FixedEditorClusterRender,
|
|
290
|
-
terminalRows: number,
|
|
291
|
-
width: number,
|
|
292
|
-
showHardwareCursor: boolean,
|
|
293
|
-
): string {
|
|
294
|
-
if (cluster.lines.length === 0) return "";
|
|
295
|
-
|
|
296
|
-
const startRow = Math.max(1, terminalRows - cluster.lines.length + 1);
|
|
297
|
-
let buffer = resetScrollRegion();
|
|
298
|
-
|
|
299
|
-
for (let i = 0; i < cluster.lines.length; i++) {
|
|
300
|
-
buffer += moveCursor(startRow + i, 1);
|
|
301
|
-
buffer += clearLine();
|
|
302
|
-
buffer += sanitizeLine(cluster.lines[i] ?? "", width);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (cluster.cursor && showHardwareCursor) {
|
|
306
|
-
buffer += moveCursor(startRow + cluster.cursor.row, Math.max(1, cluster.cursor.col + 1));
|
|
307
|
-
buffer += showCursor();
|
|
308
|
-
} else {
|
|
309
|
-
buffer += hideCursor();
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return buffer;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export class TerminalSplitCompositor {
|
|
316
|
-
private readonly tui: any;
|
|
317
|
-
private readonly terminal: TerminalLike;
|
|
318
|
-
private readonly renderCluster: (width: number, terminalRows: number) => FixedEditorClusterRender;
|
|
319
|
-
private readonly getShowHardwareCursor: () => boolean;
|
|
320
|
-
private readonly mouseScroll: boolean;
|
|
321
|
-
private readonly keyboardScrollShortcuts: KeyboardScrollShortcuts;
|
|
322
|
-
private readonly shouldCaptureKeyboardScroll: () => boolean;
|
|
323
|
-
private readonly onCopySelection: ((text: string) => void) | null;
|
|
324
|
-
private extendedKeyboardMode: ExtendedKeyboardMode | null = null;
|
|
325
|
-
private readonly rowsDescriptor: PropertyDescriptor | undefined;
|
|
326
|
-
private readonly originalWrite: (data: string) => void;
|
|
327
|
-
private readonly originalDoRender: (() => void) | null;
|
|
328
|
-
private readonly originalRender: ((width: number) => string[]) | null;
|
|
329
|
-
private originalCompositeLineAt: CompositeLineAt | null = null;
|
|
330
|
-
private readonly patchedRenders: RenderPatch[] = [];
|
|
331
|
-
private removeInputListener: (() => void) | null = null;
|
|
332
|
-
private emergencyCleanup: (() => void) | null = null;
|
|
333
|
-
private mouseReportingResumeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
334
|
-
private clipboardRestoreTimer: ReturnType<typeof setTimeout> | null = null;
|
|
335
|
-
private installed = false;
|
|
336
|
-
private disposed = false;
|
|
337
|
-
private writing = false;
|
|
338
|
-
private renderPassActive = false;
|
|
339
|
-
private renderPassCluster: RenderPassCluster | null = null;
|
|
340
|
-
private renderingCluster = false;
|
|
341
|
-
private renderingScrollableRoot = false;
|
|
342
|
-
private checkingOverlay = false;
|
|
343
|
-
private scrollOffset = 0;
|
|
344
|
-
private maxScrollOffset = 0;
|
|
345
|
-
private lastRootLineCount = 0;
|
|
346
|
-
private rootLines: string[] = [];
|
|
347
|
-
private visibleRootStart = 0;
|
|
348
|
-
private visibleScrollableRows = 0;
|
|
349
|
-
private visibleRootLines: string[] = [];
|
|
350
|
-
private visibleClusterLines: string[] = [];
|
|
351
|
-
private selectionArea: SelectionArea | null = null;
|
|
352
|
-
private selectionAnchor: SelectionPoint | null = null;
|
|
353
|
-
private selectionFocus: SelectionPoint | null = null;
|
|
354
|
-
private selectionDragging = false;
|
|
355
|
-
private preserveSelectionFocusOnRelease = false;
|
|
356
|
-
private lastLeftPress: { area: SelectionArea; line: number; at: number } | null = null;
|
|
357
|
-
|
|
358
|
-
constructor(options: TerminalSplitCompositorOptions) {
|
|
359
|
-
this.tui = options.tui;
|
|
360
|
-
this.terminal = options.terminal;
|
|
361
|
-
this.renderCluster = options.renderCluster;
|
|
362
|
-
this.getShowHardwareCursor = options.getShowHardwareCursor ?? (() => false);
|
|
363
|
-
this.mouseScroll = options.mouseScroll !== false;
|
|
364
|
-
this.keyboardScrollShortcuts = options.keyboardScrollShortcuts ?? DEFAULT_KEYBOARD_SCROLL_SHORTCUTS;
|
|
365
|
-
this.shouldCaptureKeyboardScroll = options.shouldCaptureKeyboardScroll ?? (() => true);
|
|
366
|
-
this.onCopySelection = options.onCopySelection ?? null;
|
|
367
|
-
this.rowsDescriptor = descriptorForRows(options.terminal);
|
|
368
|
-
this.originalWrite = options.terminal.write.bind(options.terminal);
|
|
369
|
-
this.originalDoRender = typeof options.tui.doRender === "function" ? options.tui.doRender.bind(options.tui) : null;
|
|
370
|
-
this.originalRender = typeof options.tui.render === "function" ? options.tui.render.bind(options.tui) : null;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
install(): void {
|
|
374
|
-
if (this.installed) return;
|
|
375
|
-
if (typeof this.terminal.write !== "function") {
|
|
376
|
-
throw new Error("[powerline-footer] Fixed editor compositor expected terminal.write(data) to exist");
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
this.originalWrite(
|
|
380
|
-
beginSynchronizedOutput()
|
|
381
|
-
+ enterAlternateScreen()
|
|
382
|
-
+ this.enableAlternateScreenKeyboardMode()
|
|
383
|
-
+ disableAlternateScrollMode()
|
|
384
|
-
+ (this.mouseScroll ? enableMouseReporting() : "")
|
|
385
|
-
+ endSynchronizedOutput(),
|
|
386
|
-
);
|
|
387
|
-
this.emergencyCleanup = () => {
|
|
388
|
-
if (!this.disposed) {
|
|
389
|
-
this.restoreTerminalStateForExit();
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
process.once("exit", this.emergencyCleanup);
|
|
393
|
-
|
|
394
|
-
Object.defineProperty(this.terminal, "rows", {
|
|
395
|
-
configurable: true,
|
|
396
|
-
get: () => this.getScrollableRows(),
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
if (this.originalRender) {
|
|
400
|
-
this.tui.render = (width: number) => this.renderScrollableRoot(width);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (typeof this.tui.addInputListener === "function") {
|
|
404
|
-
this.removeInputListener = this.tui.addInputListener((data: string) => this.handleInput(data));
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
this.terminal.write = (data: string) => this.write(data);
|
|
408
|
-
if (this.originalDoRender) {
|
|
409
|
-
this.tui.doRender = () => {
|
|
410
|
-
this.renderPassActive = true;
|
|
411
|
-
this.renderPassCluster = null;
|
|
412
|
-
try {
|
|
413
|
-
this.originalDoRender?.();
|
|
414
|
-
this.requestRepaint();
|
|
415
|
-
} finally {
|
|
416
|
-
this.renderPassActive = false;
|
|
417
|
-
this.renderPassCluster = null;
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
if (typeof this.tui.compositeLineAt === "function") {
|
|
422
|
-
this.originalCompositeLineAt = this.tui.compositeLineAt.bind(this.tui) as CompositeLineAt;
|
|
423
|
-
this.tui.compositeLineAt = (
|
|
424
|
-
baseLine: string,
|
|
425
|
-
overlayLine: string,
|
|
426
|
-
startCol: number,
|
|
427
|
-
overlayWidth: number,
|
|
428
|
-
totalWidth: number,
|
|
429
|
-
) => this.originalCompositeLineAt?.(
|
|
430
|
-
normalizeOverlayCompositionLine(baseLine),
|
|
431
|
-
normalizeOverlayCompositionLine(overlayLine),
|
|
432
|
-
startCol,
|
|
433
|
-
overlayWidth,
|
|
434
|
-
totalWidth,
|
|
435
|
-
) ?? "";
|
|
436
|
-
}
|
|
437
|
-
this.installed = true;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
hideRenderable(target: PatchedRenderable): void {
|
|
441
|
-
if (this.patchedRenders.some((patch) => patch.target === target)) return;
|
|
442
|
-
const originalRender = target.render.bind(target);
|
|
443
|
-
this.patchedRenders.push({ target, originalRender });
|
|
444
|
-
target.render = () => [];
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
renderHidden(target: PatchedRenderable, width: number): string[] {
|
|
448
|
-
const patch = this.patchedRenders.find((candidate) => candidate.target === target);
|
|
449
|
-
const render = patch?.originalRender ?? target.render.bind(target);
|
|
450
|
-
return render(width);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
jumpToPreviousRootTarget(targetLines: readonly number[]): boolean {
|
|
454
|
-
return this.jumpToRootTarget(targetLines, "previous");
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
jumpToNextRootTarget(targetLines: readonly number[]): boolean {
|
|
458
|
-
return this.jumpToRootTarget(targetLines, "next");
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
jumpToRootBottom(): boolean {
|
|
462
|
-
if (this.disposed || this.hasVisibleOverlay() || this.scrollOffset === 0) return false;
|
|
463
|
-
|
|
464
|
-
this.clearSelection();
|
|
465
|
-
this.lastLeftPress = null;
|
|
466
|
-
this.scrollOffset = 0;
|
|
467
|
-
this.requestRender();
|
|
468
|
-
return true;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
private jumpToRootTarget(targetLines: readonly number[], direction: "previous" | "next"): boolean {
|
|
472
|
-
if (this.disposed || targetLines.length === 0 || this.hasVisibleOverlay()) return false;
|
|
473
|
-
|
|
474
|
-
const start = this.visibleRootStart;
|
|
475
|
-
const candidates = direction === "previous"
|
|
476
|
-
? targetLines.filter((line) => line < start).sort((a, b) => b - a)
|
|
477
|
-
: targetLines.filter((line) => line > start).sort((a, b) => a - b);
|
|
478
|
-
|
|
479
|
-
for (const target of candidates) {
|
|
480
|
-
const nextOffset = Math.max(0, Math.min(
|
|
481
|
-
this.lastRootLineCount - Math.max(1, this.visibleScrollableRows) - target,
|
|
482
|
-
this.maxScrollOffset,
|
|
483
|
-
));
|
|
484
|
-
if (nextOffset === this.scrollOffset) continue;
|
|
485
|
-
|
|
486
|
-
this.clearSelection();
|
|
487
|
-
this.lastLeftPress = null;
|
|
488
|
-
this.scrollOffset = nextOffset;
|
|
489
|
-
this.requestRender();
|
|
490
|
-
return true;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
requestRepaint(): void {
|
|
497
|
-
if (this.disposed || this.hasVisibleOverlay()) return;
|
|
498
|
-
const rawRows = this.getRawRows();
|
|
499
|
-
const width = Math.max(1, this.terminal.columns || 80);
|
|
500
|
-
const cluster = this.getCluster(width, rawRows);
|
|
501
|
-
if (cluster.lines.length === 0) return;
|
|
502
|
-
|
|
503
|
-
this.originalWrite(
|
|
504
|
-
beginSynchronizedOutput()
|
|
505
|
-
+ buildFixedClusterPaint(this.decorateCluster(cluster), rawRows, width, this.getShowHardwareCursor())
|
|
506
|
-
+ endSynchronizedOutput(),
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
dispose(options: DisposeOptions = {}): void {
|
|
511
|
-
if (this.disposed) return;
|
|
512
|
-
this.disposed = true;
|
|
513
|
-
|
|
514
|
-
for (const patch of this.patchedRenders.splice(0)) {
|
|
515
|
-
patch.target.render = patch.originalRender;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
this.removeInputListener?.();
|
|
519
|
-
this.removeInputListener = null;
|
|
520
|
-
if (this.emergencyCleanup) {
|
|
521
|
-
process.removeListener("exit", this.emergencyCleanup);
|
|
522
|
-
this.emergencyCleanup = null;
|
|
523
|
-
}
|
|
524
|
-
if (this.mouseReportingResumeTimer) {
|
|
525
|
-
clearTimeout(this.mouseReportingResumeTimer);
|
|
526
|
-
this.mouseReportingResumeTimer = null;
|
|
527
|
-
}
|
|
528
|
-
if (this.clipboardRestoreTimer) {
|
|
529
|
-
clearTimeout(this.clipboardRestoreTimer);
|
|
530
|
-
this.clipboardRestoreTimer = null;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
this.terminal.write = this.originalWrite;
|
|
534
|
-
if (this.originalDoRender) {
|
|
535
|
-
this.tui.doRender = this.originalDoRender;
|
|
536
|
-
}
|
|
537
|
-
if (this.originalRender) {
|
|
538
|
-
this.tui.render = this.originalRender;
|
|
539
|
-
}
|
|
540
|
-
if (this.originalCompositeLineAt) {
|
|
541
|
-
this.tui.compositeLineAt = this.originalCompositeLineAt;
|
|
542
|
-
this.originalCompositeLineAt = null;
|
|
543
|
-
}
|
|
544
|
-
if (this.rowsDescriptor) {
|
|
545
|
-
Object.defineProperty(this.terminal, "rows", this.rowsDescriptor);
|
|
546
|
-
} else {
|
|
547
|
-
Reflect.deleteProperty(this.terminal, "rows");
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
this.restoreTerminalState(options);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
private getRawRows(): number {
|
|
554
|
-
return Math.max(2, readRows(this.terminal, this.rowsDescriptor));
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
private getScrollableRows(): number {
|
|
558
|
-
if (this.disposed || this.writing || this.renderingCluster || this.checkingOverlay || this.hasVisibleOverlay()) {
|
|
559
|
-
return this.getRawRows();
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
const rawRows = this.getRawRows();
|
|
563
|
-
const width = Math.max(1, this.terminal.columns || 80);
|
|
564
|
-
const cluster = this.getCluster(width, rawRows);
|
|
565
|
-
return Math.max(1, rawRows - cluster.lines.length);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
private renderScrollableRoot(width: number): string[] {
|
|
569
|
-
if (!this.originalRender || this.disposed || this.renderingScrollableRoot) {
|
|
570
|
-
return this.originalRender?.(width) ?? [];
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (this.hasVisibleOverlay()) {
|
|
574
|
-
return this.originalRender(width).map((line) => sanitizeOverlayBaseLine(line, Math.max(1, width)));
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
this.renderingScrollableRoot = true;
|
|
578
|
-
try {
|
|
579
|
-
const start = this.refreshRootWindow(width);
|
|
580
|
-
return this.visibleRootLines.map((line, index) => {
|
|
581
|
-
return this.renderSelectionHighlight(line, start + index, "root");
|
|
582
|
-
});
|
|
583
|
-
} finally {
|
|
584
|
-
this.renderingScrollableRoot = false;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
private refreshRootWindow(width: number): number {
|
|
589
|
-
if (!this.originalRender) return this.updateVisibleRootWindow();
|
|
590
|
-
|
|
591
|
-
const rawRows = this.getRawRows();
|
|
592
|
-
const renderWidth = Math.max(1, width);
|
|
593
|
-
const cluster = this.getCluster(renderWidth, rawRows);
|
|
594
|
-
const scrollableRows = Math.max(1, rawRows - cluster.lines.length);
|
|
595
|
-
const lines = this.originalRender(renderWidth);
|
|
596
|
-
this.rootLines = lines;
|
|
597
|
-
if (this.scrollOffset > 0 && this.lastRootLineCount > 0 && lines.length > this.lastRootLineCount) {
|
|
598
|
-
this.scrollOffset += lines.length - this.lastRootLineCount;
|
|
599
|
-
}
|
|
600
|
-
this.lastRootLineCount = lines.length;
|
|
601
|
-
this.maxScrollOffset = Math.max(0, lines.length - scrollableRows);
|
|
602
|
-
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, this.maxScrollOffset));
|
|
603
|
-
|
|
604
|
-
return this.updateVisibleRootWindow(scrollableRows);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
private handleInput(data: string): { consume?: boolean; data?: string } | undefined {
|
|
608
|
-
if (this.disposed || this.hasVisibleOverlay() || !this.shouldCaptureKeyboardScroll()) return undefined;
|
|
609
|
-
|
|
610
|
-
const mousePackets = this.mouseScroll ? parseSgrMousePackets(data) : null;
|
|
611
|
-
if (mousePackets) {
|
|
612
|
-
for (const packet of mousePackets) {
|
|
613
|
-
this.handleMousePacket(packet);
|
|
614
|
-
}
|
|
615
|
-
return { consume: true };
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const keyboardDelta = parseKeyboardScrollDelta(data, this.keyboardScrollShortcuts);
|
|
619
|
-
if (keyboardDelta === 0) return undefined;
|
|
620
|
-
|
|
621
|
-
this.scrollBy(keyboardDelta);
|
|
622
|
-
return { consume: true };
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
private handleMousePacket(packet: SgrMousePacket): void {
|
|
626
|
-
const delta = mouseScrollDelta(packet);
|
|
627
|
-
if (delta !== 0) {
|
|
628
|
-
this.selectionDragging = false;
|
|
629
|
-
this.scrollBy(delta);
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const location = this.selectionLocationForPacket(packet);
|
|
634
|
-
|
|
635
|
-
if (isRightPress(packet)) {
|
|
636
|
-
this.selectionDragging = false;
|
|
637
|
-
this.preserveSelectionFocusOnRelease = false;
|
|
638
|
-
const selectedText = this.isLocationInsideSelection(location) ? this.getSelectedText() : "";
|
|
639
|
-
if (selectedText) {
|
|
640
|
-
this.onCopySelection?.(selectedText);
|
|
641
|
-
this.lastLeftPress = null;
|
|
642
|
-
this.pauseMouseReportingForContextMenu(selectedText);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
this.clearSelection();
|
|
647
|
-
this.lastLeftPress = null;
|
|
648
|
-
this.pauseMouseReportingForContextMenu();
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (this.scrollSelectionAtViewportEdge(packet)) return;
|
|
653
|
-
if (this.selectionDragging && isMouseRelease(packet)) {
|
|
654
|
-
this.finishSelection(packet, location);
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
if (!location) return;
|
|
659
|
-
|
|
660
|
-
if (isLeftPress(packet)) {
|
|
661
|
-
this.startSelection(location);
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
if (this.selectionDragging && isLeftDrag(packet) && location.area === this.selectionArea) {
|
|
666
|
-
this.lastLeftPress = null;
|
|
667
|
-
this.preserveSelectionFocusOnRelease = false;
|
|
668
|
-
this.selectionFocus = location.point;
|
|
669
|
-
this.requestRender();
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
private updateVisibleRootWindow(scrollableRows = this.visibleScrollableRows): number {
|
|
675
|
-
const rows = Math.max(1, scrollableRows);
|
|
676
|
-
const start = Math.max(0, this.rootLines.length - rows - this.scrollOffset);
|
|
677
|
-
const visibleLines = this.rootLines.slice(start, start + rows);
|
|
678
|
-
while (visibleLines.length < rows) {
|
|
679
|
-
visibleLines.push("");
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
this.visibleRootStart = start;
|
|
683
|
-
this.visibleScrollableRows = rows;
|
|
684
|
-
this.visibleRootLines = visibleLines;
|
|
685
|
-
return start;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
private finishSelection(packet: SgrMousePacket, location: SelectionLocation | null): void {
|
|
689
|
-
if (!this.preserveSelectionFocusOnRelease) {
|
|
690
|
-
this.selectionFocus = location?.area === this.selectionArea
|
|
691
|
-
? location.point
|
|
692
|
-
: this.clampedSelectionPointForPacket(packet, this.selectionArea);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
this.preserveSelectionFocusOnRelease = false;
|
|
696
|
-
this.selectionDragging = false;
|
|
697
|
-
const selectedText = this.getSelectedText();
|
|
698
|
-
if (selectedText) {
|
|
699
|
-
this.lastLeftPress = null;
|
|
700
|
-
this.onCopySelection?.(selectedText);
|
|
701
|
-
} else {
|
|
702
|
-
this.clearSelection();
|
|
703
|
-
}
|
|
704
|
-
this.requestRender();
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
private startSelection(location: SelectionLocation): void {
|
|
708
|
-
const now = Date.now();
|
|
709
|
-
const line = location.point.line;
|
|
710
|
-
if (
|
|
711
|
-
this.lastLeftPress
|
|
712
|
-
&& this.lastLeftPress.area === location.area
|
|
713
|
-
&& this.lastLeftPress.line === line
|
|
714
|
-
&& now - this.lastLeftPress.at <= DOUBLE_CLICK_MS
|
|
715
|
-
) {
|
|
716
|
-
this.selectionArea = location.area;
|
|
717
|
-
this.selectionAnchor = { line, col: 0 };
|
|
718
|
-
this.selectionFocus = { line, col: this.selectionLineWidth(location.area, line) };
|
|
719
|
-
this.selectionDragging = true;
|
|
720
|
-
this.preserveSelectionFocusOnRelease = true;
|
|
721
|
-
this.lastLeftPress = null;
|
|
722
|
-
this.requestRender();
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
this.selectionArea = location.area;
|
|
727
|
-
this.selectionAnchor = location.point;
|
|
728
|
-
this.selectionFocus = location.point;
|
|
729
|
-
this.selectionDragging = true;
|
|
730
|
-
this.preserveSelectionFocusOnRelease = false;
|
|
731
|
-
this.lastLeftPress = { area: location.area, line, at: now };
|
|
732
|
-
this.requestRender();
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
private selectionLocationForPacket(packet: SgrMousePacket): SelectionLocation | null {
|
|
736
|
-
if (packet.row < 1) return null;
|
|
737
|
-
|
|
738
|
-
const col = Math.max(0, packet.col - 1);
|
|
739
|
-
if (packet.row <= this.visibleScrollableRows) {
|
|
740
|
-
return {
|
|
741
|
-
area: "root",
|
|
742
|
-
point: { line: this.visibleRootStart + packet.row - 1, col },
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
const clusterLine = packet.row - this.visibleScrollableRows - 1;
|
|
747
|
-
if (clusterLine >= this.visibleClusterLines.length) return null;
|
|
748
|
-
|
|
749
|
-
return {
|
|
750
|
-
area: "cluster",
|
|
751
|
-
point: { line: clusterLine, col },
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
private scrollSelectionAtViewportEdge(packet: SgrMousePacket): boolean {
|
|
756
|
-
if (!this.selectionDragging || this.selectionArea !== "root" || !isLeftDrag(packet)) return false;
|
|
757
|
-
|
|
758
|
-
const delta = packet.row <= 1 ? 1 : packet.row >= this.visibleScrollableRows ? -1 : 0;
|
|
759
|
-
if (delta === 0) return false;
|
|
760
|
-
|
|
761
|
-
const nextOffset = Math.max(0, Math.min(this.scrollOffset + delta, this.maxScrollOffset));
|
|
762
|
-
if (nextOffset === this.scrollOffset) return false;
|
|
763
|
-
|
|
764
|
-
this.lastLeftPress = null;
|
|
765
|
-
this.preserveSelectionFocusOnRelease = true;
|
|
766
|
-
this.scrollOffset = nextOffset;
|
|
767
|
-
const start = this.updateVisibleRootWindow();
|
|
768
|
-
const edgeLine = delta > 0 ? start : start + Math.max(0, this.visibleScrollableRows - 1);
|
|
769
|
-
this.selectionFocus = {
|
|
770
|
-
line: edgeLine,
|
|
771
|
-
col: Math.max(0, packet.col - 1),
|
|
772
|
-
};
|
|
773
|
-
this.requestRender();
|
|
774
|
-
return true;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
private clampedSelectionPointForPacket(packet: SgrMousePacket, area: SelectionArea | null): SelectionPoint {
|
|
778
|
-
if (area === "cluster") {
|
|
779
|
-
return {
|
|
780
|
-
line: Math.max(0, Math.min(packet.row - this.visibleScrollableRows - 1, this.visibleClusterLines.length - 1)),
|
|
781
|
-
col: Math.max(0, packet.col - 1),
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
const row = Math.max(1, Math.min(packet.row, this.visibleScrollableRows));
|
|
786
|
-
return {
|
|
787
|
-
line: this.visibleRootStart + row - 1,
|
|
788
|
-
col: Math.max(0, packet.col - 1),
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
private renderSelectionHighlight(line: string, lineIndex: number, area: SelectionArea): string {
|
|
793
|
-
const range = this.getSelectionRangeForLine(lineIndex, area);
|
|
794
|
-
if (!range) return line;
|
|
795
|
-
|
|
796
|
-
const plain = stripAnsi(line);
|
|
797
|
-
const startCol = Math.max(0, Math.min(range.startCol, visibleWidth(plain)));
|
|
798
|
-
const endCol = Math.max(startCol, Math.min(range.endCol, visibleWidth(plain)));
|
|
799
|
-
if (startCol === endCol) return line;
|
|
800
|
-
|
|
801
|
-
const before = sliceColumns(plain, 0, startCol);
|
|
802
|
-
const selected = sliceColumns(plain, startCol, endCol);
|
|
803
|
-
const after = sliceColumns(plain, endCol, Number.POSITIVE_INFINITY);
|
|
804
|
-
return `${before}\x1b[7m${selected}\x1b[27m${after}`;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
private selectionLineWidth(area: SelectionArea, lineIndex: number): number {
|
|
808
|
-
const lines = area === "root" ? this.visibleRootLines : this.visibleClusterLines;
|
|
809
|
-
const firstLine = area === "root" ? this.visibleRootStart : 0;
|
|
810
|
-
return visibleWidth(stripAnsi(lines[lineIndex - firstLine] ?? ""));
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
private getSelectedText(): string {
|
|
814
|
-
if (!this.selectionArea || !this.selectionAnchor || !this.selectionFocus) return "";
|
|
815
|
-
|
|
816
|
-
const start = compareSelectionPoints(this.selectionAnchor, this.selectionFocus) <= 0
|
|
817
|
-
? this.selectionAnchor
|
|
818
|
-
: this.selectionFocus;
|
|
819
|
-
const end = start === this.selectionAnchor ? this.selectionFocus : this.selectionAnchor;
|
|
820
|
-
if (start.line === end.line && start.col === end.col) return "";
|
|
821
|
-
|
|
822
|
-
const lines = this.selectionArea === "root" ? this.rootLines : this.visibleClusterLines;
|
|
823
|
-
const selected: string[] = [];
|
|
824
|
-
for (let lineIndex = start.line; lineIndex <= end.line; lineIndex++) {
|
|
825
|
-
const line = stripAnsi(lines[lineIndex] ?? "");
|
|
826
|
-
const startCol = lineIndex === start.line ? start.col : 0;
|
|
827
|
-
const endCol = lineIndex === end.line ? end.col : Number.POSITIVE_INFINITY;
|
|
828
|
-
selected.push(sliceColumns(line, startCol, endCol));
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
return selected.join("\n").replace(/[ \t]+$/gm, "").trimEnd();
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
private getSelectionRangeForLine(lineIndex: number, area: SelectionArea): { startCol: number; endCol: number } | null {
|
|
835
|
-
if (this.selectionArea !== area || !this.selectionAnchor || !this.selectionFocus) return null;
|
|
836
|
-
|
|
837
|
-
const start = compareSelectionPoints(this.selectionAnchor, this.selectionFocus) <= 0
|
|
838
|
-
? this.selectionAnchor
|
|
839
|
-
: this.selectionFocus;
|
|
840
|
-
const end = start === this.selectionAnchor ? this.selectionFocus : this.selectionAnchor;
|
|
841
|
-
if (lineIndex < start.line || lineIndex > end.line) return null;
|
|
842
|
-
|
|
843
|
-
return {
|
|
844
|
-
startCol: lineIndex === start.line ? start.col : 0,
|
|
845
|
-
endCol: lineIndex === end.line ? end.col : Number.POSITIVE_INFINITY,
|
|
846
|
-
};
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
private isLocationInsideSelection(location: SelectionLocation | null): boolean {
|
|
850
|
-
if (!location || location.area !== this.selectionArea) return false;
|
|
851
|
-
const range = this.getSelectionRangeForLine(location.point.line, location.area);
|
|
852
|
-
return Boolean(range && location.point.col >= range.startCol && location.point.col < range.endCol);
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
private scrollBy(delta: number): void {
|
|
856
|
-
const width = Math.max(1, this.terminal.columns || 80);
|
|
857
|
-
this.refreshRootWindow(width);
|
|
858
|
-
|
|
859
|
-
const nextOffset = Math.max(0, Math.min(this.scrollOffset + delta, this.maxScrollOffset));
|
|
860
|
-
if (nextOffset === this.scrollOffset) return;
|
|
861
|
-
|
|
862
|
-
this.clearSelection();
|
|
863
|
-
this.lastLeftPress = null;
|
|
864
|
-
this.scrollOffset = nextOffset;
|
|
865
|
-
this.repaintScrollableViewport(width);
|
|
866
|
-
this.requestRender();
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
private requestRender(): void {
|
|
870
|
-
if (typeof this.tui.requestRender === "function") {
|
|
871
|
-
this.tui.requestRender();
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
private repaintScrollableViewport(width: number): void {
|
|
876
|
-
if (this.disposed || this.writing || this.hasVisibleOverlay()) return;
|
|
877
|
-
|
|
878
|
-
const rawRows = this.getRawRows();
|
|
879
|
-
const cluster = this.getCluster(width, rawRows);
|
|
880
|
-
const scrollableRows = Math.max(1, rawRows - cluster.lines.length);
|
|
881
|
-
const start = this.updateVisibleRootWindow(scrollableRows);
|
|
882
|
-
let buffer = beginSynchronizedOutput() + setScrollRegion(1, scrollableRows) + moveCursor(1, 1);
|
|
883
|
-
|
|
884
|
-
for (let row = 0; row < scrollableRows; row++) {
|
|
885
|
-
if (row > 0) buffer += "\r\n";
|
|
886
|
-
buffer += clearLine();
|
|
887
|
-
buffer += sanitizeLine(this.renderSelectionHighlight(this.visibleRootLines[row] ?? "", start + row, "root"), width);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
buffer += buildFixedClusterPaint(this.decorateCluster(cluster), rawRows, width, this.getShowHardwareCursor());
|
|
891
|
-
buffer += endSynchronizedOutput();
|
|
892
|
-
this.originalWrite(buffer);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
private pauseMouseReportingForContextMenu(textToRestoreToClipboard: string | null = null): void {
|
|
896
|
-
if (this.mouseReportingResumeTimer) {
|
|
897
|
-
clearTimeout(this.mouseReportingResumeTimer);
|
|
898
|
-
}
|
|
899
|
-
if (this.clipboardRestoreTimer) {
|
|
900
|
-
clearTimeout(this.clipboardRestoreTimer);
|
|
901
|
-
this.clipboardRestoreTimer = null;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
this.originalWrite(beginSynchronizedOutput() + disableMouseReporting() + endSynchronizedOutput());
|
|
905
|
-
this.mouseReportingResumeTimer = setTimeout(() => {
|
|
906
|
-
this.mouseReportingResumeTimer = null;
|
|
907
|
-
if (!this.disposed) {
|
|
908
|
-
this.originalWrite(beginSynchronizedOutput() + enableMouseReporting() + endSynchronizedOutput());
|
|
909
|
-
}
|
|
910
|
-
}, CONTEXT_MENU_MOUSE_REPORTING_PAUSE_MS);
|
|
911
|
-
|
|
912
|
-
if (typeof this.mouseReportingResumeTimer === "object" && "unref" in this.mouseReportingResumeTimer) {
|
|
913
|
-
this.mouseReportingResumeTimer.unref();
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
const restoreClipboard = this.onCopySelection;
|
|
917
|
-
if (!textToRestoreToClipboard || !restoreClipboard) return;
|
|
918
|
-
|
|
919
|
-
let remainingRestores = Math.ceil(CONTEXT_MENU_SELECTION_RESTORE_WINDOW_MS / CONTEXT_MENU_CLIPBOARD_RESTORE_INTERVAL_MS);
|
|
920
|
-
const scheduleClipboardRestore = () => {
|
|
921
|
-
this.clipboardRestoreTimer = setTimeout(() => {
|
|
922
|
-
this.clipboardRestoreTimer = null;
|
|
923
|
-
if (this.disposed) return;
|
|
924
|
-
|
|
925
|
-
remainingRestores -= 1;
|
|
926
|
-
if (this.getSelectedText() !== textToRestoreToClipboard) return;
|
|
927
|
-
|
|
928
|
-
restoreClipboard(textToRestoreToClipboard);
|
|
929
|
-
if (remainingRestores > 0) {
|
|
930
|
-
scheduleClipboardRestore();
|
|
931
|
-
}
|
|
932
|
-
}, CONTEXT_MENU_CLIPBOARD_RESTORE_INTERVAL_MS);
|
|
933
|
-
|
|
934
|
-
if (typeof this.clipboardRestoreTimer === "object" && "unref" in this.clipboardRestoreTimer) {
|
|
935
|
-
this.clipboardRestoreTimer.unref();
|
|
936
|
-
}
|
|
937
|
-
};
|
|
938
|
-
|
|
939
|
-
scheduleClipboardRestore();
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
private clearSelection(): void {
|
|
943
|
-
this.selectionArea = null;
|
|
944
|
-
this.selectionAnchor = null;
|
|
945
|
-
this.selectionFocus = null;
|
|
946
|
-
this.selectionDragging = false;
|
|
947
|
-
this.preserveSelectionFocusOnRelease = false;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
private activeExtendedKeyboardMode(): ExtendedKeyboardMode | null {
|
|
951
|
-
if (this.terminal.kittyProtocolActive === true) return "kitty";
|
|
952
|
-
if (Reflect.get(this.terminal, "_modifyOtherKeysActive") === true) return "modifyOtherKeys";
|
|
953
|
-
return null;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
private enableAlternateScreenKeyboardMode(): string {
|
|
957
|
-
this.extendedKeyboardMode = this.activeExtendedKeyboardMode();
|
|
958
|
-
return this.extendedKeyboardMode ? enableExtendedKeyboardMode(this.extendedKeyboardMode) : "";
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
private restoreTerminalState(options: DisposeOptions = {}): void {
|
|
962
|
-
const activeMode = this.extendedKeyboardMode ?? this.activeExtendedKeyboardMode();
|
|
963
|
-
const restoreMainScreenMode = !options.resetExtendedKeyboardModes && this.extendedKeyboardMode === null && activeMode !== null;
|
|
964
|
-
|
|
965
|
-
this.originalWrite(
|
|
966
|
-
beginSynchronizedOutput()
|
|
967
|
-
+ resetScrollRegion()
|
|
968
|
-
+ (this.mouseScroll ? disableMouseReporting() : "")
|
|
969
|
-
+ (activeMode ? disableExtendedKeyboardMode(activeMode) : "")
|
|
970
|
-
+ enableAlternateScrollMode()
|
|
971
|
-
+ exitAlternateScreen()
|
|
972
|
-
+ (restoreMainScreenMode && activeMode ? enableExtendedKeyboardMode(activeMode) : "")
|
|
973
|
-
+ (options.resetExtendedKeyboardModes ? resetExtendedKeyboardModes() : "")
|
|
974
|
-
+ endSynchronizedOutput(),
|
|
975
|
-
);
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
private restoreTerminalStateForExit(): void {
|
|
979
|
-
try {
|
|
980
|
-
this.restoreTerminalState({ resetExtendedKeyboardModes: true });
|
|
981
|
-
} catch {
|
|
982
|
-
// Process-exit cleanup cannot report useful errors and must not throw.
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
private write(data: string): void {
|
|
987
|
-
if (this.disposed || this.writing || this.hasVisibleOverlay()) {
|
|
988
|
-
this.originalWrite(data);
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
this.writing = true;
|
|
993
|
-
try {
|
|
994
|
-
const rawRows = this.getRawRows();
|
|
995
|
-
const width = Math.max(1, this.terminal.columns || 80);
|
|
996
|
-
const cluster = this.getCluster(width, rawRows);
|
|
997
|
-
const reservedRows = cluster.lines.length;
|
|
998
|
-
|
|
999
|
-
if (reservedRows === 0 || rawRows <= 2) {
|
|
1000
|
-
this.originalWrite(data);
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
const scrollBottom = Math.max(1, rawRows - reservedRows);
|
|
1005
|
-
const hardwareCursorRow = typeof this.tui.hardwareCursorRow === "number"
|
|
1006
|
-
? this.tui.hardwareCursorRow
|
|
1007
|
-
: typeof this.tui.cursorRow === "number"
|
|
1008
|
-
? this.tui.cursorRow
|
|
1009
|
-
: 0;
|
|
1010
|
-
const viewportTop = typeof this.tui.previousViewportTop === "number" ? this.tui.previousViewportTop : 0;
|
|
1011
|
-
const screenRow = Math.max(1, Math.min(scrollBottom, hardwareCursorRow - viewportTop + 1));
|
|
1012
|
-
const buffer = beginSynchronizedOutput()
|
|
1013
|
-
+ setScrollRegion(1, scrollBottom)
|
|
1014
|
-
+ moveCursor(screenRow, 1)
|
|
1015
|
-
+ data
|
|
1016
|
-
+ buildFixedClusterPaint(this.decorateCluster(cluster), rawRows, width, this.getShowHardwareCursor())
|
|
1017
|
-
+ endSynchronizedOutput();
|
|
1018
|
-
|
|
1019
|
-
this.originalWrite(buffer);
|
|
1020
|
-
} finally {
|
|
1021
|
-
this.writing = false;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
private getCluster(width: number, terminalRows: number): FixedEditorClusterRender {
|
|
1026
|
-
if (
|
|
1027
|
-
this.renderPassActive &&
|
|
1028
|
-
this.renderPassCluster?.width === width &&
|
|
1029
|
-
this.renderPassCluster.terminalRows === terminalRows
|
|
1030
|
-
) {
|
|
1031
|
-
return this.renderPassCluster.cluster;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
const cluster = this.withClusterRender(() => this.renderCluster(width, terminalRows));
|
|
1035
|
-
this.visibleClusterLines = cluster.lines;
|
|
1036
|
-
if (this.renderPassActive) {
|
|
1037
|
-
this.renderPassCluster = { width, terminalRows, cluster };
|
|
1038
|
-
}
|
|
1039
|
-
return cluster;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
private decorateCluster(cluster: FixedEditorClusterRender): FixedEditorClusterRender {
|
|
1043
|
-
if (this.selectionArea !== "cluster") return cluster;
|
|
1044
|
-
|
|
1045
|
-
return {
|
|
1046
|
-
...cluster,
|
|
1047
|
-
lines: cluster.lines.map((line, index) => this.renderSelectionHighlight(line, index, "cluster")),
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
private withClusterRender<T>(render: () => T): T {
|
|
1052
|
-
const wasRenderingCluster = this.renderingCluster;
|
|
1053
|
-
this.renderingCluster = true;
|
|
1054
|
-
try {
|
|
1055
|
-
return render();
|
|
1056
|
-
} finally {
|
|
1057
|
-
this.renderingCluster = wasRenderingCluster;
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
private hasVisibleOverlay(): boolean {
|
|
1062
|
-
if (this.checkingOverlay) return false;
|
|
1063
|
-
|
|
1064
|
-
this.checkingOverlay = true;
|
|
1065
|
-
try {
|
|
1066
|
-
if (typeof this.tui.hasOverlay === "function" && this.tui.hasOverlay()) {
|
|
1067
|
-
return true;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
const overlayStack = Reflect.get(this.tui, "overlayStack");
|
|
1071
|
-
if (!Array.isArray(overlayStack)) {
|
|
1072
|
-
return false;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
return overlayStack.some((entry) => entry && entry.hidden !== true);
|
|
1076
|
-
} finally {
|
|
1077
|
-
this.checkingOverlay = false;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|