jsmdcui 0.9.0 → 0.10.1

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 CHANGED
@@ -2,6 +2,69 @@
2
2
 
3
3
  All notable user-visible changes to jsmdcui are documented here.
4
4
 
5
+ ## [0.10.1] - 2026-07-25
6
+
7
+ ### Fixed
8
+
9
+ - Resolve and prepare `--demo` and `--demo-*` selections before entering WUI
10
+ mode, so combinations such as `--wui --demo-todo` serve the selected demo
11
+ instead of falling back to `testapp.md`.
12
+
13
+ ## [0.10.0] - 2026-07-24
14
+
15
+ This update adds distribution-oriented defaults for shipping jsmdcui as a text
16
+ editor, terminal application, or browser application; makes WUI startup and
17
+ demo replacement explicit; and improves command-line control over rendering,
18
+ syntax highlighting, and single-executable builds.
19
+
20
+ ### Added
21
+
22
+ - Add `--mdcui` and `--tui` as aliases for `-encoding mdcui`, allowing an
23
+ explicit terminal Markdown UI request to override an editor-oriented
24
+ distribution default.
25
+ - Add `--overwrite-demo` as a modifier for `--demo` and the automatically
26
+ discovered `--demo-*` commands. It replaces an existing local demo with the
27
+ bundled copy before opening it.
28
+ - Add `--print-ui` for WUI launches. The generated TUI preview, raw ANSI, and
29
+ HTML are printed only when this flag is present.
30
+ - Add the `src/MDCUI_DEFAULT_EDIT` distribution marker. When present, Markdown
31
+ files open as editable UTF-8 text by default while `--mdcui` remains
32
+ available explicitly.
33
+ - Add presence-based single-executable build constants:
34
+ `MDCUI_DEFAULT_EDIT`, `MDCUI_DEFAULT_DEMO`, and
35
+ `MDCUI_DEFAULT_DEMO_WUI` select an editor, no-argument TUI demo, or
36
+ no-argument WUI demo default. `MDCUI_OVERWRITE_DEMO` optionally makes a
37
+ bundled demo replace its local copy.
38
+ - Add distribution documentation for shipping a customized root
39
+ `testapp.md` as a standalone terminal or browser application.
40
+ - Add regression coverage for the `--mdcui` alias, forced CLI filetypes,
41
+ non-overwriting and overwriting demos, and WUI demo replacement.
42
+
43
+ ### Changed
44
+
45
+ - Parse `--wui` through the normal command-line argument flow, so it can appear
46
+ before or after the Markdown filename.
47
+ - Make WUI startup quieter by default. It continues to report file generation
48
+ and the server URL, while full generated UI output is opt-in through
49
+ `--print-ui`.
50
+ - Make direct `runmd.mjs` launches recognize `--overwrite-demo` and
51
+ `--print-ui`. Demo replacement is limited to the implicit `testapp.md`;
52
+ explicitly named Markdown files are protected from replacement.
53
+ - Forward every extra argument after `--build-exe`, or after the target passed
54
+ to `--build-for`, to the underlying `bun build` command.
55
+ - Align the `start`, `tui`, and `wui` package scripts with the repository
56
+ launchers, and add a `clean` package script.
57
+ - Recommend choosing only one of the three `MDCUI_DEFAULT_*` distribution
58
+ modes per build, with `MDCUI_OVERWRITE_DEMO` treated as an optional modifier.
59
+ - Show effective `MDCUI_*` distribution settings in `--version` output,
60
+ including `MDCUI_DEFAULT_EDIT` enabled through either its build constant or
61
+ the `src/MDCUI_DEFAULT_EDIT` marker.
62
+
63
+ ### Fixed
64
+
65
+ - Make `-filetype <name>` actually override automatic syntax detection in
66
+ editor buffers and `--cat`, including input read from stdin.
67
+
5
68
  ## [0.9.0] - 2026-07-21
6
69
 
7
70
  This update expands Chrome DevTools Protocol automation for the terminal UI,
package/README.md CHANGED
@@ -163,6 +163,7 @@ bun src/index.js --wui testapp.md
163
163
  | `bun src/index.js --export-cdp-maze` | Write or overwrite `./cdp-maze.js` with the bundled CDP maze solver and exit. |
164
164
  | `bun src/index.js --demo-list` | List `testapp.md` and every bundled `demos/*.md` example with its command-line option, then exit. |
165
165
  | `bun src/index.js --demo` | Use local `testapp.md` when present, otherwise write the bundled demo; open it in the terminal UI and write five generated files beside it. |
166
+ | `bun src/index.js --overwrite-demo --demo` | Replace an existing local `testapp.md` with the bundled copy before opening it. `--overwrite-demo` can modify any `--demo-*` option. |
166
167
  | `bun src/index.js --demo-<filename>` | Load `demos/<filename>.md`; preserve an existing local copy or write the bundled copy, then open it and generate its five companion files. New files added under `demos/` work automatically. |
167
168
  | `bun src/index.js --demo-imgtool` | Compatibility alias for `--demo-image-processor`. |
168
169
  | `bun src/index.js --demo-imgtool-zh` | Compatibility alias for `--demo-image-processor.zh-TW`. |
@@ -170,6 +171,8 @@ bun src/index.js --wui testapp.md
170
171
  | `bun src/index.js --allow-url URL.md` | Download HTTP(S) Markdown to the current directory and, with Kitty mode enabled, download its HTTP(S) images; write 5 generated files and allow embedded code to run. Only use trusted URLs. |
171
172
  | `bun src/index.js --wui` | Use local `testapp.md` when present, otherwise write the bundled demo; write five generated files in the current directory, then print and serve a random URL. |
172
173
  | `bun src/index.js --wui app.md` | Write five generated files beside `app.md`, then print and serve a random URL. |
174
+ | `bun src/index.js --wui --demo-<filename>` | Load the selected bundled demo and serve it as a Web UI. |
175
+ | `bun src/index.js --wui --print-ui app.md` | Also print the generated TUI, raw ANSI, and HTML before starting the WUI server. |
173
176
  | `PORT=8080 bun src/index.js --wui app.md` | Start the browser UI on another port. |
174
177
  | `bun src/index.js` | Open the normal terminal editor with an empty buffer. |
175
178
 
@@ -742,6 +745,67 @@ or serve Markdown UI files that you trust.
742
745
  calls, but it does not protect the backend module from trusted local code and
743
746
  is not a substitute for authentication.
744
747
 
