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/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({ lines: this.lines.slice(), cursor: { ...this.cursor }, serial: this._undoSerial });
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({ lines: this.lines.slice(), cursor: { ...this.cursor }, serial: this._undoSerial });
1722
+ this.redoStack.push(this._historyState());
1580
1723
  const s = this.undoStack.pop();
1581
- this.lines = s.lines;
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({ lines: this.lines.slice(), cursor: { ...this.cursor }, serial: this._undoSerial });
1733
+ this.undoStack.push(this._historyState());
1593
1734
  const s = this.redoStack.pop();
1594
- this.lines = s.lines;
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._started = true;
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. Unix: /dev/tty Windows: \\.\CON
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 ttyPath = process.platform === "win32" ? "\\\\.\\CON" : "/dev/tty";
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")) buf.backspace();
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 buf.deleteForward();
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 hadAlert = Object.prototype.hasOwnProperty.call(globalThis, "alert");
4166
- const prevAlert = globalThis.alert;
4167
- const hadConfirm = Object.prototype.hasOwnProperty.call(globalThis, "confirm");
4168
- const prevConfirm = globalThis.confirm;
4169
- const hadPrompt = Object.prototype.hasOwnProperty.call(globalThis, "prompt");
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.
@@ -0,0 +1,5 @@
1
+ import process from "node:process";
2
+
3
+ export function controllingTerminalInputPath(platform = process.platform) {
4
+ return platform === "win32" ? "CONIN$" : "/dev/tty";
5
+ }