@code-yeongyu/senpi-tui 2026.9.29 → 2026.10.1-2
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 +38 -0
- package/dist/autocomplete.d.ts +5 -0
- package/dist/autocomplete.js +38 -14
- package/dist/colors.d.ts +73 -0
- package/dist/colors.js +273 -0
- package/dist/components/box.js +5 -3
- package/dist/components/editor.d.ts +4 -1
- package/dist/components/editor.js +23 -8
- package/dist/components/latex.js +5 -0
- package/dist/editor-component.d.ts +7 -2
- package/dist/fuzzy.js +23 -22
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/native-platform.d.ts +2 -0
- package/dist/oklab.d.ts +43 -0
- package/dist/oklab.js +210 -0
- package/dist/slash-command-autocomplete.js +3 -0
- package/dist/terminal-capabilities.js +1 -1
- package/dist/terminal-colors.d.ts +19 -2
- package/dist/terminal-colors.js +12 -7
- package/dist/terminal-image.d.ts +3 -1
- package/dist/terminal-image.js +33 -9
- package/dist/terminal.d.ts +3 -0
- package/dist/terminal.js +15 -8
- package/dist/tui-alt-screen.d.ts +12 -7
- package/dist/tui-alt-screen.js +37 -17
- package/dist/tui.d.ts +18 -17
- package/dist/tui.js +79 -62
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +108 -60
- package/dist/wheel-scroll.d.ts +23 -0
- package/dist/wheel-scroll.js +70 -0
- package/native/clipboard.h +40 -2
- package/native/darwin/README.md +1 -1
- package/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/native/darwin/src/darwin-platform.m +39 -0
- package/native/napi.h +2 -0
- package/package.json +3 -2
- package/dist/alt-screen-search.d.ts.map +0 -1
- package/dist/alt-screen-search.js.map +0 -1
- package/dist/autocomplete.d.ts.map +0 -1
- package/dist/autocomplete.js.map +0 -1
- package/dist/components/alt-screen-flash.d.ts.map +0 -1
- package/dist/components/alt-screen-flash.js.map +0 -1
- package/dist/components/box.d.ts.map +0 -1
- package/dist/components/box.js.map +0 -1
- package/dist/components/cancellable-loader.d.ts.map +0 -1
- package/dist/components/cancellable-loader.js.map +0 -1
- package/dist/components/editor-line-render.d.ts.map +0 -1
- package/dist/components/editor-line-render.js.map +0 -1
- package/dist/components/editor.d.ts.map +0 -1
- package/dist/components/editor.js.map +0 -1
- package/dist/components/h-stack.d.ts.map +0 -1
- package/dist/components/h-stack.js.map +0 -1
- package/dist/components/image.d.ts.map +0 -1
- package/dist/components/image.js.map +0 -1
- package/dist/components/input.d.ts.map +0 -1
- package/dist/components/input.js.map +0 -1
- package/dist/components/latex.d.ts.map +0 -1
- package/dist/components/latex.js.map +0 -1
- package/dist/components/loader.d.ts.map +0 -1
- package/dist/components/loader.js.map +0 -1
- package/dist/components/markdown.d.ts.map +0 -1
- package/dist/components/markdown.js.map +0 -1
- package/dist/components/mouse-region.d.ts.map +0 -1
- package/dist/components/mouse-region.js.map +0 -1
- package/dist/components/scroll-view.d.ts.map +0 -1
- package/dist/components/scroll-view.js.map +0 -1
- package/dist/components/select-list.d.ts.map +0 -1
- package/dist/components/select-list.js.map +0 -1
- package/dist/components/settings-list.d.ts.map +0 -1
- package/dist/components/settings-list.js.map +0 -1
- package/dist/components/spacer.d.ts.map +0 -1
- package/dist/components/spacer.js.map +0 -1
- package/dist/components/stack.d.ts.map +0 -1
- package/dist/components/stack.js.map +0 -1
- package/dist/components/text.d.ts.map +0 -1
- package/dist/components/text.js.map +0 -1
- package/dist/components/truncated-text.d.ts.map +0 -1
- package/dist/components/truncated-text.js.map +0 -1
- package/dist/components/v-stack.d.ts.map +0 -1
- package/dist/components/v-stack.js.map +0 -1
- package/dist/dollar-invocation-autocomplete.d.ts.map +0 -1
- package/dist/dollar-invocation-autocomplete.js.map +0 -1
- package/dist/editor-component.d.ts.map +0 -1
- package/dist/editor-component.js.map +0 -1
- package/dist/fuzzy.d.ts.map +0 -1
- package/dist/fuzzy.js.map +0 -1
- package/dist/image-markers.d.ts.map +0 -1
- package/dist/image-markers.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keybindings.d.ts.map +0 -1
- package/dist/keybindings.js.map +0 -1
- package/dist/keys.d.ts.map +0 -1
- package/dist/keys.js.map +0 -1
- package/dist/kill-ring.d.ts.map +0 -1
- package/dist/kill-ring.js.map +0 -1
- package/dist/layout-node.d.ts.map +0 -1
- package/dist/layout-node.js.map +0 -1
- package/dist/layout.d.ts.map +0 -1
- package/dist/layout.js.map +0 -1
- package/dist/mouse-input.d.ts.map +0 -1
- package/dist/mouse-input.js.map +0 -1
- package/dist/mux.d.ts.map +0 -1
- package/dist/mux.js.map +0 -1
- package/dist/native-modifiers.d.ts.map +0 -1
- package/dist/native-modifiers.js.map +0 -1
- package/dist/native-module-path.d.ts.map +0 -1
- package/dist/native-module-path.js.map +0 -1
- package/dist/native-platform.d.ts.map +0 -1
- package/dist/native-platform.js.map +0 -1
- package/dist/paste-markers.d.ts.map +0 -1
- package/dist/paste-markers.js.map +0 -1
- package/dist/slash-command-autocomplete.d.ts.map +0 -1
- package/dist/slash-command-autocomplete.js.map +0 -1
- package/dist/stderr-observer.d.ts.map +0 -1
- package/dist/stderr-observer.js.map +0 -1
- package/dist/stdin-buffer.d.ts.map +0 -1
- package/dist/stdin-buffer.js.map +0 -1
- package/dist/terminal-capabilities.d.ts.map +0 -1
- package/dist/terminal-capabilities.js.map +0 -1
- package/dist/terminal-colors.d.ts.map +0 -1
- package/dist/terminal-colors.js.map +0 -1
- package/dist/terminal-image.d.ts.map +0 -1
- package/dist/terminal-image.js.map +0 -1
- package/dist/terminal-text.d.ts.map +0 -1
- package/dist/terminal-text.js.map +0 -1
- package/dist/terminal.d.ts.map +0 -1
- package/dist/terminal.js.map +0 -1
- package/dist/tmux-cursor-query.d.ts.map +0 -1
- package/dist/tmux-cursor-query.js.map +0 -1
- package/dist/tmux-focus.d.ts.map +0 -1
- package/dist/tmux-focus.js.map +0 -1
- package/dist/tmux-image-capability.d.ts.map +0 -1
- package/dist/tmux-image-capability.js.map +0 -1
- package/dist/tmux-image-probe.d.ts.map +0 -1
- package/dist/tmux-image-probe.js.map +0 -1
- package/dist/tui-alt-screen.d.ts.map +0 -1
- package/dist/tui-alt-screen.js.map +0 -1
- package/dist/tui-main-screen.d.ts.map +0 -1
- package/dist/tui-main-screen.js.map +0 -1
- package/dist/tui.d.ts.map +0 -1
- package/dist/tui.js.map +0 -1
- package/dist/undo-stack.d.ts.map +0 -1
- package/dist/undo-stack.js.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js.map +0 -1
- package/dist/word-navigation.d.ts.map +0 -1
- package/dist/word-navigation.js.map +0 -1
package/dist/terminal.js
CHANGED
|
@@ -188,6 +188,8 @@ export class ProcessTerminal {
|
|
|
188
188
|
this._kittyProtocolActive = false;
|
|
189
189
|
this._modifyOtherKeysActive = false;
|
|
190
190
|
this.keyboardProtocolPushed = false;
|
|
191
|
+
/** DA1 replies owed to keyboard protocol queries. Later DA1 replies answer other queries and are forwarded. */
|
|
192
|
+
this.pendingKeyboardProtocolDeviceAttributes = 0;
|
|
191
193
|
this.keyboardProtocolNegotiationBuffer = "";
|
|
192
194
|
this.discardingPrivateResponse = false;
|
|
193
195
|
this.writeLogPath = (() => {
|
|
@@ -399,15 +401,15 @@ export class ProcessTerminal {
|
|
|
399
401
|
return;
|
|
400
402
|
}
|
|
401
403
|
}
|
|
402
|
-
const
|
|
403
|
-
if (
|
|
404
|
+
const negotiation = this.readKeyboardProtocolNegotiationSequence(sequence);
|
|
405
|
+
if (negotiation === "pending") {
|
|
404
406
|
this.scheduleKeyboardProtocolNegotiationBufferFlush();
|
|
405
407
|
return; // Wait briefly for the rest of a split Kitty response.
|
|
406
408
|
}
|
|
407
|
-
if (this.handleKeyboardProtocolNegotiationSequence(
|
|
409
|
+
if (negotiation && this.handleKeyboardProtocolNegotiationSequence(negotiation.parsed)) {
|
|
408
410
|
return;
|
|
409
411
|
}
|
|
410
|
-
this.forwardInputSequence(sequence);
|
|
412
|
+
this.forwardInputSequence(negotiation?.sequence ?? sequence);
|
|
411
413
|
});
|
|
412
414
|
// Re-wrap paste content with bracketed paste markers for existing editor handling
|
|
413
415
|
this.stdinBuffer.on("paste", (content) => {
|
|
@@ -443,12 +445,11 @@ export class ProcessTerminal {
|
|
|
443
445
|
this.enableModifyOtherKeys();
|
|
444
446
|
}
|
|
445
447
|
this.keyboardProtocolPushed = true;
|
|
448
|
+
this.pendingKeyboardProtocolDeviceAttributes += 1;
|
|
446
449
|
this.clearKeyboardProtocolNegotiationBuffer();
|
|
447
450
|
this.rawWrite(KITTY_KEYBOARD_PROTOCOL_QUERY);
|
|
448
451
|
}
|
|
449
452
|
handleKeyboardProtocolNegotiationSequence(negotiationSequence) {
|
|
450
|
-
if (!negotiationSequence)
|
|
451
|
-
return false;
|
|
452
453
|
this.clearKeyboardProtocolNegotiationBuffer();
|
|
453
454
|
if (negotiationSequence.type === "cursor-position") {
|
|
454
455
|
if (this.cursorQuery?.issued && this.cursorQuery.tmuxPane === undefined) {
|
|
@@ -457,6 +458,11 @@ export class ProcessTerminal {
|
|
|
457
458
|
}
|
|
458
459
|
return true;
|
|
459
460
|
}
|
|
461
|
+
if (negotiationSequence.type === "device-attributes") {
|
|
462
|
+
if (this.pendingKeyboardProtocolDeviceAttributes === 0)
|
|
463
|
+
return false;
|
|
464
|
+
this.pendingKeyboardProtocolDeviceAttributes -= 1;
|
|
465
|
+
}
|
|
460
466
|
this.keyboardNegotiationSettled = true;
|
|
461
467
|
this.issueCursorQuery();
|
|
462
468
|
if (negotiationSequence.type === "kitty-flags") {
|
|
@@ -477,13 +483,14 @@ export class ProcessTerminal {
|
|
|
477
483
|
}
|
|
478
484
|
return true;
|
|
479
485
|
}
|
|
486
|
+
/** Returns the parsed negotiation reply with its full (possibly reassembled) sequence. */
|
|
480
487
|
readKeyboardProtocolNegotiationSequence(sequence) {
|
|
481
488
|
if (this.keyboardProtocolNegotiationBuffer) {
|
|
482
489
|
const bufferedSequence = this.keyboardProtocolNegotiationBuffer + sequence;
|
|
483
490
|
const negotiationSequence = parseKeyboardProtocolNegotiationSequence(bufferedSequence);
|
|
484
491
|
if (negotiationSequence) {
|
|
485
492
|
this.clearKeyboardProtocolNegotiationBuffer();
|
|
486
|
-
return negotiationSequence;
|
|
493
|
+
return { parsed: negotiationSequence, sequence: bufferedSequence };
|
|
487
494
|
}
|
|
488
495
|
if (isKeyboardProtocolNegotiationSequencePrefix(bufferedSequence)) {
|
|
489
496
|
this.setKeyboardProtocolNegotiationBuffer(bufferedSequence);
|
|
@@ -493,7 +500,7 @@ export class ProcessTerminal {
|
|
|
493
500
|
}
|
|
494
501
|
const negotiationSequence = parseKeyboardProtocolNegotiationSequence(sequence);
|
|
495
502
|
if (negotiationSequence)
|
|
496
|
-
return negotiationSequence;
|
|
503
|
+
return { parsed: negotiationSequence, sequence };
|
|
497
504
|
if (isKeyboardProtocolNegotiationSequencePrefix(sequence)) {
|
|
498
505
|
this.setKeyboardProtocolNegotiationBuffer(sequence);
|
|
499
506
|
return "pending";
|
package/dist/tui-alt-screen.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { Terminal } from "./terminal.ts";
|
|
2
2
|
import { type Component, TuiBase, type TuiStopOptions, VIEWPORT_TUI, type ViewportTUI } from "./tui.ts";
|
|
3
|
+
import { type WheelScrollLines } from "./wheel-scroll.ts";
|
|
3
4
|
export interface TuiAltScreenOptions {
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Logical lines moved for each mouse-wheel event (default: 1). `"auto"` accelerates fast wheel
|
|
7
|
+
* spins on terminals that send one event per notch. Alt+wheel moves five times as far.
|
|
8
|
+
*/
|
|
9
|
+
wheelScrollLines?: WheelScrollLines;
|
|
6
10
|
/** Capture mouse events for viewport scrolling and application-owned text selection. */
|
|
7
11
|
mouse?: boolean;
|
|
8
12
|
/** Style a non-current transcript search match. */
|
|
@@ -23,10 +27,11 @@ export interface TuiAltScreenOptions {
|
|
|
23
27
|
/** Automatically copy selected text to the clipboard on mouse release (default: true). */
|
|
24
28
|
copyOnSelect?: boolean;
|
|
25
29
|
/**
|
|
26
|
-
* Copy selected text to the system clipboard. Return `true` on success
|
|
27
|
-
*
|
|
30
|
+
* Copy selected text to the system clipboard. Return `true` on success, an error message to
|
|
31
|
+
* display on failure, or `false` for a generic error. When omitted, the selection is copied
|
|
32
|
+
* via an OSC 52 write.
|
|
28
33
|
*/
|
|
29
|
-
copySelection?: (text: string) => Promise<boolean>;
|
|
34
|
+
copySelection?: (text: string) => Promise<boolean | string>;
|
|
30
35
|
}
|
|
31
36
|
/** Alternate-screen TUI with a scrollable, application-owned viewport. */
|
|
32
37
|
export declare class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
@@ -65,7 +70,7 @@ export declare class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
|
65
70
|
private mousePressPoint?;
|
|
66
71
|
private mousePressMoved;
|
|
67
72
|
private lastComponentClick?;
|
|
68
|
-
private readonly
|
|
73
|
+
private readonly wheelScroll;
|
|
69
74
|
private readonly mouseEnabled;
|
|
70
75
|
private readonly searchMatchStyle;
|
|
71
76
|
private readonly searchCurrentMatchStyle;
|
|
@@ -78,6 +83,7 @@ export declare class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
|
78
83
|
constructor(terminal: Terminal, showHardwareCursor?: boolean, logDirectory?: string, options?: TuiAltScreenOptions);
|
|
79
84
|
get viewportTop(): number;
|
|
80
85
|
get isFollowingOutput(): boolean;
|
|
86
|
+
setWheelScrollLines(lines: WheelScrollLines): void;
|
|
81
87
|
getCopyOnSelect(): boolean;
|
|
82
88
|
setCopyOnSelect(enabled: boolean): void;
|
|
83
89
|
/** Whether the fullscreen viewport has a non-empty active text selection. */
|
|
@@ -119,7 +125,6 @@ export declare class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
|
119
125
|
private clearTextSelection;
|
|
120
126
|
private handleMouseEvent;
|
|
121
127
|
private parseWheelEvent;
|
|
122
|
-
private getWheelScrollLines;
|
|
123
128
|
private routeWheel;
|
|
124
129
|
private parseSgrMouseEvent;
|
|
125
130
|
private handleRightClickPaste;
|
package/dist/tui-alt-screen.js
CHANGED
|
@@ -10,6 +10,7 @@ import { decodeMouseButton, isMouseSequence, parseSgrMouseEvent, parseWheelEvent
|
|
|
10
10
|
import { deleteAllKittyImages, deleteAllKittyPlacements, deleteKittyImage, getCapabilities, getKittyImagePlacement, isImageLine, setCapabilities, } from "./terminal-image.js";
|
|
11
11
|
import { Container, CURSOR_MARKER, compositeTuiLine, dispatchMouseEvent, retargetMouseEvent, TuiBase, VIEWPORT_TUI, } from "./tui.js";
|
|
12
12
|
import { extractAnsiCode, getGraphemeCellRange, getOsc8LinkAtColumn, getWordSegmenter, sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth, } from "./utils.js";
|
|
13
|
+
import { WheelScrollAccelerator } from "./wheel-scroll.js";
|
|
13
14
|
const ENTER_ALT_SCREEN = "\x1b[?1049h";
|
|
14
15
|
const EXIT_ALT_SCREEN = "\x1b[?1049l";
|
|
15
16
|
const DISABLE_AUTOWRAP = "\x1b[?7l";
|
|
@@ -29,6 +30,7 @@ const MAX_CACHED_OFFSCREEN_KITTY_IMAGES = 16;
|
|
|
29
30
|
const MAX_CACHED_OFFSCREEN_KITTY_TRANSMISSION_BYTES = 32 * 1024 * 1024;
|
|
30
31
|
const MAX_CACHED_OFFSCREEN_KITTY_DECODED_BYTES = 64 * 1024 * 1024;
|
|
31
32
|
const DOUBLE_CLICK_INTERVAL_MS = 500;
|
|
33
|
+
const COPY_ERROR_FLASH_DURATION_MS = 5000;
|
|
32
34
|
// Regular mode delegates double-click selection to the terminal emulator. Fullscreen owns mouse selection,
|
|
33
35
|
// so mirror common terminal word-selection behavior by keeping paths and kebab-case tokens whole.
|
|
34
36
|
const TERMINAL_WORD_SELECTION_JOINERS = new Set(["/", "-"]);
|
|
@@ -62,7 +64,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
62
64
|
};
|
|
63
65
|
this.implicitScrollView = new ScrollView(this.implicitDocument, { follow: "end", primary: true });
|
|
64
66
|
this.flashes = new AltScreenFlashContainer(() => this.requestRender());
|
|
65
|
-
this.
|
|
67
|
+
this.wheelScroll = new WheelScrollAccelerator(options.wheelScrollLines ?? 1);
|
|
66
68
|
this.mouseEnabled = options.mouse ?? true;
|
|
67
69
|
this.searchMatchStyle = options.searchMatchStyle ?? ((text) => `\x1b[4m${text}\x1b[24m`);
|
|
68
70
|
this.searchCurrentMatchStyle = options.searchCurrentMatchStyle ?? ((text) => `\x1b[1;7m${text}\x1b[22;27m`);
|
|
@@ -80,6 +82,9 @@ export class TuiAltScreen extends TuiBase {
|
|
|
80
82
|
get isFollowingOutput() {
|
|
81
83
|
return this.getPrimaryScrollView().isFollowingEnd;
|
|
82
84
|
}
|
|
85
|
+
setWheelScrollLines(lines) {
|
|
86
|
+
this.wheelScroll.setLines(lines);
|
|
87
|
+
}
|
|
83
88
|
getCopyOnSelect() {
|
|
84
89
|
return this.copyOnSelect;
|
|
85
90
|
}
|
|
@@ -459,9 +464,10 @@ export class TuiAltScreen extends TuiBase {
|
|
|
459
464
|
return { consume: true };
|
|
460
465
|
const wheelEvent = this.parseWheelEvent(data);
|
|
461
466
|
if (wheelEvent) {
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
467
|
+
const lines = this.wheelScroll.next(wheelEvent.direction, performance.now());
|
|
468
|
+
// SGR mouse button codes use bit 3 (value 8) for the Alt modifier.
|
|
469
|
+
const wheelDelta = wheelEvent.direction * ((wheelEvent.button & 8) !== 0 ? lines * ALT_WHEEL_SCROLL_MULTIPLIER : lines);
|
|
470
|
+
const event = this.createMouseEvent("wheel", wheelEvent.button, wheelEvent.x, wheelEvent.y, { wheelDelta });
|
|
465
471
|
const overlay = this.dispatchMouseToOverlay(event);
|
|
466
472
|
const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(event));
|
|
467
473
|
if (result) {
|
|
@@ -471,7 +477,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
471
477
|
}
|
|
472
478
|
if (this.shouldDeferViewportInputToOverlay())
|
|
473
479
|
return undefined;
|
|
474
|
-
this.routeWheel(wheelEvent);
|
|
480
|
+
this.routeWheel(wheelEvent, wheelDelta);
|
|
475
481
|
return { consume: true };
|
|
476
482
|
}
|
|
477
483
|
const mouseEvent = this.parseSgrMouseEvent(data);
|
|
@@ -721,12 +727,8 @@ export class TuiAltScreen extends TuiBase {
|
|
|
721
727
|
parseWheelEvent(data) {
|
|
722
728
|
return parseWheelEvent(data);
|
|
723
729
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
return (button & 8) !== 0 ? this.wheelScrollLines * ALT_WHEEL_SCROLL_MULTIPLIER : this.wheelScrollLines;
|
|
727
|
-
}
|
|
728
|
-
routeWheel(event) {
|
|
729
|
-
let remaining = event.direction * this.getWheelScrollLines(event.button);
|
|
730
|
+
routeWheel(event, delta) {
|
|
731
|
+
let remaining = delta;
|
|
730
732
|
const seen = new Set();
|
|
731
733
|
for (const scrollView of this.currentLayout ? getScrollViewsAt(this.currentLayout, event.x, event.y) : []) {
|
|
732
734
|
seen.add(scrollView);
|
|
@@ -1153,8 +1155,9 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1153
1155
|
// "Copied!" while leaving the system clipboard untouched (e.g. macOS Terminal.app, tmux
|
|
1154
1156
|
// without OSC 52 clipboard passthrough), so only report success when it actually copies.
|
|
1155
1157
|
if (this.copySelection) {
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
+
const result = await this.copySelection(text);
|
|
1159
|
+
const ok = result === true;
|
|
1160
|
+
this.flash(ok ? "Copied!" : typeof result === "string" ? result : "Copy failed", ok ? undefined : COPY_ERROR_FLASH_DURATION_MS);
|
|
1158
1161
|
return ok;
|
|
1159
1162
|
}
|
|
1160
1163
|
this.terminal.write(`\x1b]52;c;${Buffer.from(text).toString("base64")}\x07`);
|
|
@@ -1329,12 +1332,15 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1329
1332
|
if (row >= screen.length || isImageLine(screen[row] ?? ""))
|
|
1330
1333
|
return screen;
|
|
1331
1334
|
const scrollbarColumn = box ? getScrollbarGeometry(box)?.column : undefined;
|
|
1332
|
-
const
|
|
1333
|
-
const
|
|
1335
|
+
const label = truncateToWidth(this.scrollToEndIndicator(), clip.width, "");
|
|
1336
|
+
const labelWidth = visibleWidth(label);
|
|
1337
|
+
const column = clip.x + Math.floor((clip.width - labelWidth) / 2);
|
|
1338
|
+
const rightEdge = scrollbarColumn ?? clip.x + clip.width;
|
|
1339
|
+
const availableWidth = Math.max(0, rightEdge - column);
|
|
1340
|
+
const text = truncateToWidth(label, availableWidth, "");
|
|
1334
1341
|
const textWidth = visibleWidth(text);
|
|
1335
1342
|
if (textWidth === 0)
|
|
1336
1343
|
return screen;
|
|
1337
|
-
const column = clip.x + Math.floor((availableWidth - textWidth) / 2);
|
|
1338
1344
|
const result = [...screen];
|
|
1339
1345
|
result[row] = compositeTuiLine(result[row] ?? "", text, column, textWidth, width);
|
|
1340
1346
|
this.scrollToEndIndicatorRect = { row, column, width: textWidth };
|
|
@@ -1402,10 +1408,24 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1402
1408
|
buffer += deleteAllKittyPlacements();
|
|
1403
1409
|
}
|
|
1404
1410
|
buffer += preparedKittyScreen.evictedImageDeletion;
|
|
1411
|
+
// WezTerm erases intersecting Kitty image cells when a later EL clears a covered row.
|
|
1412
|
+
// Only separate clearing from drawing for WezTerm frames that place images; preserve the
|
|
1413
|
+
// existing interleaved output for text-only frames and every other terminal.
|
|
1414
|
+
const clearRowsBeforeKittyImages = redrawImages &&
|
|
1415
|
+
this.imageProtocol === "kitty" &&
|
|
1416
|
+
screen.some(isImageLine) &&
|
|
1417
|
+
(Boolean(process.env.WEZTERM_PANE) || process.env.TERM_PROGRAM?.toLowerCase() === "wezterm");
|
|
1418
|
+
if (clearRowsBeforeKittyImages) {
|
|
1419
|
+
for (let row = 0; row < height; row++) {
|
|
1420
|
+
if (!fullRedraw && !imagesNeedRedraw && screen[row] === this.previousScreen[row])
|
|
1421
|
+
continue;
|
|
1422
|
+
buffer += `\x1b[${row + 1};1H\x1b[2K`;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1405
1425
|
for (let row = 0; row < height; row++) {
|
|
1406
1426
|
if (!fullRedraw && !imagesNeedRedraw && screen[row] === this.previousScreen[row])
|
|
1407
1427
|
continue;
|
|
1408
|
-
buffer += `\x1b[${row + 1};1H\x1b[2K${preparedKittyScreen.lines[row] ?? ""}`;
|
|
1428
|
+
buffer += `\x1b[${row + 1};1H${clearRowsBeforeKittyImages ? "" : "\x1b[2K"}${preparedKittyScreen.lines[row] ?? ""}`;
|
|
1409
1429
|
}
|
|
1410
1430
|
if (cursorPos) {
|
|
1411
1431
|
buffer += `\x1b[${cursorPos.row + 1};${Math.min(width, cursorPos.col) + 1}H`;
|
package/dist/tui.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Minimal TUI implementation with differential rendering
|
|
3
3
|
*/
|
|
4
4
|
import type { Terminal } from "./terminal.ts";
|
|
5
|
-
import { type
|
|
5
|
+
import { type TerminalColorScheme, type TerminalColors } from "./terminal-colors.ts";
|
|
6
6
|
import { visibleWidth } from "./utils.ts";
|
|
7
7
|
/**
|
|
8
8
|
* Component interface - all components must implement this
|
|
@@ -282,8 +282,11 @@ export declare abstract class TuiBase extends Container {
|
|
|
282
282
|
private muxViewportRepaintCount;
|
|
283
283
|
private overWideCrashDumpWritten;
|
|
284
284
|
protected stopped: boolean;
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Color queries waiting for their DA1 reply, oldest first. Terminals answer in order, so color
|
|
287
|
+
* replies belong to the oldest one. Queries stay here after a timeout to collect late replies.
|
|
288
|
+
*/
|
|
289
|
+
private pendingTerminalColorQueries;
|
|
287
290
|
private terminalColorSchemeListeners;
|
|
288
291
|
private terminalColorSchemeNotificationsEnabled;
|
|
289
292
|
/** Directory for debug/crash logs. The fork keeps a concrete default (`~/.senpi/agent`) so PI_DEBUG_REDRAW and crash dumps stay in the agent directory. */
|
|
@@ -401,7 +404,9 @@ export declare abstract class TuiBase extends Container {
|
|
|
401
404
|
private scheduleRender;
|
|
402
405
|
private commitExpeditedRender;
|
|
403
406
|
private handleTerminalInput;
|
|
404
|
-
private
|
|
407
|
+
private consumeTerminalColorResponse;
|
|
408
|
+
private terminalColorQueryResult;
|
|
409
|
+
private completeTerminalColorQuery;
|
|
405
410
|
private consumeTerminalColorSchemeReport;
|
|
406
411
|
private consumeCellSizeResponse;
|
|
407
412
|
/**
|
|
@@ -468,21 +473,17 @@ export declare abstract class TuiBase extends Container {
|
|
|
468
473
|
private finishFrame;
|
|
469
474
|
private positionHardwareCursor;
|
|
470
475
|
/**
|
|
471
|
-
* Query the terminal's
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}): Promise<RgbColor | undefined>;
|
|
478
|
-
/**
|
|
479
|
-
* Query the terminal's color-scheme preference with DSR (`CSI ? 996 n`).
|
|
480
|
-
* Terminals that support the color palette notification protocol reply with
|
|
481
|
-
* `CSI ? 997 ; 1 n` for dark or `CSI ? 997 ; 2 n` for light.
|
|
476
|
+
* Query the terminal's theme colors: the default foreground (OSC 10), the default background
|
|
477
|
+
* (OSC 11), and ANSI colors 0-15 (OSC 4), followed by a DA1 request that marks the end of the
|
|
478
|
+
* replies. Resolves when the DA1 reply or all color replies arrive, or when the timeout expires.
|
|
479
|
+
* Colors the terminal did not report are undefined; the palette is only set when all 16 arrived.
|
|
480
|
+
* @param timeoutMs Query timeout in milliseconds, for terminals that do not answer DA1 either.
|
|
481
|
+
* @param onLateReply Receives the replies if the query completes after the timeout, e.g. over slow links.
|
|
482
482
|
*/
|
|
483
|
-
|
|
483
|
+
queryTerminalColors({ timeoutMs, onLateReply, }: {
|
|
484
484
|
timeoutMs: number;
|
|
485
|
-
|
|
485
|
+
onLateReply?: (colors: TerminalColors) => void;
|
|
486
|
+
}): Promise<TerminalColors>;
|
|
486
487
|
}
|
|
487
488
|
/** Legacy main-screen renderer export. */
|
|
488
489
|
export declare class TUI extends TuiBase {
|
package/dist/tui.js
CHANGED
|
@@ -7,7 +7,7 @@ import * as path from "node:path";
|
|
|
7
7
|
import { performance } from "node:perf_hooks";
|
|
8
8
|
import { isKeyRelease, matchesKey } from "./keys.js";
|
|
9
9
|
import { isMultiplexerSession, useLegacyMuxRender, viewportRenderEnabled } from "./mux.js";
|
|
10
|
-
import {
|
|
10
|
+
import { parseOscColorResponse, parseTerminalColorSchemeReport, } from "./terminal-colors.js";
|
|
11
11
|
import { deleteKittyImage, getCapabilities, isImageLine, resetCapabilitiesCache, setCellDimensions, } from "./terminal-image.js";
|
|
12
12
|
import { consumeTmuxFocusEvent, DISABLE_FOCUS_REPORTING, ENABLE_FOCUS_REPORTING } from "./tmux-focus.js";
|
|
13
13
|
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
|
|
@@ -68,8 +68,13 @@ export function dispatchMouseEvent(component, event) {
|
|
|
68
68
|
const result = component.handleMouse?.(event);
|
|
69
69
|
if (!result)
|
|
70
70
|
return undefined;
|
|
71
|
-
if ("target" in result)
|
|
72
|
-
|
|
71
|
+
if ("target" in result) {
|
|
72
|
+
// The component forwarded the event to a child it hosts. Like a delegating container, it routes
|
|
73
|
+
// keys to that child itself, so it keeps keyboard focus. Focusing the child directly would leave
|
|
74
|
+
// focus on a detached component once the host removes it, e.g. a closed settings submenu.
|
|
75
|
+
const forwarded = result;
|
|
76
|
+
return forwarded.focus && component.handleInput ? { ...forwarded, focusTarget: component } : forwarded;
|
|
77
|
+
}
|
|
73
78
|
if (!result.handled && !result.capture && !result.focus)
|
|
74
79
|
return undefined;
|
|
75
80
|
return {
|
|
@@ -95,6 +100,16 @@ export function retargetMouseEvent(event, target) {
|
|
|
95
100
|
height: target.height,
|
|
96
101
|
};
|
|
97
102
|
}
|
|
103
|
+
const TERMINAL_PALETTE_SIZE = 16;
|
|
104
|
+
/** OSC 10 and 11 plus OSC 4 for every palette color. */
|
|
105
|
+
const TERMINAL_COLOR_REPLY_COUNT = 2 + TERMINAL_PALETTE_SIZE;
|
|
106
|
+
/**
|
|
107
|
+
* Default colors, palette colors 0-15, and a trailing primary device attributes (DA1) request.
|
|
108
|
+
* Every terminal answers DA1 and terminals answer in order, so the DA1 reply marks the end of
|
|
109
|
+
* the color replies, including for terminals that ignore the color queries.
|
|
110
|
+
*/
|
|
111
|
+
const TERMINAL_COLOR_QUERY = `\x1b]10;?\x07\x1b]11;?\x07${Array.from({ length: TERMINAL_PALETTE_SIZE }, (_, index) => `\x1b]4;${index};?\x07`).join("")}\x1b[c`;
|
|
112
|
+
const DEVICE_ATTRIBUTES_RESPONSE_PATTERN = /^\x1b\[\?[\d;]*c$/;
|
|
98
113
|
/** Type guard to check if a component implements Focusable */
|
|
99
114
|
export function isFocusable(component) {
|
|
100
115
|
return component !== null && "focused" in component;
|
|
@@ -422,8 +437,11 @@ export class TuiBase extends Container {
|
|
|
422
437
|
this.muxViewportRepaintCount = 0;
|
|
423
438
|
this.overWideCrashDumpWritten = false;
|
|
424
439
|
this.stopped = false;
|
|
425
|
-
|
|
426
|
-
|
|
440
|
+
/**
|
|
441
|
+
* Color queries waiting for their DA1 reply, oldest first. Terminals answer in order, so color
|
|
442
|
+
* replies belong to the oldest one. Queries stay here after a timeout to collect late replies.
|
|
443
|
+
*/
|
|
444
|
+
this.pendingTerminalColorQueries = [];
|
|
427
445
|
this.terminalColorSchemeListeners = new Set();
|
|
428
446
|
this.terminalColorSchemeNotificationsEnabled = false;
|
|
429
447
|
// Overlay stack for modal components rendered on top of base content
|
|
@@ -1075,7 +1093,10 @@ export class TuiBase extends Container {
|
|
|
1075
1093
|
this.lastRenderAt = performance.now();
|
|
1076
1094
|
this.doRender();
|
|
1077
1095
|
}
|
|
1096
|
+
/** After stop(), the shell owns the cursor and it must stay visible, so only a running TUI hides it. */
|
|
1078
1097
|
#setCursorVisibility(visible) {
|
|
1098
|
+
if (!visible && this.stopped)
|
|
1099
|
+
return;
|
|
1079
1100
|
if (this.#lastCursorVisibility === visible)
|
|
1080
1101
|
return;
|
|
1081
1102
|
if (visible) {
|
|
@@ -1191,7 +1212,7 @@ export class TuiBase extends Container {
|
|
|
1191
1212
|
data = focus.data;
|
|
1192
1213
|
}
|
|
1193
1214
|
}
|
|
1194
|
-
if (this.
|
|
1215
|
+
if (this.consumeTerminalColorResponse(data)) {
|
|
1195
1216
|
return;
|
|
1196
1217
|
}
|
|
1197
1218
|
if (this.consumeTerminalColorSchemeReport(data)) {
|
|
@@ -1262,27 +1283,50 @@ export class TuiBase extends Container {
|
|
|
1262
1283
|
this.requestRender(false, "input");
|
|
1263
1284
|
}
|
|
1264
1285
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1286
|
+
consumeTerminalColorResponse(data) {
|
|
1287
|
+
const query = this.pendingTerminalColorQueries[0];
|
|
1288
|
+
if (!query) {
|
|
1267
1289
|
return false;
|
|
1268
1290
|
}
|
|
1269
|
-
if (
|
|
1291
|
+
if (DEVICE_ATTRIBUTES_RESPONSE_PATTERN.test(data)) {
|
|
1292
|
+
this.pendingTerminalColorQueries.shift();
|
|
1293
|
+
this.completeTerminalColorQuery(query);
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
const response = parseOscColorResponse(data);
|
|
1297
|
+
if (!response) {
|
|
1270
1298
|
return false;
|
|
1271
1299
|
}
|
|
1272
|
-
const rgb =
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
query.
|
|
1300
|
+
const { target, rgb } = response;
|
|
1301
|
+
const key = String(target);
|
|
1302
|
+
if (!query.deliver || query.replied.has(key)) {
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
query.replied.add(key);
|
|
1306
|
+
if (target === "foreground") {
|
|
1307
|
+
query.foreground = rgb;
|
|
1308
|
+
}
|
|
1309
|
+
else if (target === "background") {
|
|
1310
|
+
query.background = rgb;
|
|
1311
|
+
}
|
|
1312
|
+
else if (target < TERMINAL_PALETTE_SIZE) {
|
|
1313
|
+
query.palette[target] = rgb;
|
|
1314
|
+
}
|
|
1315
|
+
if (query.replied.size === TERMINAL_COLOR_REPLY_COUNT) {
|
|
1316
|
+
this.completeTerminalColorQuery(query);
|
|
1283
1317
|
}
|
|
1284
1318
|
return true;
|
|
1285
1319
|
}
|
|
1320
|
+
terminalColorQueryResult(query) {
|
|
1321
|
+
const palette = query.palette.every((color) => color !== undefined) ? query.palette : undefined;
|
|
1322
|
+
return { foreground: query.foreground, background: query.background, palette };
|
|
1323
|
+
}
|
|
1324
|
+
completeTerminalColorQuery(query) {
|
|
1325
|
+
const deliver = query.deliver;
|
|
1326
|
+
query.deliver = undefined;
|
|
1327
|
+
clearTimeout(query.timer);
|
|
1328
|
+
deliver?.(this.terminalColorQueryResult(query));
|
|
1329
|
+
}
|
|
1286
1330
|
consumeTerminalColorSchemeReport(data) {
|
|
1287
1331
|
const scheme = parseTerminalColorSchemeReport(data);
|
|
1288
1332
|
if (!scheme) {
|
|
@@ -2403,55 +2447,28 @@ export class TuiBase extends Container {
|
|
|
2403
2447
|
this.terminal.write(TUI.FRAME_BEGIN + buffer + TUI.FRAME_END);
|
|
2404
2448
|
}
|
|
2405
2449
|
/**
|
|
2406
|
-
* Query the terminal's
|
|
2407
|
-
*
|
|
2408
|
-
*
|
|
2450
|
+
* Query the terminal's theme colors: the default foreground (OSC 10), the default background
|
|
2451
|
+
* (OSC 11), and ANSI colors 0-15 (OSC 4), followed by a DA1 request that marks the end of the
|
|
2452
|
+
* replies. Resolves when the DA1 reply or all color replies arrive, or when the timeout expires.
|
|
2453
|
+
* Colors the terminal did not report are undefined; the palette is only set when all 16 arrived.
|
|
2454
|
+
* @param timeoutMs Query timeout in milliseconds, for terminals that do not answer DA1 either.
|
|
2455
|
+
* @param onLateReply Receives the replies if the query completes after the timeout, e.g. over slow links.
|
|
2409
2456
|
*/
|
|
2410
|
-
|
|
2457
|
+
queryTerminalColors({ timeoutMs, onLateReply, }) {
|
|
2411
2458
|
return new Promise((resolve) => {
|
|
2412
2459
|
const query = {
|
|
2413
|
-
|
|
2414
|
-
|
|
2460
|
+
palette: Array.from({ length: TERMINAL_PALETTE_SIZE }, () => undefined),
|
|
2461
|
+
replied: new Set(),
|
|
2462
|
+
deliver: resolve,
|
|
2415
2463
|
timer: undefined,
|
|
2416
2464
|
};
|
|
2465
|
+
// Resolve with the replies so far, and keep collecting late replies for `onLateReply`.
|
|
2417
2466
|
query.timer = setTimeout(() => {
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
}
|
|
2421
|
-
query.settled = true;
|
|
2422
|
-
query.timer = undefined;
|
|
2423
|
-
query.resolve?.(undefined);
|
|
2424
|
-
query.resolve = undefined;
|
|
2467
|
+
query.deliver = onLateReply;
|
|
2468
|
+
resolve(this.terminalColorQueryResult(query));
|
|
2425
2469
|
}, timeoutMs);
|
|
2426
|
-
this.
|
|
2427
|
-
this.
|
|
2428
|
-
this.terminal.write("\x1b]11;?\x07");
|
|
2429
|
-
});
|
|
2430
|
-
}
|
|
2431
|
-
/**
|
|
2432
|
-
* Query the terminal's color-scheme preference with DSR (`CSI ? 996 n`).
|
|
2433
|
-
* Terminals that support the color palette notification protocol reply with
|
|
2434
|
-
* `CSI ? 997 ; 1 n` for dark or `CSI ? 997 ; 2 n` for light.
|
|
2435
|
-
*/
|
|
2436
|
-
queryTerminalColorScheme({ timeoutMs }) {
|
|
2437
|
-
return new Promise((resolve) => {
|
|
2438
|
-
let settled = false;
|
|
2439
|
-
let timer;
|
|
2440
|
-
let unsubscribe = () => { };
|
|
2441
|
-
const settle = (scheme) => {
|
|
2442
|
-
if (settled)
|
|
2443
|
-
return;
|
|
2444
|
-
settled = true;
|
|
2445
|
-
if (timer) {
|
|
2446
|
-
clearTimeout(timer);
|
|
2447
|
-
timer = undefined;
|
|
2448
|
-
}
|
|
2449
|
-
unsubscribe();
|
|
2450
|
-
resolve(scheme);
|
|
2451
|
-
};
|
|
2452
|
-
unsubscribe = this.onTerminalColorSchemeChange(settle);
|
|
2453
|
-
timer = setTimeout(() => settle(undefined), timeoutMs);
|
|
2454
|
-
this.terminal.write("\x1b[?996n");
|
|
2470
|
+
this.pendingTerminalColorQueries.push(query);
|
|
2471
|
+
this.terminal.write(TERMINAL_COLOR_QUERY);
|
|
2455
2472
|
});
|
|
2456
2473
|
}
|
|
2457
2474
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ interface WidthCacheStats {
|
|
|
15
15
|
}
|
|
16
16
|
export declare function __widthCacheStats(): WidthCacheStats | undefined;
|
|
17
17
|
export declare const cjkBreakRegex: RegExp;
|
|
18
|
+
export declare const cjkPunctuationRegex: RegExp;
|
|
19
|
+
export declare const autocompleteSeparatorRegex: RegExp;
|
|
20
|
+
export declare const autocompleteBoundaryRegex: RegExp;
|
|
18
21
|
/**
|
|
19
22
|
* Calculate the visible width of a string in terminal columns.
|
|
20
23
|
*/
|