@xp266/dshtui 0.1.7 → 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 +1 -1
- package/lib/index.mjs +7 -4
- package/lib/{surface-DSDGGCSN.mjs → surface-CfzU6eCq.mjs} +172 -151
- package/package.json +1 -1
- package/src/index.tsx +8 -1
- package/src/log.ts +9 -2
- package/src/terminal/cursor-shape.ts +4 -2
- package/src/terminal/modes.ts +39 -0
- package/src/terminal/mouse.ts +4 -19
- package/src/ui/app.tsx +5 -6
package/lib/dialog.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import "./prod-react-Dzof4qJx.mjs";
|
|
2
|
-
import {
|
|
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
|
|
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";
|
|
@@ -9142,8 +9142,7 @@ function App({ bridge, screen, themeTick = 0 }) {
|
|
|
9142
9142
|
}
|
|
9143
9143
|
if (!exiting.current) {
|
|
9144
9144
|
exiting.current = true;
|
|
9145
|
-
|
|
9146
|
-
writeMouseDisable();
|
|
9145
|
+
restoreAllModes();
|
|
9147
9146
|
process.kill(process.pid, "SIGINT");
|
|
9148
9147
|
setTimeout(() => process.exit(0), FORCE_EXIT_DELAY_MS).unref();
|
|
9149
9148
|
}
|
|
@@ -11274,6 +11273,10 @@ function apply(ctx, config = Config(DEFAULT_CONFIG)) {
|
|
|
11274
11273
|
});
|
|
11275
11274
|
lastColumns = capture.stream.columns;
|
|
11276
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");
|
|
11277
11280
|
const onResize = () => {
|
|
11278
11281
|
lastColumns = capture.stream.columns;
|
|
11279
11282
|
lastRows = capture.stream.rows;
|
|
@@ -11331,9 +11334,9 @@ function apply(ctx, config = Config(DEFAULT_CONFIG)) {
|
|
|
11331
11334
|
disposeExtensionPoint();
|
|
11332
11335
|
stopSizePoll?.();
|
|
11333
11336
|
hotTheme?.stop();
|
|
11334
|
-
writeCursorShape("reset");
|
|
11335
11337
|
bridge?.dispose();
|
|
11336
11338
|
app?.unmount();
|
|
11339
|
+
restoreAllModes();
|
|
11337
11340
|
disposeCrashHandlers();
|
|
11338
11341
|
};
|
|
11339
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)
|
|
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)
|
|
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,154 +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
|
-
//#endregion
|
|
1438
|
-
//#region src/terminal/cursor-shape.ts
|
|
1439
|
-
const SEQUENCES = {
|
|
1440
|
-
beam: "\x1B[1 q",
|
|
1441
|
-
block: "\x1B[2 q",
|
|
1442
|
-
reset: "\x1B[0 q",
|
|
1443
|
-
hide: "\x1B[?25l",
|
|
1444
|
-
show: "\x1B[?25h"
|
|
1445
|
-
};
|
|
1446
|
-
function writeCursorShape(shape) {
|
|
1447
|
-
writeStdoutRaw(SEQUENCES[shape]);
|
|
1448
|
-
}
|
|
1449
1341
|
const RegionContext = createContext({
|
|
1450
1342
|
x: 0,
|
|
1451
1343
|
y: 0
|
|
@@ -1583,6 +1475,123 @@ function renderSegments(segments, start, end, baseColor) {
|
|
|
1583
1475
|
return out;
|
|
1584
1476
|
}
|
|
1585
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
|
|
1586
1595
|
//#region src/ui/key-arbiter.ts
|
|
1587
1596
|
/**
|
|
1588
1597
|
* Key-event suppression for ink's broadcast-based useInput.
|
|
@@ -1651,6 +1660,18 @@ function caretNonceBold(cursor) {
|
|
|
1651
1660
|
return Math.floor(cursor / 480) % 2 === 0;
|
|
1652
1661
|
}
|
|
1653
1662
|
//#endregion
|
|
1663
|
+
//#region src/terminal/cursor-shape.ts
|
|
1664
|
+
const SEQUENCES = {
|
|
1665
|
+
beam: "\x1B[1 q",
|
|
1666
|
+
block: "\x1B[2 q",
|
|
1667
|
+
reset: "\x1B[0 q",
|
|
1668
|
+
hide: "\x1B[?25l",
|
|
1669
|
+
show: "\x1B[?25h"
|
|
1670
|
+
};
|
|
1671
|
+
function writeCursorShape(shape) {
|
|
1672
|
+
enterMode("cursor", SEQUENCES[shape], "\x1B[?25h\x1B[0 q");
|
|
1673
|
+
}
|
|
1674
|
+
//#endregion
|
|
1654
1675
|
//#region src/ui/chrome/caps.tsx
|
|
1655
1676
|
function CapText({ background, top, width }) {
|
|
1656
1677
|
if (glyphs.halfBlockCaps) return /* @__PURE__ */ jsx(Text, {
|
|
@@ -3136,4 +3157,4 @@ function PanelSurface({ columns, rows, body, bodyStart, background, blockWidth }
|
|
|
3136
3157
|
}) });
|
|
3137
3158
|
}
|
|
3138
3159
|
//#endregion
|
|
3139
|
-
export {
|
|
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
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 { enterMode, restoreAllModes } from './terminal/modes.ts'
|
|
13
14
|
import { startHotTheme } from './hot-theme.ts'
|
|
14
15
|
import { applyTheme } from './apply-theme.ts'
|
|
15
16
|
import { registerPalette } from './theme.ts'
|
|
@@ -113,6 +114,12 @@ export function apply(ctx: Context, config: Config = Config(DEFAULT_CONFIG)) {
|
|
|
113
114
|
})
|
|
114
115
|
lastColumns = capture.stream.columns
|
|
115
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')
|
|
116
123
|
const onResize = () => {
|
|
117
124
|
lastColumns = capture.stream.columns
|
|
118
125
|
lastRows = capture.stream.rows
|
|
@@ -177,9 +184,9 @@ export function apply(ctx: Context, config: Config = Config(DEFAULT_CONFIG)) {
|
|
|
177
184
|
disposeExtensionPoint()
|
|
178
185
|
stopSizePoll?.()
|
|
179
186
|
hotTheme?.stop()
|
|
180
|
-
writeCursorShape('reset')
|
|
181
187
|
bridge?.dispose()
|
|
182
188
|
app?.unmount()
|
|
189
|
+
restoreAllModes()
|
|
183
190
|
disposeCrashHandlers()
|
|
184
191
|
}
|
|
185
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)
|
|
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)
|
|
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 {
|
|
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
|
-
|
|
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
|
+
}
|
package/src/terminal/mouse.ts
CHANGED
|
@@ -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
|
-
|
|
148
|
+
enterMode('mouse', MOUSE_ENABLE_SEQUENCES, MOUSE_DISABLE_SEQUENCES)
|
|
164
149
|
process.stdin.on('data', onData)
|
|
165
150
|
},
|
|
166
151
|
disable() {
|
|
167
|
-
|
|
152
|
+
exitMode('mouse')
|
|
168
153
|
process.stdin.off('data', onData)
|
|
169
154
|
},
|
|
170
155
|
}
|
package/src/ui/app.tsx
CHANGED
|
@@ -9,8 +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 {
|
|
13
|
-
import { writeCursorShape } from '../terminal/cursor-shape.ts'
|
|
12
|
+
import { restoreAllModes } from '../terminal/modes.ts'
|
|
14
13
|
import { chromeSelectionText } from './selection-registry.ts'
|
|
15
14
|
import type { ScreenCapture } from '../terminal/screen.ts'
|
|
16
15
|
import type { LineSelection } from '../model/selection.ts'
|
|
@@ -334,10 +333,10 @@ export function App({ bridge, screen, themeTick = 0 }: AppProps) {
|
|
|
334
333
|
}
|
|
335
334
|
if (!exiting.current) {
|
|
336
335
|
exiting.current = true
|
|
337
|
-
// Restore terminal
|
|
338
|
-
// the queued async writes of the dispose path can be lost
|
|
339
|
-
|
|
340
|
-
|
|
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()
|
|
341
340
|
process.kill(process.pid, 'SIGINT')
|
|
342
341
|
setTimeout(() => process.exit(0), FORCE_EXIT_DELAY_MS).unref()
|
|
343
342
|
}
|