dsh-neotui 0.2.2 → 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/CHANGELOG.md +25 -0
- package/README.md +32 -14
- package/bin/dsh-tui.js +1 -1
- package/package.json +5 -2
- package/src/config.js +46 -11
- package/src/file-picker.js +4 -4
- package/src/keybindings.js +172 -0
- package/src/md.js +23 -21
- package/src/panels.js +1772 -427
- package/src/term.js +18 -10
- package/src/text.js +11 -3
- package/src/views.js +1045 -281
- package/src/widgets.js +37 -27
package/src/term.js
CHANGED
|
@@ -30,6 +30,7 @@ export class Term {
|
|
|
30
30
|
this.onResize = onResize ?? (() => {});
|
|
31
31
|
this.kitty = kitty;
|
|
32
32
|
this.kittyActive = false; // set when the terminal answers the CSI ? u query
|
|
33
|
+
this.handoff = process.env.DSH_TUI_RESTART_HANDOFF === "1";
|
|
33
34
|
this.escTimer = null; // pending lone-ESC fallback (split-sequence safety)
|
|
34
35
|
this.decoder = new StringDecoder("utf8");
|
|
35
36
|
this.buf = "";
|
|
@@ -90,7 +91,7 @@ export class Term {
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
#emit(ev) { this.onEvent(ev); }
|
|
94
|
+
#emit(ev) { this.handoff = false; this.onEvent(ev); }
|
|
94
95
|
|
|
95
96
|
/** A lone ESC waits briefly for the rest of its sequence; if nothing
|
|
96
97
|
* arrives it becomes the standalone Escape key. */
|
|
@@ -144,13 +145,12 @@ export class Term {
|
|
|
144
145
|
continue;
|
|
145
146
|
}
|
|
146
147
|
const ch = buf[i];
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
if (ch === "[" && (buf.startsWith("[<", i) || /^\[\d+(?:;\d+)*[uMm]/.test(buf.slice(i)))) {
|
|
148
|
+
// Only a deliberately restarted process may discard one leading tail
|
|
149
|
+
// whose ESC belonged to its predecessor. Normal typed text beginning
|
|
150
|
+
// with "[99;5u" or "[<…M" must always survive.
|
|
151
|
+
if (this.handoff && i === 0 && ch === "[") {
|
|
152
152
|
const tail = /^(?:\[<\d+;\d+;\d+[Mm]|\[\d+(?:;\d+)*u)/.exec(buf.slice(i));
|
|
153
|
-
if (tail) { i += tail[0].length; continue; }
|
|
153
|
+
if (tail) { this.handoff = false; i += tail[0].length; continue; }
|
|
154
154
|
}
|
|
155
155
|
if (ch === "\x1b") {
|
|
156
156
|
// escape sequence
|
|
@@ -278,7 +278,10 @@ export class Term {
|
|
|
278
278
|
// CSI ? flags u — the terminal answered our kitty-protocol query, so
|
|
279
279
|
// it DOES honor the protocol (WezTerm etc. reply with the flags it
|
|
280
280
|
// supports; terminals with the feature off reply nothing at all).
|
|
281
|
-
if (final === "u")
|
|
281
|
+
if (final === "u") {
|
|
282
|
+
const flags = Number(params.split(";")[0] || 0);
|
|
283
|
+
if (flags & 1) this.kittyActive = true;
|
|
284
|
+
}
|
|
282
285
|
return; // other private responses (cursor pos) stay ignored
|
|
283
286
|
}
|
|
284
287
|
if (prefix === ">") return;
|
|
@@ -297,6 +300,8 @@ export class Term {
|
|
|
297
300
|
const code = nums[2] ?? 0;
|
|
298
301
|
const mod = nums[1] ?? 1;
|
|
299
302
|
const ctrl = !!(mod - 1 & 4), alt = !!(mod - 1 & 2), shift = !!(mod - 1 & 1);
|
|
303
|
+
const special = TILDE_NAMES[code];
|
|
304
|
+
if (special) { this.#emit({ type: "key", name: special, ctrl, alt, shift }); return; }
|
|
300
305
|
let text = "";
|
|
301
306
|
try { text = String.fromCodePoint(code); } catch { return; }
|
|
302
307
|
this.#emit({ type: "key", name: "char", key: text.toLowerCase(), text, ctrl, alt, shift });
|
|
@@ -318,8 +323,11 @@ export class Term {
|
|
|
318
323
|
}
|
|
319
324
|
const name = KEY_NAMES[final];
|
|
320
325
|
if (!name) return;
|
|
321
|
-
//
|
|
322
|
-
|
|
326
|
+
// Modifier is normally the last param (CSI 1;5A). Some terminals emit
|
|
327
|
+
// count-only CSI 5C/D for Ctrl+Right/Left; 2..8 are xterm modifier values
|
|
328
|
+
// and must not silently degrade to an unmodified pane switch.
|
|
329
|
+
const countOnlyModifier = nums.length === 1 && nums[0] >= 2 && nums[0] <= 8;
|
|
330
|
+
const mod = nums.length > 1 ? nums[nums.length - 1] : countOnlyModifier ? nums[0] : 1;
|
|
323
331
|
const ctrl = !!(mod - 1 & 4), alt = !!(mod - 1 & 2), shift = !!(mod - 1 & 1);
|
|
324
332
|
this.#emit({ type: "key", name, ctrl, alt, shift });
|
|
325
333
|
}
|
package/src/text.js
CHANGED
|
@@ -34,9 +34,17 @@ export function graphemes(s) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function graphemeWidth(g) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const cps = Array.from(g, (ch) => ch.codePointAt(0));
|
|
38
|
+
const widths = cps.map(wcwidth);
|
|
39
|
+
// A grapheme cluster is rendered as one terminal glyph. Emoji joined by ZWJ,
|
|
40
|
+
// skin-tone modifiers, flags, keycaps, and base+combining clusters therefore
|
|
41
|
+
// occupy the widest constituent width, not the sum of every code point.
|
|
42
|
+
const clustered = cps.length > 1 && (
|
|
43
|
+
g.includes("\u200d") || cps.some((cp) => cp >= 0x1f3fb && cp <= 0x1f3ff) ||
|
|
44
|
+
cps.every((cp) => cp >= 0x1f1e6 && cp <= 0x1f1ff) || cps.includes(0x20e3) ||
|
|
45
|
+
widths.some((width) => width === 0)
|
|
46
|
+
);
|
|
47
|
+
return clustered ? Math.max(0, ...widths) : widths.reduce((a, b) => a + b, 0);
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
export function strWidth(s) {
|