jsmdcui 0.7.0 → 0.9.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +189 -19
  3. package/cdp-maze.js +141 -0
  4. package/demos/maze.md +149 -0
  5. package/demos/todo-zh.md +98 -0
  6. package/demos/todo.md +98 -0
  7. package/edit +9 -0
  8. package/llm-maze.txt +94 -0
  9. package/package.json +1 -1
  10. package/runmd.mjs +44 -2
  11. package/runtime/help/cdp.md +5 -0
  12. package/runtime/help/help.md +189 -19
  13. package/runtime/jsplugins/cdp/cdp-server.js +26 -2
  14. package/runtime/jsplugins/cdp/cdp.js +60 -51
  15. package/runtime/jsplugins/chapter/chapter.js +6 -3
  16. package/runtime/jsplugins/example/example.js +12 -7
  17. package/runtime/syntax/markdown.yaml +1 -0
  18. package/single-exe/README.md +223 -45
  19. package/single-exe/compiled.js +6 -3
  20. package/single-exe/entry.mjs +4 -3
  21. package/single-exe/packAssets.sh +1 -1
  22. package/src/cui/fence-events.mjs +106 -0
  23. package/src/cui/id-collision.mjs +10 -28
  24. package/src/cui/kitty-debug.mjs +1 -1
  25. package/src/cui/kitty-images.mjs +11 -1
  26. package/src/cui/rpc.mjs +6 -3
  27. package/src/cui/server.mjs +13 -2
  28. package/src/index.js +425 -72
  29. package/src/lua/engine.js +1 -4
  30. package/src/platform/terminal.js +5 -0
  31. package/src/plugins/js-bridge.js +157 -8
  32. package/tests/compiled-runtime.test.js +8 -0
  33. package/tests/demo.test.js +40 -0
  34. package/tests/fence-events.test.js +405 -0
  35. package/tests/id-collision.test.js +11 -0
  36. package/tests/js-plugin-prompts.test.js +46 -0
  37. package/tests/key-event.md +10 -0
  38. package/tests/kitty-demo.md +1 -1
  39. package/tests/kitty-images.test.js +22 -0
  40. package/tests/platform-terminal.test.js +8 -0
  41. package/tests/textarea.md +8 -0
  42. package/tests/wui.test.js +16 -1
  43. package/demo.resized.jpg +0 -0
  44. package/src/runtime/compiled.js +0 -25
package/src/index.js CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- let mainPromise = globalThis.assetsLoaderPromise ||
4
- Promise.resolve();
5
-
6
3
  const jsStart = globalThis.Bun ? Bun.nanoseconds() : Date.now() * 1e6;
7
4
  const checkpoints = [
8
5
  { name: "Bun Engine Boot", time: 0 },
@@ -93,12 +90,13 @@ function printProfileReport() {
93
90
  }
94
91
 
95
92
  import child_process from "node:child_process"
96
- import { accessSync, constants, existsSync, readdirSync, statSync, unlinkSync } from "node:fs";
93
+ import { accessSync, closeSync, constants, existsSync, openSync, readSync, readdirSync, statSync, unlinkSync } from "node:fs";
97
94
  import { mkdir } from "node:fs/promises";
98
95
  import { dirname, basename, join, resolve, sep } from "node:path";
99
96
  import { fileURLToPath, pathToFileURL } from "node:url";
100
97
  import process from "node:process";
101
98
  import { toggleTaskCheckboxBeforeColumn, updateAnsiTaskCheckbox } from "./cui/task-checkbox.mjs";
99
+ import { fenceEventMap, inlineFenceEventCode } from "./cui/fence-events.mjs";
102
100
  import { checkMarkdownIdCollisions, formatMarkdownIdCheckAnsi } from "./cui/id-collision.mjs";
103
101
  import { fitKittyImageToWidth, prepareKittyImages } from "./cui/kitty-images.mjs";
104
102
  import { logKittyPlacement } from "./cui/kitty-debug.mjs";
@@ -108,7 +106,7 @@ import { cleanConfig } from "./config/clean.js";
108
106
  import { RuntimeRegistry, RTColorscheme, RTHelp } from "./runtime/registry.js";
109
107
  import { assetPath, hasInternalAssets, listInternalAssetDirs, listInternalAssetPaths, readInternalAssetText } from "./runtime/assets.js";
110
108
  //import { PluginManager } from "./plugins/manager.js";
111
- import { JsPluginManager, buildMicroGlobal, runAction, listActions } from "./plugins/js-bridge.js";
109
+ import { JsPluginManager, buildMicroGlobal, buildTuiBlockIndex, findTuiBlockInIndex, insertTuiTextareaNewline, mergeTuiTextareaBackward, mergeTuiTextareaForward, runAction, listActions } from "./plugins/js-bridge.js";
112
110
  import { Colorscheme } from "./config/colorscheme.js";
113
111
  import { detectSyntax, loadSyntaxDefinitions } from "./highlight/parser.js";
114
112
  import { Highlighter } from "./highlight/highlighter.js";
@@ -117,6 +115,7 @@ import { Screen } from "./screen/screen.js";
117
115
  import { VT100 } from "./screen/vt100.js";
118
116
  import { ClipboardManager, probeOSC52, osc52Clipboard } from "./platform/clipboard.js";
119
117
  import { platformId, run as runCommand, runSync, fetchHttpBytes, detectHttpBackend } from "./platform/commands.js";
118
+ import { controllingTerminalInputPath } from "./platform/terminal.js";
120
119
  import { shellSplit } from "./shell/shell.js";
121
120
  import { styleToAnsi } from "./display/ansi-style.js";
122
121
  import { encodeBinaryToBuffer, decodeBinaryBytes } from "./buffer/fixed3-codec.js";
@@ -145,7 +144,12 @@ if(!globalThis.Bun)
145
144
  //console.log(process.argv)
146
145
 
147
146
  console.error('Ran by node, changed to run by bun')
148
- process.execve(bunbinary,process.argv,process.env);
147
+ const launched = child_process.spawnSync(bunbinary, process.argv.slice(1), {
148
+ stdio: "inherit",
149
+ env: process.env,
150
+ });
151
+ if (launched.error) throw launched.error;
152
+ process.exit(launched.status ?? 1);
149
153
  }
