@xp266/dshtui 0.1.8 → 0.1.9

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/lib/dialog.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import "./prod-react-Dzof4qJx.mjs";
2
- import { L as panelAnchorRow, c as asTextItem, i as Dialog, l as registerWidget, n as panelLegend, o as rowHeight, r as CloseGuardContext, s as wrapStatusLines, t as PanelSurface } from "./surface-bsX86Swt.mjs";
2
+ import { I as panelAnchorRow, c as asTextItem, i as Dialog, l as registerWidget, n as panelLegend, o as rowHeight, r as CloseGuardContext, s as wrapStatusLines, t as PanelSurface } from "./surface-CfzU6eCq.mjs";
3
3
  export { CloseGuardContext, Dialog, PanelSurface, asTextItem, panelAnchorRow, panelLegend, registerWidget, rowHeight, wrapStatusLines };
package/lib/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./prod-react-Dzof4qJx.mjs";
2
- import { $ as colToCharIndex, A as Region, At as colorLevel, B as hintBlockTop, C as caretNonceText, Ct as permissionModeInfo, D as markKeymapConsumed, Dt as subscribePalettes, E as isKeyConsumed, Et as setThemeMode, F as inputContentContains, Ft as installCrashHandlers, G as segmentsKey, H as inputStatusRow, I as messageBackgroundWidth, It as warn, J as sliceByColumns, K as wrapSegments, L as panelAnchorRow, M as isMouseResidue, Mt as env, N as writeStdoutRaw, Nt as configureLogs, O as SelectableText, Ot as themeMode, P as hintSpan, Pt as error, Q as charWidth, R as panelContains, S as caretNonceColor, St as paletteColor, T as beginKeyEvent, Tt as replacePalettes, U as glyphs, V as inputFrameTop, W as mergeRuns, X as selectedRange, Y as clampFocusRow, Z as toScreenSelection, _ as editDeleteWordRight, _t as registerFieldKind, a as DIALOG_MAX_HEIGHT, at as truncate, b as writeCursorShape, bt as specialFieldFactory, ct as charactersChipLabel, d as editCursorLeft, dt as fieldStyleOf, et as errorText, f as editCursorRight, ft as hasFieldChar, g as editDeleteWordLeft, gt as linesChipLabel, h as editDelete, ht as isFieldChar, i as Dialog, it as textWidth, j as createMouseController, jt as setColorLevel, k as SelectionContext, kt as keyedRegistry, l as registerWidget, lt as fieldExpandOf, m as editCursorWordRight, mt as imageChipLabel, n as panelLegend, nt as locToPoint, ot as wrapLines, p as editCursorWordLeft, pt as hasFieldSlots, q as chromeSelectionText, r as CloseGuardContext, rt as segmentGraphemes, st as allocateField, t as PanelSurface, tt as lineBreaks, u as editBackspace, ut as fieldSlotOf, v as editInsert, vt as releaseFields, w as useCaret, wt as registerPalette, x as caretNonceBold, xt as COLORS, y as CapText, yt as releaseUnreferenced, z as scrollbarColumn } from "./surface-bsX86Swt.mjs";
2
+ import { $ as errorText, A as SelectableText, At as setColorLevel, B as inputFrameTop, C as caretNonceText, Ct as registerPalette, D as markKeymapConsumed, Dt as themeMode, E as isKeyConsumed, Et as subscribePalettes, F as messageBackgroundWidth, Ft as warn, G as wrapSegments, H as glyphs, I as panelAnchorRow, It as enterMode, J as clampFocusRow, K as chromeSelectionText, L as panelContains, Lt as restoreAllModes, M as Region, Mt as configureLogs, N as hintSpan, Nt as error, O as createMouseController, Ot as keyedRegistry, P as inputContentContains, Pt as installCrashHandlers, Q as colToCharIndex, R as scrollbarColumn, S as caretNonceColor, St as permissionModeInfo, T as beginKeyEvent, Tt as setThemeMode, U as mergeRuns, V as inputStatusRow, W as segmentsKey, X as toScreenSelection, Y as selectedRange, Z as charWidth, _ as editDeleteWordRight, _t as releaseFields, a as DIALOG_MAX_HEIGHT, at as wrapLines, b as writeCursorShape, bt as COLORS, ct as fieldExpandOf, d as editCursorLeft, dt as hasFieldChar, et as lineBreaks, f as editCursorRight, ft as hasFieldSlots, g as editDeleteWordLeft, gt as registerFieldKind, h as editDelete, ht as linesChipLabel, i as Dialog, it as truncate, j as SelectionContext, jt as env, k as isMouseResidue, kt as colorLevel, l as registerWidget, lt as fieldSlotOf, m as editCursorWordRight, mt as isFieldChar, n as panelLegend, nt as segmentGraphemes, ot as allocateField, p as editCursorWordLeft, pt as imageChipLabel, q as sliceByColumns, r as CloseGuardContext, rt as textWidth, st as charactersChipLabel, t as PanelSurface, tt as locToPoint, u as editBackspace, ut as fieldStyleOf, v as editInsert, vt as releaseUnreferenced, w as useCaret, wt as replacePalettes, x as caretNonceBold, xt as paletteColor, y as CapText, yt as specialFieldFactory, z as hintBlockTop } from "./surface-CfzU6eCq.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { readFileSync, statSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
@@ -2945,26 +2945,6 @@ function selectionText(messages, width, selection) {
2945
2945
  return lines.join("\n").replace(/[ \t]+$/gm, "").replace(/\n+$/, "");
2946
2946
  }