748
+ ## Distribution
749
+
750
+ ### Text editor distribution
751
+
752
+ Distributions intended primarily as text editors can include an empty
753
+ `src/MDCUI_DEFAULT_EDIT` file. When this marker exists, opening a `.md` file uses the
754
+ normal editable UTF-8 view instead of automatically entering `mdcui` mode.
755
+ Markdown UI support remains available explicitly with `--mdcui` or `--tui`;
756
+ both are equivalent to `-encoding mdcui`.
757
+
758
+ ### Build-time distribution constants
759
+
760
+ Single-file distributions can define one of these default modes:
761
+
762
+ - `MDCUI_DEFAULT_EDIT`: open files as editable text by default.
763
+ - `MDCUI_DEFAULT_DEMO`: add `--demo` when launched without arguments.
764
+ - `MDCUI_DEFAULT_DEMO_WUI`: add `--wui` when launched without arguments.
765
+
766
+ Choose only one of the three default-mode constants for a distribution.
767
+ `MDCUI_OVERWRITE_DEMO` is an optional modifier for the demo or WUI mode.
768
+ These are presence-based constants, so their build values do not require
769
+ shell-quoted strings:
770
+
771
+ ```sh
772
+ bun src/index.js --build-exe --define MDCUI_DEFAULT_EDIT=true
773
+ ```
774
+
775
+ ### Ship `testapp.md` as a standalone application
776
+
777
+ To turn your finished Markdown UI into a standalone executable, save it as the
778
+ repository-root `testapp.md`, then build with both demo constants:
779
+
780
+ ```sh
781
+ bun src/index.js --build-exe \
782
+ --define MDCUI_DEFAULT_DEMO=true \
783
+ --define MDCUI_OVERWRITE_DEMO=true
784
+ ```
785
+
786
+ The build first packs `testapp.md` into the executable and writes the resulting
787
+ `mdcui` binary in the current directory. In that binary,
788
+ `MDCUI_DEFAULT_DEMO` adds `--demo` when the user launches it without arguments,
789
+ and `MDCUI_OVERWRITE_DEMO` adds `--overwrite-demo`. Consequently, running
790
+ `./mdcui` writes the bundled application to `./testapp.md`, replacing an older
791
+ copy, and starts it as the terminal UI.
792
+
793
+ To make a no-argument launch start the browser UI instead, build with
794
+ `MDCUI_DEFAULT_DEMO_WUI`:
795
+
796
+ ```sh
797
+ bun src/index.js --build-exe \
798
+ --define MDCUI_DEFAULT_DEMO_WUI=true \
799
+ --define MDCUI_OVERWRITE_DEMO=true
800
+ ```
801
+
802
+ This adds `--wui` when the executable is launched without arguments.
803
+
804
+ You can rename and distribute the resulting binary. It contains the Bun
805
+ runtime, jsmdcui, the packed runtime assets, and your `testapp.md`; the target
806
+ directory must remain writable because launching the application creates
807
+ `testapp.md` and its generated companion files there.
808
+
745
809
  ## Development
746
810
 
747
811
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsmdcui",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Markdown as a Common UI for Terminals and Web Browsers",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -9,9 +9,11 @@
9
9
  "jsmdcui": "./src/index.js"
10
10
  },
11
11
  "scripts": {
12
- "start": "bun ./src/index.js",
13
- "tui": "bun ./src/index.js ./testapp.md",
14
- "wui": "bun ./runmd.mjs",
15
- "check": "node --check ./src/index.js"
12
+ "start": "bun ./src/index.js --demo",
13
+ "tui": "bun ./tui",
14
+ "wui": "bun ./wui",
15
+ "edit": "bun ./edit",
16
+ "check": "bun ./src/index.js --check",
17
+ "clean": "bun ./clean.sh"
16
18
  }
17
19
  }
package/runmd.mjs CHANGED
@@ -29,10 +29,10 @@ async function readTemplate(pathname)
29
29
  await Bun.file(path.join(REPO_ROOT, pathname)).text()
30
30
  }
31
31
 
