@xp266/dshtui 0.1.6 → 0.1.8

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 { 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-B7w3R_kh.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";
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 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, J as clampFocusRow, K as chromeSelectionText, L as panelContains, 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-B7w3R_kh.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";
3
3
  import { createRequire } from "node:module";
4
4
  import { readFileSync, statSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
@@ -2945,6 +2945,26 @@ 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
2948
2968
  //#region src/ui/hooks/use-terminal-size.ts
2949
2969
  function useTerminalSize() {
2950
2970
  const { stdout } = useStdout();
@@ -9142,6 +9162,7 @@ function App({ bridge, screen, themeTick = 0 }) {
9142
9162
  }
9143
9163
  if (!exiting.current) {
9144
9164
  exiting.current = true;
9165
+ restoreTerminal();
9145
9166
  process.kill(process.pid, "SIGINT");
9146
9167
  setTimeout(() => process.exit(0), FORCE_EXIT_DELAY_MS).unref();
9147
9168
  }
@@ -11332,6 +11353,7 @@ function apply(ctx, config = Config(DEFAULT_CONFIG)) {
11332
11353
  writeCursorShape("reset");
11333
11354
  bridge?.dispose();
11334
11355
  app?.unmount();
11356
+ restoreTerminal();
11335
11357
  disposeCrashHandlers();
11336
11358
  };
11337
11359
  });
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync, unlinkSync } from "node:fs";
2
+ import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync, unlinkSync, writeSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { Box, Text, useCursor, useInput, usePaste, useStdout } from "ink";
@@ -1298,6 +1298,142 @@ function scrollbarColumn(columns) {
1298
1298
  function messageBackgroundWidth(width) {
1299
1299
  return width - 2 - 3 - 1;
1300
1300
  }
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
+ }
1301
1437
  const RegionContext = createContext({
1302
1438
  x: 0,
1303
1439
  y: 0
@@ -1435,129 +1571,6 @@ function renderSegments(segments, start, end, baseColor) {
1435
1571
  return out;
1436
1572
  }
1437
1573
  //#endregion
1438
- //#region src/terminal/mouse.ts
1439
- const MOUSE_ENABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000h\x1B[?1002h\x1B[?1006h";
1440
- const MOUSE_DISABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000l\x1B[?1002l\x1B[?1006l";
1441
- function writeMouseEnable() {
1442
- process.stdout.write(MOUSE_ENABLE_SEQUENCES);
1443
- }
1444
- function writeMouseDisable() {
1445
- process.stdout.write(MOUSE_DISABLE_SEQUENCES);
1446
- }
1447
- const MAX_BUFFER = 64;
1448
- const SGR_PREFIX = "\x1B[<";
1449
- function partialPrefixLength(buffer) {
1450
- for (let keep = Math.min(2, buffer.length); keep > 0; keep--) if (SGR_PREFIX.startsWith(buffer.slice(-keep))) return keep;
1451
- return 0;
1452
- }
1453
- function createMouseParser(onEvent) {
1454
- let buffer = "";
1455
- const pressedButtons = /* @__PURE__ */ new Set();
1456
- function parseSequenceAt(str, offset) {
1457
- if (!str.startsWith("\x1B[<", offset)) return null;
1458
- let index = offset + 3;
1459
- const values = [];
1460
- let current = 0;
1461
- let hasDigit = false;
1462
- let terminator = "";
1463
- for (; index < str.length; index++) {
1464
- const ch = str[index];
1465
- if (ch >= "0" && ch <= "9") {
1466
- current = current * 10 + (ch.charCodeAt(0) - 48);
1467
- hasDigit = true;
1468
- } else if (ch === ";" && hasDigit) {
1469
- values.push(current);
1470
- current = 0;
1471
- hasDigit = false;
1472
- } else if (ch === "M" || ch === "m") {
1473
- if (!hasDigit) return null;
1474
- values.push(current);
1475
- terminator = ch;
1476
- break;
1477
- } else return null;
1478
- }
1479
- if (terminator === "") return null;
1480
- if (values.length !== 3) return null;
1481
- return {
1482
- event: decodeSgrEvent(values[0], values[1], values[2], terminator, pressedButtons),
1483
- consumed: index - offset + 1
1484
- };
1485
- }
1486
- return { feed(chunk) {
1487
- buffer += chunk;
1488
- while (true) {
1489
- const start = buffer.indexOf("\x1B[<");
1490
- if (start === -1) {
1491
- const keep = partialPrefixLength(buffer);
1492
- buffer = keep === 0 ? "" : buffer.slice(-keep);
1493
- return;
1494
- }
1495
- if (start > 0) {
1496
- buffer = buffer.slice(start);
1497
- continue;
1498
- }
1499
- if (buffer.length > MAX_BUFFER) {
1500
- buffer = "";
1501
- return;
1502
- }
1503
- const parsed = parseSequenceAt(buffer, 0);
1504
- if (parsed === null) return;
1505
- const { event, consumed } = parsed;
1506
- buffer = buffer.slice(consumed);
1507
- onEvent(event);
1508
- }
1509
- } };
1510
- }
1511
- function decodeSgrEvent(rawButtonCode, wireX, wireY, pressRelease, pressed) {
1512
- const button = rawButtonCode & 3;
1513
- const isScroll = (rawButtonCode & 64) !== 0;
1514
- const isMotion = (rawButtonCode & 32) !== 0;
1515
- const modifiers = {
1516
- shift: (rawButtonCode & 4) !== 0,
1517
- alt: (rawButtonCode & 8) !== 0,
1518
- ctrl: (rawButtonCode & 16) !== 0
1519
- };
1520
- let type;
1521
- let scrollDirection;
1522
- if (isScroll) {
1523
- type = "scroll";
1524
- scrollDirection = button === 0 ? "up" : "down";
1525
- } else if (isMotion) type = pressed.size > 0 ? "drag" : "move";
1526
- else if (pressRelease === "M") {
1527
- type = "down";
1528
- if (button !== 3) pressed.add(button);
1529
- } else {
1530
- type = "up";
1531
- if (button === 3) pressed.clear();
1532
- else pressed.delete(button);
1533
- }
1534
- return {
1535
- type,
1536
- button: button === 3 ? 0 : button,
1537
- x: wireX - 1,
1538
- y: wireY - 1,
1539
- modifiers,
1540
- ...scrollDirection === void 0 ? {} : { scrollDirection }
1541
- };
1542
- }
1543
- function isMouseResidue(input) {
1544
- return input.includes("\x1B") || /^\[<\d+(;\d+)*[Mm]$/.test(input);
1545
- }
1546
- function createMouseController(onEvent) {
1547
- const parser = createMouseParser(onEvent);
1548
- const onData = (chunk) => parser.feed(chunk.toString("latin1"));
1549
- return {
1550
- enable() {
1551
- writeMouseEnable();
1552
- process.stdin.on("data", onData);
1553
- },
1554
- disable() {
1555
- writeMouseDisable();
1556
- process.stdin.off("data", onData);
1557
- }
1558
- };
1559
- }
1560
- //#endregion
1561
1574
  //#region src/ui/key-arbiter.ts
1562
1575
  /**
1563
1576
  * Key-event suppression for ink's broadcast-based useInput.
@@ -1635,7 +1648,7 @@ const SEQUENCES = {
1635
1648
  show: "\x1B[?25h"
1636
1649
  };
1637
1650
  function writeCursorShape(shape) {
1638
- process.stdout.write(SEQUENCES[shape]);
1651
+ writeStdoutRaw(SEQUENCES[shape]);
1639
1652
  }
1640
1653
  //#endregion
1641
1654
  //#region src/ui/chrome/caps.tsx
@@ -3123,4 +3136,4 @@ function PanelSurface({ columns, rows, body, bodyStart, background, blockWidth }
3123
3136
  }) });
3124
3137
  }
3125
3138
  //#endregion
3126
- 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, clampFocusRow as J, chromeSelectionText as K, panelContains as L, 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 };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xp266/dshtui",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "ink-based terminal UI plugin for DeepSeek Harness",
6
6
  "engines": {
package/src/index.tsx CHANGED
@@ -10,6 +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
14
  import { startHotTheme } from './hot-theme.ts'
14
15
  import { applyTheme } from './apply-theme.ts'
15
16
  import { registerPalette } from './theme.ts'
@@ -180,6 +181,7 @@ export function apply(ctx: Context, config: Config = Config(DEFAULT_CONFIG)) {
180
181
  writeCursorShape('reset')
181
182
  bridge?.dispose()
182
183
  app?.unmount()
184
+ restoreTerminal()
183
185
  disposeCrashHandlers()
184
186
  }
185
187
  })
@@ -1,3 +1,6 @@
1
+
2
+ import { writeStdoutRaw } from './mouse.ts'
3
+
1
4
  const SEQUENCES = {
2
5
  beam: '\x1b[1 q',
3
6
  block: '\x1b[2 q',
@@ -9,5 +12,5 @@ const SEQUENCES = {
9
12
  export type CursorShape = keyof typeof SEQUENCES
10
13
 
11
14
  export function writeCursorShape(shape: CursorShape): void {
12
- process.stdout.write(SEQUENCES[shape])
15
+ writeStdoutRaw(SEQUENCES[shape])
13
16
  }
@@ -1,3 +1,5 @@
1
+ import { writeSync } from 'node:fs'
2
+
1
3
  export type MouseEventType = 'down' | 'up' | 'drag' | 'move' | 'scroll'
2
4
 
3
5
  export interface MouseEventData {
@@ -12,12 +14,27 @@ export interface MouseEventData {
12
14
  const MOUSE_ENABLE_SEQUENCES = '\x1b[?1003l\x1b[?1000h\x1b[?1002h\x1b[?1006h'
13
15
  const MOUSE_DISABLE_SEQUENCES = '\x1b[?1003l\x1b[?1000l\x1b[?1002l\x1b[?1006l'
14
16
 
17
+ /**
18
+ * Terminal-mode sequences must reach the device even when the process exits
19
+ * right after: on Windows, TTY writes through process.stdout are
20
+ * asynchronous and a trailing restore sequence is silently dropped, leaving
21
+ * the terminal in mouse-tracking mode. fd 1 writes are synchronous.
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
+
15
32
  export function writeMouseEnable(): void {
16
- process.stdout.write(MOUSE_ENABLE_SEQUENCES)
33
+ writeStdoutRaw(MOUSE_ENABLE_SEQUENCES)
17
34
  }
18
35
 
19
36
  export function writeMouseDisable(): void {
20
- process.stdout.write(MOUSE_DISABLE_SEQUENCES)
37
+ writeStdoutRaw(MOUSE_DISABLE_SEQUENCES)
21
38
  }
22
39
 
23
40
  interface ParsedSequence {
@@ -0,0 +1,20 @@
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
+ }
package/src/ui/app.tsx CHANGED
@@ -9,6 +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
13
  import { chromeSelectionText } from './selection-registry.ts'
13
14
  import type { ScreenCapture } from '../terminal/screen.ts'
14
15
  import type { LineSelection } from '../model/selection.ts'
@@ -332,6 +333,9 @@ export function App({ bridge, screen, themeTick = 0 }: AppProps) {
332
333
  }
333
334
  if (!exiting.current) {
334
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()
335
339
  process.kill(process.pid, 'SIGINT')
336
340
  setTimeout(() => process.exit(0), FORCE_EXIT_DELAY_MS).unref()
337
341
  }