pi-cc-extensions 0.3.0 → 0.5.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 +1 -18
- package/extensions/claude-code-style.ts +194 -41
- package/package.json +2 -3
- package/assets/readme/showcase.webp +0 -0
- package/assets/readme/welcome.webp +0 -0
- package/extensions/ccstyle-powerline/bash-mode/completion.ts +0 -556
- package/extensions/ccstyle-powerline/bash-mode/editor.ts +0 -442
- package/extensions/ccstyle-powerline/bash-mode/history.ts +0 -151
- package/extensions/ccstyle-powerline/bash-mode/shell-session.ts +0 -286
- package/extensions/ccstyle-powerline/bash-mode/transcript.ts +0 -108
- package/extensions/ccstyle-powerline/bash-mode/types.ts +0 -59
- package/extensions/ccstyle-powerline/colors.ts +0 -69
- package/extensions/ccstyle-powerline/context-usage.ts +0 -46
- package/extensions/ccstyle-powerline/fixed-editor/cluster.ts +0 -107
- package/extensions/ccstyle-powerline/fixed-editor/terminal-split.ts +0 -1389
- package/extensions/ccstyle-powerline/git-status.ts +0 -283
- package/extensions/ccstyle-powerline/icons.ts +0 -181
- package/extensions/ccstyle-powerline/powerline-config.ts +0 -234
- package/extensions/ccstyle-powerline/powerline.ts +0 -3218
- package/extensions/ccstyle-powerline/presets.ts +0 -109
- package/extensions/ccstyle-powerline/render-scheduler.ts +0 -24
- package/extensions/ccstyle-powerline/segments.ts +0 -486
- package/extensions/ccstyle-powerline/separators.ts +0 -41
- package/extensions/ccstyle-powerline/shortcuts.ts +0 -57
- package/extensions/ccstyle-powerline/theme.json +0 -26
- package/extensions/ccstyle-powerline/theme.ts +0 -241
- package/extensions/ccstyle-powerline/types.ts +0 -200
- package/extensions/ccstyle-powerline/welcome-dismiss.ts +0 -34
- package/extensions/ccstyle-powerline/welcome.ts +0 -611
- package/extensions/ccstyle-powerline/working-vibes.ts +0 -695
|
@@ -1,1389 +0,0 @@
|
|
|
1
|
-
import { deleteAllKittyImages, 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
|
-
export interface RootClickResult {
|
|
18
|
-
handled: true;
|
|
19
|
-
action?: "expand" | "collapse";
|
|
20
|
-
key?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface TerminalSplitCompositorOptions {
|
|
24
|
-
tui: any;
|
|
25
|
-
terminal: TerminalLike;
|
|
26
|
-
renderCluster: (width: number, terminalRows: number) => FixedEditorClusterRender;
|
|
27
|
-
getShowHardwareCursor?: () => boolean;
|
|
28
|
-
mouseScroll?: boolean;
|
|
29
|
-
keyboardScrollShortcuts?: KeyboardScrollShortcuts;
|
|
30
|
-
jumpToBottomShortcut?: string;
|
|
31
|
-
onJumpToBottom?: () => void;
|
|
32
|
-
onCopySelection?: (text: string) => void;
|
|
33
|
-
/** Handle a left click on a fixed-editor cluster line. Return true to consume it. */
|
|
34
|
-
onClusterClick?: (line: number, col: number, text: string) => boolean;
|
|
35
|
-
/** Handle a left click on a scrollable chat line. Return a result to preserve expansion anchors. */
|
|
36
|
-
onRootClick?: (line: number, col: number, text: string) => boolean | RootClickResult;
|
|
37
|
-
/** Return true when a scrollable chat line should show hover feedback. */
|
|
38
|
-
onRootHover?: (line: number, col: number, text: string) => boolean;
|
|
39
|
-
/** Apply stable presentation-only styling to the complete rendered transcript. */
|
|
40
|
-
decorateRootLines?: (lines: string[]) => string[];
|
|
41
|
-
/** Notify when manual scrolling reaches the newest output. */
|
|
42
|
-
onReachRootBottom?: () => void;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface PatchedRenderable {
|
|
46
|
-
render(width: number): string[];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface RenderPatch {
|
|
50
|
-
target: PatchedRenderable;
|
|
51
|
-
originalRender: (width: number) => string[];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface RenderPassCluster {
|
|
55
|
-
width: number;
|
|
56
|
-
terminalRows: number;
|
|
57
|
-
cluster: FixedEditorClusterRender;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
type CompositeLineAt = (
|
|
61
|
-
baseLine: string,
|
|
62
|
-
overlayLine: string,
|
|
63
|
-
startCol: number,
|
|
64
|
-
overlayWidth: number,
|
|
65
|
-
totalWidth: number,
|
|
66
|
-
) => string;
|
|
67
|
-
|
|
68
|
-
interface SgrMousePacket {
|
|
69
|
-
code: number;
|
|
70
|
-
col: number;
|
|
71
|
-
row: number;
|
|
72
|
-
final: "M" | "m";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
interface SelectionPoint {
|
|
76
|
-
line: number;
|
|
77
|
-
col: number;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
type SelectionArea = "root" | "cluster";
|
|
81
|
-
|
|
82
|
-
interface SelectionLocation {
|
|
83
|
-
area: SelectionArea;
|
|
84
|
-
point: SelectionPoint;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface DisposeOptions {
|
|
88
|
-
resetExtendedKeyboardModes?: boolean;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
type ExtendedKeyboardMode = "kitty" | "modifyOtherKeys";
|
|
92
|
-
|
|
93
|
-
const CONTEXT_MENU_MOUSE_REPORTING_PAUSE_MS = 1200;
|
|
94
|
-
const CONTEXT_MENU_SELECTION_RESTORE_WINDOW_MS = 5000;
|
|
95
|
-
const CONTEXT_MENU_CLIPBOARD_RESTORE_INTERVAL_MS = 100;
|
|
96
|
-
const DOUBLE_CLICK_MS = 500;
|
|
97
|
-
const DEFAULT_KEYBOARD_SCROLL_SHORTCUTS: KeyboardScrollShortcuts = {
|
|
98
|
-
up: "super+up",
|
|
99
|
-
down: "super+down",
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
export function beginSynchronizedOutput(): string {
|
|
103
|
-
return "\x1b[?2026h";
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function endSynchronizedOutput(): string {
|
|
107
|
-
return "\x1b[?2026l";
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function setScrollRegion(top: number, bottom: number): string {
|
|
111
|
-
return `\x1b[${top};${bottom}r`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function resetScrollRegion(): string {
|
|
115
|
-
return "\x1b[r";
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function moveCursor(row: number, col: number): string {
|
|
119
|
-
return `\x1b[${row};${col}H`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function clearLine(): string {
|
|
123
|
-
return "\x1b[2K";
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function hideCursor(): string {
|
|
127
|
-
return "\x1b[?25l";
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function showCursor(): string {
|
|
131
|
-
return "\x1b[?25h";
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function enterAlternateScreen(): string {
|
|
135
|
-
return "\x1b[?1049h";
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function exitAlternateScreen(): string {
|
|
139
|
-
return "\x1b[?1049l";
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function enableAlternateScrollMode(): string {
|
|
143
|
-
return "\x1b[?1007h";
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function disableAlternateScrollMode(): string {
|
|
147
|
-
return "\x1b[?1007l";
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function disableAutoWrap(): string {
|
|
151
|
-
return "\x1b[?7l";
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function enableAutoWrap(): string {
|
|
155
|
-
return "\x1b[?7h";
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function enableMouseReporting(): string {
|
|
159
|
-
return "\x1b[?1003h\x1b[?1006h";
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function disableMouseReporting(): string {
|
|
163
|
-
return "\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l";
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function enableExtendedKeyboardMode(mode: ExtendedKeyboardMode): string {
|
|
167
|
-
return mode === "kitty" ? "\x1b[>7u" : "\x1b[>4;2m";
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function disableExtendedKeyboardMode(mode: ExtendedKeyboardMode): string {
|
|
171
|
-
return mode === "kitty" ? "\x1b[<u" : "\x1b[>4;0m";
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function resetExtendedKeyboardModes(): string {
|
|
175
|
-
return "\x1b[<999u\x1b[>4;0m";
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function emergencyTerminalModeReset(): string {
|
|
179
|
-
return beginSynchronizedOutput()
|
|
180
|
-
+ resetScrollRegion()
|
|
181
|
-
+ disableMouseReporting()
|
|
182
|
-
+ enableAlternateScrollMode()
|
|
183
|
-
+ exitAlternateScreen()
|
|
184
|
-
+ resetExtendedKeyboardModes()
|
|
185
|
-
+ endSynchronizedOutput();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function parseKeyboardScrollDelta(data: string, shortcuts: KeyboardScrollShortcuts = DEFAULT_KEYBOARD_SCROLL_SHORTCUTS): number {
|
|
189
|
-
if (isKeyRelease(data)) return 0;
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
matchesConfiguredShortcut(data, shortcuts.up)
|
|
193
|
-
|| matchesKey(data, "pageUp")
|
|
194
|
-
|| matchesKey(data, "shift+pageUp")
|
|
195
|
-
|| matchesKey(data, "ctrl+pageUp")
|
|
196
|
-
|| matchesKey(data, "ctrl+shift+up")
|
|
197
|
-
|| /^\x1b\[(?:5;9(?::[12])?~|1;6(?::[12])?A|57421;9(?::[12])?u|57419;6(?::[12])?u)$/.test(data)
|
|
198
|
-
) return 10;
|
|
199
|
-
if (
|
|
200
|
-
matchesConfiguredShortcut(data, shortcuts.down)
|
|
201
|
-
|| matchesKey(data, "pageDown")
|
|
202
|
-
|| matchesKey(data, "shift+pageDown")
|
|
203
|
-
|| matchesKey(data, "ctrl+pageDown")
|
|
204
|
-
|| matchesKey(data, "ctrl+shift+down")
|
|
205
|
-
|| /^\x1b\[(?:6;9(?::[12])?~|1;6(?::[12])?B|57422;9(?::[12])?u|57420;6(?::[12])?u)$/.test(data)
|
|
206
|
-
) return -10;
|
|
207
|
-
return 0;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
|
|
211
|
-
const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
212
|
-
const packets: SgrMousePacket[] = [];
|
|
213
|
-
let offset = 0;
|
|
214
|
-
|
|
215
|
-
for (const match of data.matchAll(pattern)) {
|
|
216
|
-
if (match.index !== offset) return null;
|
|
217
|
-
offset = match.index + match[0].length;
|
|
218
|
-
packets.push({
|
|
219
|
-
code: Number(match[1]),
|
|
220
|
-
col: Number(match[2]),
|
|
221
|
-
row: Number(match[3]),
|
|
222
|
-
final: match[4] as "M" | "m",
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return packets.length > 0 && offset === data.length ? packets : null;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function mouseBaseButton(code: number): number {
|
|
230
|
-
return code & ~(4 | 8 | 16 | 32);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function mouseScrollDelta(packet: SgrMousePacket): number {
|
|
234
|
-
if (packet.final !== "M") return 0;
|
|
235
|
-
const baseButton = mouseBaseButton(packet.code);
|
|
236
|
-
if (baseButton === 64) return 3;
|
|
237
|
-
if (baseButton === 65) return -3;
|
|
238
|
-
return 0;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function isLeftPress(packet: SgrMousePacket): boolean {
|
|
242
|
-
return packet.final === "M" && mouseBaseButton(packet.code) === 0 && (packet.code & 32) === 0;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function isLeftDrag(packet: SgrMousePacket): boolean {
|
|
246
|
-
return packet.final === "M" && mouseBaseButton(packet.code) === 0 && (packet.code & 32) !== 0;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function isMouseMotion(packet: SgrMousePacket): boolean {
|
|
250
|
-
return packet.final === "M" && (packet.code & 32) !== 0;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function isRightPress(packet: SgrMousePacket): boolean {
|
|
254
|
-
return packet.final === "M" && mouseBaseButton(packet.code) === 2 && (packet.code & 32) === 0;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function isMouseRelease(packet: SgrMousePacket): boolean {
|
|
258
|
-
return packet.final === "m";
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function stripOscSequences(line: string): string {
|
|
262
|
-
return line.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "");
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function stripAnsi(line: string): string {
|
|
266
|
-
return stripOscSequences(line).replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
270
|
-
|
|
271
|
-
function sliceColumns(text: string, startCol: number, endCol: number): string {
|
|
272
|
-
let col = 0;
|
|
273
|
-
let result = "";
|
|
274
|
-
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
275
|
-
const width = Math.max(0, visibleWidth(segment));
|
|
276
|
-
if (col >= startCol && col < endCol) {
|
|
277
|
-
result += segment;
|
|
278
|
-
}
|
|
279
|
-
col += width;
|
|
280
|
-
}
|
|
281
|
-
return result;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function compareSelectionPoints(a: SelectionPoint, b: SelectionPoint): number {
|
|
285
|
-
return a.line === b.line ? a.col - b.col : a.line - b.line;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function descriptorForRows(terminal: TerminalLike): PropertyDescriptor | undefined {
|
|
289
|
-
let target: object | null = terminal;
|
|
290
|
-
while (target) {
|
|
291
|
-
const descriptor = Object.getOwnPropertyDescriptor(target, "rows");
|
|
292
|
-
if (descriptor) return descriptor;
|
|
293
|
-
target = Object.getPrototypeOf(target);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return undefined;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function readRows(terminal: TerminalLike, descriptor: PropertyDescriptor | undefined): number {
|
|
300
|
-
if (descriptor?.get) {
|
|
301
|
-
const value = descriptor.get.call(terminal);
|
|
302
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 24;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const value = Reflect.get(terminal, "rows");
|
|
306
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 24;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function sanitizeLine(line: string, width: number): string {
|
|
310
|
-
return visibleWidth(line) > width ? truncateToWidth(line, width, "", true) : line;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function sanitizeOverlayBaseLine(line: string, width: number): string {
|
|
314
|
-
return sanitizeLine(stripOscSequences(line), width);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function normalizeOverlayCompositionLine(line: string): string {
|
|
318
|
-
return line.includes("\t") ? line.replace(/\t/g, " ") : line;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export function buildFixedClusterPaint(
|
|
322
|
-
cluster: FixedEditorClusterRender,
|
|
323
|
-
terminalRows: number,
|
|
324
|
-
width: number,
|
|
325
|
-
showHardwareCursor: boolean,
|
|
326
|
-
): string {
|
|
327
|
-
if (cluster.lines.length === 0) return "";
|
|
328
|
-
|
|
329
|
-
const startRow = Math.max(1, terminalRows - cluster.lines.length + 1);
|
|
330
|
-
let buffer = resetScrollRegion();
|
|
331
|
-
|
|
332
|
-
for (let i = 0; i < cluster.lines.length; i++) {
|
|
333
|
-
buffer += moveCursor(startRow + i, 1);
|
|
334
|
-
buffer += clearLine();
|
|
335
|
-
buffer += sanitizeLine(cluster.lines[i] ?? "", width);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (cluster.cursor) {
|
|
339
|
-
// Keep the real terminal cursor parked at the logical editor cursor even
|
|
340
|
-
// when it is visually hidden. macOS IME candidate windows anchor to the
|
|
341
|
-
// terminal cursor position, so skipping the move when PI_HARDWARE_CURSOR is
|
|
342
|
-
// off leaves the candidate window at a stale/offset location.
|
|
343
|
-
buffer += moveCursor(startRow + cluster.cursor.row, Math.max(1, cluster.cursor.col + 1));
|
|
344
|
-
buffer += showHardwareCursor ? showCursor() : hideCursor();
|
|
345
|
-
} else {
|
|
346
|
-
buffer += hideCursor();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return buffer;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export class TerminalSplitCompositor {
|
|
353
|
-
private readonly tui: any;
|
|
354
|
-
private readonly terminal: TerminalLike;
|
|
355
|
-
private readonly renderCluster: (width: number, terminalRows: number) => FixedEditorClusterRender;
|
|
356
|
-
private readonly getShowHardwareCursor: () => boolean;
|
|
357
|
-
private readonly mouseScroll: boolean;
|
|
358
|
-
private readonly keyboardScrollShortcuts: KeyboardScrollShortcuts;
|
|
359
|
-
private readonly jumpToBottomShortcut: string | null;
|
|
360
|
-
private readonly onJumpToBottom: (() => void) | null;
|
|
361
|
-
private readonly onCopySelection: ((text: string) => void) | null;
|
|
362
|
-
private readonly onClusterClick: ((line: number, col: number, text: string) => boolean) | null;
|
|
363
|
-
private readonly onRootClick: ((line: number, col: number, text: string) => boolean | RootClickResult) | null;
|
|
364
|
-
private readonly onRootHover: ((line: number, col: number, text: string) => boolean) | null;
|
|
365
|
-
private readonly decorateRootLines: ((lines: string[]) => string[]) | null;
|
|
366
|
-
private readonly onReachRootBottom: (() => void) | null;
|
|
367
|
-
private extendedKeyboardMode: ExtendedKeyboardMode | null = null;
|
|
368
|
-
private readonly rowsDescriptor: PropertyDescriptor | undefined;
|
|
369
|
-
private readonly originalWrite: (data: string) => void;
|
|
370
|
-
private readonly originalDoRender: (() => void) | null;
|
|
371
|
-
private readonly originalRender: ((width: number) => string[]) | null;
|
|
372
|
-
private originalCompositeLineAt: CompositeLineAt | null = null;
|
|
373
|
-
private readonly patchedRenders: RenderPatch[] = [];
|
|
374
|
-
private removeInputListener: (() => void) | null = null;
|
|
375
|
-
private emergencyCleanup: (() => void) | null = null;
|
|
376
|
-
private mouseReportingResumeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
377
|
-
private clipboardRestoreTimer: ReturnType<typeof setTimeout> | null = null;
|
|
378
|
-
private installed = false;
|
|
379
|
-
private disposed = false;
|
|
380
|
-
private writing = false;
|
|
381
|
-
private renderPassActive = false;
|
|
382
|
-
private wroteDuringRenderPass = false;
|
|
383
|
-
private renderPassCluster: RenderPassCluster | null = null;
|
|
384
|
-
private renderingCluster = false;
|
|
385
|
-
private renderingScrollableRoot = false;
|
|
386
|
-
private checkingOverlay = false;
|
|
387
|
-
private scrollOffset = 0;
|
|
388
|
-
private maxScrollOffset = 0;
|
|
389
|
-
private lastRootLineCount = 0;
|
|
390
|
-
private rootLines: string[] = [];
|
|
391
|
-
private rootWindowWidth = 0;
|
|
392
|
-
private rootWindowRawRows = 0;
|
|
393
|
-
private suppressRootGrowthAdjustment = false;
|
|
394
|
-
private readonly toolExpansionStarts = new Map<string, number>();
|
|
395
|
-
private visibleRootStart = 0;
|
|
396
|
-
private visibleScrollableRows = 0;
|
|
397
|
-
private visibleRootLines: string[] = [];
|
|
398
|
-
private visibleClusterLines: string[] = [];
|
|
399
|
-
private hoveredRootLine: number | null = null;
|
|
400
|
-
private selectionArea: SelectionArea | null = null;
|
|
401
|
-
private selectionAnchor: SelectionPoint | null = null;
|
|
402
|
-
private selectionFocus: SelectionPoint | null = null;
|
|
403
|
-
private selectionDragging = false;
|
|
404
|
-
private batchingMouseInput = false;
|
|
405
|
-
private selectionRepaintPending = false;
|
|
406
|
-
private preserveSelectionFocusOnRelease = false;
|
|
407
|
-
private lastLeftPress: { area: SelectionArea; line: number; at: number } | null = null;
|
|
408
|
-
private consumedClusterClick = false;
|
|
409
|
-
private pendingImageCleanup = false;
|
|
410
|
-
|
|
411
|
-
constructor(options: TerminalSplitCompositorOptions) {
|
|
412
|
-
this.tui = options.tui;
|
|
413
|
-
this.terminal = options.terminal;
|
|
414
|
-
this.renderCluster = options.renderCluster;
|
|
415
|
-
this.getShowHardwareCursor = options.getShowHardwareCursor ?? (() => false);
|
|
416
|
-
this.mouseScroll = options.mouseScroll !== false;
|
|
417
|
-
this.keyboardScrollShortcuts = options.keyboardScrollShortcuts ?? DEFAULT_KEYBOARD_SCROLL_SHORTCUTS;
|
|
418
|
-
this.jumpToBottomShortcut = options.jumpToBottomShortcut ?? null;
|
|
419
|
-
this.onJumpToBottom = options.onJumpToBottom ?? null;
|
|
420
|
-
this.onCopySelection = options.onCopySelection ?? null;
|
|
421
|
-
this.onClusterClick = options.onClusterClick ?? null;
|
|
422
|
-
this.onRootClick = options.onRootClick ?? null;
|
|
423
|
-
this.onRootHover = options.onRootHover ?? null;
|
|
424
|
-
this.decorateRootLines = options.decorateRootLines ?? null;
|
|
425
|
-
this.onReachRootBottom = options.onReachRootBottom ?? null;
|
|
426
|
-
this.rowsDescriptor = descriptorForRows(options.terminal);
|
|
427
|
-
this.originalWrite = options.terminal.write.bind(options.terminal);
|
|
428
|
-
this.originalDoRender = typeof options.tui.doRender === "function" ? options.tui.doRender.bind(options.tui) : null;
|
|
429
|
-
this.originalRender = typeof options.tui.render === "function" ? options.tui.render.bind(options.tui) : null;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
install(): void {
|
|
433
|
-
if (this.installed) return;
|
|
434
|
-
if (typeof this.terminal.write !== "function") {
|
|
435
|
-
throw new Error("[powerline-footer] Fixed editor compositor expected terminal.write(data) to exist");
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
this.originalWrite(
|
|
439
|
-
beginSynchronizedOutput()
|
|
440
|
-
+ enterAlternateScreen()
|
|
441
|
-
+ this.enableAlternateScreenKeyboardMode()
|
|
442
|
-
+ disableAlternateScrollMode()
|
|
443
|
-
+ this.mouseReportingStateGuard()
|
|
444
|
-
+ endSynchronizedOutput(),
|
|
445
|
-
);
|
|
446
|
-
this.emergencyCleanup = () => {
|
|
447
|
-
if (!this.disposed) {
|
|
448
|
-
this.restoreTerminalStateForExit();
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
process.once("exit", this.emergencyCleanup);
|
|
452
|
-
|
|
453
|
-
Object.defineProperty(this.terminal, "rows", {
|
|
454
|
-
configurable: true,
|
|
455
|
-
get: () => this.getScrollableRows(),
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
if (this.originalRender) {
|
|
459
|
-
this.tui.render = (width: number) => this.renderScrollableRoot(width);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (typeof this.tui.addInputListener === "function") {
|
|
463
|
-
this.removeInputListener = this.tui.addInputListener((data: string) => this.handleInput(data));
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
this.terminal.write = (data: string) => this.write(data);
|
|
467
|
-
if (this.originalDoRender) {
|
|
468
|
-
this.tui.doRender = () => {
|
|
469
|
-
this.renderPassActive = true;
|
|
470
|
-
this.wroteDuringRenderPass = false;
|
|
471
|
-
this.renderPassCluster = null;
|
|
472
|
-
try {
|
|
473
|
-
this.originalDoRender?.();
|
|
474
|
-
// Patched terminal writes already append the fixed cluster. Repainting
|
|
475
|
-
// again here doubles terminal output and is a major source of flicker.
|
|
476
|
-
if (!this.wroteDuringRenderPass) this.requestRepaint();
|
|
477
|
-
} finally {
|
|
478
|
-
this.renderPassActive = false;
|
|
479
|
-
this.wroteDuringRenderPass = false;
|
|
480
|
-
this.renderPassCluster = null;
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
if (typeof this.tui.compositeLineAt === "function") {
|
|
485
|
-
this.originalCompositeLineAt = this.tui.compositeLineAt.bind(this.tui) as CompositeLineAt;
|
|
486
|
-
this.tui.compositeLineAt = (
|
|
487
|
-
baseLine: string,
|
|
488
|
-
overlayLine: string,
|
|
489
|
-
startCol: number,
|
|
490
|
-
overlayWidth: number,
|
|
491
|
-
totalWidth: number,
|
|
492
|
-
) => this.originalCompositeLineAt?.(
|
|
493
|
-
normalizeOverlayCompositionLine(baseLine),
|
|
494
|
-
normalizeOverlayCompositionLine(overlayLine),
|
|
495
|
-
startCol,
|
|
496
|
-
overlayWidth,
|
|
497
|
-
totalWidth,
|
|
498
|
-
) ?? "";
|
|
499
|
-
}
|
|
500
|
-
this.installed = true;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
hideRenderable(target: PatchedRenderable): void {
|
|
504
|
-
if (this.patchedRenders.some((patch) => patch.target === target)) return;
|
|
505
|
-
const originalRender = target.render.bind(target);
|
|
506
|
-
this.patchedRenders.push({ target, originalRender });
|
|
507
|
-
target.render = () => [];
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
renderHidden(target: PatchedRenderable, width: number): string[] {
|
|
511
|
-
const patch = this.patchedRenders.find((candidate) => candidate.target === target);
|
|
512
|
-
const render = patch?.originalRender ?? target.render.bind(target);
|
|
513
|
-
return render(width);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
jumpToPreviousRootTarget(targetLines: readonly number[]): boolean {
|
|
517
|
-
return this.jumpToRootTarget(targetLines, "previous");
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
jumpToNextRootTarget(targetLines: readonly number[]): boolean {
|
|
521
|
-
return this.jumpToRootTarget(targetLines, "next");
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
jumpToRootBottom(): boolean {
|
|
525
|
-
if (this.disposed || this.hasVisibleOverlay()) return false;
|
|
526
|
-
|
|
527
|
-
const moved = this.scrollOffset !== 0;
|
|
528
|
-
this.clearSelection();
|
|
529
|
-
this.lastLeftPress = null;
|
|
530
|
-
this.scrollOffset = 0;
|
|
531
|
-
this.pendingImageCleanup = true;
|
|
532
|
-
|
|
533
|
-
// Always force a full repaint. After the first jump, some terminals can
|
|
534
|
-
// move their physical viewport while our logical offset remains zero; the
|
|
535
|
-
// old early return then made every later Ctrl+End a no-op.
|
|
536
|
-
if (typeof this.tui.requestRender === "function") {
|
|
537
|
-
this.tui.requestRender(true);
|
|
538
|
-
}
|
|
539
|
-
this.requestRepaint();
|
|
540
|
-
return moved;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/** Whether the chat viewport is currently following the newest output. */
|
|
544
|
-
isAtRootBottom(): boolean {
|
|
545
|
-
return this.scrollOffset === 0;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/**
|
|
549
|
-
* Drop viewport state whose line indexes belong to an obsolete transcript.
|
|
550
|
-
* Compaction rebuilds historical message components, so retaining expansion
|
|
551
|
-
* anchors or the previous line count makes later tool expansion drift.
|
|
552
|
-
*/
|
|
553
|
-
resetTranscriptLayout(): void {
|
|
554
|
-
if (this.disposed) return;
|
|
555
|
-
|
|
556
|
-
this.toolExpansionStarts.clear();
|
|
557
|
-
this.clearSelection();
|
|
558
|
-
this.lastLeftPress = null;
|
|
559
|
-
this.hoveredRootLine = null;
|
|
560
|
-
this.rootLines = [];
|
|
561
|
-
this.rootWindowWidth = 0;
|
|
562
|
-
this.rootWindowRawRows = 0;
|
|
563
|
-
this.visibleRootLines = [];
|
|
564
|
-
this.visibleRootStart = 0;
|
|
565
|
-
this.lastRootLineCount = 0;
|
|
566
|
-
this.scrollOffset = 0;
|
|
567
|
-
this.maxScrollOffset = 0;
|
|
568
|
-
this.pendingImageCleanup = true;
|
|
569
|
-
|
|
570
|
-
if (typeof this.tui.requestRender === "function") {
|
|
571
|
-
this.tui.requestRender(true);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
private jumpToRootTarget(targetLines: readonly number[], direction: "previous" | "next"): boolean {
|
|
576
|
-
if (this.disposed || targetLines.length === 0 || this.hasVisibleOverlay()) return false;
|
|
577
|
-
|
|
578
|
-
const start = this.visibleRootStart;
|
|
579
|
-
const candidates = direction === "previous"
|
|
580
|
-
? targetLines.filter((line) => line < start).sort((a, b) => b - a)
|
|
581
|
-
: targetLines.filter((line) => line > start).sort((a, b) => a - b);
|
|
582
|
-
|
|
583
|
-
for (const target of candidates) {
|
|
584
|
-
const nextOffset = Math.max(0, Math.min(
|
|
585
|
-
this.lastRootLineCount - Math.max(1, this.visibleScrollableRows) - target,
|
|
586
|
-
this.maxScrollOffset,
|
|
587
|
-
));
|
|
588
|
-
if (nextOffset === this.scrollOffset) continue;
|
|
589
|
-
|
|
590
|
-
this.clearSelection();
|
|
591
|
-
this.lastLeftPress = null;
|
|
592
|
-
this.scrollOffset = nextOffset;
|
|
593
|
-
this.pendingImageCleanup = true;
|
|
594
|
-
this.requestRender();
|
|
595
|
-
return true;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
return false;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
requestRepaint(): void {
|
|
602
|
-
if (this.disposed || this.hasVisibleOverlay()) return;
|
|
603
|
-
const rawRows = this.getRawRows();
|
|
604
|
-
const width = Math.max(1, this.terminal.columns || 80);
|
|
605
|
-
const cluster = this.getCluster(width, rawRows);
|
|
606
|
-
const scrollableRows = Math.max(1, rawRows - cluster.lines.length);
|
|
607
|
-
// A cluster-only paint cannot change the split point safely. Fall back to
|
|
608
|
-
// the normal frame when width, terminal height, or cluster height changed.
|
|
609
|
-
if (
|
|
610
|
-
this.rootWindowWidth !== width
|
|
611
|
-
|| this.rootWindowRawRows !== rawRows
|
|
612
|
-
|| (this.visibleScrollableRows > 0 && this.visibleScrollableRows !== scrollableRows)
|
|
613
|
-
) {
|
|
614
|
-
this.requestRender();
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
if (cluster.lines.length === 0) return;
|
|
618
|
-
|
|
619
|
-
this.originalWrite(
|
|
620
|
-
beginSynchronizedOutput()
|
|
621
|
-
+ disableAutoWrap()
|
|
622
|
-
+ buildFixedClusterPaint(this.decorateCluster(cluster), rawRows, width, this.getShowHardwareCursor())
|
|
623
|
-
+ enableAutoWrap()
|
|
624
|
-
+ this.mouseReportingStateGuard()
|
|
625
|
-
+ endSynchronizedOutput(),
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
dispose(options: DisposeOptions = {}): void {
|
|
630
|
-
if (this.disposed) return;
|
|
631
|
-
this.disposed = true;
|
|
632
|
-
|
|
633
|
-
for (const patch of this.patchedRenders.splice(0)) {
|
|
634
|
-
patch.target.render = patch.originalRender;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
this.removeInputListener?.();
|
|
638
|
-
this.removeInputListener = null;
|
|
639
|
-
if (this.emergencyCleanup) {
|
|
640
|
-
process.removeListener("exit", this.emergencyCleanup);
|
|
641
|
-
this.emergencyCleanup = null;
|
|
642
|
-
}
|
|
643
|
-
if (this.mouseReportingResumeTimer) {
|
|
644
|
-
clearTimeout(this.mouseReportingResumeTimer);
|
|
645
|
-
this.mouseReportingResumeTimer = null;
|
|
646
|
-
}
|
|
647
|
-
if (this.clipboardRestoreTimer) {
|
|
648
|
-
clearTimeout(this.clipboardRestoreTimer);
|
|
649
|
-
this.clipboardRestoreTimer = null;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
this.terminal.write = this.originalWrite;
|
|
653
|
-
if (this.originalDoRender) {
|
|
654
|
-
this.tui.doRender = this.originalDoRender;
|
|
655
|
-
}
|
|
656
|
-
if (this.originalRender) {
|
|
657
|
-
this.tui.render = this.originalRender;
|
|
658
|
-
}
|
|
659
|
-
if (this.originalCompositeLineAt) {
|
|
660
|
-
this.tui.compositeLineAt = this.originalCompositeLineAt;
|
|
661
|
-
this.originalCompositeLineAt = null;
|
|
662
|
-
}
|
|
663
|
-
if (this.rowsDescriptor) {
|
|
664
|
-
Object.defineProperty(this.terminal, "rows", this.rowsDescriptor);
|
|
665
|
-
} else {
|
|
666
|
-
Reflect.deleteProperty(this.terminal, "rows");
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
this.restoreTerminalState(options);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
private getRawRows(): number {
|
|
673
|
-
return Math.max(2, readRows(this.terminal, this.rowsDescriptor));
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
private getScrollableRows(): number {
|
|
677
|
-
if (this.disposed || this.writing || this.renderingCluster || this.checkingOverlay || this.hasVisibleOverlay()) {
|
|
678
|
-
return this.getRawRows();
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const rawRows = this.getRawRows();
|
|
682
|
-
const width = Math.max(1, this.terminal.columns || 80);
|
|
683
|
-
const cluster = this.getCluster(width, rawRows);
|
|
684
|
-
return Math.max(1, rawRows - cluster.lines.length);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
private renderScrollableRoot(width: number): string[] {
|
|
688
|
-
if (!this.originalRender || this.disposed || this.renderingScrollableRoot) {
|
|
689
|
-
return this.originalRender?.(width) ?? [];
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (this.hasVisibleOverlay()) {
|
|
693
|
-
return this.originalRender(width).map((line) => sanitizeOverlayBaseLine(line, Math.max(1, width)));
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
this.renderingScrollableRoot = true;
|
|
697
|
-
try {
|
|
698
|
-
const start = this.refreshRootWindow(width);
|
|
699
|
-
return this.visibleRootLines.map((line, index) => {
|
|
700
|
-
const hovered = this.renderHoverHighlight(line, start + index, "root");
|
|
701
|
-
const selected = this.renderSelectionHighlight(hovered, start + index, "root");
|
|
702
|
-
return sanitizeLine(selected, Math.max(1, width));
|
|
703
|
-
});
|
|
704
|
-
} finally {
|
|
705
|
-
this.renderingScrollableRoot = false;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
private refreshRootWindow(width: number): number {
|
|
710
|
-
if (!this.originalRender) return this.updateVisibleRootWindow();
|
|
711
|
-
|
|
712
|
-
const rawRows = this.getRawRows();
|
|
713
|
-
const renderWidth = Math.max(1, width);
|
|
714
|
-
this.rootWindowWidth = renderWidth;
|
|
715
|
-
this.rootWindowRawRows = rawRows;
|
|
716
|
-
const cluster = this.getCluster(renderWidth, rawRows);
|
|
717
|
-
const scrollableRows = Math.max(1, rawRows - cluster.lines.length);
|
|
718
|
-
const renderedLines = this.originalRender(renderWidth);
|
|
719
|
-
const lines = this.decorateRootLines?.(renderedLines) ?? renderedLines;
|
|
720
|
-
this.rootLines = lines;
|
|
721
|
-
if (!this.suppressRootGrowthAdjustment
|
|
722
|
-
&& this.scrollOffset > 0
|
|
723
|
-
&& this.lastRootLineCount > 0
|
|
724
|
-
&& lines.length > this.lastRootLineCount) {
|
|
725
|
-
this.scrollOffset += lines.length - this.lastRootLineCount;
|
|
726
|
-
}
|
|
727
|
-
this.lastRootLineCount = lines.length;
|
|
728
|
-
const previousMaxScrollOffset = this.maxScrollOffset;
|
|
729
|
-
this.maxScrollOffset = Math.max(0, lines.length - scrollableRows);
|
|
730
|
-
const nextScrollOffset = Math.max(0, Math.min(this.scrollOffset, this.maxScrollOffset));
|
|
731
|
-
if (nextScrollOffset !== this.scrollOffset || this.maxScrollOffset !== previousMaxScrollOffset) {
|
|
732
|
-
this.pendingImageCleanup = true;
|
|
733
|
-
}
|
|
734
|
-
this.scrollOffset = nextScrollOffset;
|
|
735
|
-
|
|
736
|
-
return this.updateVisibleRootWindow(scrollableRows);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
private ensureRootWindow(width: number): void {
|
|
740
|
-
if (!this.originalRender) return;
|
|
741
|
-
|
|
742
|
-
const renderWidth = Math.max(1, width);
|
|
743
|
-
const rawRows = this.getRawRows();
|
|
744
|
-
const cluster = this.getCluster(renderWidth, rawRows);
|
|
745
|
-
const scrollableRows = Math.max(1, rawRows - cluster.lines.length);
|
|
746
|
-
const sameRootDimensions = this.rootWindowWidth === renderWidth && this.rootWindowRawRows === rawRows;
|
|
747
|
-
|
|
748
|
-
if (sameRootDimensions && this.visibleScrollableRows > 0) {
|
|
749
|
-
if (this.visibleScrollableRows !== scrollableRows) {
|
|
750
|
-
this.maxScrollOffset = Math.max(0, this.rootLines.length - scrollableRows);
|
|
751
|
-
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, this.maxScrollOffset));
|
|
752
|
-
this.pendingImageCleanup = true;
|
|
753
|
-
this.updateVisibleRootWindow(scrollableRows);
|
|
754
|
-
}
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
this.refreshRootWindow(renderWidth);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
private handleInput(data: string): { consume?: boolean; data?: string } | undefined {
|
|
762
|
-
if (this.disposed || this.hasVisibleOverlay()) return undefined;
|
|
763
|
-
|
|
764
|
-
// Handle this at the compositor input layer rather than relying on the
|
|
765
|
-
// editor wrapper. Pi can replace/rebind the editor during a session, which
|
|
766
|
-
// made the shortcut work once and then disappear on subsequent scrolls.
|
|
767
|
-
if (
|
|
768
|
-
!isKeyRelease(data)
|
|
769
|
-
&& this.jumpToBottomShortcut
|
|
770
|
-
&& matchesConfiguredShortcut(data, this.jumpToBottomShortcut)
|
|
771
|
-
) {
|
|
772
|
-
this.jumpToRootBottom();
|
|
773
|
-
this.onJumpToBottom?.();
|
|
774
|
-
return { consume: true };
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const mousePackets = this.mouseScroll ? parseSgrMousePackets(data) : null;
|
|
778
|
-
if (mousePackets) {
|
|
779
|
-
this.batchingMouseInput = true;
|
|
780
|
-
try {
|
|
781
|
-
for (const packet of mousePackets) {
|
|
782
|
-
this.handleMousePacket(packet);
|
|
783
|
-
}
|
|
784
|
-
} finally {
|
|
785
|
-
this.batchingMouseInput = false;
|
|
786
|
-
}
|
|
787
|
-
if (this.selectionRepaintPending) {
|
|
788
|
-
this.selectionRepaintPending = false;
|
|
789
|
-
this.repaintSelection(Math.max(1, this.terminal.columns || 80));
|
|
790
|
-
}
|
|
791
|
-
return { consume: true };
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
const keyboardDelta = parseKeyboardScrollDelta(data, this.keyboardScrollShortcuts);
|
|
795
|
-
if (keyboardDelta === 0) return undefined;
|
|
796
|
-
|
|
797
|
-
this.scrollBy(keyboardDelta);
|
|
798
|
-
return { consume: true };
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
private handleMousePacket(packet: SgrMousePacket): void {
|
|
802
|
-
const delta = mouseScrollDelta(packet);
|
|
803
|
-
if (delta !== 0) {
|
|
804
|
-
this.selectionDragging = false;
|
|
805
|
-
this.scrollBy(delta);
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// Mode 1003 reports every mouse move. Hover can use the last rendered
|
|
810
|
-
// viewport; rebuilding the whole root for each packet causes visible stalls.
|
|
811
|
-
if (isMouseMotion(packet) && !this.selectionDragging) {
|
|
812
|
-
const location = this.selectionLocationForPacket(packet);
|
|
813
|
-
const rootText = location?.area === "root"
|
|
814
|
-
? this.visibleRootLines[location.point.line - this.visibleRootStart] ?? ""
|
|
815
|
-
: "";
|
|
816
|
-
const nextHoveredLine = location?.area === "root" && this.onRootHover?.(
|
|
817
|
-
location.point.line,
|
|
818
|
-
location.point.col,
|
|
819
|
-
rootText,
|
|
820
|
-
)
|
|
821
|
-
? location.point.line
|
|
822
|
-
: null;
|
|
823
|
-
if (nextHoveredLine !== this.hoveredRootLine) {
|
|
824
|
-
this.hoveredRootLine = nextHoveredLine;
|
|
825
|
-
this.requestRender();
|
|
826
|
-
}
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const width = Math.max(1, this.terminal.columns || 80);
|
|
831
|
-
// Mouse selection and clicks operate on the last painted root window. Avoid
|
|
832
|
-
// re-rendering the complete transcript for every mouse packet; refresh only
|
|
833
|
-
// on the first packet or after a terminal resize.
|
|
834
|
-
this.ensureRootWindow(width);
|
|
835
|
-
const location = this.selectionLocationForPacket(packet);
|
|
836
|
-
|
|
837
|
-
if (isRightPress(packet)) {
|
|
838
|
-
this.selectionDragging = false;
|
|
839
|
-
this.preserveSelectionFocusOnRelease = false;
|
|
840
|
-
const selectedText = this.isLocationInsideSelection(location) ? this.getSelectedText() : "";
|
|
841
|
-
if (selectedText) {
|
|
842
|
-
this.onCopySelection?.(selectedText);
|
|
843
|
-
this.lastLeftPress = null;
|
|
844
|
-
this.pauseMouseReportingForContextMenu(selectedText);
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
this.clearSelection();
|
|
849
|
-
this.lastLeftPress = null;
|
|
850
|
-
this.pauseMouseReportingForContextMenu();
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
if (this.scrollSelectionAtViewportEdge(packet)) return;
|
|
855
|
-
if (isMouseRelease(packet) && this.consumedClusterClick) {
|
|
856
|
-
this.consumedClusterClick = false;
|
|
857
|
-
this.clearSelection();
|
|
858
|
-
this.lastLeftPress = null;
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
if (this.selectionDragging && isMouseRelease(packet)) {
|
|
862
|
-
this.finishSelection(packet, location);
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
if (!location) return;
|
|
867
|
-
|
|
868
|
-
if (isLeftPress(packet)) {
|
|
869
|
-
if (location.area === "root" && this.onRootClick) {
|
|
870
|
-
const rootText = this.visibleRootLines[location.point.line - this.visibleRootStart] ?? "";
|
|
871
|
-
// Keep the clicked row anchored while a tool opens downward. Each tool
|
|
872
|
-
// remembers the viewport start from before expansion so collapsing it can
|
|
873
|
-
// return to exactly that position, even after scrolling inside the panel.
|
|
874
|
-
const viewportStartBeforeClick = this.visibleRootStart;
|
|
875
|
-
this.suppressRootGrowthAdjustment = true;
|
|
876
|
-
let clickResult: boolean | RootClickResult = false;
|
|
877
|
-
try {
|
|
878
|
-
clickResult = this.onRootClick(location.point.line, location.point.col, rootText);
|
|
879
|
-
if (clickResult) {
|
|
880
|
-
this.refreshRootWindow(Math.max(1, this.terminal.columns || 80));
|
|
881
|
-
const detail = typeof clickResult === "object" ? clickResult : null;
|
|
882
|
-
let targetStart = viewportStartBeforeClick;
|
|
883
|
-
if (detail?.key && detail.action === "expand") {
|
|
884
|
-
this.toolExpansionStarts.set(detail.key, viewportStartBeforeClick);
|
|
885
|
-
} else if (detail?.key && detail.action === "collapse") {
|
|
886
|
-
targetStart = this.toolExpansionStarts.get(detail.key) ?? viewportStartBeforeClick;
|
|
887
|
-
this.toolExpansionStarts.delete(detail.key);
|
|
888
|
-
}
|
|
889
|
-
this.scrollOffset = Math.max(0, Math.min(
|
|
890
|
-
this.rootLines.length - Math.max(1, this.visibleScrollableRows) - targetStart,
|
|
891
|
-
this.maxScrollOffset,
|
|
892
|
-
));
|
|
893
|
-
this.updateVisibleRootWindow();
|
|
894
|
-
}
|
|
895
|
-
} finally {
|
|
896
|
-
this.suppressRootGrowthAdjustment = false;
|
|
897
|
-
}
|
|
898
|
-
if (clickResult) {
|
|
899
|
-
this.clearSelection();
|
|
900
|
-
this.lastLeftPress = null;
|
|
901
|
-
this.consumedClusterClick = true;
|
|
902
|
-
// refreshRootWindow() updated the logical lines above, but the TUI
|
|
903
|
-
// render is scheduled asynchronously. Paint the collapsed/expanded
|
|
904
|
-
// viewport immediately so stale rows cannot remain until the next drag.
|
|
905
|
-
this.repaintScrollableViewport(Math.max(1, this.terminal.columns || 80));
|
|
906
|
-
this.requestRender();
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (location.area === "cluster" && this.onClusterClick?.(
|
|
912
|
-
location.point.line,
|
|
913
|
-
location.point.col,
|
|
914
|
-
this.visibleClusterLines[location.point.line] ?? "",
|
|
915
|
-
)) {
|
|
916
|
-
this.clearSelection();
|
|
917
|
-
this.lastLeftPress = null;
|
|
918
|
-
this.consumedClusterClick = true;
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
this.startSelection(location);
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
if (this.selectionDragging && isLeftDrag(packet) && location.area === this.selectionArea) {
|
|
927
|
-
this.lastLeftPress = null;
|
|
928
|
-
this.preserveSelectionFocusOnRelease = false;
|
|
929
|
-
this.selectionFocus = location.point;
|
|
930
|
-
this.repaintSelection(width);
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
private updateVisibleRootWindow(scrollableRows = this.visibleScrollableRows): number {
|
|
936
|
-
const rows = Math.max(1, scrollableRows);
|
|
937
|
-
const start = Math.max(0, this.rootLines.length - rows - this.scrollOffset);
|
|
938
|
-
const visibleLines = this.rootLines.slice(start, start + rows);
|
|
939
|
-
while (visibleLines.length < rows) {
|
|
940
|
-
visibleLines.push("");
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
this.visibleRootStart = start;
|
|
944
|
-
this.visibleScrollableRows = rows;
|
|
945
|
-
this.visibleRootLines = visibleLines;
|
|
946
|
-
return start;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
private finishSelection(packet: SgrMousePacket, location: SelectionLocation | null): void {
|
|
950
|
-
if (!this.preserveSelectionFocusOnRelease) {
|
|
951
|
-
this.selectionFocus = location?.area === this.selectionArea
|
|
952
|
-
? location.point
|
|
953
|
-
: this.clampedSelectionPointForPacket(packet, this.selectionArea);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
this.preserveSelectionFocusOnRelease = false;
|
|
957
|
-
this.selectionDragging = false;
|
|
958
|
-
const selectedText = this.getSelectedText();
|
|
959
|
-
if (selectedText) {
|
|
960
|
-
this.lastLeftPress = null;
|
|
961
|
-
this.onCopySelection?.(selectedText);
|
|
962
|
-
} else {
|
|
963
|
-
this.clearSelection();
|
|
964
|
-
}
|
|
965
|
-
this.repaintSelection(Math.max(1, this.terminal.columns || 80));
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
private startSelection(location: SelectionLocation): void {
|
|
969
|
-
const now = Date.now();
|
|
970
|
-
const line = location.point.line;
|
|
971
|
-
if (
|
|
972
|
-
this.lastLeftPress
|
|
973
|
-
&& this.lastLeftPress.area === location.area
|
|
974
|
-
&& this.lastLeftPress.line === line
|
|
975
|
-
&& now - this.lastLeftPress.at <= DOUBLE_CLICK_MS
|
|
976
|
-
) {
|
|
977
|
-
this.selectionArea = location.area;
|
|
978
|
-
this.selectionAnchor = { line, col: 0 };
|
|
979
|
-
this.selectionFocus = { line, col: this.selectionLineWidth(location.area, line) };
|
|
980
|
-
this.selectionDragging = true;
|
|
981
|
-
this.preserveSelectionFocusOnRelease = true;
|
|
982
|
-
this.lastLeftPress = null;
|
|
983
|
-
this.repaintSelection(Math.max(1, this.terminal.columns || 80));
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
this.selectionArea = location.area;
|
|
988
|
-
this.selectionAnchor = location.point;
|
|
989
|
-
this.selectionFocus = location.point;
|
|
990
|
-
this.selectionDragging = true;
|
|
991
|
-
this.preserveSelectionFocusOnRelease = false;
|
|
992
|
-
this.lastLeftPress = { area: location.area, line, at: now };
|
|
993
|
-
this.repaintSelection(Math.max(1, this.terminal.columns || 80));
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
private selectionLocationForPacket(packet: SgrMousePacket): SelectionLocation | null {
|
|
997
|
-
if (packet.row < 1) return null;
|
|
998
|
-
|
|
999
|
-
const col = Math.max(0, packet.col - 1);
|
|
1000
|
-
if (packet.row <= this.visibleScrollableRows) {
|
|
1001
|
-
return {
|
|
1002
|
-
area: "root",
|
|
1003
|
-
point: { line: this.visibleRootStart + packet.row - 1, col },
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
const clusterLine = packet.row - this.visibleScrollableRows - 1;
|
|
1008
|
-
if (clusterLine >= this.visibleClusterLines.length) return null;
|
|
1009
|
-
|
|
1010
|
-
return {
|
|
1011
|
-
area: "cluster",
|
|
1012
|
-
point: { line: clusterLine, col },
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
private scrollSelectionAtViewportEdge(packet: SgrMousePacket): boolean {
|
|
1017
|
-
if (!this.selectionDragging || this.selectionArea !== "root" || !isLeftDrag(packet)) return false;
|
|
1018
|
-
|
|
1019
|
-
const delta = packet.row <= 1 ? 1 : packet.row >= this.visibleScrollableRows ? -1 : 0;
|
|
1020
|
-
if (delta === 0) return false;
|
|
1021
|
-
|
|
1022
|
-
const nextOffset = Math.max(0, Math.min(this.scrollOffset + delta, this.maxScrollOffset));
|
|
1023
|
-
if (nextOffset === this.scrollOffset) return false;
|
|
1024
|
-
|
|
1025
|
-
this.lastLeftPress = null;
|
|
1026
|
-
this.preserveSelectionFocusOnRelease = true;
|
|
1027
|
-
this.scrollOffset = nextOffset;
|
|
1028
|
-
this.pendingImageCleanup = true;
|
|
1029
|
-
const start = this.updateVisibleRootWindow();
|
|
1030
|
-
const edgeLine = delta > 0 ? start : start + Math.max(0, this.visibleScrollableRows - 1);
|
|
1031
|
-
this.selectionFocus = {
|
|
1032
|
-
line: edgeLine,
|
|
1033
|
-
col: Math.max(0, packet.col - 1),
|
|
1034
|
-
};
|
|
1035
|
-
this.repaintSelection(Math.max(1, this.terminal.columns || 80));
|
|
1036
|
-
return true;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
private clampedSelectionPointForPacket(packet: SgrMousePacket, area: SelectionArea | null): SelectionPoint {
|
|
1040
|
-
if (area === "cluster") {
|
|
1041
|
-
return {
|
|
1042
|
-
line: Math.max(0, Math.min(packet.row - this.visibleScrollableRows - 1, this.visibleClusterLines.length - 1)),
|
|
1043
|
-
col: Math.max(0, packet.col - 1),
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
const row = Math.max(1, Math.min(packet.row, this.visibleScrollableRows));
|
|
1048
|
-
return {
|
|
1049
|
-
line: this.visibleRootStart + row - 1,
|
|
1050
|
-
col: Math.max(0, packet.col - 1),
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
private renderHoverHighlight(line: string, lineIndex: number, area: SelectionArea): string {
|
|
1055
|
-
if (area !== "root" || this.hoveredRootLine !== lineIndex) return line;
|
|
1056
|
-
const plain = stripAnsi(line);
|
|
1057
|
-
if (!plain.trim()) return line;
|
|
1058
|
-
return `\x1b[1;97m${plain}\x1b[22;39m`;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
private renderSelectionHighlight(line: string, lineIndex: number, area: SelectionArea): string {
|
|
1062
|
-
const range = this.getSelectionRangeForLine(lineIndex, area);
|
|
1063
|
-
if (!range) return line;
|
|
1064
|
-
|
|
1065
|
-
const plain = stripAnsi(line);
|
|
1066
|
-
const startCol = Math.max(0, Math.min(range.startCol, visibleWidth(plain)));
|
|
1067
|
-
const endCol = Math.max(startCol, Math.min(range.endCol, visibleWidth(plain)));
|
|
1068
|
-
if (startCol === endCol) return line;
|
|
1069
|
-
|
|
1070
|
-
const before = sliceColumns(plain, 0, startCol);
|
|
1071
|
-
const selected = sliceColumns(plain, startCol, endCol);
|
|
1072
|
-
const after = sliceColumns(plain, endCol, Number.POSITIVE_INFINITY);
|
|
1073
|
-
return `${before}\x1b[7m${selected}\x1b[27m${after}`;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
private selectionLineWidth(area: SelectionArea, lineIndex: number): number {
|
|
1077
|
-
const lines = area === "root" ? this.visibleRootLines : this.visibleClusterLines;
|
|
1078
|
-
const firstLine = area === "root" ? this.visibleRootStart : 0;
|
|
1079
|
-
return visibleWidth(stripAnsi(lines[lineIndex - firstLine] ?? ""));
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
private getSelectedText(): string {
|
|
1083
|
-
if (!this.selectionArea || !this.selectionAnchor || !this.selectionFocus) return "";
|
|
1084
|
-
|
|
1085
|
-
const start = compareSelectionPoints(this.selectionAnchor, this.selectionFocus) <= 0
|
|
1086
|
-
? this.selectionAnchor
|
|
1087
|
-
: this.selectionFocus;
|
|
1088
|
-
const end = start === this.selectionAnchor ? this.selectionFocus : this.selectionAnchor;
|
|
1089
|
-
if (start.line === end.line && start.col === end.col) return "";
|
|
1090
|
-
|
|
1091
|
-
const lines = this.selectionArea === "root" ? this.rootLines : this.visibleClusterLines;
|
|
1092
|
-
const selected: string[] = [];
|
|
1093
|
-
for (let lineIndex = start.line; lineIndex <= end.line; lineIndex++) {
|
|
1094
|
-
const line = stripAnsi(lines[lineIndex] ?? "");
|
|
1095
|
-
const startCol = lineIndex === start.line ? start.col : 0;
|
|
1096
|
-
const endCol = lineIndex === end.line ? end.col : Number.POSITIVE_INFINITY;
|
|
1097
|
-
selected.push(sliceColumns(line, startCol, endCol));
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
return selected.join("\n").replace(/[ \t]+$/gm, "").trimEnd();
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
private getSelectionRangeForLine(lineIndex: number, area: SelectionArea): { startCol: number; endCol: number } | null {
|
|
1104
|
-
if (this.selectionArea !== area || !this.selectionAnchor || !this.selectionFocus) return null;
|
|
1105
|
-
|
|
1106
|
-
const start = compareSelectionPoints(this.selectionAnchor, this.selectionFocus) <= 0
|
|
1107
|
-
? this.selectionAnchor
|
|
1108
|
-
: this.selectionFocus;
|
|
1109
|
-
const end = start === this.selectionAnchor ? this.selectionFocus : this.selectionAnchor;
|
|
1110
|
-
if (lineIndex < start.line || lineIndex > end.line) return null;
|
|
1111
|
-
|
|
1112
|
-
return {
|
|
1113
|
-
startCol: lineIndex === start.line ? start.col : 0,
|
|
1114
|
-
endCol: lineIndex === end.line ? end.col : Number.POSITIVE_INFINITY,
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
private isLocationInsideSelection(location: SelectionLocation | null): boolean {
|
|
1119
|
-
if (!location || location.area !== this.selectionArea) return false;
|
|
1120
|
-
const range = this.getSelectionRangeForLine(location.point.line, location.area);
|
|
1121
|
-
return Boolean(range && location.point.col >= range.startCol && location.point.col < range.endCol);
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
private scrollBy(delta: number): void {
|
|
1125
|
-
const width = Math.max(1, this.terminal.columns || 80);
|
|
1126
|
-
this.ensureRootWindow(width);
|
|
1127
|
-
|
|
1128
|
-
const wasScrolled = this.scrollOffset > 0;
|
|
1129
|
-
const nextOffset = Math.max(0, Math.min(this.scrollOffset + delta, this.maxScrollOffset));
|
|
1130
|
-
if (nextOffset === this.scrollOffset) return;
|
|
1131
|
-
|
|
1132
|
-
this.clearSelection();
|
|
1133
|
-
this.lastLeftPress = null;
|
|
1134
|
-
this.hoveredRootLine = null;
|
|
1135
|
-
this.scrollOffset = nextOffset;
|
|
1136
|
-
this.pendingImageCleanup = true;
|
|
1137
|
-
if (wasScrolled && nextOffset === 0) this.onReachRootBottom?.();
|
|
1138
|
-
// This writes the complete scrollable viewport synchronously; scheduling a
|
|
1139
|
-
// second full TUI render here only repaints the same frame again.
|
|
1140
|
-
this.repaintScrollableViewport(width);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
private requestRender(): void {
|
|
1144
|
-
if (typeof this.tui.requestRender === "function") {
|
|
1145
|
-
this.tui.requestRender();
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
private repaintSelection(width: number): void {
|
|
1150
|
-
if (this.batchingMouseInput) {
|
|
1151
|
-
this.selectionRepaintPending = true;
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
if (!this.repaintScrollableViewport(width)) {
|
|
1155
|
-
this.requestRender();
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
private repaintScrollableViewport(width: number): boolean {
|
|
1160
|
-
if (this.disposed || this.writing || this.hasVisibleOverlay()) return false;
|
|
1161
|
-
|
|
1162
|
-
const rawRows = this.getRawRows();
|
|
1163
|
-
const cluster = this.getCluster(width, rawRows);
|
|
1164
|
-
const scrollableRows = Math.max(1, rawRows - cluster.lines.length);
|
|
1165
|
-
this.maxScrollOffset = Math.max(0, this.rootLines.length - scrollableRows);
|
|
1166
|
-
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, this.maxScrollOffset));
|
|
1167
|
-
const start = this.updateVisibleRootWindow(scrollableRows);
|
|
1168
|
-
let buffer = beginSynchronizedOutput()
|
|
1169
|
-
+ this.consumePendingImageCleanup()
|
|
1170
|
-
+ disableAutoWrap()
|
|
1171
|
-
+ setScrollRegion(1, scrollableRows)
|
|
1172
|
-
+ moveCursor(1, 1);
|
|
1173
|
-
|
|
1174
|
-
for (let row = 0; row < scrollableRows; row++) {
|
|
1175
|
-
if (row > 0) buffer += "\r\n";
|
|
1176
|
-
buffer += clearLine();
|
|
1177
|
-
const hovered = this.renderHoverHighlight(this.visibleRootLines[row] ?? "", start + row, "root");
|
|
1178
|
-
buffer += sanitizeLine(this.renderSelectionHighlight(hovered, start + row, "root"), width);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
buffer += buildFixedClusterPaint(this.decorateCluster(cluster), rawRows, width, this.getShowHardwareCursor());
|
|
1182
|
-
buffer += enableAutoWrap();
|
|
1183
|
-
buffer += this.mouseReportingStateGuard();
|
|
1184
|
-
buffer += endSynchronizedOutput();
|
|
1185
|
-
this.originalWrite(buffer);
|
|
1186
|
-
return true;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
private pauseMouseReportingForContextMenu(textToRestoreToClipboard: string | null = null): void {
|
|
1190
|
-
if (this.mouseReportingResumeTimer) {
|
|
1191
|
-
clearTimeout(this.mouseReportingResumeTimer);
|
|
1192
|
-
}
|
|
1193
|
-
if (this.clipboardRestoreTimer) {
|
|
1194
|
-
clearTimeout(this.clipboardRestoreTimer);
|
|
1195
|
-
this.clipboardRestoreTimer = null;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
this.originalWrite(beginSynchronizedOutput() + disableMouseReporting() + endSynchronizedOutput());
|
|
1199
|
-
this.mouseReportingResumeTimer = setTimeout(() => {
|
|
1200
|
-
this.mouseReportingResumeTimer = null;
|
|
1201
|
-
if (!this.disposed) {
|
|
1202
|
-
this.originalWrite(beginSynchronizedOutput() + enableMouseReporting() + endSynchronizedOutput());
|
|
1203
|
-
}
|
|
1204
|
-
}, CONTEXT_MENU_MOUSE_REPORTING_PAUSE_MS);
|
|
1205
|
-
|
|
1206
|
-
if (typeof this.mouseReportingResumeTimer === "object" && "unref" in this.mouseReportingResumeTimer) {
|
|
1207
|
-
this.mouseReportingResumeTimer.unref();
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
const restoreClipboard = this.onCopySelection;
|
|
1211
|
-
if (!textToRestoreToClipboard || !restoreClipboard) return;
|
|
1212
|
-
|
|
1213
|
-
let remainingRestores = Math.ceil(CONTEXT_MENU_SELECTION_RESTORE_WINDOW_MS / CONTEXT_MENU_CLIPBOARD_RESTORE_INTERVAL_MS);
|
|
1214
|
-
const scheduleClipboardRestore = () => {
|
|
1215
|
-
this.clipboardRestoreTimer = setTimeout(() => {
|
|
1216
|
-
this.clipboardRestoreTimer = null;
|
|
1217
|
-
if (this.disposed) return;
|
|
1218
|
-
|
|
1219
|
-
remainingRestores -= 1;
|
|
1220
|
-
if (this.getSelectedText() !== textToRestoreToClipboard) return;
|
|
1221
|
-
|
|
1222
|
-
restoreClipboard(textToRestoreToClipboard);
|
|
1223
|
-
if (remainingRestores > 0) {
|
|
1224
|
-
scheduleClipboardRestore();
|
|
1225
|
-
}
|
|
1226
|
-
}, CONTEXT_MENU_CLIPBOARD_RESTORE_INTERVAL_MS);
|
|
1227
|
-
|
|
1228
|
-
if (typeof this.clipboardRestoreTimer === "object" && "unref" in this.clipboardRestoreTimer) {
|
|
1229
|
-
this.clipboardRestoreTimer.unref();
|
|
1230
|
-
}
|
|
1231
|
-
};
|
|
1232
|
-
|
|
1233
|
-
scheduleClipboardRestore();
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
private clearSelection(): void {
|
|
1237
|
-
this.selectionArea = null;
|
|
1238
|
-
this.selectionAnchor = null;
|
|
1239
|
-
this.selectionFocus = null;
|
|
1240
|
-
this.selectionDragging = false;
|
|
1241
|
-
this.preserveSelectionFocusOnRelease = false;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
private activeExtendedKeyboardMode(): ExtendedKeyboardMode | null {
|
|
1245
|
-
if (this.terminal.kittyProtocolActive === true) return "kitty";
|
|
1246
|
-
if (Reflect.get(this.terminal, "_modifyOtherKeysActive") === true) return "modifyOtherKeys";
|
|
1247
|
-
return null;
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
private enableAlternateScreenKeyboardMode(): string {
|
|
1251
|
-
this.extendedKeyboardMode = this.activeExtendedKeyboardMode();
|
|
1252
|
-
return this.extendedKeyboardMode ? enableExtendedKeyboardMode(this.extendedKeyboardMode) : "";
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
private restoreTerminalState(options: DisposeOptions = {}): void {
|
|
1256
|
-
const activeMode = this.extendedKeyboardMode ?? this.activeExtendedKeyboardMode();
|
|
1257
|
-
const restoreMainScreenMode = !options.resetExtendedKeyboardModes && this.extendedKeyboardMode === null && activeMode !== null;
|
|
1258
|
-
|
|
1259
|
-
this.originalWrite(
|
|
1260
|
-
beginSynchronizedOutput()
|
|
1261
|
-
+ resetScrollRegion()
|
|
1262
|
-
+ (this.mouseScroll ? disableMouseReporting() : "")
|
|
1263
|
-
+ (activeMode ? disableExtendedKeyboardMode(activeMode) : "")
|
|
1264
|
-
+ enableAlternateScrollMode()
|
|
1265
|
-
+ exitAlternateScreen()
|
|
1266
|
-
+ (restoreMainScreenMode && activeMode ? enableExtendedKeyboardMode(activeMode) : "")
|
|
1267
|
-
+ (options.resetExtendedKeyboardModes ? resetExtendedKeyboardModes() : "")
|
|
1268
|
-
+ endSynchronizedOutput(),
|
|
1269
|
-
);
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
private restoreTerminalStateForExit(): void {
|
|
1273
|
-
try {
|
|
1274
|
-
this.restoreTerminalState({ resetExtendedKeyboardModes: true });
|
|
1275
|
-
} catch {
|
|
1276
|
-
// Process-exit cleanup cannot report useful errors and must not throw.
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
private write(data: string): void {
|
|
1281
|
-
if (this.renderPassActive) this.wroteDuringRenderPass = true;
|
|
1282
|
-
if (this.disposed || this.writing || this.hasVisibleOverlay()) {
|
|
1283
|
-
this.originalWrite(data);
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
this.writing = true;
|
|
1288
|
-
try {
|
|
1289
|
-
const rawRows = this.getRawRows();
|
|
1290
|
-
const width = Math.max(1, this.terminal.columns || 80);
|
|
1291
|
-
const cluster = this.getCluster(width, rawRows);
|
|
1292
|
-
const reservedRows = cluster.lines.length;
|
|
1293
|
-
|
|
1294
|
-
if (reservedRows === 0 || rawRows <= 2) {
|
|
1295
|
-
this.originalWrite(data);
|
|
1296
|
-
return;
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
const scrollBottom = Math.max(1, rawRows - reservedRows);
|
|
1300
|
-
const hardwareCursorRow = typeof this.tui.hardwareCursorRow === "number"
|
|
1301
|
-
? this.tui.hardwareCursorRow
|
|
1302
|
-
: typeof this.tui.cursorRow === "number"
|
|
1303
|
-
? this.tui.cursorRow
|
|
1304
|
-
: 0;
|
|
1305
|
-
const viewportTop = typeof this.tui.previousViewportTop === "number" ? this.tui.previousViewportTop : 0;
|
|
1306
|
-
const screenRow = Math.max(1, Math.min(scrollBottom, hardwareCursorRow - viewportTop + 1));
|
|
1307
|
-
const buffer = beginSynchronizedOutput()
|
|
1308
|
-
+ this.consumePendingImageCleanup()
|
|
1309
|
-
+ disableAutoWrap()
|
|
1310
|
-
+ setScrollRegion(1, scrollBottom)
|
|
1311
|
-
+ moveCursor(screenRow, 1)
|
|
1312
|
-
+ data
|
|
1313
|
-
+ buildFixedClusterPaint(this.decorateCluster(cluster), rawRows, width, this.getShowHardwareCursor())
|
|
1314
|
-
+ enableAutoWrap()
|
|
1315
|
-
+ this.mouseReportingStateGuard()
|
|
1316
|
-
+ endSynchronizedOutput();
|
|
1317
|
-
|
|
1318
|
-
this.originalWrite(buffer);
|
|
1319
|
-
} finally {
|
|
1320
|
-
this.writing = false;
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
private consumePendingImageCleanup(): string {
|
|
1325
|
-
if (!this.pendingImageCleanup) return "";
|
|
1326
|
-
this.pendingImageCleanup = false;
|
|
1327
|
-
return deleteAllKittyImages();
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
private mouseReportingStateGuard(): string {
|
|
1331
|
-
return this.mouseScroll && !this.mouseReportingResumeTimer ? enableMouseReporting() : "";
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
private getCluster(width: number, terminalRows: number): FixedEditorClusterRender {
|
|
1335
|
-
if (
|
|
1336
|
-
this.renderPassActive &&
|
|
1337
|
-
this.renderPassCluster?.width === width &&
|
|
1338
|
-
this.renderPassCluster.terminalRows === terminalRows
|
|
1339
|
-
) {
|
|
1340
|
-
return this.renderPassCluster.cluster;
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
const cluster = this.withClusterRender(() => this.renderCluster(width, terminalRows));
|
|
1344
|
-
this.visibleClusterLines = cluster.lines;
|
|
1345
|
-
if (this.renderPassActive) {
|
|
1346
|
-
this.renderPassCluster = { width, terminalRows, cluster };
|
|
1347
|
-
}
|
|
1348
|
-
return cluster;
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
private decorateCluster(cluster: FixedEditorClusterRender): FixedEditorClusterRender {
|
|
1352
|
-
if (this.selectionArea !== "cluster") return cluster;
|
|
1353
|
-
|
|
1354
|
-
return {
|
|
1355
|
-
...cluster,
|
|
1356
|
-
lines: cluster.lines.map((line, index) => this.renderSelectionHighlight(line, index, "cluster")),
|
|
1357
|
-
};
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
private withClusterRender<T>(render: () => T): T {
|
|
1361
|
-
const wasRenderingCluster = this.renderingCluster;
|
|
1362
|
-
this.renderingCluster = true;
|
|
1363
|
-
try {
|
|
1364
|
-
return render();
|
|
1365
|
-
} finally {
|
|
1366
|
-
this.renderingCluster = wasRenderingCluster;
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
private hasVisibleOverlay(): boolean {
|
|
1371
|
-
if (this.checkingOverlay) return false;
|
|
1372
|
-
|
|
1373
|
-
this.checkingOverlay = true;
|
|
1374
|
-
try {
|
|
1375
|
-
if (typeof this.tui.hasOverlay === "function" && this.tui.hasOverlay()) {
|
|
1376
|
-
return true;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
const overlayStack = Reflect.get(this.tui, "overlayStack");
|
|
1380
|
-
if (!Array.isArray(overlayStack)) {
|
|
1381
|
-
return false;
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
return overlayStack.some((entry) => entry && entry.hidden !== true);
|
|
1385
|
-
} finally {
|
|
1386
|
-
this.checkingOverlay = false;
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
}
|