150
154
  catch(e){
151
155
  console.log(`
@@ -368,6 +372,7 @@ async function renderMdcui(markdown, width = process.stdout.columns || 80, mdpat
368
372
  ? { rendered: tui, images: [] }
369
373
  : await prepareKittyImages(tui, resolvedImageBasePath, width, {
370
374
  allowUrl: allowRemoteKittyImages,
375
+ kittyMode: kittyImageMode,
371
376
  });
372
377
  return {
373
378
  rendered: prepared.rendered,
@@ -668,6 +673,21 @@ function canEditMdcuiSelection(buf, selection) {
668
673
  return first.y === last.y && prefixLength > 0 && first.x >= prefixLength;
669
674
  }
670
675
 
676
+ function mdcuiTextareaAtCursor(buf) {
677
+ if (!isMdcuiEncoding(buf?.encoding)) return null;
678
+ let cache = buf._mdcuiControlBlockIndex;
679
+ if (!cache || cache.lines !== buf.lines || cache.lineCount !== buf.lines.length) {
680
+ cache = {
681
+ lines: buf.lines,
682
+ lineCount: buf.lines.length,
683
+ blocks: buildTuiBlockIndex(buf.lines),
684
+ };
685
+ buf._mdcuiControlBlockIndex = cache;
686
+ }
687
+ const block = findTuiBlockInIndex(cache.blocks, buf.cursor.y);
688
+ return block?.header?.tag === "textarea" ? block : null;
689
+ }
690
+
671
691
  function isEditLockedBuffer(buf) {
672
692
  return isMdcuiEncoding(buf?.encoding ?? buf?.Settings?.encoding);
673
693
  }
@@ -970,6 +990,100 @@ function takeDisplay(text, maxWidth) {
970
990
  return out;
971
991
  }
972
992
 
993
+ function keyboardEventJson(event) {
994
+ const target = event?.target;
995
+ return {
996
+ type: String(event?.type ?? ""),
997
+ key: String(event?.key ?? ""),
998
+ code: String(event?.code ?? ""),
999
+ raw: String(event?.raw ?? ""),
1000
+ ctrlKey: Boolean(event?.ctrlKey),
1001
+ shiftKey: Boolean(event?.shiftKey),
1002
+ altKey: Boolean(event?.altKey),
1003
+ metaKey: Boolean(event?.metaKey),
1004
+ repeat: Boolean(event?.repeat),
1005
+ defaultPrevented: Boolean(event?.defaultPrevented),
1006
+ target: {
1007
+ id: String(target?.id ?? ""),
1008
+ tagName: String(target?.tagName ?? ""),
1009
+ className: String(target?.className ?? ""),
1010
+ value: String(target?.value ?? ""),
1011
+ },
1012
+ };
1013
+ }
1014
+
1015
+ function tuiKeyEventForFront(event, target, type) {
1016
+ const sequence = String(event?.key ?? "");
1017
+ const parts = sequence.split("-");
1018
+ const aliases = {
1019
+ enter: "Enter",
1020
+ escape: "Escape",
1021
+ tab: "Tab",
1022
+ backtab: "Tab",
1023
+ backspace: "Backspace",
1024
+ delete: "Delete",
1025
+ left: "ArrowLeft",
1026
+ right: "ArrowRight",
1027
+ up: "ArrowUp",
1028
+ down: "ArrowDown",
1029
+ home: "Home",
1030
+ end: "End",
1031
+ pageup: "PageUp",
1032
+ pagedown: "PageDown",
1033
+ space: " ",
1034
+ };
1035
+ let base = parts.at(-1) || sequence;
1036
+ if (sequence === "backtab") base = "tab";
1037
+ const raw = String(event?.raw ?? "");
1038
+ const key = aliases[base] ?? (sequence === raw && [...raw].length === 1 ? raw : base);
1039
+ let defaultPrevented = false;
1040
+ let propagationStopped = false;
1041
+ const result = {
1042
+ type,
1043
+ key,
1044
+ raw,
1045
+ ctrlKey: parts.includes("ctrl"),
1046
+ altKey: parts.includes("alt"),
1047
+ shiftKey: sequence === "backtab" || parts.includes("shift"),
1048
+ metaKey: parts.includes("meta"),
1049
+ repeat: false,
1050
+ target,
1051
+ currentTarget: target,
1052
+ get defaultPrevented() { return defaultPrevented; },
1053
+ get propagationStopped() { return propagationStopped; },
1054
+ preventDefault() { defaultPrevented = true; },
1055
+ stopPropagation() { propagationStopped = true; },
1056
+ };
1057
+ Object.defineProperty(result, "toJSON", {
1058
+ configurable: true,
1059
+ value() { return keyboardEventJson(this); },
1060
+ });
1061
+ return result;
1062
+ }
1063
+
1064
+ function indexedMdcuiFenceBlockAtLine(buffer, lineIndex) {
1065
+ const declarations = buffer?._mdcuiFenceEvents;
1066
+ if (!isMdcuiEncoding(buffer?.encoding) || declarations?.size === 0) return null;
1067
+ let cache = buffer._mdcuiFenceBlockIndex;
1068
+ // Render/reopen replaces the lines array; structural selector edits explicitly
1069
+ // clear this cache. Ordinary character edits leave block boundaries unchanged.
1070
+ if (
1071
+ !cache
1072
+ || cache.lines !== buffer.lines
1073
+ || cache.lineCount !== buffer.lines.length
1074
+ || cache.declarations !== declarations
1075
+ ) {
1076
+ cache = {
1077
+ lines: buffer.lines,
1078
+ lineCount: buffer.lines.length,
1079
+ declarations,
1080
+ blocks: buildTuiBlockIndex(buffer.lines, declarations),
1081
+ };
1082
+ buffer._mdcuiFenceBlockIndex = cache;
1083
+ }
1084
+ return findTuiBlockInIndex(cache.blocks, lineIndex);
1085
+ }
1086
+
973
1087
  function parseArgs(argv) {
974
1088
  const flags = {
975
1089
  version: false,
@@ -980,6 +1094,7 @@ function parseArgs(argv) {
980
1094
  cat: false,
981
1095
  docs: false,
982
1096
  exportReadme: false,
1097
+ exportCdpMaze: false,
983
1098
  changelog: false,
984
1099
  testapp: false,
985
1100
  demoList: false,
@@ -991,6 +1106,7 @@ function parseArgs(argv) {
991
1106
  debug: false,
992
1107
  profile: false,
993
1108
  plugin: "",
1109
+ cdpMaze: false,
994
1110
  cdpPort: 0,
995
1111
  cdpAddress: "",
996
1112
  kittyMode: "off",
@@ -1024,9 +1140,14 @@ function parseArgs(argv) {
1024
1140
  }
1025
1141
  else if (arg === "--docs" || arg === "--readme") flags.docs = true;
1026
1142
  else if (arg === "--export-readme") flags.exportReadme = true;
1143
+ else if (arg === "--export-cdp-maze") flags.exportCdpMaze = true;
1027
1144
  else if (arg === "--changelog") flags.changelog = true;
1028
1145
  else if (arg === "--testapp.md") flags.testapp = true;
1029
1146
  else if (arg === "--demo-list") flags.demoList = true;
1147
+ else if (arg === "--cdp-maze") {
1148
+ flags.cdpMaze = true;
1149
+ flags.demo = { option: arg, filename: "maze.md", asset: "demos/maze.md" };
1150
+ }
1030
1151
  else if (arg === "--demo") {
1031
1152
  flags.demo = { option: arg, filename: "testapp.md", asset: "testapp.md" };
1032
1153
  }
@@ -1066,6 +1187,8 @@ function parseArgs(argv) {
1066
1187
  }
1067
1188
  }
1068
1189
 
1190
+ if (flags.cdpMaze && !flags.cdpPort) flags.cdpPort = 9222;
1191
+
1069
1192
  return { flags, files };
1070
1193
  }
1071
1194
 
@@ -1108,6 +1231,8 @@ Settings:
1108
1231
  List all setting names and defaults, then exit.
1109
1232
 
1110
1233
  CDP:
1234
+ --cdp-maze
1235
+ Open the maze demo, start CDP on localhost:9222, and solve it automatically
1111
1236
  --remote-debugging-port=PORT
1112
1237
  Start CDP (Chrome DevTools Protocol) server on PORT at launch
1113
1238
  --remote-debugging-address=ADDRESS
@@ -1122,6 +1247,8 @@ Information:
1122
1247
  Show ${pkg.name}'s README.md & exit
1123
1248
  --export-readme
1124
1249
  Write or overwrite ./README.md with the bundled README.md & exit
1250
+ --export-cdp-maze
1251
+ Write or overwrite ./cdp-maze.js with the bundled CDP maze solver & exit
1125
1252
  --changelog
1126
1253
  Show CHANGELOG.md & exit
1127
1254
  -profile, --profile
@@ -1207,6 +1334,7 @@ class BufferModel {
1207
1334
  this._mdcuiAnsiText = isMdcuiEncoding(this.encoding) ? String(ansiText ?? "") : null;
1208
1335
  this._mdcuiSourceText = isMdcuiEncoding(this.encoding) ? String(sourceText ?? text) : null;
1209
1336
  this._mdcuiTuiSourceText = isMdcuiEncoding(this.encoding) ? String(tuiSourceText ?? sourceText ?? text) : null;
1337
+ this._mdcuiFenceEvents = isMdcuiEncoding(this.encoding) ? fenceEventMap(sourceText ?? tuiSourceText ?? text) : new Map();
1210
1338
  this._mdcuiImages = isMdcuiEncoding(this.encoding) ? (mdcuiImages ?? []) : [];
1211
1339
  this._mdcuiRenderWidth = Math.trunc(Number(mdcuiRenderWidth) || 0);
1212
1340
  this.cursor = { x: 0, y: 0 };
@@ -1565,9 +1693,39 @@ class BufferModel {
1565
1693
  this.cursor.x = x;
1566
1694
  }
1567
1695
 
1696
+ _historyState() {
1697
+ return {
1698
+ lines: this.lines.slice(),
1699
+ cursor: { ...this.cursor },
1700
+ serial: this._undoSerial,
1701
+ ansiStyleLines: Array.isArray(this._ansiStyleLines)
1702
+ ? this._ansiStyleLines.map((line) => Array.isArray(line) ? line.slice() : line)
1703
+ : this._ansiStyleLines,
1704
+ mdcuiAnsiText: this._mdcuiAnsiText,
1705
+ mdcuiImages: Array.isArray(this._mdcuiImages)
1706
+ ? this._mdcuiImages.map((image) => ({ ...image }))
1707
+ : this._mdcuiImages,
1708
+ headingTaskListAnchors: this._mdcuiHeadingTaskListAnchors instanceof Map
1709
+ ? new Map([...this._mdcuiHeadingTaskListAnchors].map(([key, value]) => [key, { ...value }]))
1710
+ : this._mdcuiHeadingTaskListAnchors,
1711
+ };
1712
+ }
1713
+
1714
+ _restoreHistoryState(state) {
1715
+ this.lines = state.lines;
1716
+ this.cursor = { ...state.cursor };
1717
+ this._undoSerial = state.serial ?? 0;
1718
+ this._ansiStyleLines = state.ansiStyleLines;
1719
+ this._mdcuiAnsiText = state.mdcuiAnsiText;
1720
+ this._mdcuiImages = state.mdcuiImages;
1721
+ this._mdcuiHeadingTaskListAnchors = state.headingTaskListAnchors;
1722
+ this._mdcuiFenceBlockIndex = null;
1723
+ this._mdcuiControlBlockIndex = null;
1724
+ }
1725
+
1568
1726
  pushUndo(force = false) {
1569
1727
  if (!force && this.isEditLocked() && !canEditMdcuiAtCursor(this)) return;
1570
- this.undoStack.push({ lines: this.lines.slice(), cursor: { ...this.cursor }, serial: this._undoSerial });
1728
+ this.undoStack.push(this._historyState());
1571
1729
  this._undoSerial = (this._undoSerial ?? 0) + 1;
1572
1730
  if (this.undoStack.length > 500) this.undoStack.shift();
1573
1731
  this.redoStack = [];
@@ -1576,12 +1734,10 @@ class BufferModel {
1576
1734
  undo() {
1577
1735
  if (this.isEditLocked() && !isMdcuiEncoding(this.encoding)) return false;
1578
1736
  if (!this.undoStack.length) return false;
1579
- this.redoStack.push({ lines: this.lines.slice(), cursor: { ...this.cursor }, serial: this._undoSerial });
1737
+ this.redoStack.push(this._historyState());
1580
1738
  const s = this.undoStack.pop();
1581
- this.lines = s.lines;
1739
+ this._restoreHistoryState(s);
1582
1740
  this.invalidateHighlightFrom(0, { force: true });
1583
- this.cursor = { ...s.cursor };
1584
- this._undoSerial = s.serial ?? 0;
1585
1741
  this.modified = this._undoSerial !== this._savedSerial;
1586
1742
  return true;
1587
1743
  }
@@ -1589,12 +1745,10 @@ class BufferModel {
1589
1745
  redo() {
1590
1746
  if (this.isEditLocked() && !isMdcuiEncoding(this.encoding)) return false;
1591
1747
  if (!this.redoStack.length) return false;
1592
- this.undoStack.push({ lines: this.lines.slice(), cursor: { ...this.cursor }, serial: this._undoSerial });
1748
+ this.undoStack.push(this._historyState());
1593
1749
  const s = this.redoStack.pop();
1594
- this.lines = s.lines;
1750
+ this._restoreHistoryState(s);
1595
1751
  this.invalidateHighlightFrom(0, { force: true });
1596
- this.cursor = { ...s.cursor };
1597
- this._undoSerial = s.serial ?? 0;
1598
1752
  this.modified = this._undoSerial !== this._savedSerial;
1599
1753
  return true;
1600
1754
  }
@@ -1725,6 +1879,7 @@ class BufferModel {
1725
1879
  this._mdcuiAnsiText = isMdcuiEncoding(this.encoding) ? String(decoded.ansiText ?? "") : null;
1726
1880
  this._mdcuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.sourceText ?? text) : null;
1727
1881
  this._mdcuiTuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.tuiSourceText ?? decoded.sourceText ?? text) : null;
1882
+ this._mdcuiFenceEvents = isMdcuiEncoding(this.encoding) ? fenceEventMap(decoded.sourceText ?? decoded.tuiSourceText ?? text) : new Map();
1728
1883
  this._mdcuiRenderWidth = decoded.mdcuiRenderWidth ?? 0;
1729
1884
  this._mdcuiImages = decoded.mdcuiImages ?? [];
1730
1885
  const readonly = isMdcuiEncoding(this.encoding);
@@ -1758,6 +1913,7 @@ class BufferModel {
1758
1913
  this._mdcuiAnsiText = isMdcuiEncoding(this.encoding) ? String(decoded.ansiText ?? "") : null;
1759
1914
  this._mdcuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.sourceText ?? text) : null;
1760
1915
  this._mdcuiTuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.tuiSourceText ?? decoded.sourceText ?? text) : null;
1916
+ this._mdcuiFenceEvents = isMdcuiEncoding(this.encoding) ? fenceEventMap(decoded.sourceText ?? decoded.tuiSourceText ?? text) : new Map();
1761
1917
  this._mdcuiRenderWidth = decoded.mdcuiRenderWidth ?? 0;
1762
1918
  this._mdcuiImages = decoded.mdcuiImages ?? [];
1763
1919
  this.fileformat = detectFileFormat(text, this.Settings.fileformat ?? DEFAULT_SETTINGS.fileformat);
@@ -2503,16 +2659,14 @@ class App {
2503
2659
  }
2504
2660
 
2505
2661
  async start() {
2506
- this._started = true;
2662
+ this.installProtectedPrompts();
2507
2663
  // When stdin was a pipe (content already consumed in loadBuffers), open the
2508
2664
  // controlling terminal directly so the event loop has a live handle and
2509
- // keyboard input works. Unix: /dev/tty Windows: \\.\CON
2665
+ // keyboard input works. Unix: /dev/tty; Windows: CONIN$.
2510
2666
  if (!process.stdin.isTTY) {
2511
2667
  try {
2512
- const { openSync } = await import("node:fs");
2513
2668
  const { ReadStream } = await import("node:tty");
2514
- const ttyPath = process.platform === "win32" ? "\\\\.\\CON" : "/dev/tty";
2515
- const fd = openSync(ttyPath, "r+");
2669
+ const fd = openSync(controllingTerminalInputPath(), "r");
2516
2670
  this._ttyStream = new ReadStream(fd);
2517
2671
  } catch {
2518
2672
  this._ttyStream = process.stdin;
@@ -2542,6 +2696,7 @@ class App {
2542
2696
  });
2543
2697
  process.on("SIGINT", () => {}); // Ctrl+C is handled as copy in handleEvent
2544
2698
  this.screen.init();
2699
+ this._started = true;
2545
2700
  // Update backup prompt to screen-aware version now that TUI is running.
2546
2701
  if (this.context._termPrompt) {
2547
2702
  this.context._termPrompt = async (msg) => {
@@ -2626,6 +2781,8 @@ class App {
2626
2781
  if (p.type === "term") p.terminal?.close();
2627
2782
  (this._ttyStream ?? process.stdin).setRawMode?.(false);
2628
2783
  this.screen.fini();
2784
+ this._started = false;
2785
+ this.restoreProtectedPrompts();
2629
2786
 
2630
2787
  if (this.context?.config?.getGlobalOption("savehistory") !== false) {
2631
2788
  try { await saveHistory(this.context.config.configDir); } catch {}
@@ -2644,6 +2801,157 @@ class App {
2644
2801
  process.exit(code);
2645
2802
  }
2646
2803
 
2804
+ installProtectedPrompts() {
2805
+ if (this._protectedPrompts) return;
2806
+ const saved = {
2807
+ alert: {
2808
+ had: Object.prototype.hasOwnProperty.call(globalThis, "alert"),
2809
+ value: globalThis.alert,
2810
+ },
2811
+ confirm: {
2812
+ had: Object.prototype.hasOwnProperty.call(globalThis, "confirm"),
2813
+ value: globalThis.confirm,
2814
+ },
2815
+ prompt: {
2816
+ had: Object.prototype.hasOwnProperty.call(globalThis, "prompt"),
2817
+ value: globalThis.prompt,
2818
+ },
2819
+ };
2820
+ this._protectedPrompts = saved;
2821
+ this._protectedPromptGlobals = {
2822
+ alert: this.protectedAlert.bind(this),
2823
+ confirm: this.protectedConfirm.bind(this),
2824
+ prompt: this.protectedPrompt.bind(this),
2825
+ };
2826
+ Object.assign(globalThis, this._protectedPromptGlobals);
2827
+ }
2828
+
2829
+ runProtectedPrompt(name, fallback, args) {
2830
+ const nativeFn = this._protectedPrompts?.[name]?.value;
2831
+ const tty = this._ttyStream ?? process.stdin;
2832
+ const wasRaw = typeof tty?.isRaw === "boolean" ? tty.isRaw : true;
2833
+ if (
2834
+ !this._started
2835
+ || this.shellRunning
2836
+ || this._alertRunning
2837
+ || !wasRaw
2838
+ || typeof nativeFn !== "function"
2839
+ ) {
2840
+ this.message = String(args[0] ?? "");
2841
+ return fallback;
2842
+ }
2843
+
2844
+ const inputHandler = this._inputHandler;
2845
+ const listenerAttached = Boolean(
2846
+ inputHandler && tty.listeners?.("data").includes(inputHandler),
2847
+ );
2848
+ const wasPaused = tty.isPaused?.() ?? false;
2849
+ let value;
2850
+ let failed = false;
2851
+ let failure;
2852
+ const cleanupFailures = [];
2853
+ const cleanup = (fn) => {
2854
+ try { fn(); } catch (error) { cleanupFailures.push(error); }
2855
+ };
2856
+
2857
+ try {
2858
+ this._alertRunning = true;
2859
+ // Do not let the editor's flowing-mode data listener race the blocking
2860
+ // prompt for bytes from the same terminal.
2861
+ if (listenerAttached) tty.removeListener("data", inputHandler);
2862
+ tty.pause?.();
2863
+ tty.setRawMode?.(false);
2864
+ this.screen.fini();
2865
+ this.screen.previous = null;
2866
+ // Bun's native prompts always read fd 0. If Markdown itself came from a
2867
+ // pipe, read synchronously from the controlling terminal instead.
2868
+ value = tty !== process.stdin
2869
+ ? this.runProtectedTtyPrompt(name, fallback, args)
2870
+ : Reflect.apply(nativeFn, globalThis, args);
2871
+ } catch (error) {
2872
+ failed = true;
2873
+ failure = error;
2874
+ } finally {
2875
+ cleanup(() => tty.setRawMode?.(wasRaw));
2876
+ cleanup(() => { this.screen.previous = null; });
2877
+ cleanup(() => this.screen.init());
2878
+ cleanup(() => { this._alertRunning = false; });
2879
+ cleanup(() => {
2880
+ if (listenerAttached) tty.on("data", inputHandler);
2881
+ });
2882
+ cleanup(() => {
2883
+ if (!wasPaused) tty.resume?.();
2884
+ });
2885
+ cleanup(() => this.render());
2886
+ }
2887
+ if (failed) throw failure;
2888
+ if (cleanupFailures.length > 0) throw cleanupFailures[0];
2889
+ return value;
2890
+ }
2891
+
2892
+ runProtectedTtyPrompt(name, fallback, args) {
2893
+ const message = String(args[0] ?? "");
2894
+ const defaultValue = String(args[1] ?? "");
2895
+ const fd = openSync(controllingTerminalInputPath(), "r");
2896
+ try {
2897
+ if (name === "alert") {
2898
+ process.stdout.write(`${message}\n\nPress ENTER to continue...`);
2899
+ this.readProtectedPromptLine(fd);
2900
+ return undefined;
2901
+ }
2902
+ const suffix = name === "confirm"
2903
+ ? " [y/N] "
2904
+ : defaultValue ? ` (${defaultValue}) ` : " ";
2905
+ process.stdout.write(message + suffix);
2906
+ const answer = this.readProtectedPromptLine(fd);
2907
+ if (answer == null) return fallback;
2908
+ if (name === "confirm") return /^(?:y|yes)$/i.test(answer.trim());
2909
+ return answer === "" ? defaultValue : answer;
2910
+ } finally {
2911
+ closeSync(fd);
2912
+ }
2913
+ }
2914
+
2915
+ readProtectedPromptLine(fd) {
2916
+ const chunks = [];
2917
+ const chunk = Buffer.allocUnsafe(1024);
2918
+ while (true) {
2919
+ const length = readSync(fd, chunk, 0, chunk.length, null);
2920
+ if (length === 0) return chunks.length > 0 ? Buffer.concat(chunks).toString() : null;
2921
+ const bytes = chunk.subarray(0, length);
2922
+ const newline = bytes.findIndex((byte) => byte === 0x0a || byte === 0x0d);
2923
+ chunks.push(Buffer.from(newline < 0 ? bytes : bytes.subarray(0, newline)));
2924
+ if (newline >= 0) return Buffer.concat(chunks).toString();
2925
+ }
2926
+ }
2927
+
2928
+ protectedAlert(msg = "") {
2929
+ return this.runProtectedPrompt("alert", undefined, [String(msg)]);
2930
+ }
2931
+
2932
+ protectedConfirm(msg = "") {
2933
+ return this.runProtectedPrompt("confirm", false, [String(msg)]);
2934
+ }
2935
+
2936
+ protectedPrompt(msg = "", defaultValue = "") {
2937
+ return this.runProtectedPrompt(
2938
+ "prompt",
2939
+ defaultValue,
2940
+ [String(msg), String(defaultValue)],
2941
+ );
2942
+ }
2943
+
2944
+ restoreProtectedPrompts() {
2945
+ const saved = this._protectedPrompts;
2946
+ if (!saved) return;
2947
+ for (const name of ["alert", "confirm", "prompt"]) {
2948
+ if (saved[name].had) globalThis[name] = saved[name].value;
2949
+ else delete globalThis[name];
2950
+ }
2951
+ this._protectedPrompts = null;
2952
+ this._protectedPromptGlobals = null;
2953
+ }
2954
+
2647
2955
  layoutEditorArea() {
2648
2956
  const promptHeight = this.prompt ? 1 : 0;
2649
2957
  const tabBarHeight = this.tabs.length > 1 ? 1 : 0;
@@ -3754,6 +4062,7 @@ class App {
3754
4062
 
3755
4063
  const text = event.raw;
3756
4064
  const seq = event.key;
4065
+ const keydownBlock = indexedMdcuiFenceBlockAtLine(buf, buf?.cursor.y);
3757
4066
  if (buf) buf.allowCursorOffscreen = false;
3758
4067
 
3759
4068
  if (this._rawMode) {
@@ -3764,6 +4073,12 @@ class App {
3764
4073
  return;
3765
4074
  }
3766
4075
 
4076
+ const keydownEvent = await this.dispatchMdcuiFenceEvent(buf, keydownBlock, event, "keydown");
4077
+ if (keydownEvent?.defaultPrevented && !["ctrl-q", "alt-q", "escape"].includes(seq)) {
4078
+ this.render();
4079
+ return;
4080
+ }
4081
+
3767
4082
  // Reset undo insert chain on any non-printable-char key
3768
4083
  if (!(seq === text && text.length === 1 && text >= " ")) this._undoInsertChain = false;
3769
4084
 
@@ -3952,15 +4267,28 @@ class App {
3952
4267
  case "backspace":
3953
4268
  buf.pushUndo();
3954
4269
  if (this.pane?.selection) deleteSelection(buf, this.pane);
3955
- else if (await this.runPluginBool("preBackspace")) buf.backspace();
4270
+ else if (await this.runPluginBool("preBackspace")) {
4271
+ const textarea = mdcuiTextareaAtCursor(buf);
4272
+ if (!mergeTuiTextareaBackward(buf, textarea)) buf.backspace();
4273
+ }
3956
4274
  break;
3957
4275
  case "delete":
3958
4276
  buf.pushUndo();
3959
4277
  if (this.pane?.selection) deleteSelection(buf, this.pane);
3960
- else buf.deleteForward();
4278
+ else {
4279
+ const textarea = mdcuiTextareaAtCursor(buf);
4280
+ if (!mergeTuiTextareaForward(buf, textarea)) buf.deleteForward();
4281
+ }
3961
4282
  break;
3962
4283
  case "enter":
3963
4284
  if (isMdcuiEncoding(buf?.encoding)) {
4285
+ const textarea = mdcuiTextareaAtCursor(buf);
4286
+ if (textarea && canEditMdcuiAtCursor(buf)) {
4287
+ buf.pushUndo();
4288
+ if (this.pane?.selection) deleteSelection(buf, this.pane);
4289
+ insertTuiTextareaNewline(buf, textarea);
4290
+ break;
4291
+ }
3964
4292
  await this.handleMdcuiCellCallback(buf, buf.cursor.y, buf.cursor.x, "enter");
3965
4293
  break;
3966
4294
  }
@@ -4136,6 +4464,48 @@ class App {
4136
4464
  this.render();
4137
4465
  }
4138
4466
 
4467
+ async dispatchMdcuiFenceEvent(buf, block, inputEvent, eventName) {
4468
+ const id = block?.header?.id;
4469
+ const declaration = id ? buf?._mdcuiFenceEvents?.get(id) : null;
4470
+ const handler = declaration?.events?.get(eventName);
4471
+ const code = inlineFenceEventCode(handler);
4472
+ if (
4473
+ !code
4474
+ || declaration.tag !== block?.header?.tag
4475
+ || !buf?.path
4476
+ || isHttpUrl(buf.path)
4477
+ ) return false;
4478
+
4479
+ const frontPath = `${buf.path}.front.js`;
4480
+ if (!existsSync(frontPath)) return false;
4481
+ try {
4482
+ const selection = globalThis.$?.(`${declaration.tag}#${id}`);
4483
+ const target = {
4484
+ id,
4485
+ tagName: declaration.tag.toUpperCase(),
4486
+ className: declaration.classes.join(" "),
4487
+ };
4488
+ Object.defineProperty(target, "value", {
4489
+ enumerable: true,
4490
+ get: () => selection?.val?.() ?? "",
4491
+ set: (value) => selection?.val?.(value),
4492
+ });
4493
+ const event = tuiKeyEventForFront(inputEvent, target, eventName);
4494
+ const [{ evalFront }, frontMod] = await Promise.all([
4495
+ import("./cui/rpc.mjs"),
4496
+ import(localModuleUrl(frontPath)),
4497
+ ]);
4498
+ const result = await evalFront(frontMod, code, { event });
4499
+ if (result && typeof result === "object" && result.ok === false) {
4500
+ this.message = `mdcui ${eventName}: ${String(result.error ?? "Unknown error")}`;
4501
+ }
4502
+ return event;
4503
+ } catch (error) {
4504
+ this.message = `mdcui ${eventName}: ${String(error?.message || error)}`;
4505
+ return null;
4506
+ }
4507
+ }
4508
+
4139
4509
  async handleMdcuiCellCallback(buf, y = buf?.cursor?.y ?? 0, x = buf?.cursor?.x ?? 0, trigger = "unknown") {
4140
4510
  const payload = mdcuiCellPayload(buf, y, x, trigger);
4141
4511
  if (!payload) return false;
@@ -4162,50 +4532,11 @@ class App {
4162
4532
  import("./cui/rpc.mjs"),
4163
4533
  import(localModuleUrl(`${buf.path}.front.js`)),
4164
4534
  ]);
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;
4535
+ const result = await evalFront(frontMod, payload.link);
4536
+ if (result && typeof result === "object" && result.ok === false) {
4537
+ this.protectedAlert(String(result.error ?? "Unknown error"));
4538
+ } else if (result != null) {
4539
+ this.message = String(result);
4209
4540
  }
