jsmdcui 0.7.0 → 0.8.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 +61 -0
- package/README.md +112 -18
- package/demos/maze.md +144 -0
- package/demos/todo-zh.md +98 -0
- package/demos/todo.md +98 -0
- package/edit +9 -0
- package/package.json +1 -1
- package/runmd.mjs +44 -2
- package/runtime/help/help.md +112 -18
- package/runtime/jsplugins/cdp/cdp.js +5 -2
- package/runtime/jsplugins/chapter/chapter.js +4 -2
- package/runtime/jsplugins/example/example.js +10 -7
- package/runtime/syntax/markdown.yaml +1 -0
- package/src/cui/fence-events.mjs +106 -0
- package/src/cui/id-collision.mjs +10 -28
- package/src/cui/kitty-images.mjs +11 -1
- package/src/cui/rpc.mjs +6 -3
- package/src/cui/server.mjs +13 -2
- package/src/index.js +379 -62
- package/src/platform/terminal.js +5 -0
- package/src/plugins/js-bridge.js +114 -8
- package/tests/fence-events.test.js +405 -0
- package/tests/id-collision.test.js +11 -0
- package/tests/js-plugin-prompts.test.js +46 -0
- package/tests/key-event.md +10 -0
- package/tests/kitty-images.test.js +22 -0
- package/tests/platform-terminal.test.js +8 -0
- package/tests/textarea.md +8 -0
- package/tests/wui.test.js +16 -1
package/src/index.js
CHANGED
|
@@ -93,12 +93,13 @@ function printProfileReport() {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
import child_process from "node:child_process"
|
|
96
|
-
import { accessSync, constants, existsSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
96
|
+
import { accessSync, closeSync, constants, existsSync, openSync, readSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
97
97
|
import { mkdir } from "node:fs/promises";
|
|
98
98
|
import { dirname, basename, join, resolve, sep } from "node:path";
|
|
99
99
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
100
100
|
import process from "node:process";
|
|
101
101
|
import { toggleTaskCheckboxBeforeColumn, updateAnsiTaskCheckbox } from "./cui/task-checkbox.mjs";
|
|
102
|
+
import { fenceEventMap, inlineFenceEventCode } from "./cui/fence-events.mjs";
|
|
102
103
|
import { checkMarkdownIdCollisions, formatMarkdownIdCheckAnsi } from "./cui/id-collision.mjs";
|
|
103
104
|
import { fitKittyImageToWidth, prepareKittyImages } from "./cui/kitty-images.mjs";
|
|
104
105
|
import { logKittyPlacement } from "./cui/kitty-debug.mjs";
|
|
@@ -108,7 +109,7 @@ import { cleanConfig } from "./config/clean.js";
|
|
|
108
109
|
import { RuntimeRegistry, RTColorscheme, RTHelp } from "./runtime/registry.js";
|
|
109
110
|
import { assetPath, hasInternalAssets, listInternalAssetDirs, listInternalAssetPaths, readInternalAssetText } from "./runtime/assets.js";
|
|
110
111
|
//import { PluginManager } from "./plugins/manager.js";
|
|
111
|
-
import { JsPluginManager, buildMicroGlobal, runAction, listActions } from "./plugins/js-bridge.js";
|
|
112
|
+
import { JsPluginManager, buildMicroGlobal, buildTuiBlockIndex, findTuiBlockInIndex, insertTuiTextareaNewline, mergeTuiTextareaBackward, mergeTuiTextareaForward, runAction, listActions } from "./plugins/js-bridge.js";
|
|
112
113
|
import { Colorscheme } from "./config/colorscheme.js";
|
|
113
114
|
import { detectSyntax, loadSyntaxDefinitions } from "./highlight/parser.js";
|
|
114
115
|
import { Highlighter } from "./highlight/highlighter.js";
|
|
@@ -117,6 +118,7 @@ import { Screen } from "./screen/screen.js";
|
|
|
117
118
|
import { VT100 } from "./screen/vt100.js";
|
|
118
119
|
import { ClipboardManager, probeOSC52, osc52Clipboard } from "./platform/clipboard.js";
|
|
119
120
|
import { platformId, run as runCommand, runSync, fetchHttpBytes, detectHttpBackend } from "./platform/commands.js";
|
|
121
|
+
import { controllingTerminalInputPath } from "./platform/terminal.js";
|
|
120
122
|
import { shellSplit } from "./shell/shell.js";
|
|
121
123
|
import { styleToAnsi } from "./display/ansi-style.js";
|
|
122
124
|
import { encodeBinaryToBuffer, decodeBinaryBytes } from "./buffer/fixed3-codec.js";
|
|
@@ -368,6 +370,7 @@ async function renderMdcui(markdown, width = process.stdout.columns || 80, mdpat
|
|
|
368
370
|
? { rendered: tui, images: [] }
|
|
369
371
|
: await prepareKittyImages(tui, resolvedImageBasePath, width, {
|
|
370
372
|
allowUrl: allowRemoteKittyImages,
|
|
373
|
+
kittyMode: kittyImageMode,
|
|
371
374
|
});
|
|
372
375
|
return {
|
|
373
376
|
rendered: prepared.rendered,
|
|
@@ -668,6 +671,21 @@ function canEditMdcuiSelection(buf, selection) {
|
|
|
668
671
|
return first.y === last.y && prefixLength > 0 && first.x >= prefixLength;
|
|
669
672
|
}
|
|
670
673
|
|
|
674
|
+
function mdcuiTextareaAtCursor(buf) {
|
|
675
|
+
if (!isMdcuiEncoding(buf?.encoding)) return null;
|
|
676
|
+
let cache = buf._mdcuiControlBlockIndex;
|
|
677
|
+
if (!cache || cache.lines !== buf.lines || cache.lineCount !== buf.lines.length) {
|
|
678
|
+
cache = {
|
|
679
|
+
lines: buf.lines,
|
|
680
|
+
lineCount: buf.lines.length,
|
|
681
|
+
blocks: buildTuiBlockIndex(buf.lines),
|
|
682
|
+
};
|
|
683
|
+
buf._mdcuiControlBlockIndex = cache;
|
|
684
|
+
}
|
|
685
|
+
const block = findTuiBlockInIndex(cache.blocks, buf.cursor.y);
|
|
686
|
+
return block?.header?.tag === "textarea" ? block : null;
|
|
687
|
+
}
|
|
688
|
+
|
|
671
689
|
function isEditLockedBuffer(buf) {
|
|
672
690
|
return isMdcuiEncoding(buf?.encoding ?? buf?.Settings?.encoding);
|
|
673
691
|
}
|
|
@@ -970,6 +988,100 @@ function takeDisplay(text, maxWidth) {
|
|
|
970
988
|
return out;
|
|
971
989
|
}
|
|
972
990
|
|
|
991
|
+
function keyboardEventJson(event) {
|
|
992
|
+
const target = event?.target;
|
|
993
|
+
return {
|
|
994
|
+
type: String(event?.type ?? ""),
|
|
995
|
+
key: String(event?.key ?? ""),
|
|
996
|
+
code: String(event?.code ?? ""),
|
|
997
|
+
raw: String(event?.raw ?? ""),
|
|
998
|
+
ctrlKey: Boolean(event?.ctrlKey),
|
|
999
|
+
shiftKey: Boolean(event?.shiftKey),
|
|
1000
|
+
altKey: Boolean(event?.altKey),
|
|
1001
|
+
metaKey: Boolean(event?.metaKey),
|
|
1002
|
+
repeat: Boolean(event?.repeat),
|
|
1003
|
+
defaultPrevented: Boolean(event?.defaultPrevented),
|
|
1004
|
+
target: {
|
|
1005
|
+
id: String(target?.id ?? ""),
|
|
1006
|
+
tagName: String(target?.tagName ?? ""),
|
|
1007
|
+
className: String(target?.className ?? ""),
|
|
1008
|
+
value: String(target?.value ?? ""),
|
|
1009
|
+
},
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function tuiKeyEventForFront(event, target, type) {
|
|
1014
|
+
const sequence = String(event?.key ?? "");
|
|
1015
|
+
const parts = sequence.split("-");
|
|
1016
|
+
const aliases = {
|
|
1017
|
+
enter: "Enter",
|
|
1018
|
+
escape: "Escape",
|
|
1019
|
+
tab: "Tab",
|
|
1020
|
+
backtab: "Tab",
|
|
1021
|
+
backspace: "Backspace",
|
|
1022
|
+
delete: "Delete",
|
|
1023
|
+
left: "ArrowLeft",
|
|
1024
|
+
right: "ArrowRight",
|
|
1025
|
+
up: "ArrowUp",
|
|
1026
|
+
down: "ArrowDown",
|
|
1027
|
+
home: "Home",
|
|
1028
|
+
end: "End",
|
|
1029
|
+
pageup: "PageUp",
|
|
1030
|
+
pagedown: "PageDown",
|
|
1031
|
+
space: " ",
|
|
1032
|
+
};
|
|
1033
|
+
let base = parts.at(-1) || sequence;
|
|
1034
|
+
if (sequence === "backtab") base = "tab";
|
|
1035
|
+
const raw = String(event?.raw ?? "");
|
|
1036
|
+
const key = aliases[base] ?? (sequence === raw && [...raw].length === 1 ? raw : base);
|
|
1037
|
+
let defaultPrevented = false;
|
|
1038
|
+
let propagationStopped = false;
|
|
1039
|
+
const result = {
|
|
1040
|
+
type,
|
|
1041
|
+
key,
|
|
1042
|
+
raw,
|
|
1043
|
+
ctrlKey: parts.includes("ctrl"),
|
|
1044
|
+
altKey: parts.includes("alt"),
|
|
1045
|
+
shiftKey: sequence === "backtab" || parts.includes("shift"),
|
|
1046
|
+
metaKey: parts.includes("meta"),
|
|
1047
|
+
repeat: false,
|
|
1048
|
+
target,
|
|
1049
|
+
currentTarget: target,
|
|
1050
|
+
get defaultPrevented() { return defaultPrevented; },
|
|
1051
|
+
get propagationStopped() { return propagationStopped; },
|
|
1052
|
+
preventDefault() { defaultPrevented = true; },
|
|
1053
|
+
stopPropagation() { propagationStopped = true; },
|
|
1054
|
+
};
|
|
1055
|
+
Object.defineProperty(result, "toJSON", {
|
|
1056
|
+
configurable: true,
|
|
1057
|
+
value() { return keyboardEventJson(this); },
|
|
1058
|
+
});
|
|
1059
|
+
return result;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function indexedMdcuiFenceBlockAtLine(buffer, lineIndex) {
|
|
1063
|
+
const declarations = buffer?._mdcuiFenceEvents;
|
|
1064
|
+
if (!isMdcuiEncoding(buffer?.encoding) || declarations?.size === 0) return null;
|
|
1065
|
+
let cache = buffer._mdcuiFenceBlockIndex;
|
|
1066
|
+
// Render/reopen replaces the lines array; structural selector edits explicitly
|
|
1067
|
+
// clear this cache. Ordinary character edits leave block boundaries unchanged.
|
|
1068
|
+
if (
|
|
1069
|
+
!cache
|
|
1070
|
+
|| cache.lines !== buffer.lines
|
|
1071
|
+
|| cache.lineCount !== buffer.lines.length
|
|
1072
|
+
|| cache.declarations !== declarations
|
|
1073
|
+
) {
|
|
1074
|
+
cache = {
|
|
1075
|
+
lines: buffer.lines,
|
|
1076
|
+
lineCount: buffer.lines.length,
|
|
1077
|
+
declarations,
|
|
1078
|
+
blocks: buildTuiBlockIndex(buffer.lines, declarations),
|
|
1079
|
+
};
|
|
1080
|
+
buffer._mdcuiFenceBlockIndex = cache;
|
|
1081
|
+
}
|
|
1082
|
+
return findTuiBlockInIndex(cache.blocks, lineIndex);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
973
1085
|
function parseArgs(argv) {
|
|
974
1086
|
const flags = {
|
|
975
1087
|
version: false,
|
|
@@ -1207,6 +1319,7 @@ class BufferModel {
|
|
|
1207
1319
|
this._mdcuiAnsiText = isMdcuiEncoding(this.encoding) ? String(ansiText ?? "") : null;
|
|
1208
1320
|
this._mdcuiSourceText = isMdcuiEncoding(this.encoding) ? String(sourceText ?? text) : null;
|
|
1209
1321
|
this._mdcuiTuiSourceText = isMdcuiEncoding(this.encoding) ? String(tuiSourceText ?? sourceText ?? text) : null;
|
|
1322
|
+
this._mdcuiFenceEvents = isMdcuiEncoding(this.encoding) ? fenceEventMap(sourceText ?? tuiSourceText ?? text) : new Map();
|
|
1210
1323
|
this._mdcuiImages = isMdcuiEncoding(this.encoding) ? (mdcuiImages ?? []) : [];
|
|
1211
1324
|
this._mdcuiRenderWidth = Math.trunc(Number(mdcuiRenderWidth) || 0);
|
|
1212
1325
|
this.cursor = { x: 0, y: 0 };
|
|
@@ -1565,9 +1678,39 @@ class BufferModel {
|
|
|
1565
1678
|
this.cursor.x = x;
|
|
1566
1679
|
}
|
|
1567
1680
|
|
|
1681
|
+
_historyState() {
|
|
1682
|
+
return {
|
|
1683
|
+
lines: this.lines.slice(),
|
|
1684
|
+
cursor: { ...this.cursor },
|
|
1685
|
+
serial: this._undoSerial,
|
|
1686
|
+
ansiStyleLines: Array.isArray(this._ansiStyleLines)
|
|
1687
|
+
? this._ansiStyleLines.map((line) => Array.isArray(line) ? line.slice() : line)
|
|
1688
|
+
: this._ansiStyleLines,
|
|
1689
|
+
mdcuiAnsiText: this._mdcuiAnsiText,
|
|
1690
|
+
mdcuiImages: Array.isArray(this._mdcuiImages)
|
|
1691
|
+
? this._mdcuiImages.map((image) => ({ ...image }))
|
|
1692
|
+
: this._mdcuiImages,
|
|
1693
|
+
headingTaskListAnchors: this._mdcuiHeadingTaskListAnchors instanceof Map
|
|
1694
|
+
? new Map([...this._mdcuiHeadingTaskListAnchors].map(([key, value]) => [key, { ...value }]))
|
|
1695
|
+
: this._mdcuiHeadingTaskListAnchors,
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
_restoreHistoryState(state) {
|
|
1700
|
+
this.lines = state.lines;
|
|
1701
|
+
this.cursor = { ...state.cursor };
|
|
1702
|
+
this._undoSerial = state.serial ?? 0;
|
|
1703
|
+
this._ansiStyleLines = state.ansiStyleLines;
|
|
1704
|
+
this._mdcuiAnsiText = state.mdcuiAnsiText;
|
|
1705
|
+
this._mdcuiImages = state.mdcuiImages;
|
|
1706
|
+
this._mdcuiHeadingTaskListAnchors = state.headingTaskListAnchors;
|
|
1707
|
+
this._mdcuiFenceBlockIndex = null;
|
|
1708
|
+
this._mdcuiControlBlockIndex = null;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1568
1711
|
pushUndo(force = false) {
|
|
1569
1712
|
if (!force && this.isEditLocked() && !canEditMdcuiAtCursor(this)) return;
|
|
1570
|
-
this.undoStack.push(
|
|
1713
|
+
this.undoStack.push(this._historyState());
|
|
1571
1714
|
this._undoSerial = (this._undoSerial ?? 0) + 1;
|
|
1572
1715
|
if (this.undoStack.length > 500) this.undoStack.shift();
|
|
1573
1716
|
this.redoStack = [];
|
|
@@ -1576,12 +1719,10 @@ class BufferModel {
|
|
|
1576
1719
|
undo() {
|
|
1577
1720
|
if (this.isEditLocked() && !isMdcuiEncoding(this.encoding)) return false;
|
|
1578
1721
|
if (!this.undoStack.length) return false;
|
|
1579
|
-
this.redoStack.push(
|
|
1722
|
+
this.redoStack.push(this._historyState());
|
|
1580
1723
|
const s = this.undoStack.pop();
|
|
1581
|
-
this.
|
|
1724
|
+
this._restoreHistoryState(s);
|
|
1582
1725
|
this.invalidateHighlightFrom(0, { force: true });
|
|
1583
|
-
this.cursor = { ...s.cursor };
|
|
1584
|
-
this._undoSerial = s.serial ?? 0;
|
|
1585
1726
|
this.modified = this._undoSerial !== this._savedSerial;
|
|
1586
1727
|
return true;
|
|
1587
1728
|
}
|
|
@@ -1589,12 +1730,10 @@ class BufferModel {
|
|
|
1589
1730
|
redo() {
|
|
1590
1731
|
if (this.isEditLocked() && !isMdcuiEncoding(this.encoding)) return false;
|
|
1591
1732
|
if (!this.redoStack.length) return false;
|
|
1592
|
-
this.undoStack.push(
|
|
1733
|
+
this.undoStack.push(this._historyState());
|
|
1593
1734
|
const s = this.redoStack.pop();
|
|
1594
|
-
this.
|
|
1735
|
+
this._restoreHistoryState(s);
|
|
1595
1736
|
this.invalidateHighlightFrom(0, { force: true });
|
|
1596
|
-
this.cursor = { ...s.cursor };
|
|
1597
|
-
this._undoSerial = s.serial ?? 0;
|
|
1598
1737
|
this.modified = this._undoSerial !== this._savedSerial;
|
|
1599
1738
|
return true;
|
|
1600
1739
|
}
|
|
@@ -1725,6 +1864,7 @@ class BufferModel {
|
|
|
1725
1864
|
this._mdcuiAnsiText = isMdcuiEncoding(this.encoding) ? String(decoded.ansiText ?? "") : null;
|
|
1726
1865
|
this._mdcuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.sourceText ?? text) : null;
|
|
1727
1866
|
this._mdcuiTuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.tuiSourceText ?? decoded.sourceText ?? text) : null;
|
|
1867
|
+
this._mdcuiFenceEvents = isMdcuiEncoding(this.encoding) ? fenceEventMap(decoded.sourceText ?? decoded.tuiSourceText ?? text) : new Map();
|
|
1728
1868
|
this._mdcuiRenderWidth = decoded.mdcuiRenderWidth ?? 0;
|
|
1729
1869
|
this._mdcuiImages = decoded.mdcuiImages ?? [];
|
|
1730
1870
|
const readonly = isMdcuiEncoding(this.encoding);
|
|
@@ -1758,6 +1898,7 @@ class BufferModel {
|
|
|
1758
1898
|
this._mdcuiAnsiText = isMdcuiEncoding(this.encoding) ? String(decoded.ansiText ?? "") : null;
|
|
1759
1899
|
this._mdcuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.sourceText ?? text) : null;
|
|
1760
1900
|
this._mdcuiTuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.tuiSourceText ?? decoded.sourceText ?? text) : null;
|
|
1901
|
+
this._mdcuiFenceEvents = isMdcuiEncoding(this.encoding) ? fenceEventMap(decoded.sourceText ?? decoded.tuiSourceText ?? text) : new Map();
|
|
1761
1902
|
this._mdcuiRenderWidth = decoded.mdcuiRenderWidth ?? 0;
|
|
1762
1903
|
this._mdcuiImages = decoded.mdcuiImages ?? [];
|
|
1763
1904
|
this.fileformat = detectFileFormat(text, this.Settings.fileformat ?? DEFAULT_SETTINGS.fileformat);
|
|
@@ -2503,16 +2644,14 @@ class App {
|
|
|
2503
2644
|
}
|
|
2504
2645
|
|
|
2505
2646
|
async start() {
|
|
2506
|
-
this.
|
|
2647
|
+
this.installProtectedPrompts();
|
|
2507
2648
|
// When stdin was a pipe (content already consumed in loadBuffers), open the
|
|
2508
2649
|
// controlling terminal directly so the event loop has a live handle and
|
|
2509
|
-
// keyboard input works.
|
|
2650
|
+
// keyboard input works. Unix: /dev/tty; Windows: CONIN$.
|
|
2510
2651
|
if (!process.stdin.isTTY) {
|
|
2511
2652
|
try {
|
|
2512
|
-
const { openSync } = await import("node:fs");
|
|
2513
2653
|
const { ReadStream } = await import("node:tty");
|
|
2514
|
-
const
|
|
2515
|
-
const fd = openSync(ttyPath, "r+");
|
|
2654
|
+
const fd = openSync(controllingTerminalInputPath(), "r");
|
|
2516
2655
|
this._ttyStream = new ReadStream(fd);
|
|
2517
2656
|
} catch {
|
|
2518
2657
|
this._ttyStream = process.stdin;
|
|
@@ -2542,6 +2681,7 @@ class App {
|
|
|
2542
2681
|
});
|
|
2543
2682
|
process.on("SIGINT", () => {}); // Ctrl+C is handled as copy in handleEvent
|
|
2544
2683
|
this.screen.init();
|
|
2684
|
+
this._started = true;
|
|
2545
2685
|
// Update backup prompt to screen-aware version now that TUI is running.
|
|
2546
2686
|
if (this.context._termPrompt) {
|
|
2547
2687
|
this.context._termPrompt = async (msg) => {
|
|
@@ -2626,6 +2766,8 @@ class App {
|
|
|
2626
2766
|
if (p.type === "term") p.terminal?.close();
|
|
2627
2767
|
(this._ttyStream ?? process.stdin).setRawMode?.(false);
|
|
2628
2768
|
this.screen.fini();
|
|
2769
|
+
this._started = false;
|
|
2770
|
+
this.restoreProtectedPrompts();
|
|
2629
2771
|
|
|
2630
2772
|
if (this.context?.config?.getGlobalOption("savehistory") !== false) {
|
|
2631
2773
|
try { await saveHistory(this.context.config.configDir); } catch {}
|
|
@@ -2644,6 +2786,157 @@ class App {
|
|
|
2644
2786
|
process.exit(code);
|
|
2645
2787
|
}
|
|
2646
2788
|
|
|
2789
|
+
installProtectedPrompts() {
|
|
2790
|
+
if (this._protectedPrompts) return;
|
|
2791
|
+
const saved = {
|
|
2792
|
+
alert: {
|
|
2793
|
+
had: Object.prototype.hasOwnProperty.call(globalThis, "alert"),
|
|
2794
|
+
value: globalThis.alert,
|
|
2795
|
+
},
|
|
2796
|
+
confirm: {
|
|
2797
|
+
had: Object.prototype.hasOwnProperty.call(globalThis, "confirm"),
|
|
2798
|
+
value: globalThis.confirm,
|
|
2799
|
+
},
|
|
2800
|
+
prompt: {
|
|
2801
|
+
had: Object.prototype.hasOwnProperty.call(globalThis, "prompt"),
|
|
2802
|
+
value: globalThis.prompt,
|
|
2803
|
+
},
|
|
2804
|
+
};
|
|
2805
|
+
this._protectedPrompts = saved;
|
|
2806
|
+
this._protectedPromptGlobals = {
|
|
2807
|
+
alert: this.protectedAlert.bind(this),
|
|
2808
|
+
confirm: this.protectedConfirm.bind(this),
|
|
2809
|
+
prompt: this.protectedPrompt.bind(this),
|
|
2810
|
+
};
|
|
2811
|
+
Object.assign(globalThis, this._protectedPromptGlobals);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
runProtectedPrompt(name, fallback, args) {
|
|
2815
|
+
const nativeFn = this._protectedPrompts?.[name]?.value;
|
|
2816
|
+
const tty = this._ttyStream ?? process.stdin;
|
|
2817
|
+
const wasRaw = typeof tty?.isRaw === "boolean" ? tty.isRaw : true;
|
|
2818
|
+
if (
|
|
2819
|
+
!this._started
|
|
2820
|
+
|| this.shellRunning
|
|
2821
|
+
|| this._alertRunning
|
|
2822
|
+
|| !wasRaw
|
|
2823
|
+
|| typeof nativeFn !== "function"
|
|
2824
|
+
) {
|
|
2825
|
+
this.message = String(args[0] ?? "");
|
|
2826
|
+
return fallback;
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
const inputHandler = this._inputHandler;
|
|
2830
|
+
const listenerAttached = Boolean(
|
|
2831
|
+
inputHandler && tty.listeners?.("data").includes(inputHandler),
|
|
2832
|
+
);
|
|
2833
|
+
const wasPaused = tty.isPaused?.() ?? false;
|
|
2834
|
+
let value;
|
|
2835
|
+
let failed = false;
|
|
2836
|
+
let failure;
|
|
2837
|
+
const cleanupFailures = [];
|
|
2838
|
+
const cleanup = (fn) => {
|
|
2839
|
+
try { fn(); } catch (error) { cleanupFailures.push(error); }
|
|
2840
|
+
};
|
|
2841
|
+
|
|
2842
|
+
try {
|
|
2843
|
+
this._alertRunning = true;
|
|
2844
|
+
// Do not let the editor's flowing-mode data listener race the blocking
|
|
2845
|
+
// prompt for bytes from the same terminal.
|
|
2846
|
+
if (listenerAttached) tty.removeListener("data", inputHandler);
|
|
2847
|
+
tty.pause?.();
|
|
2848
|
+
tty.setRawMode?.(false);
|
|
2849
|
+
this.screen.fini();
|
|
2850
|
+
this.screen.previous = null;
|
|
2851
|
+
// Bun's native prompts always read fd 0. If Markdown itself came from a
|
|
2852
|
+
// pipe, read synchronously from the controlling terminal instead.
|
|
2853
|
+
value = tty !== process.stdin
|
|
2854
|
+
? this.runProtectedTtyPrompt(name, fallback, args)
|
|
2855
|
+
: Reflect.apply(nativeFn, globalThis, args);
|
|
2856
|
+
} catch (error) {
|
|
2857
|
+
failed = true;
|
|
2858
|
+
failure = error;
|
|
2859
|
+
} finally {
|
|
2860
|
+
cleanup(() => tty.setRawMode?.(wasRaw));
|
|
2861
|
+
cleanup(() => { this.screen.previous = null; });
|
|
2862
|
+
cleanup(() => this.screen.init());
|
|
2863
|
+
cleanup(() => { this._alertRunning = false; });
|
|
2864
|
+
cleanup(() => {
|
|
2865
|
+
if (listenerAttached) tty.on("data", inputHandler);
|
|
2866
|
+
});
|
|
2867
|
+
cleanup(() => {
|
|
2868
|
+
if (!wasPaused) tty.resume?.();
|
|
2869
|
+
});
|
|
2870
|
+
cleanup(() => this.render());
|
|
2871
|
+
}
|
|
2872
|
+
if (failed) throw failure;
|
|
2873
|
+
if (cleanupFailures.length > 0) throw cleanupFailures[0];
|
|
2874
|
+
return value;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
runProtectedTtyPrompt(name, fallback, args) {
|
|
2878
|
+
const message = String(args[0] ?? "");
|
|
2879
|
+
const defaultValue = String(args[1] ?? "");
|
|
2880
|
+
const fd = openSync(controllingTerminalInputPath(), "r");
|
|
2881
|
+
try {
|
|
2882
|
+
if (name === "alert") {
|
|
2883
|
+
process.stdout.write(`${message}\n\nPress ENTER to continue...`);
|
|
2884
|
+
this.readProtectedPromptLine(fd);
|
|
2885
|
+
return undefined;
|
|
2886
|
+
}
|
|
2887
|
+
const suffix = name === "confirm"
|
|
2888
|
+
? " [y/N] "
|
|
2889
|
+
: defaultValue ? ` (${defaultValue}) ` : " ";
|
|
2890
|
+
process.stdout.write(message + suffix);
|
|
2891
|
+
const answer = this.readProtectedPromptLine(fd);
|
|
2892
|
+
if (answer == null) return fallback;
|
|
2893
|
+
if (name === "confirm") return /^(?:y|yes)$/i.test(answer.trim());
|
|
2894
|
+
return answer === "" ? defaultValue : answer;
|
|
2895
|
+
} finally {
|
|
2896
|
+
closeSync(fd);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
readProtectedPromptLine(fd) {
|
|
2901
|
+
const chunks = [];
|
|
2902
|
+
const chunk = Buffer.allocUnsafe(1024);
|
|
2903
|
+
while (true) {
|
|
2904
|
+
const length = readSync(fd, chunk, 0, chunk.length, null);
|
|
2905
|
+
if (length === 0) return chunks.length > 0 ? Buffer.concat(chunks).toString() : null;
|
|
2906
|
+
const bytes = chunk.subarray(0, length);
|
|
2907
|
+
const newline = bytes.findIndex((byte) => byte === 0x0a || byte === 0x0d);
|
|
2908
|
+
chunks.push(Buffer.from(newline < 0 ? bytes : bytes.subarray(0, newline)));
|
|
2909
|
+
if (newline >= 0) return Buffer.concat(chunks).toString();
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
protectedAlert(msg = "") {
|
|
2914
|
+
return this.runProtectedPrompt("alert", undefined, [String(msg)]);
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
protectedConfirm(msg = "") {
|
|
2918
|
+
return this.runProtectedPrompt("confirm", false, [String(msg)]);
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
protectedPrompt(msg = "", defaultValue = "") {
|
|
2922
|
+
return this.runProtectedPrompt(
|
|
2923
|
+
"prompt",
|
|
2924
|
+
defaultValue,
|
|
2925
|
+
[String(msg), String(defaultValue)],
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
restoreProtectedPrompts() {
|
|
2930
|
+
const saved = this._protectedPrompts;
|
|
2931
|
+
if (!saved) return;
|
|
2932
|
+
for (const name of ["alert", "confirm", "prompt"]) {
|
|
2933
|
+
if (saved[name].had) globalThis[name] = saved[name].value;
|
|
2934
|
+
else delete globalThis[name];
|
|
2935
|
+
}
|
|
2936
|
+
this._protectedPrompts = null;
|
|
2937
|
+
this._protectedPromptGlobals = null;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2647
2940
|
layoutEditorArea() {
|
|
2648
2941
|
const promptHeight = this.prompt ? 1 : 0;
|
|
2649
2942
|
const tabBarHeight = this.tabs.length > 1 ? 1 : 0;
|
|
@@ -3754,6 +4047,7 @@ class App {
|
|
|
3754
4047
|
|
|
3755
4048
|
const text = event.raw;
|
|
3756
4049
|
const seq = event.key;
|
|
4050
|
+
const keydownBlock = indexedMdcuiFenceBlockAtLine(buf, buf?.cursor.y);
|
|
3757
4051
|
if (buf) buf.allowCursorOffscreen = false;
|
|
3758
4052
|
|
|
3759
4053
|
if (this._rawMode) {
|
|
@@ -3764,6 +4058,12 @@ class App {
|
|
|
3764
4058
|
return;
|
|
3765
4059
|
}
|
|
3766
4060
|
|
|
4061
|
+
const keydownEvent = await this.dispatchMdcuiFenceEvent(buf, keydownBlock, event, "keydown");
|
|
4062
|
+
if (keydownEvent?.defaultPrevented && !["ctrl-q", "alt-q", "escape"].includes(seq)) {
|
|
4063
|
+
this.render();
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
|
|
3767
4067
|
// Reset undo insert chain on any non-printable-char key
|
|
3768
4068
|
if (!(seq === text && text.length === 1 && text >= " ")) this._undoInsertChain = false;
|
|
3769
4069
|
|
|
@@ -3952,15 +4252,28 @@ class App {
|
|
|
3952
4252
|
case "backspace":
|
|
3953
4253
|
buf.pushUndo();
|
|
3954
4254
|
if (this.pane?.selection) deleteSelection(buf, this.pane);
|
|
3955
|
-
else if (await this.runPluginBool("preBackspace"))
|
|
4255
|
+
else if (await this.runPluginBool("preBackspace")) {
|
|
4256
|
+
const textarea = mdcuiTextareaAtCursor(buf);
|
|
4257
|
+
if (!mergeTuiTextareaBackward(buf, textarea)) buf.backspace();
|
|
4258
|
+
}
|
|
3956
4259
|
break;
|
|
3957
4260
|
case "delete":
|
|
3958
4261
|
buf.pushUndo();
|
|
3959
4262
|
if (this.pane?.selection) deleteSelection(buf, this.pane);
|
|
3960
|
-
else
|
|
4263
|
+
else {
|
|
4264
|
+
const textarea = mdcuiTextareaAtCursor(buf);
|
|
4265
|
+
if (!mergeTuiTextareaForward(buf, textarea)) buf.deleteForward();
|
|
4266
|
+
}
|
|
3961
4267
|
break;
|
|
3962
4268
|
case "enter":
|
|
3963
4269
|
if (isMdcuiEncoding(buf?.encoding)) {
|
|
4270
|
+
const textarea = mdcuiTextareaAtCursor(buf);
|
|
4271
|
+
if (textarea && canEditMdcuiAtCursor(buf)) {
|
|
4272
|
+
buf.pushUndo();
|
|
4273
|
+
if (this.pane?.selection) deleteSelection(buf, this.pane);
|
|
4274
|
+
insertTuiTextareaNewline(buf, textarea);
|
|
4275
|
+
break;
|
|
4276
|
+
}
|
|
3964
4277
|
await this.handleMdcuiCellCallback(buf, buf.cursor.y, buf.cursor.x, "enter");
|
|
3965
4278
|
break;
|
|
3966
4279
|
}
|
|
@@ -4136,6 +4449,48 @@ class App {
|
|
|
4136
4449
|
this.render();
|
|
4137
4450
|
}
|
|
4138
4451
|
|
|
4452
|
+
async dispatchMdcuiFenceEvent(buf, block, inputEvent, eventName) {
|
|
4453
|
+
const id = block?.header?.id;
|
|
4454
|
+
const declaration = id ? buf?._mdcuiFenceEvents?.get(id) : null;
|
|
4455
|
+
const handler = declaration?.events?.get(eventName);
|
|
4456
|
+
const code = inlineFenceEventCode(handler);
|
|
4457
|
+
if (
|
|
4458
|
+
!code
|
|
4459
|
+
|| declaration.tag !== block?.header?.tag
|
|
4460
|
+
|| !buf?.path
|
|
4461
|
+
|| isHttpUrl(buf.path)
|
|
4462
|
+
) return false;
|
|
4463
|
+
|
|
4464
|
+
const frontPath = `${buf.path}.front.js`;
|
|
4465
|
+
if (!existsSync(frontPath)) return false;
|
|
4466
|
+
try {
|
|
4467
|
+
const selection = globalThis.$?.(`${declaration.tag}#${id}`);
|
|
4468
|
+
const target = {
|
|
4469
|
+
id,
|
|
4470
|
+
tagName: declaration.tag.toUpperCase(),
|
|
4471
|
+
className: declaration.classes.join(" "),
|
|
4472
|
+
};
|
|
4473
|
+
Object.defineProperty(target, "value", {
|
|
4474
|
+
enumerable: true,
|
|
4475
|
+
get: () => selection?.val?.() ?? "",
|
|
4476
|
+
set: (value) => selection?.val?.(value),
|
|
4477
|
+
});
|
|
4478
|
+
const event = tuiKeyEventForFront(inputEvent, target, eventName);
|
|
4479
|
+
const [{ evalFront }, frontMod] = await Promise.all([
|
|
4480
|
+
import("./cui/rpc.mjs"),
|
|
4481
|
+
import(localModuleUrl(frontPath)),
|
|
4482
|
+
]);
|
|
4483
|
+
const result = await evalFront(frontMod, code, { event });
|
|
4484
|
+
if (result && typeof result === "object" && result.ok === false) {
|
|
4485
|
+
this.message = `mdcui ${eventName}: ${String(result.error ?? "Unknown error")}`;
|
|
4486
|
+
}
|
|
4487
|
+
return event;
|
|
4488
|
+
} catch (error) {
|
|
4489
|
+
this.message = `mdcui ${eventName}: ${String(error?.message || error)}`;
|
|
4490
|
+
return null;
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4139
4494
|
async handleMdcuiCellCallback(buf, y = buf?.cursor?.y ?? 0, x = buf?.cursor?.x ?? 0, trigger = "unknown") {
|
|
4140
4495
|
const payload = mdcuiCellPayload(buf, y, x, trigger);
|
|
4141
4496
|
if (!payload) return false;
|
|
@@ -4162,50 +4517,11 @@ class App {
|
|
|
4162
4517
|
import("./cui/rpc.mjs"),
|
|
4163
4518
|
import(localModuleUrl(`${buf.path}.front.js`)),
|
|
4164
4519
|
]);
|
|
4165
|
-
const
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
const prevPrompt = globalThis.prompt;
|
|
4171
|
-
const nativeAlert = typeof prevAlert === "function" ? prevAlert : null;
|
|
4172
|
-
const withNativePrompt = (nativeFn, fallback, args) => {
|
|
4173
|
-
if (!this._started || typeof nativeFn !== "function") {
|
|
4174
|
-
this.message = String(args[0] ?? "");
|
|
4175
|
-
return fallback;
|
|
4176
|
-
}
|
|
4177
|
-
const tty = this._ttyStream ?? process.stdin;
|
|
4178
|
-
this._alertRunning = true;
|
|
4179
|
-
tty.setRawMode?.(false);
|
|
4180
|
-
this.screen.fini();
|
|
4181
|
-
this.screen.previous = null;
|
|
4182
|
-
try {
|
|
4183
|
-
return nativeFn(...args);
|
|
4184
|
-
} finally {
|
|
4185
|
-
tty.setRawMode?.(true);
|
|
4186
|
-
this.screen.previous = null;
|
|
4187
|
-
this.screen.init();
|
|
4188
|
-
this._alertRunning = false;
|
|
4189
|
-
this.render();
|
|
4190
|
-
}
|
|
4191
|
-
};
|
|
4192
|
-
globalThis.alert = (msg = "") => withNativePrompt(nativeAlert, undefined, [String(msg)]);
|
|
4193
|
-
globalThis.confirm = (msg = "") => withNativePrompt(prevConfirm, false, [String(msg)]);
|
|
4194
|
-
globalThis.prompt = (msg = "", defaultValue = "") => withNativePrompt(prevPrompt, defaultValue, [String(msg), String(defaultValue)]);
|
|
4195
|
-
try {
|
|
4196
|
-
const result = await evalFront(frontMod, payload.link);
|
|
4197
|
-
if (result && typeof result === "object" && result.ok === false) {
|
|
4198
|
-
globalThis.alert(String(result.error ?? "Unknown error"));
|
|
4199
|
-
} else if (result != null) {
|
|
4200
|
-
this.message = String(result);
|
|
4201
|
-
}
|
|
4202
|
-
} finally {
|
|
4203
|
-
if (hadAlert) globalThis.alert = prevAlert;
|
|
4204
|
-
else delete globalThis.alert;
|
|
4205
|
-
if (hadConfirm) globalThis.confirm = prevConfirm;
|
|
4206
|
-
else delete globalThis.confirm;
|
|
4207
|
-
if (hadPrompt) globalThis.prompt = prevPrompt;
|
|
4208
|
-
else delete globalThis.prompt;
|
|
4520
|
+
const result = await evalFront(frontMod, payload.link);
|
|
4521
|
+
if (result && typeof result === "object" && result.ok === false) {
|
|
4522
|
+
this.protectedAlert(String(result.error ?? "Unknown error"));
|
|
4523
|
+
} else if (result != null) {
|
|
4524
|
+
this.message = String(result);
|
|
4209
4525
|
}
|
|
4210
4526
|
return true;
|
|
4211
4527
|
} catch (error) {
|
|
@@ -8033,6 +8349,7 @@ async function main() {
|
|
|
8033
8349
|
addCheckpoint("App Instantiation");
|
|
8034
8350
|
const app = new App(buffers, context);
|
|
8035
8351
|
jsPlugins.setApp(app);
|
|
8352
|
+
app.installProtectedPrompts();
|
|
8036
8353
|
// if (plugins && !pluginErr && app.buffer) plugins.curPaneAdapter = makePaneAdapter(app.buffer, app);
|
|
8037
8354
|
// Dispatch all JS plugin lifecycle hooks after setApp so TermMessage,
|
|
8038
8355
|
// CurPane, cmd/action proxies, and buffer APIs all work correctly.
|