@tuidom/core 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/iTerminalBackend.d.ts +10 -0
- package/dist/dom/tuiElement.js +13 -2
- package/dist/input/keyInputParser.js +11 -1
- package/dist/input/rawTerminalToken.d.ts +10 -4
- package/dist/input/serializeKey.d.ts +3 -0
- package/dist/input/serializeKey.js +32 -0
- package/dist/input/tokenize.d.ts +13 -0
- package/dist/input/tokenize.js +38 -0
- package/package.json +1 -1
|
@@ -27,6 +27,16 @@ export interface ITerminalBackend {
|
|
|
27
27
|
* reply within the probe window). Fire-and-forget — callers must not block on it.
|
|
28
28
|
*/
|
|
29
29
|
probeKeyboardProtocol(onResult: (supported: boolean) => void): void;
|
|
30
|
+
/**
|
|
31
|
+
* Asynchronously ask the terminal for its name and version (XTVERSION: `CSI > 0 q`
|
|
32
|
+
* → `DCS > | <name(version)> ST`). `onResult` fires exactly once with the reported
|
|
33
|
+
* string as-is (e.g. `"iTerm2 3.5.0"`, `"kitty(0.35.2)"`, `"WezTerm 20240203…"`), or
|
|
34
|
+
* `undefined` if the terminal doesn't answer within the probe window. The reply is
|
|
35
|
+
* consumed by the backend and never reaches `onInput`. Inside tmux the query goes
|
|
36
|
+
* to tmux itself (like the other probes), so the answer is tmux's own (`"tmux 3.4"`).
|
|
37
|
+
* Fire-and-forget — callers must not block on it.
|
|
38
|
+
*/
|
|
39
|
+
probeTerminalVersion(onResult: (nameAndVersion: string | undefined) => void): void;
|
|
30
40
|
/**
|
|
31
41
|
* Render a frame: receive the current grid and cursor position.
|
|
32
42
|
* The backend decides how to output it (ANSI diffing, simple copy, etc.).
|
package/dist/dom/tuiElement.js
CHANGED
|
@@ -379,7 +379,9 @@ export class TUIElement {
|
|
|
379
379
|
/** Гасит фокус, если activeElement — этот элемент или его потомок. */
|
|
380
380
|
releaseFocusIfInside() {
|
|
381
381
|
const fm = this.getRoot()?.focusManager ?? null;
|
|
382
|
-
|
|
382
|
+
if (fm === null)
|
|
383
|
+
return;
|
|
384
|
+
let node = fm.activeElement ?? null;
|
|
383
385
|
while (node !== null && node !== this) {
|
|
384
386
|
node = node.getParent();
|
|
385
387
|
}
|
|
@@ -731,6 +733,7 @@ export class TUIElement {
|
|
|
731
733
|
* состояние нужно ДО стилевого прохода, прямо в performLayout.
|
|
732
734
|
*/
|
|
733
735
|
hasStyleStateWithin(state) {
|
|
736
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
734
737
|
let node = this;
|
|
735
738
|
while (node !== null) {
|
|
736
739
|
if (node.styleStatesSet?.has(state) === true)
|
|
@@ -769,6 +772,7 @@ export class TUIElement {
|
|
|
769
772
|
// цепочка предков (иначе любой markDirty = полноэкранный damage).
|
|
770
773
|
this.isPaintDirty = true;
|
|
771
774
|
this.isLayoutDirty = true;
|
|
775
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
772
776
|
let top = this;
|
|
773
777
|
for (let current = this._parent; current !== null; current = current._parent) {
|
|
774
778
|
current.isLayoutDirty = true;
|
|
@@ -802,7 +806,11 @@ export class TUIElement {
|
|
|
802
806
|
}
|
|
803
807
|
const rect = new Rect(new Point(parentOrigin.x + this.localPosition.dx, parentOrigin.y + this.localPosition.dy), this.allocatedSize);
|
|
804
808
|
const old = this.lastPaintedRect;
|
|
805
|
-
|
|
809
|
+
// Ещё не рисовался — тоже moved. Optional chain здесь не подходит: после
|
|
810
|
+
// первого `old?.x` тип уже сужен, и остальные `old?.` стали бы лишними.
|
|
811
|
+
const moved =
|
|
812
|
+
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
|
813
|
+
old === null ||
|
|
806
814
|
old.x !== rect.x ||
|
|
807
815
|
old.y !== rect.y ||
|
|
808
816
|
old.width !== rect.width ||
|
|
@@ -835,6 +843,9 @@ export class TUIElement {
|
|
|
835
843
|
* собственный хром по состоянию ребёнка вне его rect'а (ScrollBarDecorator:
|
|
836
844
|
* бегунок в колонке за пределами ребёнка).
|
|
837
845
|
*/
|
|
846
|
+
// Именно геттер, не readonly-поле: подклассы переопределяют его геттером
|
|
847
|
+
// (ListViewElement, ScrollContainerElement), а поле базы затенило бы их прототипный аксессор.
|
|
848
|
+
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
|
|
838
849
|
get paintsSubtreeAtomically() {
|
|
839
850
|
return false;
|
|
840
851
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { convertTokenToKeyPressEvent } from "./convertToken.js";
|
|
2
2
|
import { createKeyPressEvent } from "./keyEvent.js";
|
|
3
|
-
import { parseCSI, parseOSC, tokenize } from "./tokenize.js";
|
|
3
|
+
import { parseCSI, parseOSC, parseXtversionReply, tokenize, XTVERSION_REPLY_PREFIX } from "./tokenize.js";
|
|
4
4
|
/**
|
|
5
5
|
* Index where an *incomplete* trailing escape sequence begins, or -1 if `data`
|
|
6
6
|
* ends on a clean token boundary.
|
|
@@ -16,6 +16,12 @@ import { parseCSI, parseOSC, tokenize } from "./tokenize.js";
|
|
|
16
16
|
* incomplete — anything before it is already followed by more bytes.
|
|
17
17
|
*/
|
|
18
18
|
function incompleteTailStart(data) {
|
|
19
|
+
// Ответ XTVERSION (`ESC P > | … ESC \\`) — единственная строка, внутри которой
|
|
20
|
+
// может стоять «последний» ESC (от ST, разрезанного между чтениями), поэтому тело
|
|
21
|
+
// без терминатора проверяем раньше, чем ищем последний ESC.
|
|
22
|
+
const dcs = data.lastIndexOf(XTVERSION_REPLY_PREFIX);
|
|
23
|
+
if (dcs !== -1 && parseXtversionReply(data, dcs) === null)
|
|
24
|
+
return dcs;
|
|
19
25
|
const esc = data.lastIndexOf("\x1b");
|
|
20
26
|
if (esc === -1)
|
|
21
27
|
return -1;
|
|
@@ -30,6 +36,10 @@ function incompleteTailStart(data) {
|
|
|
30
36
|
return parseOSC(data, esc) === null ? esc : -1; // ESC ]
|
|
31
37
|
if (next === 0x4f)
|
|
32
38
|
return esc + 2 >= data.length ? esc : -1; // ESC O (SS3) needs its final letter
|
|
39
|
+
// ESC P: незавершённое начало ответа XTVERSION (`ESC P >`) ждём. Хвост, уже
|
|
40
|
+
// разошедшийся с префиксом, — обычный Alt+Shift+P (ESC + символ), он завершён.
|
|
41
|
+
if (next === 0x50)
|
|
42
|
+
return XTVERSION_REPLY_PREFIX.startsWith(data.slice(esc)) ? esc : -1;
|
|
33
43
|
// ESC + printable / control / special is complete once the second byte is present (it is).
|
|
34
44
|
return -1;
|
|
35
45
|
}
|
|
@@ -111,18 +111,24 @@ export interface OscToken {
|
|
|
111
111
|
readonly raw: string;
|
|
112
112
|
}
|
|
113
113
|
/**
|
|
114
|
-
* A
|
|
114
|
+
* A response to one of our capability probes — NOT a key event.
|
|
115
115
|
* - "kitty-flags": reply to the Kitty keyboard-protocol query `CSI ? u` → `CSI ? <flags> u`.
|
|
116
116
|
* - "da1": reply to Primary Device Attributes `CSI c` → `CSI ? <attrs> c`.
|
|
117
|
-
*
|
|
117
|
+
* - "xtversion": reply to XTVERSION `CSI > 0 q` → `DCS > | <name(version)> ST`.
|
|
118
|
+
* The CSI replies begin with the private marker `?`, which never appears in a real key
|
|
119
|
+
* event; the XTVERSION reply is a DCS string (`ESC P > |`), not a key either.
|
|
118
120
|
*/
|
|
119
121
|
export interface DeviceReportToken {
|
|
120
122
|
readonly kind: "device-report";
|
|
121
|
-
readonly report:
|
|
122
|
-
/**
|
|
123
|
+
readonly report: DeviceReportKind;
|
|
124
|
+
/**
|
|
125
|
+
* For CSI replies — raw parameter bytes including the leading `?` (e.g. "?15" or
|
|
126
|
+
* "?62;1;6"). For "xtversion" — the payload between `DCS > |` and ST (e.g. "iTerm2 3.5.0").
|
|
127
|
+
*/
|
|
123
128
|
readonly params: string;
|
|
124
129
|
readonly raw: string;
|
|
125
130
|
}
|
|
131
|
+
export type DeviceReportKind = "kitty-flags" | "da1" | "xtversion";
|
|
126
132
|
export type MouseButton = "left" | "middle" | "right" | "none";
|
|
127
133
|
export type MouseAction = "press" | "release" | "move" | "scroll-up" | "scroll-down" | "scroll-left" | "scroll-right";
|
|
128
134
|
export interface MouseToken {
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* serializeKey('Ctrl+ArrowUp') → '\x1b[1;5A'
|
|
9
9
|
* serializeKey('F5') → '\x1b[15~'
|
|
10
10
|
* serializeKey('Alt+a') → '\x1ba'
|
|
11
|
+
* serializeKey('Meta+s') → '\x1b[115;9u'
|
|
12
|
+
* serializeKey('Ctrl+Shift+a') → '\x1b[97;6u'
|
|
11
13
|
*
|
|
12
14
|
* Supports modifier prefixes: Ctrl+, Shift+, Alt+, Meta+ (combinable).
|
|
15
|
+
* Meta — это super-бит kitty (8): так приходит Cmd на маке.
|
|
13
16
|
*/
|
|
14
17
|
export declare function serializeKey(name: string): string;
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
* serializeKey('Ctrl+ArrowUp') → '\x1b[1;5A'
|
|
9
9
|
* serializeKey('F5') → '\x1b[15~'
|
|
10
10
|
* serializeKey('Alt+a') → '\x1ba'
|
|
11
|
+
* serializeKey('Meta+s') → '\x1b[115;9u'
|
|
12
|
+
* serializeKey('Ctrl+Shift+a') → '\x1b[97;6u'
|
|
11
13
|
*
|
|
12
14
|
* Supports modifier prefixes: Ctrl+, Shift+, Alt+, Meta+ (combinable).
|
|
15
|
+
* Meta — это super-бит kitty (8): так приходит Cmd на маке.
|
|
13
16
|
*/
|
|
17
|
+
import { kittyCodepointMap } from "./tokenize.js";
|
|
14
18
|
/** Simple special keys (no modifiers, no CSI) */
|
|
15
19
|
const simpleSpecialKeys = {
|
|
16
20
|
Enter: "\x0d",
|
|
@@ -73,6 +77,24 @@ function encodeModifier(ctrl, shift, alt, meta) {
|
|
|
73
77
|
mod += 8;
|
|
74
78
|
return mod;
|
|
75
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Codepoint для общей CSI u-формы, если `ch` — один печатный символ, который
|
|
82
|
+
* parseInput вернёт как есть: не управляющий и не занятый kitty под
|
|
83
|
+
* функциональную клавишу (PUA). Буква сворачивается в нижний регистр — так
|
|
84
|
+
* kitty шлёт базовую клавишу; регистр в событии даёт Shift-бит.
|
|
85
|
+
*/
|
|
86
|
+
function printableCodepoint(ch) {
|
|
87
|
+
const lower = ch.toLowerCase();
|
|
88
|
+
const codepoint = lower.codePointAt(0);
|
|
89
|
+
// Пусто («Meta+»), имя клавиши («Foo») или буква, чей lowercase — не один символ («İ»).
|
|
90
|
+
if (codepoint === undefined || String.fromCodePoint(codepoint) !== lower)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (codepoint < 0x20 || (codepoint >= 0x7f && codepoint <= 0x9f))
|
|
93
|
+
return undefined;
|
|
94
|
+
if (kittyCodepointMap[codepoint] !== undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
return codepoint;
|
|
97
|
+
}
|
|
76
98
|
export function serializeKey(name) {
|
|
77
99
|
// Parse modifier prefixes: "Ctrl+Shift+ArrowUp" → modifiers + "ArrowUp"
|
|
78
100
|
let ctrl = false;
|
|
@@ -175,5 +197,15 @@ export function serializeKey(name) {
|
|
|
175
197
|
if (!hasModifiers && remaining.length === 1) {
|
|
176
198
|
return remaining;
|
|
177
199
|
}
|
|
200
|
+
// Печатный символ с любым другим набором модификаторов — общая форма kitty
|
|
201
|
+
// CSI <codepoint>;<mod>u. Сюда доходят только комбинации, у которых нет
|
|
202
|
+
// legacy-кодировки (ветки выше их уже вернули): Meta+s, Ctrl+Shift+a, Shift+1…
|
|
203
|
+
// Как и kitty, шлём codepoint базовой клавиши: буква — в нижнем регистре, а
|
|
204
|
+
// регистр задаёт только Shift-префикс (Meta+S ≡ Meta+s, как Ctrl+A ≡ Ctrl+a).
|
|
205
|
+
const codepoint = printableCodepoint(remaining === "Space" ? " " : remaining);
|
|
206
|
+
if (codepoint !== undefined) {
|
|
207
|
+
const mod = encodeModifier(ctrl, shift, alt, meta);
|
|
208
|
+
return `\x1b[${codepoint.toString()};${mod.toString()}u`;
|
|
209
|
+
}
|
|
178
210
|
throw new Error(`serializeKey: unknown key name "${name}". Add it to the mapping.`);
|
|
179
211
|
}
|
package/dist/input/tokenize.d.ts
CHANGED
|
@@ -56,4 +56,17 @@ interface OSCTokenResult {
|
|
|
56
56
|
* Returns null if the sequence is not complete.
|
|
57
57
|
*/
|
|
58
58
|
export declare function parseOSC(data: string, start: number): OSCTokenResult | null;
|
|
59
|
+
/**
|
|
60
|
+
* Начало ответа на XTVERSION (`CSI > 0 q`): `DCS > |`. Разбираем только его: голое
|
|
61
|
+
* `ESC P` — это ещё и Alt+Shift+P legacy-терминала, так что произвольный DCS не ловим.
|
|
62
|
+
*/
|
|
63
|
+
export declare const XTVERSION_REPLY_PREFIX = "\u001BP>|";
|
|
64
|
+
/**
|
|
65
|
+
* Parse an XTVERSION reply `ESC P > | <text> ESC \` starting at data[start] = ESC.
|
|
66
|
+
* Returns null if the string terminator (ST) hasn't arrived yet.
|
|
67
|
+
*/
|
|
68
|
+
export declare function parseXtversionReply(data: string, start: number): {
|
|
69
|
+
token: DeviceReportToken;
|
|
70
|
+
nextIndex: number;
|
|
71
|
+
} | null;
|
|
59
72
|
export {};
|
package/dist/input/tokenize.js
CHANGED
|
@@ -57,6 +57,18 @@ export function tokenize(data) {
|
|
|
57
57
|
tokens.push({ kind: "standalone-esc", raw: data[i] });
|
|
58
58
|
i++;
|
|
59
59
|
}
|
|
60
|
+
else if (next === 0x50 && data.startsWith(XTVERSION_REPLY_PREFIX, i)) {
|
|
61
|
+
// DCS-ответ на XTVERSION: \x1bP>|<name(version)>\x1b\\. Без ST (обрезан, и
|
|
62
|
+
// KeyInputParser его уже не ждёт) — падаем в ESC+символ, как было до разбора DCS.
|
|
63
|
+
const dcsResult = parseXtversionReply(data, i);
|
|
64
|
+
if (dcsResult) {
|
|
65
|
+
tokens.push(dcsResult.token);
|
|
66
|
+
i = dcsResult.nextIndex;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
tokens.push({ kind: "esc-char", char: data[i + 1], charCode: next, raw: data.slice(i, i + 2) });
|
|
70
|
+
i += 2;
|
|
71
|
+
}
|
|
60
72
|
else if (next === 0x4f) {
|
|
61
73
|
// SS3 sequence: \x1bO<letter>
|
|
62
74
|
if (i + 2 < data.length) {
|
|
@@ -679,3 +691,29 @@ function buildOscResult(body, raw, nextIndex) {
|
|
|
679
691
|
nextIndex,
|
|
680
692
|
};
|
|
681
693
|
}
|
|
694
|
+
// ─── DCS: XTVERSION reply ───
|
|
695
|
+
/**
|
|
696
|
+
* Начало ответа на XTVERSION (`CSI > 0 q`): `DCS > |`. Разбираем только его: голое
|
|
697
|
+
* `ESC P` — это ещё и Alt+Shift+P legacy-терминала, так что произвольный DCS не ловим.
|
|
698
|
+
*/
|
|
699
|
+
export const XTVERSION_REPLY_PREFIX = "\x1bP>|";
|
|
700
|
+
/**
|
|
701
|
+
* Parse an XTVERSION reply `ESC P > | <text> ESC \` starting at data[start] = ESC.
|
|
702
|
+
* Returns null if the string terminator (ST) hasn't arrived yet.
|
|
703
|
+
*/
|
|
704
|
+
export function parseXtversionReply(data, start) {
|
|
705
|
+
const bodyStart = start + XTVERSION_REPLY_PREFIX.length;
|
|
706
|
+
const end = data.indexOf("\x1b\\", bodyStart);
|
|
707
|
+
if (end === -1)
|
|
708
|
+
return null;
|
|
709
|
+
const nextIndex = end + 2;
|
|
710
|
+
return {
|
|
711
|
+
token: {
|
|
712
|
+
kind: "device-report",
|
|
713
|
+
report: "xtversion",
|
|
714
|
+
params: data.slice(bodyStart, end),
|
|
715
|
+
raw: data.slice(start, nextIndex),
|
|
716
|
+
},
|
|
717
|
+
nextIndex,
|
|
718
|
+
};
|
|
719
|
+
}
|
package/package.json
CHANGED