2947
2947
  //#endregion
2948
- //#region src/terminal/restore.ts
2949
- /**
2950
- * Synchronously restore every terminal mode this TUI enables: cursor
2951
- * visibility and shape, mouse tracking, bracketed paste, the kitty keyboard
2952
- * protocol, and the alternate screen. Windows TTY writes through
2953
- * process.stdout are asynchronous, so the restores ink queues at unmount
2954
- * can be dropped when the process exits right after — every exit path must
2955
- * call this synchronously before the process leaves.
2956
- */
2957
- function restoreTerminal() {
2958
- writeStdoutRaw([
2959
- "\x1B[?25h",
2960
- "\x1B[0 q",
2961
- "\x1B[?1003l\x1B[?1000l\x1B[?1002l\x1B[?1006l",
2962
- "\x1B[?2004l",
2963
- "\x1B[<u",
2964
- "\x1B[?1049l"
2965
- ].join(""));
2966
- }
2967
- //#endregion
2968
2948
  //#region src/ui/hooks/use-terminal-size.ts
2969
2949
  function useTerminalSize() {
2970
2950
  const { stdout } = useStdout();
@@ -9162,7 +9142,7 @@ function App({ bridge, screen, themeTick = 0 }) {
9162
9142
  }
9163
9143
  if (!exiting.current) {
9164
9144
  exiting.current = true;
9165
- restoreTerminal();
9145
+ restoreAllModes();
9166
9146
  process.kill(process.pid, "SIGINT");
9167
9147
  setTimeout(() => process.exit(0), FORCE_EXIT_DELAY_MS).unref();
9168
9148
  }
@@ -11293,6 +11273,10 @@ function apply(ctx, config = Config(DEFAULT_CONFIG)) {
11293
11273
  });
11294
11274
  lastColumns = capture.stream.columns;
11295
11275
  lastRows = capture.stream.rows;
11276
+ if (config.alternateScreen) enterMode("alt-screen", "", "\x1B[?1049l");
11277
+ enterMode("kitty-keyboard", "", "\x1B[<u");
11278
+ enterMode("bracketed-paste", "", "\x1B[?2004l");
11279
+ enterMode("cursor", "", "\x1B[?25h\x1B[0 q");
11296
11280
  const onResize = () => {
11297
11281
  lastColumns = capture.stream.columns;
11298
11282
  lastRows = capture.stream.rows;
@@ -11350,10 +11334,9 @@ function apply(ctx, config = Config(DEFAULT_CONFIG)) {
11350
11334
  disposeExtensionPoint();
11351
11335
  stopSizePoll?.();
11352
11336
  hotTheme?.stop();
11353
- writeCursorShape("reset");
11354
11337
  bridge?.dispose();
11355
11338
  app?.unmount();
11356
- restoreTerminal();
11339
+ restoreAllModes();
11357
11340
  disposeCrashHandlers();
11358
11341
  };
11359
11342
  });
@@ -25,6 +25,40 @@ function resolveDshHome(env = process.env) {
25
25
  return resolve(expandHomePath(selected));
26
26
  }
27
27
  //#endregion
28
+ //#region src/terminal/modes.ts
29
+ /**
30
+ * Single choke point for terminal mode changes: every enter/exit of a
31
+ * terminal mode registers its restore sequence here, so one
32
+ * restoreAllModes() call on any exit path inverts everything that was
33
+ * applied — including the modes ink enables on our behalf (seeded with an
34
+ * empty enter sequence). Windows TTY writes through process.stdout are
35
+ * asynchronous, so mode sequences must be written synchronously through
36
+ * fd 1 or they are silently dropped at exit.
37
+ */
38
+ const applied = /* @__PURE__ */ new Map();
39
+ function writeStdoutRaw(text) {
40
+ try {
41
+ writeSync(1, text);
42
+ } catch {
43
+ process.stdout.write(text);
44
+ }
45
+ }
46
+ function enterMode(name, sequence, restore) {
47
+ applied.set(name, restore);
48
+ if (sequence !== "") writeStdoutRaw(sequence);
49
+ }
50
+ function exitMode(name) {
51
+ const restore = applied.get(name);
52
+ if (restore === void 0) return;
53
+ applied.delete(name);
54
+ writeStdoutRaw(restore);
55
+ }
56
+ /** Restore every still-applied mode, newest first. */
57
+ function restoreAllModes() {
58
+ for (const restore of [...applied.values()].reverse()) writeStdoutRaw(restore);
59
+ applied.clear();
60
+ }
61
+ //#endregion
28
62
  //#region src/log.ts