4210
4541
  return true;
4211
4542
  } catch (error) {
@@ -7687,6 +8018,12 @@ async function exportReadme() {
7687
8018
  console.log(`Wrote ${readmePath}`);
7688
8019
  }
7689
8020
 
8021
+ async function exportCdpMaze() {
8022
+ const outputPath = resolve("cdp-maze.js");
8023
+ await Bun.write(outputPath, await bundledMarkdownSource("cdp-maze.js"));
8024
+ console.log(`Wrote ${outputPath}`);
8025
+ }
8026
+
7690
8027
  async function printChangelogDocs() {
7691
8028
  const changelog = readInternalAssetText("CHANGELOG.md") ?? await Bun.file(join(REPO_ROOT, "CHANGELOG.md")).text();
7692
8029
  process.stdout.write(Bun.markdown.ansi(changelog, { hyperlinks: true }));
@@ -7808,6 +8145,10 @@ async function main() {
7808
8145
  await exportReadme();
7809
8146
  return;
7810
8147
  }
8148
+ if (flags.exportCdpMaze) {
8149
+ await exportCdpMaze();
8150
+ return;
8151
+ }
7811
8152
  if (flags.changelog) {
7812
8153
  await printChangelogDocs();
7813
8154
  return;
@@ -8033,6 +8374,7 @@ async function main() {
8033
8374
  addCheckpoint("App Instantiation");
8034
8375
  const app = new App(buffers, context);
8035
8376
  jsPlugins.setApp(app);
8377
+ app.installProtectedPrompts();
8036
8378
  // if (plugins && !pluginErr && app.buffer) plugins.curPaneAdapter = makePaneAdapter(app.buffer, app);
8037
8379
  // Dispatch all JS plugin lifecycle hooks after setApp so TermMessage,
8038
8380
  // CurPane, cmd/action proxies, and buffer APIs all work correctly.
@@ -8053,6 +8395,23 @@ async function main() {
8053
8395
  if (flags.cdpAddress) cdpArgs.push(`--address=${flags.cdpAddress}`);
8054
8396
  await app.handleCommand(`cdp ${cdpArgs.join(" ")}`);
8055
8397
  }
8398
+
8399
+ if (flags.cdpMaze) {
8400
+ const solverHost = !flags.cdpAddress || flags.cdpAddress === "0.0.0.0"
8401
+ ? "127.0.0.1"
8402
+ : flags.cdpAddress;
8403
+ const solverUrl = `ws://${solverHost}:${flags.cdpPort}/devtools/browser/cdp-server`;
8404
+ setTimeout(async () => {
8405
+ try {
8406
+ const { runCdpMaze } = await import("../cdp-maze.js");
8407
+ app.message = await runCdpMaze(solverUrl);
8408
+ app.render?.();
8409
+ } catch (error) {
8410
+ app.message = `CDP maze solver failed: ${error?.message || error}`;
8411
+ app.render?.();
8412
+ }
8413
+ }, 3000);
8414
+ }
8056
8415
 
8057
8416
  if (flags.profile) {
8058
8417
  addCheckpoint("Clipboard Probing");
@@ -8520,9 +8879,6 @@ async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAUL
8520
8879
  }
8521
8880
 
8522
8881
 
8523
- mainPromise.then(r=>{
8524
-
8525
-
8526
8882
  main().catch((error) => {
8527
8883
  try {
8528
8884
  (_activeTtyStream ?? process.stdin).setRawMode?.(false);
@@ -8532,6 +8888,3 @@ main().catch((error) => {
8532
8888
  process.exit(1);
8533
8889
  }
8534
8890
  });
8535
-
8536
-
8537
- })