@visual-z/xeno 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +143 -33
- package/docs/install.md +23 -6
- package/docs/providers.md +147 -0
- package/package.json +8 -2
- package/src/apps/cli.ts +45 -18
- package/src/apps/commands.ts +32 -4
- package/src/apps/completions.ts +9 -6
- package/src/apps/export-cli.ts +24 -0
- package/src/apps/extension-ui.ts +330 -0
- package/src/apps/input.ts +16 -1
- package/src/apps/model-catalog.ts +42 -0
- package/src/apps/project-trust.ts +49 -0
- package/src/apps/render.ts +1 -0
- package/src/apps/resource-browser.ts +75 -0
- package/src/apps/resources.ts +25 -0
- package/src/apps/session-browser.ts +113 -0
- package/src/apps/session-picker.ts +55 -0
- package/src/apps/shell.ts +1177 -378
- package/src/apps/startup-input.ts +94 -0
- package/src/apps/startup.ts +413 -295
- package/src/core/action-defaults.ts +306 -0
- package/src/core/attachment-input.ts +17 -0
- package/src/core/catalog-merge.ts +19 -0
- package/src/core/event-validation.ts +19 -2
- package/src/core/events.ts +45 -2
- package/src/core/export.ts +6 -2
- package/src/core/extension-flags.ts +14 -0
- package/src/core/images.ts +46 -0
- package/src/core/json-event.ts +2 -0
- package/src/core/keymap.ts +43 -119
- package/src/core/model-replay.ts +22 -0
- package/src/core/model-scope.ts +53 -0
- package/src/core/packages.ts +47 -0
- package/src/core/project.ts +21 -3
- package/src/core/prompts.ts +33 -5
- package/src/core/queue.ts +18 -0
- package/src/core/resource-selection.ts +36 -0
- package/src/{runtime → core}/resume.ts +15 -22
- package/src/core/session-archive.ts +4 -0
- package/src/core/session-browser.ts +82 -0
- package/src/core/session-entry.ts +56 -0
- package/src/core/session-position.ts +26 -0
- package/src/core/settings.ts +169 -154
- package/src/core/theme.ts +15 -6
- package/src/core/thinking.ts +26 -0
- package/src/core/tokens.ts +2 -0
- package/src/core/transcript.ts +15 -5
- package/src/runtime/auth.ts +116 -11
- package/src/runtime/catalog.ts +54 -151
- package/src/runtime/clipboard.ts +114 -0
- package/src/runtime/extension-loader.ts +37 -0
- package/src/runtime/extension-ui.ts +55 -0
- package/src/runtime/extensions.ts +273 -0
- package/src/runtime/images.ts +76 -0
- package/src/runtime/loop.ts +213 -83
- package/src/runtime/model-config.ts +232 -0
- package/src/runtime/model-data.LICENSE +27 -0
- package/src/runtime/model-data.ts +21 -0
- package/src/runtime/model-replay.ts +10 -0
- package/src/runtime/model-scope.ts +20 -0
- package/src/runtime/model-sources.ts +151 -0
- package/src/runtime/model.ts +74 -11
- package/src/runtime/models-fetch.ts +6 -104
- package/src/runtime/oauth.ts +4 -3
- package/src/runtime/openai-responses.ts +12 -4
- package/src/runtime/openai.ts +13 -4
- package/src/runtime/package-manager.ts +125 -0
- package/src/runtime/pi-provider.ts +235 -0
- package/src/runtime/progress.ts +26 -0
- package/src/runtime/provider-access.ts +21 -0
- package/src/runtime/provider-protocols.ts +29 -0
- package/src/runtime/provider-request.ts +73 -0
- package/src/runtime/provider.ts +26 -6
- package/src/runtime/rich-text.ts +36 -0
- package/src/runtime/tools.ts +41 -5
- package/src/store/command.ts +7 -1
- package/src/store/context-files.ts +1 -1
- package/src/store/log.ts +65 -0
- package/src/store/models.ts +28 -9
- package/src/store/observations.ts +4 -2
- package/src/store/packages.ts +98 -0
- package/src/store/recovery.ts +8 -0
- package/src/store/resource-index.ts +170 -0
- package/src/store/resources.ts +75 -142
- package/src/store/session-archive.ts +162 -0
- package/src/store/session-browser.ts +68 -0
- package/src/store/session-position.ts +27 -0
- package/src/store/sessions.ts +87 -66
- package/src/store/trust.ts +13 -0
- package/src/tui/chat-viewport.ts +47 -0
- package/src/tui/editor.ts +62 -15
- package/src/tui/host.ts +50 -9
- package/src/tui/input.ts +8 -2
- package/src/tui/select.ts +24 -6
- package/src/tui/settings.ts +4 -0
- package/src/tui/system-theme.ts +700 -0
- package/src/tui/transcript.ts +63 -11
- package/src/tui/tree.ts +161 -95
- package/src/vendor/pi-rich/LICENSE.upstream +21 -0
- package/src/vendor/pi-rich/UPSTREAM.json +17 -0
- package/src/vendor/pi-rich/clipboard-command.ts +44 -0
- package/src/vendor/pi-rich/exif-orientation.ts +182 -0
- package/src/vendor/pi-rich/html.ts +51 -0
- package/src/vendor/pi-rich/image-convert.ts +49 -0
- package/src/vendor/pi-rich/image-resize-core.ts +164 -0
- package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
- package/src/vendor/pi-rich/image-resize.ts +123 -0
- package/src/vendor/pi-rich/mime.ts +116 -0
- package/src/vendor/pi-rich/photon.ts +139 -0
- package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
- package/src/vendor/pi-tui/UPSTREAM.json +50 -0
- package/src/vendor/pi-tui/VENDOR.md +7 -5
- package/src/vendor/pi-tui/autocomplete.ts +35 -10
- package/src/vendor/pi-tui/colors.ts +367 -0
- package/src/vendor/pi-tui/components/box.ts +8 -9
- package/src/vendor/pi-tui/components/editor.ts +4 -2
- package/src/vendor/pi-tui/components/markdown.ts +8 -2
- package/src/vendor/pi-tui/index.ts +31 -1
- package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
- package/src/vendor/pi-tui/native/clipboard.h +208 -0
- package/src/vendor/pi-tui/native/darwin/README.md +22 -0
- package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
- package/src/vendor/pi-tui/native/linux/README.md +23 -0
- package/src/vendor/pi-tui/native/linux/build.sh +65 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
- package/src/vendor/pi-tui/native/napi.h +91 -0
- package/src/vendor/pi-tui/native/win32/README.md +31 -0
- package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
- package/src/vendor/pi-tui/native-platform.ts +2 -0
- package/src/vendor/pi-tui/oklab.ts +233 -0
- package/src/vendor/pi-tui/terminal-colors.ts +26 -8
- package/src/vendor/pi-tui/terminal-image.ts +43 -8
- package/src/vendor/pi-tui/terminal.ts +16 -9
- package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
- package/src/vendor/pi-tui/tui.ts +112 -75
- package/src/vendor/pi-tui/utils.ts +89 -49
- package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
|
@@ -141,6 +141,8 @@ export class ProcessTerminal implements Terminal {
|
|
|
141
141
|
private _kittyProtocolActive = false;
|
|
142
142
|
private _modifyOtherKeysActive = false;
|
|
143
143
|
private keyboardProtocolPushed = false;
|
|
144
|
+
/** DA1 replies owed to keyboard protocol queries. Later DA1 replies answer other queries and are forwarded. */
|
|
145
|
+
private pendingKeyboardProtocolDeviceAttributes = 0;
|
|
144
146
|
private keyboardProtocolNegotiationBuffer = "";
|
|
145
147
|
private keyboardProtocolBufferFlushTimer?: ReturnType<typeof setTimeout>;
|
|
146
148
|
private stdinBuffer?: StdinBuffer;
|
|
@@ -215,16 +217,16 @@ export class ProcessTerminal implements Terminal {
|
|
|
215
217
|
|
|
216
218
|
// Forward individual sequences to the input handler
|
|
217
219
|
this.stdinBuffer.on("data", (sequence) => {
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
220
|
+
const negotiation = this.readKeyboardProtocolNegotiationSequence(sequence);
|
|
221
|
+
if (negotiation === "pending") {
|
|
220
222
|
this.scheduleKeyboardProtocolNegotiationBufferFlush();
|
|
221
223
|
return; // Wait briefly for the rest of a split Kitty response.
|
|
222
224
|
}
|
|
223
|
-
if (this.handleKeyboardProtocolNegotiationSequence(
|
|
225
|
+
if (negotiation && this.handleKeyboardProtocolNegotiationSequence(negotiation.parsed)) {
|
|
224
226
|
return;
|
|
225
227
|
}
|
|
226
228
|
|
|
227
|
-
this.forwardInputSequence(sequence);
|
|
229
|
+
this.forwardInputSequence(negotiation?.sequence ?? sequence);
|
|
228
230
|
});
|
|
229
231
|
|
|
230
232
|
// Re-wrap paste content with bracketed paste markers for existing editor handling
|
|
@@ -257,15 +259,19 @@ export class ProcessTerminal implements Terminal {
|
|
|
257
259
|
this.setupStdinBuffer();
|
|
258
260
|
process.stdin.on("data", this.stdinDataHandler!);
|
|
259
261
|
this.keyboardProtocolPushed = true;
|
|
262
|
+
this.pendingKeyboardProtocolDeviceAttributes += 1;
|
|
260
263
|
this.clearKeyboardProtocolNegotiationBuffer();
|
|
261
264
|
process.stdout.write(KITTY_KEYBOARD_PROTOCOL_QUERY);
|
|
262
265
|
}
|
|
263
266
|
|
|
264
267
|
private handleKeyboardProtocolNegotiationSequence(
|
|
265
|
-
negotiationSequence: KeyboardProtocolNegotiationSequence
|
|
268
|
+
negotiationSequence: KeyboardProtocolNegotiationSequence,
|
|
266
269
|
): boolean {
|
|
267
|
-
if (!negotiationSequence) return false;
|
|
268
270
|
this.clearKeyboardProtocolNegotiationBuffer();
|
|
271
|
+
if (negotiationSequence.type === "device-attributes") {
|
|
272
|
+
if (this.pendingKeyboardProtocolDeviceAttributes === 0) return false;
|
|
273
|
+
this.pendingKeyboardProtocolDeviceAttributes -= 1;
|
|
274
|
+
}
|
|
269
275
|
if (negotiationSequence.type === "kitty-flags") {
|
|
270
276
|
if (negotiationSequence.flags !== 0) {
|
|
271
277
|
this.disableModifyOtherKeys();
|
|
@@ -285,15 +291,16 @@ export class ProcessTerminal implements Terminal {
|
|
|
285
291
|
return true;
|
|
286
292
|
}
|
|
287
293
|
|
|
294
|
+
/** Returns the parsed negotiation reply with its full (possibly reassembled) sequence. */
|
|
288
295
|
private readKeyboardProtocolNegotiationSequence(
|
|
289
296
|
sequence: string,
|
|
290
|
-
): KeyboardProtocolNegotiationSequence | "pending" | undefined {
|
|
297
|
+
): { parsed: KeyboardProtocolNegotiationSequence; sequence: string } | "pending" | undefined {
|
|
291
298
|
if (this.keyboardProtocolNegotiationBuffer) {
|
|
292
299
|
const bufferedSequence = this.keyboardProtocolNegotiationBuffer + sequence;
|
|
293
300
|
const negotiationSequence = parseKeyboardProtocolNegotiationSequence(bufferedSequence);
|
|
294
301
|
if (negotiationSequence) {
|
|
295
302
|
this.clearKeyboardProtocolNegotiationBuffer();
|
|
296
|
-
return negotiationSequence;
|
|
303
|
+
return { parsed: negotiationSequence, sequence: bufferedSequence };
|
|
297
304
|
}
|
|
298
305
|
if (isKeyboardProtocolNegotiationSequencePrefix(bufferedSequence)) {
|
|
299
306
|
this.setKeyboardProtocolNegotiationBuffer(bufferedSequence);
|
|
@@ -303,7 +310,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
303
310
|
}
|
|
304
311
|
|
|
305
312
|
const negotiationSequence = parseKeyboardProtocolNegotiationSequence(sequence);
|
|
306
|
-
if (negotiationSequence) return negotiationSequence;
|
|
313
|
+
if (negotiationSequence) return { parsed: negotiationSequence, sequence };
|
|
307
314
|
if (isKeyboardProtocolNegotiationSequencePrefix(sequence)) {
|
|
308
315
|
this.setKeyboardProtocolNegotiationBuffer(sequence);
|
|
309
316
|
return "pending";
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
truncateToWidth,
|
|
58
58
|
visibleWidth,
|
|
59
59
|
} from "./utils.ts";
|
|
60
|
+
import { WheelScrollAccelerator, type WheelScrollLines } from "./wheel-scroll.ts";
|
|
60
61
|
|
|
61
62
|
const ENTER_ALT_SCREEN = "\x1b[?1049h";
|
|
62
63
|
const EXIT_ALT_SCREEN = "\x1b[?1049l";
|
|
@@ -164,8 +165,11 @@ interface SearchHighlightRange {
|
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
export interface TuiAltScreenOptions {
|
|
167
|
-
/**
|
|
168
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Logical lines moved for each mouse-wheel event (default: 1). `"auto"` accelerates fast wheel
|
|
170
|
+
* spins on terminals that send one event per notch. Alt+wheel moves five times as far.
|
|
171
|
+
*/
|
|
172
|
+
wheelScrollLines?: WheelScrollLines;
|
|
169
173
|
/** Capture mouse events for viewport scrolling and application-owned text selection. */
|
|
170
174
|
mouse?: boolean;
|
|
171
175
|
/** Style a non-current transcript search match. */
|
|
@@ -236,7 +240,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
|
236
240
|
x: number;
|
|
237
241
|
y: number;
|
|
238
242
|
};
|
|
239
|
-
private readonly
|
|
243
|
+
private readonly wheelScroll: WheelScrollAccelerator;
|
|
240
244
|
private readonly mouseEnabled: boolean;
|
|
241
245
|
private readonly searchMatchStyle: (text: string) => string;
|
|
242
246
|
private readonly searchCurrentMatchStyle: (text: string) => string;
|
|
@@ -263,7 +267,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
|
263
267
|
};
|
|
264
268
|
this.implicitScrollView = new ScrollView(this.implicitDocument, { follow: "end", primary: true });
|
|
265
269
|
this.flashes = new AltScreenFlashContainer(() => this.requestRender());
|
|
266
|
-
this.
|
|
270
|
+
this.wheelScroll = new WheelScrollAccelerator(options.wheelScrollLines ?? 1);
|
|
267
271
|
this.mouseEnabled = options.mouse ?? true;
|
|
268
272
|
this.searchMatchStyle = options.searchMatchStyle ?? ((text) => `\x1b[4m${text}\x1b[24m`);
|
|
269
273
|
this.searchCurrentMatchStyle = options.searchCurrentMatchStyle ?? ((text) => `\x1b[1;7m${text}\x1b[22;27m`);
|
|
@@ -284,6 +288,10 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
|
284
288
|
return this.getPrimaryScrollView().isFollowingEnd;
|
|
285
289
|
}
|
|
286
290
|
|
|
291
|
+
setWheelScrollLines(lines: WheelScrollLines): void {
|
|
292
|
+
this.wheelScroll.setLines(lines);
|
|
293
|
+
}
|
|
294
|
+
|
|
287
295
|
getCopyOnSelect(): boolean {
|
|
288
296
|
return this.copyOnSelect;
|
|
289
297
|
}
|
|
@@ -682,9 +690,11 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
|
682
690
|
|
|
683
691
|
const wheelEvent = this.parseWheelEvent(data);
|
|
684
692
|
if (wheelEvent) {
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
693
|
+
const lines = this.wheelScroll.next(wheelEvent.direction, performance.now());
|
|
694
|
+
// SGR mouse button codes use bit 3 (value 8) for the Alt modifier.
|
|
695
|
+
const wheelDelta =
|
|
696
|
+
wheelEvent.direction * ((wheelEvent.button & 8) !== 0 ? lines * ALT_WHEEL_SCROLL_MULTIPLIER : lines);
|
|
697
|
+
const event = this.createMouseEvent("wheel", wheelEvent.button, wheelEvent.x, wheelEvent.y, { wheelDelta });
|
|
688
698
|
const overlay = this.dispatchMouseToOverlay(event);
|
|
689
699
|
const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(event));
|
|
690
700
|
if (result) {
|
|
@@ -692,7 +702,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
|
692
702
|
return { consume: true };
|
|
693
703
|
}
|
|
694
704
|
if (this.shouldDeferViewportInputToOverlay()) return undefined;
|
|
695
|
-
this.routeWheel(wheelEvent);
|
|
705
|
+
this.routeWheel(wheelEvent, wheelDelta);
|
|
696
706
|
return { consume: true };
|
|
697
707
|
}
|
|
698
708
|
const mouseEvent = this.parseSgrMouseEvent(data);
|
|
@@ -967,13 +977,8 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
|
967
977
|
return undefined;
|
|
968
978
|
}
|
|
969
979
|
|
|
970
|
-
private
|
|
971
|
-
|
|
972
|
-
return (button & 8) !== 0 ? this.wheelScrollLines * ALT_WHEEL_SCROLL_MULTIPLIER : this.wheelScrollLines;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
private routeWheel(event: WheelEvent): void {
|
|
976
|
-
let remaining = event.direction * this.getWheelScrollLines(event.button);
|
|
980
|
+
private routeWheel(event: WheelEvent, delta: number): void {
|
|
981
|
+
let remaining = delta;
|
|
977
982
|
const seen = new Set<ScrollView>();
|
|
978
983
|
for (const scrollView of this.currentLayout ? getScrollViewsAt(this.currentLayout, event.x, event.y) : []) {
|
|
979
984
|
seen.add(scrollView);
|
|
@@ -1630,11 +1635,14 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
|
1630
1635
|
const row = clip.y + clip.height - 1;
|
|
1631
1636
|
if (row >= screen.length || isImageLine(screen[row] ?? "")) return screen;
|
|
1632
1637
|
const scrollbarColumn = box ? getScrollbarGeometry(box)?.column : undefined;
|
|
1633
|
-
const
|
|
1634
|
-
const
|
|
1638
|
+
const label = truncateToWidth(this.scrollToEndIndicator(), clip.width, "");
|
|
1639
|
+
const labelWidth = visibleWidth(label);
|
|
1640
|
+
const column = clip.x + Math.floor((clip.width - labelWidth) / 2);
|
|
1641
|
+
const rightEdge = scrollbarColumn ?? clip.x + clip.width;
|
|
1642
|
+
const availableWidth = Math.max(0, rightEdge - column);
|
|
1643
|
+
const text = truncateToWidth(label, availableWidth, "");
|
|
1635
1644
|
const textWidth = visibleWidth(text);
|
|
1636
1645
|
if (textWidth === 0) return screen;
|
|
1637
|
-
const column = clip.x + Math.floor((availableWidth - textWidth) / 2);
|
|
1638
1646
|
const result = [...screen];
|
|
1639
1647
|
result[row] = compositeTuiLine(result[row] ?? "", text, column, textWidth, width);
|
|
1640
1648
|
this.scrollToEndIndicatorRect = { row, column, width: textWidth };
|
package/src/vendor/pi-tui/tui.ts
CHANGED
|
@@ -6,11 +6,11 @@ import { performance } from "node:perf_hooks";
|
|
|
6
6
|
import { isKeyRelease, matchesKey } from "./keys.ts";
|
|
7
7
|
import type { Terminal } from "./terminal.ts";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
parseOsc11BackgroundColor,
|
|
9
|
+
parseOscColorResponse,
|
|
11
10
|
parseTerminalColorSchemeReport,
|
|
12
11
|
type RgbColor,
|
|
13
12
|
type TerminalColorScheme,
|
|
13
|
+
type TerminalColors,
|
|
14
14
|
} from "./terminal-colors.ts";
|
|
15
15
|
import { getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.ts";
|
|
16
16
|
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.ts";
|
|
@@ -81,7 +81,13 @@ export interface TuiMouseDispatchResult extends TuiMouseEventResult {
|
|
|
81
81
|
export function dispatchMouseEvent(component: Component, event: TuiMouseEvent): TuiMouseDispatchResult | undefined {
|
|
82
82
|
const result = component.handleMouse?.(event);
|
|
83
83
|
if (!result) return undefined;
|
|
84
|
-
if ("target" in result)
|
|
84
|
+
if ("target" in result) {
|
|
85
|
+
// The component forwarded the event to a child it hosts. Like a delegating container, it routes
|
|
86
|
+
// keys to that child itself, so it keeps keyboard focus. Focusing the child directly would leave
|
|
87
|
+
// focus on a detached component once the host removes it, e.g. a closed settings submenu.
|
|
88
|
+
const forwarded = result as TuiMouseDispatchResult;
|
|
89
|
+
return forwarded.focus && component.handleInput ? { ...forwarded, focusTarget: component } : forwarded;
|
|
90
|
+
}
|
|
85
91
|
if (!result.handled && !result.capture && !result.focus) return undefined;
|
|
86
92
|
return {
|
|
87
93
|
...result,
|
|
@@ -137,12 +143,34 @@ export interface Component {
|
|
|
137
143
|
|
|
138
144
|
export type TuiInputListenerResult = { consume?: boolean; data?: string } | undefined;
|
|
139
145
|
export type TuiInputListener = (data: string) => TuiInputListenerResult;
|
|
140
|
-
type
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
type PendingTerminalColorQuery = {
|
|
147
|
+
foreground?: RgbColor;
|
|
148
|
+
background?: RgbColor;
|
|
149
|
+
palette: Array<RgbColor | undefined>;
|
|
150
|
+
/** Targets that already replied, so duplicates do not count twice. */
|
|
151
|
+
replied: Set<string>;
|
|
152
|
+
/**
|
|
153
|
+
* Receives the result: the promise's resolve until the timeout, then `onLateReply`. Unset once the
|
|
154
|
+
* query completed (on the DA1 reply or once every color replied); later replies are ignored.
|
|
155
|
+
*/
|
|
156
|
+
deliver: ((colors: TerminalColors) => void) | undefined;
|
|
143
157
|
timer: NodeJS.Timeout | undefined;
|
|
144
158
|
};
|
|
145
159
|
|
|
160
|
+
const TERMINAL_PALETTE_SIZE = 16;
|
|
161
|
+
/** OSC 10 and 11 plus OSC 4 for every palette color. */
|
|
162
|
+
const TERMINAL_COLOR_REPLY_COUNT = 2 + TERMINAL_PALETTE_SIZE;
|
|
163
|
+
/**
|
|
164
|
+
* Default colors, palette colors 0-15, and a trailing primary device attributes (DA1) request.
|
|
165
|
+
* Every terminal answers DA1 and terminals answer in order, so the DA1 reply marks the end of
|
|
166
|
+
* the color replies, including for terminals that ignore the color queries.
|
|
167
|
+
*/
|
|
168
|
+
const TERMINAL_COLOR_QUERY = `\x1b]10;?\x07\x1b]11;?\x07${Array.from(
|
|
169
|
+
{ length: TERMINAL_PALETTE_SIZE },
|
|
170
|
+
(_, index) => `\x1b]4;${index};?\x07`,
|
|
171
|
+
).join("")}\x1b[c`;
|
|
172
|
+
const DEVICE_ATTRIBUTES_RESPONSE_PATTERN = /^\x1b\[\?[\d;]*c$/;
|
|
173
|
+
|
|
146
174
|
/**
|
|
147
175
|
* Interface for components that can receive focus and display a hardware cursor.
|
|
148
176
|
* When focused, the component should emit CURSOR_MARKER at the cursor position
|
|
@@ -447,8 +475,10 @@ export interface TUI extends Component {
|
|
|
447
475
|
removeInputListener(listener: TuiInputListener): void;
|
|
448
476
|
onTerminalColorSchemeChange(listener: (scheme: TerminalColorScheme) => void): () => void;
|
|
449
477
|
setTerminalColorSchemeNotifications(enabled: boolean): void;
|
|
450
|
-
|
|
451
|
-
|
|
478
|
+
queryTerminalColors(options: {
|
|
479
|
+
timeoutMs: number;
|
|
480
|
+
onLateReply?: (colors: TerminalColors) => void;
|
|
481
|
+
}): Promise<TerminalColors>;
|
|
452
482
|
}
|
|
453
483
|
|
|
454
484
|
export const VIEWPORT_TUI = Symbol.for("@earendil-works/pi-tui/viewport");
|
|
@@ -479,8 +509,11 @@ export abstract class TuiBase extends Container implements TUI {
|
|
|
479
509
|
private clearOnShrink = false;
|
|
480
510
|
protected fullRedrawCount = 0;
|
|
481
511
|
protected stopped = false;
|
|
482
|
-
|
|
483
|
-
|
|
512
|
+
/**
|
|
513
|
+
* Color queries waiting for their DA1 reply, oldest first. Terminals answer in order, so color
|
|
514
|
+
* replies belong to the oldest one. Queries stay here after a timeout to collect late replies.
|
|
515
|
+
*/
|
|
516
|
+
private pendingTerminalColorQueries: PendingTerminalColorQuery[] = [];
|
|
484
517
|
private terminalColorSchemeListeners = new Set<(scheme: TerminalColorScheme) => void>();
|
|
485
518
|
private terminalColorSchemeNotificationsEnabled = false;
|
|
486
519
|
/** Directory for debug/crash logs. When undefined, debug logging is disabled and crash dumps fall back to the OS temp directory. */
|
|
@@ -529,7 +562,7 @@ export abstract class TuiBase extends Container implements TUI {
|
|
|
529
562
|
if (this.showHardwareCursor === enabled) return;
|
|
530
563
|
this.showHardwareCursor = enabled;
|
|
531
564
|
if (!enabled) {
|
|
532
|
-
this.
|
|
565
|
+
this.hideTerminalCursor();
|
|
533
566
|
}
|
|
534
567
|
this.requestRender();
|
|
535
568
|
}
|
|
@@ -695,7 +728,7 @@ export abstract class TuiBase extends Container implements TUI {
|
|
|
695
728
|
if (!options?.nonCapturing && this.isOverlayVisible(entry)) {
|
|
696
729
|
this.setFocus(component);
|
|
697
730
|
}
|
|
698
|
-
this.
|
|
731
|
+
this.hideTerminalCursor();
|
|
699
732
|
this.requestRender();
|
|
700
733
|
|
|
701
734
|
// Return handle for controlling this overlay
|
|
@@ -711,7 +744,7 @@ export abstract class TuiBase extends Container implements TUI {
|
|
|
711
744
|
const topVisible = this.getTopmostVisibleOverlay();
|
|
712
745
|
this.setFocus(topVisible?.component ?? entry.preFocus);
|
|
713
746
|
}
|
|
714
|
-
if (this.overlayStack.length === 0) this.
|
|
747
|
+
if (this.overlayStack.length === 0) this.hideTerminalCursor();
|
|
715
748
|
this.requestRender();
|
|
716
749
|
}
|
|
717
750
|
},
|
|
@@ -793,10 +826,15 @@ export abstract class TuiBase extends Container implements TUI {
|
|
|
793
826
|
const topVisible = this.getTopmostVisibleOverlay();
|
|
794
827
|
this.setFocus(topVisible?.component ?? overlay.preFocus);
|
|
795
828
|
}
|
|
796
|
-
if (this.overlayStack.length === 0) this.
|
|
829
|
+
if (this.overlayStack.length === 0) this.hideTerminalCursor();
|
|
797
830
|
this.requestRender();
|
|
798
831
|
}
|
|
799
832
|
|
|
833
|
+
/** Hide the cursor while running. After stop(), the shell owns the cursor and it must stay visible. */
|
|
834
|
+
private hideTerminalCursor(): void {
|
|
835
|
+
if (!this.stopped) this.terminal.hideCursor();
|
|
836
|
+
}
|
|
837
|
+
|
|
800
838
|
/** Check if there are any visible overlays */
|
|
801
839
|
hasOverlay(): boolean {
|
|
802
840
|
return this.overlayStack.some((o) => this.isOverlayVisible(o));
|
|
@@ -1004,7 +1042,7 @@ export abstract class TuiBase extends Container implements TUI {
|
|
|
1004
1042
|
}
|
|
1005
1043
|
|
|
1006
1044
|
private handleTerminalInput(data: string): void {
|
|
1007
|
-
if (this.
|
|
1045
|
+
if (this.consumeTerminalColorResponse(data)) {
|
|
1008
1046
|
return;
|
|
1009
1047
|
}
|
|
1010
1048
|
if (this.consumeTerminalColorSchemeReport(data)) {
|
|
@@ -1081,30 +1119,52 @@ export abstract class TuiBase extends Container implements TUI {
|
|
|
1081
1119
|
}
|
|
1082
1120
|
}
|
|
1083
1121
|
|
|
1084
|
-
private
|
|
1085
|
-
|
|
1122
|
+
private consumeTerminalColorResponse(data: string): boolean {
|
|
1123
|
+
const query = this.pendingTerminalColorQueries[0];
|
|
1124
|
+
if (!query) {
|
|
1086
1125
|
return false;
|
|
1087
1126
|
}
|
|
1127
|
+
if (DEVICE_ATTRIBUTES_RESPONSE_PATTERN.test(data)) {
|
|
1128
|
+
this.pendingTerminalColorQueries.shift();
|
|
1129
|
+
this.completeTerminalColorQuery(query);
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1088
1132
|
|
|
1089
|
-
|
|
1133
|
+
const response = parseOscColorResponse(data);
|
|
1134
|
+
if (!response) {
|
|
1090
1135
|
return false;
|
|
1091
1136
|
}
|
|
1092
|
-
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
query.
|
|
1137
|
+
const { target, rgb } = response;
|
|
1138
|
+
const key = String(target);
|
|
1139
|
+
if (!query.deliver || query.replied.has(key)) {
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
query.replied.add(key);
|
|
1143
|
+
if (target === "foreground") {
|
|
1144
|
+
query.foreground = rgb;
|
|
1145
|
+
} else if (target === "background") {
|
|
1146
|
+
query.background = rgb;
|
|
1147
|
+
} else if (target < TERMINAL_PALETTE_SIZE) {
|
|
1148
|
+
query.palette[target] = rgb;
|
|
1149
|
+
}
|
|
1150
|
+
if (query.replied.size === TERMINAL_COLOR_REPLY_COUNT) {
|
|
1151
|
+
this.completeTerminalColorQuery(query);
|
|
1104
1152
|
}
|
|
1105
1153
|
return true;
|
|
1106
1154
|
}
|
|
1107
1155
|
|
|
1156
|
+
private terminalColorQueryResult(query: PendingTerminalColorQuery): TerminalColors {
|
|
1157
|
+
const palette = query.palette.every((color) => color !== undefined) ? (query.palette as RgbColor[]) : undefined;
|
|
1158
|
+
return { foreground: query.foreground, background: query.background, palette };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
private completeTerminalColorQuery(query: PendingTerminalColorQuery): void {
|
|
1162
|
+
const deliver = query.deliver;
|
|
1163
|
+
query.deliver = undefined;
|
|
1164
|
+
clearTimeout(query.timer);
|
|
1165
|
+
deliver?.(this.terminalColorQueryResult(query));
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1108
1168
|
private consumeTerminalColorSchemeReport(data: string): boolean {
|
|
1109
1169
|
const scheme = parseTerminalColorSchemeReport(data);
|
|
1110
1170
|
if (!scheme) {
|
|
@@ -1400,57 +1460,34 @@ export abstract class TuiBase extends Container implements TUI {
|
|
|
1400
1460
|
}
|
|
1401
1461
|
|
|
1402
1462
|
/**
|
|
1403
|
-
* Query the terminal's
|
|
1404
|
-
*
|
|
1405
|
-
*
|
|
1463
|
+
* Query the terminal's theme colors: the default foreground (OSC 10), the default background
|
|
1464
|
+
* (OSC 11), and ANSI colors 0-15 (OSC 4), followed by a DA1 request that marks the end of the
|
|
1465
|
+
* replies. Resolves when the DA1 reply or all color replies arrive, or when the timeout expires.
|
|
1466
|
+
* Colors the terminal did not report are undefined; the palette is only set when all 16 arrived.
|
|
1467
|
+
* @param timeoutMs Query timeout in milliseconds, for terminals that do not answer DA1 either.
|
|
1468
|
+
* @param onLateReply Receives the replies if the query completes after the timeout, e.g. over slow links.
|
|
1406
1469
|
*/
|
|
1407
|
-
|
|
1470
|
+
queryTerminalColors({
|
|
1471
|
+
timeoutMs,
|
|
1472
|
+
onLateReply,
|
|
1473
|
+
}: {
|
|
1474
|
+
timeoutMs: number;
|
|
1475
|
+
onLateReply?: (colors: TerminalColors) => void;
|
|
1476
|
+
}): Promise<TerminalColors> {
|
|
1408
1477
|
return new Promise((resolve) => {
|
|
1409
|
-
const query:
|
|
1410
|
-
|
|
1411
|
-
|
|
1478
|
+
const query: PendingTerminalColorQuery = {
|
|
1479
|
+
palette: Array.from({ length: TERMINAL_PALETTE_SIZE }, () => undefined),
|
|
1480
|
+
replied: new Set(),
|
|
1481
|
+
deliver: resolve,
|
|
1412
1482
|
timer: undefined,
|
|
1413
1483
|
};
|
|
1414
|
-
|
|
1484
|
+
// Resolve with the replies so far, and keep collecting late replies for `onLateReply`.
|
|
1415
1485
|
query.timer = setTimeout(() => {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
}
|
|
1419
|
-
query.settled = true;
|
|
1420
|
-
query.timer = undefined;
|
|
1421
|
-
query.resolve?.(undefined);
|
|
1422
|
-
query.resolve = undefined;
|
|
1486
|
+
query.deliver = onLateReply;
|
|
1487
|
+
resolve(this.terminalColorQueryResult(query));
|
|
1423
1488
|
}, timeoutMs);
|
|
1424
|
-
this.
|
|
1425
|
-
this.
|
|
1426
|
-
this.terminal.write("\x1b]11;?\x07");
|
|
1427
|
-
});
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
/**
|
|
1431
|
-
* Query the terminal's color-scheme preference with DSR (`CSI ? 996 n`).
|
|
1432
|
-
* Terminals that support the color palette notification protocol reply with
|
|
1433
|
-
* `CSI ? 997 ; 1 n` for dark or `CSI ? 997 ; 2 n` for light.
|
|
1434
|
-
*/
|
|
1435
|
-
queryTerminalColorScheme({ timeoutMs }: { timeoutMs: number }): Promise<TerminalColorScheme | undefined> {
|
|
1436
|
-
return new Promise((resolve) => {
|
|
1437
|
-
let settled = false;
|
|
1438
|
-
let timer: NodeJS.Timeout | undefined;
|
|
1439
|
-
let unsubscribe: () => void = () => {};
|
|
1440
|
-
const settle = (scheme: TerminalColorScheme | undefined) => {
|
|
1441
|
-
if (settled) return;
|
|
1442
|
-
settled = true;
|
|
1443
|
-
if (timer) {
|
|
1444
|
-
clearTimeout(timer);
|
|
1445
|
-
timer = undefined;
|
|
1446
|
-
}
|
|
1447
|
-
unsubscribe();
|
|
1448
|
-
resolve(scheme);
|
|
1449
|
-
};
|
|
1450
|
-
|
|
1451
|
-
unsubscribe = this.onTerminalColorSchemeChange(settle);
|
|
1452
|
-
timer = setTimeout(() => settle(undefined), timeoutMs);
|
|
1453
|
-
this.terminal.write("\x1b[?996n");
|
|
1489
|
+
this.pendingTerminalColorQueries.push(query);
|
|
1490
|
+
this.terminal.write(TERMINAL_COLOR_QUERY);
|
|
1454
1491
|
});
|
|
1455
1492
|
}
|
|
1456
1493
|
}
|