29
63
  const LOG_LEVELS = [
30
64
  "debug",
@@ -175,7 +209,10 @@ function installCrashHandlers(options = {}) {
175
209
  message: stringifyError(cause),
176
210
  stack: cause instanceof Error ? cause.stack : void 0
177
211
  });
178
- if (options.exit === true) process.exit(1);
212
+ if (options.exit === true) {
213
+ restoreAllModes();
214
+ process.exit(1);
215
+ }
179
216
  };
180
217
  const onRejected = (cause) => {
181
218
  writeCrashReport({
@@ -183,7 +220,10 @@ function installCrashHandlers(options = {}) {
183
220
  message: stringifyError(cause),
184
221
  stack: cause instanceof Error ? cause.stack : void 0
185
222
  });
186
- if (options.exit === true) process.exit(1);
223
+ if (options.exit === true) {
224
+ restoreAllModes();
225
+ process.exit(1);
226
+ }
187
227
  };
188
228
  process.on("uncaughtException", onUncaught);
189
229
  process.on("unhandledRejection", onRejected);
@@ -1298,142 +1338,6 @@ function scrollbarColumn(columns) {
1298
1338
  function messageBackgroundWidth(width) {
1299
1339
  return width - 2 - 3 - 1;
1300
1340
  }
1301
- //#endregion
1302
- //#region src/terminal/mouse.ts
1303
- const MOUSE_ENABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000h\x1B[?1002h\x1B[?1006h";
1304
- const MOUSE_DISABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000l\x1B[?1002l\x1B[?1006l";
1305
- /**
1306
- * Terminal-mode sequences must reach the device even when the process exits
1307
- * right after: on Windows, TTY writes through process.stdout are
1308
- * asynchronous and a trailing restore sequence is silently dropped, leaving
1309
- * the terminal in mouse-tracking mode. fd 1 writes are synchronous.
1310
- */
1311
- function writeStdoutRaw(text) {
1312
- try {
1313
- writeSync(1, text);
1314
- } catch {
1315
- process.stdout.write(text);
1316
- }
1317
- }
1318
- function writeMouseEnable() {
1319
- writeStdoutRaw(MOUSE_ENABLE_SEQUENCES);
1320
- }
1321
- function writeMouseDisable() {
1322
- writeStdoutRaw(MOUSE_DISABLE_SEQUENCES);
1323
- }
1324
- const MAX_BUFFER = 64;
1325
- const SGR_PREFIX = "\x1B[<";
1326
- function partialPrefixLength(buffer) {
1327
- for (let keep = Math.min(2, buffer.length); keep > 0; keep--) if (SGR_PREFIX.startsWith(buffer.slice(-keep))) return keep;
1328
- return 0;
1329
- }
1330
- function createMouseParser(onEvent) {
1331
- let buffer = "";
1332
- const pressedButtons = /* @__PURE__ */ new Set();
1333
- function parseSequenceAt(str, offset) {
1334
- if (!str.startsWith("\x1B[<", offset)) return null;
1335
- let index = offset + 3;
1336
- const values = [];
1337
- let current = 0;
1338
- let hasDigit = false;
1339
- let terminator = "";
1340
- for (; index < str.length; index++) {
1341
- const ch = str[index];
1342
- if (ch >= "0" && ch <= "9") {
1343
- current = current * 10 + (ch.charCodeAt(0) - 48);
1344
- hasDigit = true;
1345
- } else if (ch === ";" && hasDigit) {
1346
- values.push(current);
1347
- current = 0;
1348
- hasDigit = false;
1349
- } else if (ch === "M" || ch === "m") {
1350
- if (!hasDigit) return null;
1351
- values.push(current);
1352
- terminator = ch;
1353
- break;
1354
- } else return null;
1355
- }
1356
- if (terminator === "") return null;
1357
- if (values.length !== 3) return null;
1358
- return {
1359
- event: decodeSgrEvent(values[0], values[1], values[2], terminator, pressedButtons),
1360
- consumed: index - offset + 1
1361
- };
1362
- }
1363
- return { feed(chunk) {
1364
- buffer += chunk;
1365
- while (true) {
1366
- const start = buffer.indexOf("\x1B[<");
1367
- if (start === -1) {
1368
- const keep = partialPrefixLength(buffer);
1369
- buffer = keep === 0 ? "" : buffer.slice(-keep);
1370
- return;
1371
- }
1372
- if (start > 0) {
1373
- buffer = buffer.slice(start);
1374
- continue;
1375
- }
1376
- if (buffer.length > MAX_BUFFER) {
1377
- buffer = "";
1378
- return;
1379
- }
1380
- const parsed = parseSequenceAt(buffer, 0);
1381
- if (parsed === null) return;
1382
- const { event, consumed } = parsed;
1383
- buffer = buffer.slice(consumed);
1384
- onEvent(event);
1385
- }
1386
- } };
1387
- }
1388
- function decodeSgrEvent(rawButtonCode, wireX, wireY, pressRelease, pressed) {
1389
- const button = rawButtonCode & 3;
1390
- const isScroll = (rawButtonCode & 64) !== 0;
1391
- const isMotion = (rawButtonCode & 32) !== 0;
1392
- const modifiers = {
1393
- shift: (rawButtonCode & 4) !== 0,
1394
- alt: (rawButtonCode & 8) !== 0,
1395
- ctrl: (rawButtonCode & 16) !== 0
1396
- };
1397
- let type;
1398
- let scrollDirection;
1399
- if (isScroll) {
1400
- type = "scroll";
1401
- scrollDirection = button === 0 ? "up" : "down";
1402
- } else if (isMotion) type = pressed.size > 0 ? "drag" : "move";
1403
- else if (pressRelease === "M") {
1404
- type = "down";
1405
- if (button !== 3) pressed.add(button);
1406
- } else {
1407
- type = "up";
1408
- if (button === 3) pressed.clear();
1409
- else pressed.delete(button);
1410
- }
1411
- return {
1412
- type,
1413
- button: button === 3 ? 0 : button,
1414
- x: wireX - 1,
1415
- y: wireY - 1,
1416
- modifiers,
1417
- ...scrollDirection === void 0 ? {} : { scrollDirection }
1418
- };
1419
- }
1420
- function isMouseResidue(input) {
1421
- return input.includes("\x1B") || /^\[<\d+(;\d+)*[Mm]$/.test(input);
1422
- }
1423
- function createMouseController(onEvent) {
1424
- const parser = createMouseParser(onEvent);
1425
- const onData = (chunk) => parser.feed(chunk.toString("latin1"));
1426
- return {
1427
- enable() {
1428
- writeMouseEnable();
1429
- process.stdin.on("data", onData);
1430
- },
1431
- disable() {
1432
- writeMouseDisable();
1433
- process.stdin.off("data", onData);
1434
- }
1435
- };
1436
- }
1437
1341
  const RegionContext = createContext({
1438
1342
  x: 0,
1439
1343
  y: 0
@@ -1571,6 +1475,123 @@ function renderSegments(segments, start, end, baseColor) {
1571
1475
  return out;
1572
1476
  }
1573
1477
  //#endregion
1478
+ //#region src/terminal/mouse.ts
1479
+ const MOUSE_ENABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000h\x1B[?1002h\x1B[?1006h";
1480
+ const MOUSE_DISABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000l\x1B[?1002l\x1B[?1006l";
1481
+ const MAX_BUFFER = 64;
1482
+ const SGR_PREFIX = "\x1B[<";
1483
+ function partialPrefixLength(buffer) {
1484
+ for (let keep = Math.min(2, buffer.length); keep > 0; keep--) if (SGR_PREFIX.startsWith(buffer.slice(-keep))) return keep;
1485
+ return 0;
1486
+ }
1487
+ function createMouseParser(onEvent) {
1488
+ let buffer = "";
1489
+ const pressedButtons = /* @__PURE__ */ new Set();
1490
+ function parseSequenceAt(str, offset) {
1491
+ if (!str.startsWith("\x1B[<", offset)) return null;
1492
+ let index = offset + 3;
1493
+ const values = [];
1494
+ let current = 0;
1495
+ let hasDigit = false;
1496
+ let terminator = "";
1497
+ for (; index < str.length; index++) {
1498
+ const ch = str[index];
1499
+ if (ch >= "0" && ch <= "9") {
1500
+ current = current * 10 + (ch.charCodeAt(0) - 48);
1501
+ hasDigit = true;
1502
+ } else if (ch === ";" && hasDigit) {
1503
+ values.push(current);
1504
+ current = 0;
1505
+ hasDigit = false;
1506
+ } else if (ch === "M" || ch === "m") {
1507
+ if (!hasDigit) return null;
1508
+ values.push(current);
1509
+ terminator = ch;
1510
+ break;
1511
+ } else return null;
1512
+ }
1513
+ if (terminator === "") return null;
1514
+ if (values.length !== 3) return null;
1515
+ return {
1516
+ event: decodeSgrEvent(values[0], values[1], values[2], terminator, pressedButtons),
1517
+ consumed: index - offset + 1
1518
+ };
1519
+ }
1520
+ return { feed(chunk) {
1521
+ buffer += chunk;
1522
+ while (true) {
1523
+ const start = buffer.indexOf("\x1B[<");
1524
+ if (start === -1) {
1525
+ const keep = partialPrefixLength(buffer);
1526
+ buffer = keep === 0 ? "" : buffer.slice(-keep);
1527
+ return;
1528
+ }
1529
+ if (start > 0) {
1530
+ buffer = buffer.slice(start);
1531
+ continue;
1532
+ }
1533
+ if (buffer.length > MAX_BUFFER) {
1534
+ buffer = "";
1535
+ return;
1536
+ }
1537
+ const parsed = parseSequenceAt(buffer, 0);
1538
+ if (parsed === null) return;
1539
+ const { event, consumed } = parsed;
1540
+ buffer = buffer.slice(consumed);
1541
+ onEvent(event);
1542
+ }
1543
+ } };
1544
+ }
1545
+ function decodeSgrEvent(rawButtonCode, wireX, wireY, pressRelease, pressed) {
1546
+ const button = rawButtonCode & 3;
1547
+ const isScroll = (rawButtonCode & 64) !== 0;
1548
+ const isMotion = (rawButtonCode & 32) !== 0;
1549
+ const modifiers = {
1550
+ shift: (rawButtonCode & 4) !== 0,
1551
+ alt: (rawButtonCode & 8) !== 0,
1552
+ ctrl: (rawButtonCode & 16) !== 0
1553
+ };
1554
+ let type;
1555
+ let scrollDirection;
1556
+ if (isScroll) {
1557
+ type = "scroll";
1558
+ scrollDirection = button === 0 ? "up" : "down";
1559
+ } else if (isMotion) type = pressed.size > 0 ? "drag" : "move";
1560
+ else if (pressRelease === "M") {
1561
+ type = "down";
1562
+ if (button !== 3) pressed.add(button);
1563
+ } else {
1564
+ type = "up";
1565
+ if (button === 3) pressed.clear();
1566
+ else pressed.delete(button);
1567
+ }
1568
+ return {
1569
+ type,
1570
+ button: button === 3 ? 0 : button,
1571
+ x: wireX - 1,
1572
+ y: wireY - 1,
1573
+ modifiers,
1574
+ ...scrollDirection === void 0 ? {} : { scrollDirection }
1575
+ };
1576
+ }
1577
+ function isMouseResidue(input) {
1578
+ return input.includes("\x1B") || /^\[<\d+(;\d+)*[Mm]$/.test(input);
1579
+ }
1580
+ function createMouseController(onEvent) {
1581
+ const parser = createMouseParser(onEvent);
1582
+ const onData = (chunk) => parser.feed(chunk.toString("latin1"));
1583
+ return {
1584
+ enable() {
1585
+ enterMode("mouse", MOUSE_ENABLE_SEQUENCES, MOUSE_DISABLE_SEQUENCES);
1586
+ process.stdin.on("data", onData);
1587
+ },
1588
+ disable() {
1589
+ exitMode("mouse");
1590
+ process.stdin.off("data", onData);
1591
+ }
1592
+ };
1593
+ }
1594
+ //#endregion
1574
1595
  //#region src/ui/key-arbiter.ts
1575
1596
  /**
1576
1597
  * Key-event suppression for ink's broadcast-based useInput.
@@ -1648,7 +1669,7 @@ const SEQUENCES = {
1648
1669
  show: "\x1B[?25h"
1649
1670
  };
1650
1671
  function writeCursorShape(shape) {
1651
- writeStdoutRaw(SEQUENCES[shape]);
1672
+ enterMode("cursor", SEQUENCES[shape], "\x1B[?25h\x1B[0 q");
1652
1673
  }
1653
1674
  //#endregion
1654
1675
  //#region src/ui/chrome/caps.tsx
@@ -3136,4 +3157,4 @@ function PanelSurface({ columns, rows, body, bodyStart, background, blockWidth }
3136
3157
  }) });
3137
3158
  }
3138
3159
  //#endregion
3139
- export { colToCharIndex as $, Region as A, colorLevel as At, hintBlockTop as B, caretNonceText as C, permissionModeInfo as Ct, markKeymapConsumed as D, subscribePalettes as Dt, isKeyConsumed as E, setThemeMode as Et, inputContentContains as F, installCrashHandlers as Ft, segmentsKey as G, inputStatusRow as H, messageBackgroundWidth as I, warn as It, sliceByColumns as J, wrapSegments as K, panelAnchorRow as L, isMouseResidue as M, env as Mt, writeStdoutRaw as N, configureLogs as Nt, SelectableText as O, themeMode as Ot, hintSpan as P, error as Pt, charWidth as Q, panelContains as R, caretNonceColor as S, paletteColor as St, beginKeyEvent as T, replacePalettes as Tt, glyphs as U, inputFrameTop as V, mergeRuns as W, selectedRange as X, clampFocusRow as Y, toScreenSelection as Z, editDeleteWordRight as _, registerFieldKind as _t, DIALOG_MAX_HEIGHT as a, truncate as at, writeCursorShape as b, specialFieldFactory as bt, asTextItem as c, charactersChipLabel as ct, editCursorLeft as d, fieldStyleOf as dt, errorText as et, editCursorRight as f, hasFieldChar as ft, editDeleteWordLeft as g, linesChipLabel as gt, editDelete as h, isFieldChar as ht, Dialog as i, textWidth as it, createMouseController as j, setColorLevel as jt, SelectionContext as k, keyedRegistry as kt, registerWidget as l, fieldExpandOf as lt, editCursorWordRight as m, imageChipLabel as mt, panelLegend as n, locToPoint as nt, rowHeight as o, wrapLines as ot, editCursorWordLeft as p, hasFieldSlots as pt, chromeSelectionText as q, CloseGuardContext as r, segmentGraphemes as rt, wrapStatusLines as s, allocateField as st, PanelSurface as t, lineBreaks as tt, editBackspace as u, fieldSlotOf as ut, editInsert as v, releaseFields as vt, useCaret as w, registerPalette as wt, caretNonceBold as x, COLORS as xt, CapText as y, releaseUnreferenced as yt, scrollbarColumn as z };
3160
+ export { errorText as $, SelectableText as A, setColorLevel as At, inputFrameTop as B, caretNonceText as C, registerPalette as Ct, markKeymapConsumed as D, themeMode as Dt, isKeyConsumed as E, subscribePalettes as Et, messageBackgroundWidth as F, warn as Ft, wrapSegments as G, glyphs as H, panelAnchorRow as I, enterMode as It, clampFocusRow as J, chromeSelectionText as K, panelContains as L, restoreAllModes as Lt, Region as M, configureLogs as Mt, hintSpan as N, error as Nt, createMouseController as O, keyedRegistry as Ot, inputContentContains as P, installCrashHandlers as Pt, colToCharIndex as Q, scrollbarColumn as R, caretNonceColor as S, permissionModeInfo as St, beginKeyEvent as T, setThemeMode as Tt, mergeRuns as U, inputStatusRow as V, segmentsKey as W, toScreenSelection as X, selectedRange as Y, charWidth as Z, editDeleteWordRight as _, releaseFields as _t, DIALOG_MAX_HEIGHT as a, wrapLines as at, writeCursorShape as b, COLORS as bt, asTextItem as c, fieldExpandOf as ct, editCursorLeft as d, hasFieldChar as dt, lineBreaks as et, editCursorRight as f, hasFieldSlots as ft, editDeleteWordLeft as g, registerFieldKind as gt, editDelete as h, linesChipLabel as ht, Dialog as i, truncate as it, SelectionContext as j, env as jt, isMouseResidue as k, colorLevel as kt, registerWidget as l, fieldSlotOf as lt, editCursorWordRight as m, isFieldChar as mt, panelLegend as n, segmentGraphemes as nt, rowHeight as o, allocateField as ot, editCursorWordLeft as p, imageChipLabel as pt, sliceByColumns as q, CloseGuardContext as r, textWidth as rt, wrapStatusLines as s, charactersChipLabel as st, PanelSurface as t, locToPoint as tt, editBackspace as u, fieldStyleOf as ut, editInsert as v, releaseUnreferenced as vt, useCaret as w, replacePalettes as wt, caretNonceBold as x, paletteColor as xt, CapText as y, specialFieldFactory as yt, hintBlockTop as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xp266/dshtui",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "ink-based terminal UI plugin for DeepSeek Harness",
6
6
  "engines": {
package/src/index.tsx CHANGED
@@ -10,7 +10,7 @@ import { createChatBridge } from './chat/bridge.ts'
10
10
  import type { ChatBridge } from './chat/bridge.ts'
11
11
  import { createScreenCapture } from './terminal/screen.ts'
12
12
  import { writeCursorShape } from './terminal/cursor-shape.ts'
13
- import { restoreTerminal } from './terminal/restore.ts'
13
+ import { enterMode, restoreAllModes } from './terminal/modes.ts'
14
14
  import { startHotTheme } from './hot-theme.ts'
15
15
  import { applyTheme } from './apply-theme.ts'
16
16
  import { registerPalette } from './theme.ts'
@@ -114,6 +114,12 @@ export function apply(ctx: Context, config: Config = Config(DEFAULT_CONFIG)) {
114
114
  })
115
115
  lastColumns = capture.stream.columns
116
116
  lastRows = capture.stream.rows
117
+ // Seed the modes ink enables on our behalf (its own enter writes are
118
+ // internal); a config-off alternate screen is never registered.
119
+ if (config.alternateScreen) enterMode('alt-screen', '', '\x1b[?1049l')
120
+ enterMode('kitty-keyboard', '', '\x1b[<u')
121
+ enterMode('bracketed-paste', '', '\x1b[?2004l')
122
+ enterMode('cursor', '', '\x1b[?25h\x1b[0 q')
117
123
  const onResize = () => {
118
124
  lastColumns = capture.stream.columns
119
125
  lastRows = capture.stream.rows
@@ -178,10 +184,9 @@ export function apply(ctx: Context, config: Config = Config(DEFAULT_CONFIG)) {
178
184
  disposeExtensionPoint()
179
185
  stopSizePoll?.()
180
186
  hotTheme?.stop()
181
- writeCursorShape('reset')
182
187
  bridge?.dispose()
183
188
  app?.unmount()
184
- restoreTerminal()
189
+ restoreAllModes()
185
190
  disposeCrashHandlers()
186
191
  }
187
192
  })
package/src/log.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync, unlinkSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
3
  import { resolveDshHome } from './harness-home.ts'
4
+ import { restoreAllModes } from './terminal/modes.ts'
4
5
 
5
6
  const LOG_LEVELS = ['debug', 'info', 'warn', 'error'] as const
6
7
  export type LogLevel = (typeof LOG_LEVELS)[number]
@@ -228,7 +229,10 @@ export function installCrashHandlers(options: { exit?: boolean } = {}): () => vo
228
229
  message: stringifyError(cause),
229
230
  stack: cause instanceof Error ? cause.stack : undefined,
230
231
  })
231
- if (options.exit === true) process.exit(1)
232
+ if (options.exit === true) {
233
+ restoreAllModes()
234
+ process.exit(1)
235
+ }
232
236
  }
233
237
  const onRejected = (cause: unknown): void => {
234
238
  writeCrashReport({
@@ -236,7 +240,10 @@ export function installCrashHandlers(options: { exit?: boolean } = {}): () => vo
236
240
  message: stringifyError(cause),
237
241
  stack: cause instanceof Error ? cause.stack : undefined,
238
242
  })
239
- if (options.exit === true) process.exit(1)
243
+ if (options.exit === true) {
244
+ restoreAllModes()
245
+ process.exit(1)
246
+ }
240
247
  }
241
248
  process.on('uncaughtException', onUncaught)
242
249
  process.on('unhandledRejection', onRejected)
@@ -1,5 +1,5 @@
1
1
 
2
- import { writeStdoutRaw } from './mouse.ts'
2
+ import { enterMode } from './modes.ts'
3
3
 
4
4
  const SEQUENCES = {
5
5
  beam: '\x1b[1 q',
@@ -12,5 +12,7 @@ const SEQUENCES = {
12
12
  export type CursorShape = keyof typeof SEQUENCES
13
13
 
14
14
  export function writeCursorShape(shape: CursorShape): void {
15
- writeStdoutRaw(SEQUENCES[shape])
15
+ // The restore covers visibility and shape together: panels toggle hide /
16
+ // show freely, and the last writer's state is what an exit must undo.
17
+ enterMode('cursor', SEQUENCES[shape], '\x1b[?25h\x1b[0 q')
16
18
  }
@@ -0,0 +1,39 @@
1
+ import { writeSync } from 'node:fs'
2
+
3
+ /**
4
+ * Single choke point for terminal mode changes: every enter/exit of a
5
+ * terminal mode registers its restore sequence here, so one
6
+ * restoreAllModes() call on any exit path inverts everything that was
7
+ * applied — including the modes ink enables on our behalf (seeded with an
8
+ * empty enter sequence). Windows TTY writes through process.stdout are
9
+ * asynchronous, so mode sequences must be written synchronously through
10
+ * fd 1 or they are silently dropped at exit.
11
+ */
12
+ const applied = new Map<string, string>()
13
+
14
+ export function writeStdoutRaw(text: string): void {
15
+ try {
16
+ writeSync(1, text)
17
+ } catch {
18
+ // A full stdout buffer (EAGAIN) can only be retried asynchronously.
19
+ process.stdout.write(text)
20
+ }
21
+ }
22
+
23
+ export function enterMode(name: string, sequence: string, restore: string): void {
24
+ applied.set(name, restore)
25
+ if (sequence !== '') writeStdoutRaw(sequence)
26
+ }
27
+
28
+ export function exitMode(name: string): void {
29
+ const restore = applied.get(name)
30
+ if (restore === undefined) return
31
+ applied.delete(name)
32
+ writeStdoutRaw(restore)
33
+ }
34
+
35
+ /** Restore every still-applied mode, newest first. */
36
+ export function restoreAllModes(): void {
37
+ for (const restore of [...applied.values()].reverse()) writeStdoutRaw(restore)
38
+ applied.clear()
39
+ }
@@ -1,5 +1,3 @@
1
- import { writeSync } from 'node:fs'
2
-
3
1
  export type MouseEventType = 'down' | 'up' | 'drag' | 'move' | 'scroll'
4
2
 
5
3
  export interface MouseEventData {
@@ -11,6 +9,8 @@ export interface MouseEventData {
11
9
  scrollDirection?: 'up' | 'down'
12
10
  }
13
11
 
12
+ import { enterMode, exitMode } from './modes.ts'
13
+
14
14
  const MOUSE_ENABLE_SEQUENCES = '\x1b[?1003l\x1b[?1000h\x1b[?1002h\x1b[?1006h'
15
15
  const MOUSE_DISABLE_SEQUENCES = '\x1b[?1003l\x1b[?1000l\x1b[?1002l\x1b[?1006l'
16
16
 
@@ -20,22 +20,7 @@ const MOUSE_DISABLE_SEQUENCES = '\x1b[?1003l\x1b[?1000l\x1b[?1002l\x1b[?1006l'
20
20
  * asynchronous and a trailing restore sequence is silently dropped, leaving
21
21
  * the terminal in mouse-tracking mode. fd 1 writes are synchronous.
22
22
  */
23
- export function writeStdoutRaw(text: string): void {
24
- try {
25
- writeSync(1, text)
26
- } catch {
27
- // A full stdout buffer (EAGAIN) can only be retried asynchronously.
28
- process.stdout.write(text)
29
- }
30
- }
31
23
 
32
- export function writeMouseEnable(): void {
33
- writeStdoutRaw(MOUSE_ENABLE_SEQUENCES)
34
- }
35
-
36
- export function writeMouseDisable(): void {
37
- writeStdoutRaw(MOUSE_DISABLE_SEQUENCES)
38
- }
39
24
 
40
25
  interface ParsedSequence {
41
26
  event: MouseEventData
@@ -160,11 +145,11 @@ export function createMouseController(onEvent: (event: MouseEventData) => void):
160
145
  const onData = (chunk: Buffer) => parser.feed(chunk.toString('latin1'))
161
146
  return {
162
147
  enable() {
163
- writeMouseEnable()
148
+ enterMode('mouse', MOUSE_ENABLE_SEQUENCES, MOUSE_DISABLE_SEQUENCES)
164
149
  process.stdin.on('data', onData)
165
150
  },
166
151
  disable() {
167
- writeMouseDisable()
152
+ exitMode('mouse')
168
153
  process.stdin.off('data', onData)
169
154
  },
170
155
  }
package/src/ui/app.tsx CHANGED
@@ -9,7 +9,7 @@ import type { ChatBridge } from '../chat/bridge.ts'
9
9
  import type { AgentActivity } from '../chat/store.ts'
10
10
  import { rowIndexFor, selectionText } from './message/layout.ts'
11
11
  import { glyphs } from '../terminal/glyphs.ts'
12
- import { restoreTerminal } from '../terminal/restore.ts'
12
+ import { restoreAllModes } from '../terminal/modes.ts'
13
13
  import { chromeSelectionText } from './selection-registry.ts'
14
14
  import type { ScreenCapture } from '../terminal/screen.ts'
15
15
  import type { LineSelection } from '../model/selection.ts'
@@ -333,9 +333,10 @@ export function App({ bridge, screen, themeTick = 0 }: AppProps) {
333
333
  }
334
334
  if (!exiting.current) {
335
335
  exiting.current = true
336
- // Restore terminal modes synchronously before signaling: on Windows
337
- // the queued async writes of the dispose path can be lost at exit.
338
- restoreTerminal()
336
+ // Restore every terminal mode synchronously before signaling: on
337
+ // Windows the queued async writes of the dispose path can be lost
338
+ // at exit.
339
+ restoreAllModes()
339
340
  process.kill(process.pid, 'SIGINT')
340
341
  setTimeout(() => process.exit(0), FORCE_EXIT_DELAY_MS).unref()
341
342
  }
@@ -1,20 +0,0 @@
1
- import { writeStdoutRaw } from './mouse.ts'
2
-
3
- /**
4
- * Synchronously restore every terminal mode this TUI enables: cursor
5
- * visibility and shape, mouse tracking, bracketed paste, the kitty keyboard
6
- * protocol, and the alternate screen. Windows TTY writes through
7
- * process.stdout are asynchronous, so the restores ink queues at unmount
8
- * can be dropped when the process exits right after — every exit path must
9
- * call this synchronously before the process leaves.
10
- */
11
- export function restoreTerminal(): void {
12
- writeStdoutRaw([
13
- '\x1b[?25h',
14
- '\x1b[0 q',
15
- '\x1b[?1003l\x1b[?1000l\x1b[?1002l\x1b[?1006l',
16
- '\x1b[?2004l',
17
- '\x1b[<u',
18
- '\x1b[?1049l',
19
- ].join(''))
20
- }