@duckmind/dm-darwin-arm64 0.35.4 → 0.35.6

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