32
- export async function readMarkdownInput(mdpath)
32
+ export async function readMarkdownInput(mdpath, overwriteDemo = false)
33
33
  {
34
34
  const file = Bun.file(mdpath);
35
- if (await file.exists()) return await file.text();
35
+ if (!overwriteDemo && await file.exists()) return await file.text();
36
36
  const assetName = path.basename(mdpath);
37
37
  const internalText = readInternalAssetText(assetName);
38
38
  if (internalText != null) {
@@ -50,30 +50,39 @@ export async function readMarkdownInput(mdpath)
50
50
  }
51
51
 
52
52
 
53
- export async function main(tuiWidth=30)
53
+ export async function main(tuiWidth=30, {
54
+ mdpath: requestedMdpath = null,
55
+ overwriteDemo = process.argv.includes("--overwrite-demo"),
56
+ printUi = process.argv.includes("--print-ui"),
57
+ } = {})
54
58
  {
55
- const mdpath = process.argv.filter(i=>i.endsWith('.md'))[0] || 'testapp.md'
59
+ const explicitMdpath = process.argv.find(i=>i.endsWith('.md'))
60
+ const mdpath = requestedMdpath || explicitMdpath || 'testapp.md'
56
61
 
57
62
 
58
63
  // 1. Read markdown file
59
64
  console.error('Reading:',mdpath)
60
- let md = await readMarkdownInput(mdpath)
65
+ let md = await readMarkdownInput(mdpath, overwriteDemo && !requestedMdpath && !explicitMdpath)
61
66
 
62
67
  // 2. Extract js files
63
68
  md = await extractJs(md,mdpath);
64
69
 
65
70
  // 3. Create Terminal UI
66
71
  let tui = createTui(md,tuiWidth)
67
- cse(mda("\n# TUI"))
68
- cse(tui)
69
- cse(mda('## TUI raw'))
70
- cse(jss(tui))
72
+ if (printUi) {
73
+ cse(mda("\n# TUI"))
74
+ cse(tui)
75
+ cse(mda('## TUI raw'))
76
+ cse(jss(tui))
77
+ }
71
78
 
72
79
 
73
80
  // 4. Create Web UI
74
81
  let wui = await createWui(md,mdpath)
75
- cse(mda('\n# HTML'))
76
- cse(wui)
82
+ if (printUi) {
83
+ cse(mda('\n# HTML'))
84
+ cse(wui)
85
+ }
77
86
 
78
87
 
79
88
  /*
@@ -163,6 +163,7 @@ bun src/index.js --wui testapp.md
163
163
  | `bun src/index.js --export-cdp-maze` | Write or overwrite `./cdp-maze.js` with the bundled CDP maze solver and exit. |
164
164
  | `bun src/index.js --demo-list` | List `testapp.md` and every bundled `demos/*.md` example with its command-line option, then exit. |
165
165
  | `bun src/index.js --demo` | Use local `testapp.md` when present, otherwise write the bundled demo; open it in the terminal UI and write five generated files beside it. |
166
+ | `bun src/index.js --overwrite-demo --demo` | Replace an existing local `testapp.md` with the bundled copy before opening it. `--overwrite-demo` can modify any `--demo-*` option. |
166
167
  | `bun src/index.js --demo-<filename>` | Load `demos/<filename>.md`; preserve an existing local copy or write the bundled copy, then open it and generate its five companion files. New files added under `demos/` work automatically. |
167
168
  | `bun src/index.js --demo-imgtool` | Compatibility alias for `--demo-image-processor`. |
168
169
  | `bun src/index.js --demo-imgtool-zh` | Compatibility alias for `--demo-image-processor.zh-TW`. |
@@ -170,6 +171,8 @@ bun src/index.js --wui testapp.md
170
171
  | `bun src/index.js --allow-url URL.md` | Download HTTP(S) Markdown to the current directory and, with Kitty mode enabled, download its HTTP(S) images; write 5 generated files and allow embedded code to run. Only use trusted URLs. |
171
172
  | `bun src/index.js --wui` | Use local `testapp.md` when present, otherwise write the bundled demo; write five generated files in the current directory, then print and serve a random URL. |
172
173
  | `bun src/index.js --wui app.md` | Write five generated files beside `app.md`, then print and serve a random URL. |
174
+ | `bun src/index.js --wui --demo-<filename>` | Load the selected bundled demo and serve it as a Web UI. |
175
+ | `bun src/index.js --wui --print-ui app.md` | Also print the generated TUI, raw ANSI, and HTML before starting the WUI server. |
173
176
  | `PORT=8080 bun src/index.js --wui app.md` | Start the browser UI on another port. |
174
177
  | `bun src/index.js` | Open the normal terminal editor with an empty buffer. |
175
178
 
@@ -742,6 +745,67 @@ or serve Markdown UI files that you trust.
742
745
  calls, but it does not protect the backend module from trusted local code and
743
746
  is not a substitute for authentication.
744
747
 
748
+ ## Distribution
749
+
750
+ ### Text editor distribution
751
+
752
+ Distributions intended primarily as text editors can include an empty
753
+ `src/MDCUI_DEFAULT_EDIT` file. When this marker exists, opening a `.md` file uses the
754
+ normal editable UTF-8 view instead of automatically entering `mdcui` mode.
755
+ Markdown UI support remains available explicitly with `--mdcui` or `--tui`;
756
+ both are equivalent to `-encoding mdcui`.
757
+
758
+ ### Build-time distribution constants
759
+
760
+ Single-file distributions can define one of these default modes:
761
+
762
+ - `MDCUI_DEFAULT_EDIT`: open files as editable text by default.
763
+ - `MDCUI_DEFAULT_DEMO`: add `--demo` when launched without arguments.
764
+ - `MDCUI_DEFAULT_DEMO_WUI`: add `--wui` when launched without arguments.
765
+
766
+ Choose only one of the three default-mode constants for a distribution.
767
+ `MDCUI_OVERWRITE_DEMO` is an optional modifier for the demo or WUI mode.
768
+ These are presence-based constants, so their build values do not require
769
+ shell-quoted strings:
770
+
771
+ ```sh
772
+ bun src/index.js --build-exe --define MDCUI_DEFAULT_EDIT=true
773
+ ```
774
+
775
+ ### Ship `testapp.md` as a standalone application
776
+
777
+ To turn your finished Markdown UI into a standalone executable, save it as the
778
+ repository-root `testapp.md`, then build with both demo constants:
779
+
780
+ ```sh
781
+ bun src/index.js --build-exe \
782
+ --define MDCUI_DEFAULT_DEMO=true \
783
+ --define MDCUI_OVERWRITE_DEMO=true
784
+ ```
785
+
786
+ The build first packs `testapp.md` into the executable and writes the resulting
787
+ `mdcui` binary in the current directory. In that binary,
788
+ `MDCUI_DEFAULT_DEMO` adds `--demo` when the user launches it without arguments,
789
+ and `MDCUI_OVERWRITE_DEMO` adds `--overwrite-demo`. Consequently, running
790
+ `./mdcui` writes the bundled application to `./testapp.md`, replacing an older
791
+ copy, and starts it as the terminal UI.
792
+
793
+ To make a no-argument launch start the browser UI instead, build with
794
+ `MDCUI_DEFAULT_DEMO_WUI`:
795
+
796
+ ```sh
797
+ bun src/index.js --build-exe \
798
+ --define MDCUI_DEFAULT_DEMO_WUI=true \
799
+ --define MDCUI_OVERWRITE_DEMO=true
800
+ ```
801
+
802
+ This adds `--wui` when the executable is launched without arguments.
803
+
804
+ You can rename and distribute the resulting binary. It contains the Bun
805
+ runtime, jsmdcui, the packed runtime assets, and your `testapp.md`; the target
806
+ directory must remain writable because launching the application creates
807
+ `testapp.md` and its generated companion files there.
808
+
745
809
  ## Development
746
810
 
747
811
  ```sh
@@ -31,6 +31,51 @@ executable to the current directory. To pass a Bun compilation target, use:
31
31
  bun ./src/index.js --build-for <target>
32
32
  ```
33
33
 
34
+ Any additional arguments after `--build-exe`, or after the target passed to
35
+ `--build-for`, are forwarded to the `bun build` command:
36
+
37
+ ```shell
38
+ bun ./src/index.js --build-exe --sourcemap
39
+ bun ./src/index.js --build-for <target> --sourcemap
40
+ ```
41
+
42
+ The following presence-based build constants select distribution defaults:
43
+
44
+ - `MDCUI_DEFAULT_EDIT`: open files as editable text by default.
45
+ - `MDCUI_DEFAULT_DEMO`: add `--demo` when launched without arguments.
46
+ - `MDCUI_DEFAULT_DEMO_WUI`: add `--wui` when launched without arguments.
47
+
48
+ Choose only one of these three default-mode constants for each build.
49
+ `MDCUI_OVERWRITE_DEMO` is an optional modifier for a demo or WUI build.
50
+
51
+ For example, build a default text editor with:
52
+
53
+ ```shell
54
+ bun ./src/index.js --build-exe \
55
+ --define MDCUI_DEFAULT_EDIT=true
56
+ ```
57
+
58
+ To build a bundled terminal demo that replaces its local copy:
59
+
60
+ ```shell
61
+ bun ./src/index.js --build-exe \
62
+ --define MDCUI_DEFAULT_DEMO=true \
63
+ --define MDCUI_OVERWRITE_DEMO=true
64
+ ```
65
+
66
+ `MDCUI_DEFAULT_DEMO=true` makes an executable launched without arguments behave
67
+ as if `--demo` was passed. `MDCUI_OVERWRITE_DEMO=true` makes every selected demo
68
+ replace an existing local copy; it does not select a demo by itself.
69
+
70
+ For a browser UI that automatically adds `--wui` when launched without
71
+ arguments, use `MDCUI_DEFAULT_DEMO_WUI` instead:
72
+
73
+ ```shell
74
+ bun ./src/index.js --build-exe \
75
+ --define MDCUI_DEFAULT_DEMO_WUI=true \
76
+ --define MDCUI_OVERWRITE_DEMO=true
77
+ ```
78
+
34
79
  To perform the same steps manually, run these commands from `single-exe/`:
35
80
 
36
81
  ```shell
@@ -38,10 +38,11 @@ export function getDirnameFromUrl(importMetaUrl) {
38
38
  return dirname(fileURLToPath(importMetaUrl));
39
39
  }
40
40
 
41
- export async function buildExecutable(target = "",build_outfile="single.exe") {
41
+ export async function buildExecutable(target = "",build_outfile="single.exe", bunArgs = []) {
42
42
 
43
43
  const outfile = resolve(process.cwd(), build_outfile);
44
44
  const normalizedTarget = String(target || "").trim();
45
+ const extraBunArgs = Array.from(bunArgs ?? [], String);
45
46
  if(!globalThis.Bun || IS_COMPILED)
46
47
  {
47
48
  console.log("Build exe can only be run by Bun in the source tree");
@@ -71,6 +72,7 @@ export async function buildExecutable(target = "",build_outfile="single.exe") {
71
72
  `--outfile=${outfile}`,
72
73
  `--metafile-md=${outfile}.meta.md`,
73
74
  ...(normalizedTarget ? [`--target=${normalizedTarget}`] : []),
75
+ ...extraBunArgs,
74
76
  ],
75
77
  },
76
78
  ];
@@ -145,8 +147,8 @@ export async function buildEarlyExit(argv,build_outfile) {
145
147
  console.error("Missing target value for --build-for");
146
148
  process.exit(2);
147
149
  }
148
- process.exit(await buildExe(target,build_outfile));
150
+ process.exit(await buildExe(target, build_outfile, argv.slice(buildForIndex + 2)));
149
151
  }
150
152
 
151
- process.exit(await buildExe(null,build_outfile));
153
+ process.exit(await buildExe(null, build_outfile, argv.slice(buildExeIndex + 1)));
152
154
  }
package/src/index.js CHANGED
@@ -181,6 +181,11 @@ const VERSION = pkg.version;
181
181
  const SINGLE_EXE_DIR = resolve(REPO_ROOT, "single-exe");
182
182
  const SINGLE_EXE_ENTRY = resolve(SINGLE_EXE_DIR, "entry.mjs");
183
183
  const DEFAULT_BUILD_OUTFILE = "mdcui";
184
+ const MDCUI_DEFAULT_EDIT_ENABLED = typeof MDCUI_DEFAULT_EDIT !== "undefined";
185
+ const MDCUI_DEFAULT_DEMO_ENABLED = typeof MDCUI_DEFAULT_DEMO !== "undefined";
186
+ const MDCUI_DEFAULT_DEMO_WUI_ENABLED = typeof MDCUI_DEFAULT_DEMO_WUI !== "undefined";
187
+ const MDCUI_OVERWRITE_DEMO_ENABLED = typeof MDCUI_OVERWRITE_DEMO !== "undefined";
188
+ let defaultEdit = false;
184
189
  const decoder = new TextDecoder();
185
190
  let _activeTtyStream = null; // set in App.start() for use by the global error handler
186
191
 
@@ -340,7 +345,7 @@ function normalizeEncodingLabel(encoding = "utf-8") {
340
345
 
341
346
  function encodingForPath(pathOrUrl, encoding = DEFAULT_SETTINGS.encoding, inferMdcui = true) {
342
347
  const normalized = normalizeEncodingLabel(encoding);
343
- if (normalized !== "utf-8" || !inferMdcui) return normalized;
348
+ if (normalized !== "utf-8" || !inferMdcui || defaultEdit) return normalized;
344
349
  let pathname = String(pathOrUrl ?? "").replace(/[?#].*$/, "");
345
350
  try {
346
351
  if (isHttpUrl(pathname)) pathname = new URL(pathname).pathname;
@@ -1092,6 +1097,8 @@ function parseArgs(argv) {
1092
1097
  clean: false,
1093
1098
  check: false,
1094
1099
  cat: false,
1100
+ wui: false,
1101
+ printUi: false,
1095
1102
  docs: false,
1096
1103
  exportReadme: false,
1097
1104
  exportCdpMaze: false,
@@ -1099,6 +1106,7 @@ function parseArgs(argv) {
1099
1106
  testapp: false,
1100
1107
  demoList: false,
1101
1108
  demo: null,
1109
+ overwriteDemo: false,
1102
1110
  allowUrl: false,
1103
1111
  buildExe: false,
1104
1112
  buildFor: "",
@@ -1122,6 +1130,8 @@ function parseArgs(argv) {
1122
1130
  else if (arg === "-clean") flags.clean = true;
1123
1131
  else if (arg === "--check") flags.check = true;
1124
1132
  else if (arg === "--cat" || arg === "-cat" || arg === "--ccat" || arg === "-ccat" || arg === "--bat" || arg === "-bat" || arg === "--glow" || arg === "-glow") flags.cat = true;
1133
+ else if (arg === "--wui") flags.wui = true;
1134
+ else if (arg === "--print-ui") flags.printUi = true;
1125
1135
  else if (arg === "--xxd" || arg === "--hexdump") {
1126
1136
  flags.cat = true;
1127
1137
  flags.settings.set("encoding", "hex3");
@@ -1138,12 +1148,16 @@ function parseArgs(argv) {
1138
1148
  else if (arg === "--edit") {
1139
1149
  flags.settings.set("encoding", "utf-8");
1140
1150
  }
1151
+ else if (arg === "--mdcui" || arg === "--tui") {
1152
+ flags.settings.set("encoding", "mdcui");
1153
+ }
1141
1154
  else if (arg === "--docs" || arg === "--readme") flags.docs = true;
1142
1155
  else if (arg === "--export-readme") flags.exportReadme = true;
1143
1156
  else if (arg === "--export-cdp-maze") flags.exportCdpMaze = true;
1144
1157
  else if (arg === "--changelog") flags.changelog = true;
1145
1158
  else if (arg === "--testapp.md") flags.testapp = true;
1146
1159
  else if (arg === "--demo-list") flags.demoList = true;
1160
+ else if (arg === "--overwrite-demo") flags.overwriteDemo = true;
1147
1161
  else if (arg === "--cdp-maze") {
1148
1162
  flags.cdpMaze = true;
1149
1163
  flags.demo = { option: arg, filename: "maze.md", asset: "demos/maze.md" };
@@ -1195,17 +1209,56 @@ function parseArgs(argv) {
1195
1209
  function usage() {
1196
1210
  return [
1197
1211
  `Usage:
1198
- ${pkg.name} [OPTIONS] [FILE.md]
1212
+ ${pkg.name} [OPTIONS] [FILE]
1213
+
1214
+ Execute .md:
1215
+ ${pkg.name} [FILE.md]
1199
1216
  ${pkg.name} --wui [FILE.md]
1217
+ Develop .md:
1218
+ ${pkg.name} --edit [FILE.md]
1219
+ ${pkg.name} --check <FILE.md>
1200
1220
 
1201
1221
  Modes:
1202
- --check FILE.md
1203
- Check heading and fenced-block IDs for collisions, print details, and exit
1204
- Exits 0 when IDs are unique, 1 on collisions, and 2 on usage/read errors
1222
+ --tui, --mdcui, -encoding mdcui
1223
+ .md files use this by default
1224
+ Execute a Markdown App
1225
+ Create a Terminal UI
1226
+ Generate & overwrite
1227
+ .front.js, .back.js,
1228
+ .html, -rpc.js, -server.js
1229
+ beside the .md file
1230
+
1205
1231
  --wui [FILE.md]
1206
- Generate or overwrite Markdown UI files beside FILE.md and start the server
1207
- Without FILE.md, use the existing ./testapp.md without overwriting it
1232
+ Execute a Markdown App
1233
+ Create a Web UI
1234
+ Start a web server
1235
+ Generate & overwrite
1236
+ .front.js, .back.js,
1237
+ .html, -rpc.js, -server.js
1238
+ beside the .md file
1239
+
1240
+ Without FILE.md,
1241
+ default to ./testapp.md
1208
1242
  If ./testapp.md is missing, write the bundled demo there first
1243
+ Combine with any --demo option to start that demo as a Web UI
1244
+
1245
+ --print-ui
1246
+ Must be combined with --wui
1247
+ Print the generated TUI, raw ANSI, and HTML before starting the server
1248
+
1249
+ --kitty
1250
+ Display Markdown images with Kitty graphics and the jsgotty MIME extension
1251
+ --kitty-compat
1252
+ Display Markdown images with Kitty graphics without the non-standard MIME U field
1253
+
1254
+ --edit
1255
+ Open files as editable UTF-8 text
1256
+ Override .md mdcui detection
1257
+
1258
+ --check <FILE.md>
1259
+ Check heading and fenced-block IDs for collisions, print details, and exit
1260
+ Exits 0 when IDs are unique, 1 on collisions, and 2 on usage/read errors
1261
+
1209
1262
  --cat, --ccat, --bat, --glow
1210
1263
  Render file(s) and write to stdout, then exit (.md uses mdcui/createTui)
1211
1264
  A local .md file also writes or overwrites five generated files beside it
@@ -1214,15 +1267,6 @@ Modes:
1214
1267
  --hex3, --hex3gz, --hex3zst
1215
1268
  Set -encoding hex3, hex3gz, or hex3zst for this session
1216
1269
  hex3 shows raw bytes; gz/zst variants compress the same hex3 view
1217
- --edit
1218
- Open files as editable UTF-8 text, overriding .md mdcui detection
1219
- -encoding mdcui
1220
- Render Markdown through runmd.mjs#createTui; .md files use this automatically
1221
- Writes .front.js, .back.js, .html, -rpc.js, and -server.js beside the .md file
1222
- --kitty
1223
- Display Markdown images with Kitty graphics and the jsgotty MIME extension
1224
- --kitty-compat
1225
- Display Markdown images with Kitty graphics without the non-standard MIME U field
1226
1270
 
1227
1271
  Settings:
1228
1272
  -SETTING VALUE
@@ -1230,54 +1274,73 @@ Settings:
1230
1274
  -options
1231
1275
  List all setting names and defaults, then exit.
1232
1276
 
1233
- CDP:
1277
+ CDP(Chrome DevTools Protocol):
1234
1278
  --cdp-maze
1235
1279
  Open the maze demo, start CDP on localhost:9222, and solve it automatically
1280
+ --export-cdp-maze
1281
+ Write or overwrite ./cdp-maze.js with the bundled CDP maze solver & exit
1282
+
1236
1283
  --remote-debugging-port=PORT
1237
1284
  Start CDP (Chrome DevTools Protocol) server on PORT at launch
1238
1285
  --remote-debugging-address=ADDRESS
1239
1286
  Bind CDP server to ADDRESS (default: 127.0.0.1); use 0.0.0.0 for all interfaces
1240
1287
 
1241
1288
  Information:
1242
- -help, -h, --help
1289
+ --help, -h, -help
1243
1290
  Show this help & exit
1244
- -version, -V, --version
1291
+ --version, -V, -version
1245
1292
  Show version+backend info & exit
1246
- --docs, --readme
1293
+
1294
+ --readme, --docs
1247
1295
  Show ${pkg.name}'s README.md & exit
1248
1296
  --export-readme
1249
1297
  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
1298
+
1252
1299
  --changelog
1253
1300
  Show CHANGELOG.md & exit
1254
- -profile, --profile
1301
+ --profile, -profile
1255
1302
  Print startup performance profile and exit
1256
1303
 
1257
1304
  Demo:
1258
1305
  --testapp.md
1259
- Write the bundled testapp.md to stdout & exit
1306
+ Print the bundled testapp.md to stdout & exit
1260
1307
  --demo-list
1261
- List the bundled demos and their command-line options, then exit
1308
+ List the bundled demos & exit
1309
+
1262
1310
  --demo
1263
- Use the existing ./testapp.md without overwriting it, or write the bundled demo if missing
1311
+ Execute an existing ./testapp.md
1312
+ Or write the bundled demo if missing
1264
1313
  Open it in the TUI and write 5 generated files beside it
1314
+
1265
1315
  --demo-<filename>
1266
- Load demos/<filename>.md, preserving an existing ./<filename>.md or writing the bundled copy
1316
+ Execute an existing <filename>.md
1317
+ Or write the bundled demos/<filename>.md if missing
1267
1318
  Open it in the TUI and write 5 generated files beside it
1268
1319
  For example: --demo-select, --demo-todo, or --demo-todo-zh
1320
+
1269
1321
  --demo-imgtool
1270
1322
  Alias for --demo-image-processor
1271
1323
  --demo-imgtool-zh
1272
1324
  Alias for --demo-image-processor.zh-TW
1273
1325
 
1326
+ --overwrite-demo
1327
+ Overwrite an existing local demo with the bundled copy; combine with any --demo option
1328
+
1274
1329
  Remote Markdown:
1275
1330
  --allow-url
1276
1331
  Download HTTP(S) Markdown and, with Kitty mode, its HTTP(S) images; allow its code to run
1277
1332
 
1278
- Experimental:
1279
- --build-exe Build a Bun single-file executable and exit
1280
- --build-for <target> Build a Bun single-file executable for target`
1333
+ Experimental single-exe:
1334
+ --build-exe [BUN_ARGS...]
1335
+ Build a Bun single-file executable & exit
1336
+ --build-for <target> [BUN_ARGS...]
1337
+ Build a Bun single-file executable for target & exit
1338
+
1339
+ [BUN_ARGS...]
1340
+ --define MDCUI_XXX=true
1341
+ More info in --readme
1342
+ https://bun.com/docs/bundler/executables
1343
+ `
1281
1344
  ].join("\n");
1282
1345
  }
1283
1346
 
@@ -8073,19 +8136,42 @@ function printDemoList() {
8073
8136
  console.log(" --demo-imgtool-zh --demo-image-processor.zh-TW");
8074
8137
  }
8075
8138
 
8076
- async function main() {
8077
- if (process.argv[2] === "--wui") {
8078
- process.argv.splice(2, 1);
8079
- const runmd = await import("../runmd.mjs");
8080
- await runmd.main();
8081
- return;
8139
+ async function prepareDemo(flags, rawFiles) {
8140
+ if (!flags.demo) return true;
8141
+ if (flags.demo.error) {
8142
+ console.error(`Invalid demo option ${flags.demo.option}: ${flags.demo.error}`);
8143
+ process.exitCode = 2;
8144
+ return false;
8145
+ }
8146
+ let demoSource;
8147
+ try {
8148
+ demoSource = await bundledMarkdownSource(flags.demo.asset);
8149
+ } catch {
8150
+ console.error(`Unknown demo ${flags.demo.option}: ${flags.demo.asset} was not found`);
8151
+ process.exitCode = 2;
8152
+ return false;
8153
+ }
8154
+ const demoPath = resolve(flags.demo.filename);
8155
+ if (flags.overwriteDemo || !(await Bun.file(demoPath).exists())) {
8156
+ await Bun.write(demoPath, demoSource);
8082
8157
  }
8158
+ rawFiles.splice(0, rawFiles.length, demoPath);
8159
+ return true;
8160
+ }
8083
8161
 
8162
+ async function main() {
8084
8163
  addCheckpoint("Argument Parsing");
8085
8164
 
8086
8165
  await buildEarlyExit(null,DEFAULT_BUILD_OUTFILE)
8166
+ defaultEdit = await Bun.file(join(REPO_ROOT, "src", "MDCUI_DEFAULT_EDIT")).exists();
8087
8167
 
8088
- const { flags, files: rawFiles } = parseArgs(process.argv.slice(2));
8168
+ const runtimeArgs = process.argv.slice(2);
8169
+ const noRuntimeArgs = runtimeArgs.length === 0;
8170
+ if (MDCUI_DEFAULT_EDIT_ENABLED) runtimeArgs.unshift("--edit");
8171
+ if (noRuntimeArgs && MDCUI_DEFAULT_DEMO_ENABLED) runtimeArgs.push("--demo");
8172
+ if (noRuntimeArgs && MDCUI_DEFAULT_DEMO_WUI_ENABLED) runtimeArgs.push("--wui");
8173
+ if (MDCUI_OVERWRITE_DEMO_ENABLED) runtimeArgs.push("--overwrite-demo");
8174
+ const { flags, files: rawFiles } = parseArgs(runtimeArgs);
8089
8175
  kittyImageMode = flags.kittyMode;
8090
8176
  allowRemoteKittyImages = flags.allowUrl;
8091
8177
 
@@ -8096,7 +8182,7 @@ async function main() {
8096
8182
  if (flags.version) {
8097
8183
  const ttsCmd = detectTtsCmd();
8098
8184
  console.log(pkg.name+":",pkg.description)
8099
- console.log(" Rewritten by: Dr. John (醫者小智)")
8185
+ console.log(" Made by: Dr. John (醫者小智)")
8100
8186
  console.log("")
8101
8187
  console.log("Version:", VERSION);
8102
8188
  console.log("Runtime:", `Bun ${Bun.version}`);
@@ -8104,6 +8190,11 @@ async function main() {
8104
8190
  console.log("Http client:",detectHttpBackend());
8105
8191
  console.log("TTS:", ttsCmd ? ttsCmd.cmd[0] : "not found");
8106
8192
  console.log({SUPPORTED_ENCODING_LABELS})
8193
+ console.log("Distribution settings:");
8194
+ console.log(" MDCUI_DEFAULT_EDIT:", (MDCUI_DEFAULT_EDIT_ENABLED || defaultEdit) ? "enabled" : "disabled");
8195
+ console.log(" MDCUI_DEFAULT_DEMO:", MDCUI_DEFAULT_DEMO_ENABLED ? "enabled" : "disabled");
8196
+ console.log(" MDCUI_DEFAULT_DEMO_WUI:", MDCUI_DEFAULT_DEMO_WUI_ENABLED ? "enabled" : "disabled");
8197
+ console.log(" MDCUI_OVERWRITE_DEMO:", MDCUI_OVERWRITE_DEMO_ENABLED ? "enabled" : "disabled");
8107
8198
  const clipboard = new ClipboardManager();
8108
8199
  let osc52Available = false;
8109
8200
  if (process.stdin.isTTY && process.stdout.isTTY) {
@@ -8118,6 +8209,16 @@ async function main() {
8118
8209
  console.log("Clipboard:", backends);
8119
8210
  return;
8120
8211
  }
8212
+ if (flags.wui) {
8213
+ if (!(await prepareDemo(flags, rawFiles))) return;
8214
+ const runmd = await import("../runmd.mjs");
8215
+ await runmd.main(30, {
8216
+ mdpath: rawFiles[0] ?? null,
8217
+ overwriteDemo: flags.overwriteDemo && rawFiles.length === 0,
8218
+ printUi: flags.printUi,
8219
+ });
8220
+ return;
8221
+ }
8121
8222
  if (flags.check) {
8122
8223
  if (rawFiles.length !== 1) {
8123
8224
  console.error("Usage: jsmdcui --check FILE.md");
@@ -8162,24 +8263,7 @@ async function main() {
8162
8263
  return;
8163
8264
  }
8164
8265
  if (flags.demo) {
8165
- if (flags.demo.error) {
8166
- console.error(`Invalid demo option ${flags.demo.option}: ${flags.demo.error}`);
8167
- process.exitCode = 2;
8168
- return;
8169
- }
8170
- let demoSource;
8171
- try {
8172
- demoSource = await bundledMarkdownSource(flags.demo.asset);
8173
- } catch {
8174
- console.error(`Unknown demo ${flags.demo.option}: ${flags.demo.asset} was not found`);
8175
- process.exitCode = 2;
8176
- return;
8177
- }
8178
- const demoPath = resolve(flags.demo.filename);
8179
- if (!(await Bun.file(demoPath).exists())) {
8180
- await Bun.write(demoPath, demoSource);
8181
- }
8182
- rawFiles.splice(0, rawFiles.length, demoPath);
8266
+ if (!(await prepareDemo(flags, rawFiles))) return;
8183
8267
  }
8184
8268
  if (flags.options) {
8185
8269
  for (const [key, value] of Object.entries(defaultAllSettings()).sort(([a], [b]) => a.localeCompare(b))) {
@@ -8192,6 +8276,7 @@ async function main() {
8192
8276
  const config = await new Config({ configDir: flags.configDir }).init();
8193
8277
  config.applyCliSettings(flags.settings);
8194
8278
  const encodingExplicit = flags.settings.has("encoding") || Object.hasOwn(config.parsedSettings, "encoding");
8279
+ const inputFiletype = flags.settings.has("filetype") ? String(flags.settings.get("filetype")) : null;
8195
8280
  syncEditorSettings(config);
8196
8281
 
8197
8282
  addCheckpoint("Runtime Registry Init");
@@ -8203,7 +8288,14 @@ async function main() {
8203
8288
  const syntaxDefinitions = await loadSyntaxDefinitions(runtime);
8204
8289
 
8205
8290
  if (flags.cat) {
8206
- await catFiles(rawFiles, colorscheme, syntaxDefinitions, config.getGlobalOption("encoding"), !encodingExplicit);
8291
+ await catFiles(
8292
+ rawFiles,
8293
+ colorscheme,
8294
+ syntaxDefinitions,
8295
+ config.getGlobalOption("encoding"),
8296
+ !encodingExplicit,
8297
+ inputFiletype,
8298
+ );
8207
8299
  return;
8208
8300
  }
8209
8301
 
@@ -8268,6 +8360,7 @@ async function main() {
8268
8360
  runtime,
8269
8361
  jsPlugins,
8270
8362
  encodingExplicit,
8363
+ inputFiletype,
8271
8364
  allowUrl: flags.allowUrl,
8272
8365
  kittyMode: flags.kittyMode,
8273
8366
  };
@@ -8725,9 +8818,12 @@ function getSelectionText(buf, selection) {
8725
8818
 
8726
8819
  function attachSyntax(buffer, context, path, text) {
8727
8820
  buffer._syntaxContext = context;
8728
- const def = detectBufferSyntax(context.syntaxDefinitions, path, text);
8821
+ const explicitFiletype = context.inputFiletype;
8822
+ const def = explicitFiletype == null
8823
+ ? detectBufferSyntax(context.syntaxDefinitions, path, text)
8824
+ : context.syntaxDefinitions?.find((candidate) => candidate.filetype === explicitFiletype) ?? null;
8729
8825
  buffer.syntaxDefinition = def;
8730
- buffer.filetype = def?.filetype ?? "unknown";
8826
+ buffer.filetype = explicitFiletype == null ? (def?.filetype ?? "unknown") : String(explicitFiletype);
8731
8827
  buffer.Settings.filetype = buffer.filetype;
8732
8828
  buffer.highlighter = def ? new Highlighter(def, context.syntaxDefinitions ?? []) : null;
8733
8829
  buffer._highlightCache = null;
@@ -8805,7 +8901,7 @@ function syncEditorSettings(config) {
8805
8901
  }
8806
8902
  }
8807
8903
 
8808
- async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAULT_SETTINGS.encoding, inferMdcui = true) {
8904
+ async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAULT_SETTINGS.encoding, inferMdcui = true, filetype = null) {
8809
8905
  const targets = files.length > 0 ? files.map((f) => ({ path: f, stdin: false })) : [{ path: null, stdin: true }];
8810
8906
  for (const { path: filePath, stdin } of targets) {
8811
8907
  let content;
@@ -8836,7 +8932,7 @@ async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAUL
8836
8932
  process.stdout.write(ansiContent ?? content);
8837
8933
  if (!(ansiContent ?? content).endsWith("\n")) process.stdout.write("\n");
8838
8934
  continue;
8839
- } else if (effectivePath && /\.md$/i.test(effectivePath)) {
8935
+ } else if (filetype == null && effectivePath && /\.md$/i.test(effectivePath)) {
8840
8936
  process.stdout.write(
8841
8937
  Bun.markdown.ansi(content,{
8842
8938
  hyperlinks:true
@@ -8845,11 +8941,13 @@ async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAUL
8845
8941
  continue;
8846
8942
  }
8847
8943
  const lines = normalizeBufferText(content).split("\n");
8848
- const def = detectSyntax(syntaxDefinitions, {
8849
- path: effectivePath ?? "",
8850
- firstLine: lines[0] ?? "",
8851
- lines: lines.slice(0, 50),
8852
- });
8944
+ const def = filetype == null
8945
+ ? detectSyntax(syntaxDefinitions, {
8946
+ path: effectivePath ?? "",
8947
+ firstLine: lines[0] ?? "",
8948
+ lines: lines.slice(0, 50),
8949
+ })
8950
+ : syntaxDefinitions.find((candidate) => candidate.filetype === filetype) ?? null;
8853
8951
  const highlighter = def ? new Highlighter(def, syntaxDefinitions) : null;
8854
8952
  if (!highlighter) {
8855
8953
  process.stdout.write(content);
@@ -78,3 +78,62 @@ test("--edit overrides the .md mdcui default with utf8", async () => {
78
78
  await rm(dir, { recursive: true, force: true });
79
79
  }
80
80
  });
81
+
82
+ test("--mdcui and --tui are equivalent to -encoding mdcui", async () => {
83
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-cat-mdcui-"));
84
+ const markdownPath = join(dir, "sample.md");
85
+ await writeFile(markdownPath, "# Heading\n\n- one\n- two\n");
86
+
87
+ try {
88
+ const flag = Bun.spawnSync([bunBin, tui, "-cat", "--mdcui", markdownPath], {
89
+ cwd: dir,
90
+ stdout: "pipe",
91
+ stderr: "pipe",
92
+ });
93
+ const encoding = Bun.spawnSync([bunBin, tui, "-cat", "-encoding", "mdcui", markdownPath], {
94
+ cwd: dir,
95
+ stdout: "pipe",
96
+ stderr: "pipe",
97
+ });
98
+ const tuiFlag = Bun.spawnSync([bunBin, tui, "-cat", "--tui", markdownPath], {
99
+ cwd: dir,
100
+ stdout: "pipe",
101
+ stderr: "pipe",
102
+ });
103
+
104
+ expect(flag.exitCode).toBe(0);
105
+ expect(encoding.exitCode).toBe(0);
106
+ expect(tuiFlag.exitCode).toBe(0);
107
+ expect(flag.stdout.toString()).toBe(encoding.stdout.toString());
108
+ expect(tuiFlag.stdout.toString()).toBe(encoding.stdout.toString());
109
+ } finally {
110
+ await rm(dir, { recursive: true, force: true });
111
+ }
112
+ });
113
+
114
+ test("-filetype forces syntax highlighting independently of the filename", async () => {
115
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-cat-filetype-"));
116
+ const textPath = join(dir, "sample.txt");
117
+ await writeFile(textPath, "const answer = 42;\n");
118
+
119
+ try {
120
+ const automatic = Bun.spawnSync([bunBin, tui, "-cat", textPath], {
121
+ cwd: dir,
122
+ stdout: "pipe",
123
+ stderr: "pipe",
124
+ });
125
+ const javascript = Bun.spawnSync([bunBin, tui, "-cat", "-filetype", "javascript", textPath], {
126
+ cwd: dir,
127
+ stdout: "pipe",
128
+ stderr: "pipe",
129
+ });
130
+
131
+ expect(automatic.exitCode).toBe(0);
132
+ expect(javascript.exitCode).toBe(0);
133
+ expect(automatic.stdout.toString()).not.toContain("\x1b[");
134
+ expect(javascript.stdout.toString()).toContain("\x1b[");
135
+ expect(Bun.stripANSI(javascript.stdout.toString()).trimEnd()).toBe("const answer = 42;");
136
+ } finally {
137
+ await rm(dir, { recursive: true, force: true });
138
+ }
139
+ });
@@ -2,6 +2,7 @@ import { expect, test } from "bun:test";
2
2
  import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { readMarkdownInput } from "../runmd.mjs";
5
6
 
6
7
  const tui = join(import.meta.dir, "..", "tui");
7
8
  const bunBin = Bun.which("bun") || process.argv0;
@@ -14,8 +15,8 @@ test("--help describes the non-overwriting demo behavior", () => {
14
15
 
15
16
  expect(result.exitCode).toBe(0);
16
17
  const output = result.stdout.toString();
17
- expect(output).toContain("use the existing ./testapp.md without overwriting it");
18
- expect(output).toContain("If ./testapp.md is missing, write the bundled demo there first");
18
+ expect(output).toContain("Execute an existing ./testapp.md");
19
+ expect(output).toContain("Or write the bundled demo if missing");
19
20
  expect(output).toContain("--demo-<filename>");
20
21
  expect(output).toContain("demos/<filename>.md");
21
22
  expect(output.match(/Open it in the TUI and write 5 generated files beside it/g)?.length).toBe(2);
@@ -97,6 +98,44 @@ test("--demo preserves an existing testapp.md", async () => {
97
98
  }
98
99
  });
99
100
 
101
+ test("--overwrite-demo replaces an existing demo with the bundled copy", async () => {
102
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-overwrite-"));
103
+ const existing = "# Replace my demo\n";
104
+ try {
105
+ await writeFile(join(dir, "testapp.md"), existing);
106
+ const result = Bun.spawnSync(
107
+ [bunBin, tui, "--overwrite-demo", "--demo", "-cat", "-encoding", "utf8"],
108
+ {
109
+ cwd: dir,
110
+ stdout: "pipe",
111
+ stderr: "pipe",
112
+ },
113
+ );
114
+
115
+ expect(result.exitCode).toBe(0);
116
+ const written = await readFile(join(dir, "testapp.md"), "utf8");
117
+ expect(written).toContain("計算機");
118
+ expect(written).not.toBe(existing);
119
+ expect(Bun.stripANSI(result.stdout.toString())).toContain("計算機");
120
+ } finally {
121
+ await rm(dir, { recursive: true, force: true });
122
+ }
123
+ });
124
+
125
+ test("WUI demo loading can overwrite an existing testapp.md", async () => {
126
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-wui-demo-overwrite-"));
127
+ const demoPath = join(dir, "testapp.md");
128
+ try {
129
+ await writeFile(demoPath, "# Replace my WUI demo\n");
130
+ const source = await readMarkdownInput(demoPath, true);
131
+
132
+ expect(source).toContain("計算機");
133
+ expect(await readFile(demoPath, "utf8")).toBe(source);
134
+ } finally {
135
+ await rm(dir, { recursive: true, force: true });
136
+ }
137
+ });
138
+
100
139
  test("--demo-<filename> automatically loads a matching demos file", async () => {
101
140
  const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-generic-"));
102
141
  try {