jsmdcui 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +3 -0
- package/package.json +1 -1
- package/runmd.mjs +1 -0
- package/runtime/help/help.md +3 -0
- package/src/buffer/backup.js +8 -3
- package/src/config/config.js +3 -0
- package/src/config/defaults.js +1 -1
- package/src/index.js +132 -35
- package/tests/backup.test.js +25 -0
- package/tests/cat-markdown.test.js +79 -0
- package/tests/config-settings.test.js +52 -0
- package/tests/demo.test.js +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,86 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-visible changes to jsmdcui are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-07-16
|
|
6
|
+
|
|
7
|
+
This release separates executable Markdown UI views from ordinary editable
|
|
8
|
+
buffers, makes encoding behavior predictable across opens, tabs, splits, and
|
|
9
|
+
reopens, and adds explicit demo and trusted-remote execution modes.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Add `--demo`. It outputs and overwrites `./testapp.md`, opens it as a terminal
|
|
14
|
+
Markdown UI, and writes the five generated companion files beside it.
|
|
15
|
+
- Add `--allow-url` for trusted remote Markdown apps. It downloads an HTTP(S)
|
|
16
|
+
resource into the current directory, opens the downloaded copy through the
|
|
17
|
+
normal local Markdown UI pipeline, writes the five generated companion files,
|
|
18
|
+
and permits its embedded frontend and backend code to run.
|
|
19
|
+
- Add regression coverage for global encoding handling, mdcui backup isolation,
|
|
20
|
+
the bundled demo workflow, implicit Markdown rendering, and explicit UTF-8
|
|
21
|
+
editing.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Treat `mdcui` buffers as independent, derived, read-only views. Every mdcui
|
|
26
|
+
open creates a new buffer instead of entering the same-path editable-buffer
|
|
27
|
+
cache.
|
|
28
|
+
- Continue sharing ordinary buffers opened from the same absolute path.
|
|
29
|
+
UTF-8 and other non-mdcui views share content, modification state, saves, and
|
|
30
|
+
reopens.
|
|
31
|
+
- Re-evaluate buffer sharing after `reopen`:
|
|
32
|
+
- reopening an mdcui view as a normal encoding joins an existing same-path
|
|
33
|
+
editable buffer, or registers itself when no such buffer exists;
|
|
34
|
+
- reopening a shared editable view as mdcui detaches only the current pane
|
|
35
|
+
into a new mdcui buffer and leaves the other shared panes unchanged.
|
|
36
|
+
- Make interactive `open`, `tab`, `vsplit`, and `hsplit` operations perform
|
|
37
|
+
fresh `.md` automatic detection for files that are not already represented
|
|
38
|
+
by a normal cached buffer.
|
|
39
|
+
- Ignore a top-level global `encoding` value read from `settings.json`.
|
|
40
|
+
Encoding is selected per invocation or buffer; command-line encoding options
|
|
41
|
+
remain effective for the current run and are not persisted.
|
|
42
|
+
- Organize `--testapp.md` and `--demo` under a dedicated `Demo` section in
|
|
43
|
+
`--help`, and document commands that overwrite source or generated files.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- Fix `.md` files opened from inside the editor failing to enter mdcui mode.
|
|
48
|
+
- Fix encoding changes leaking between independent mdcui tabs.
|
|
49
|
+
- Fix `reopen mdcui` mutating every pane that shared the original editable
|
|
50
|
+
buffer.
|
|
51
|
+
- Fix mdcui-to-UTF-8 reopens remaining outside the normal same-path buffer
|
|
52
|
+
cache.
|
|
53
|
+
- Delay applying an explicit reopen encoding until the reopen is confirmed, so
|
|
54
|
+
canceling the save prompt does not alter the shared buffer.
|
|
55
|
+
|
|
56
|
+
### Security
|
|
57
|
+
|
|
58
|
+
- Exclude mdcui buffers completely from crash-recovery backups: they do not
|
|
59
|
+
create, apply, prompt for, or remove backups. This prevents a derived
|
|
60
|
+
read-only view from consuming or deleting a backup belonging to an editable
|
|
61
|
+
view of the same Markdown source.
|
|
62
|
+
- Remote HTTP(S) Markdown remains non-executable and does not write companion
|
|
63
|
+
files by default.
|
|
64
|
+
- `--allow-url` is an explicit trust boundary. It writes downloaded content and
|
|
65
|
+
generated JavaScript/HTML files into the current directory, may overwrite
|
|
66
|
+
files with the same names, and runs code from the remote document with the
|
|
67
|
+
permissions of the jsmdcui process. Use it only with trusted URLs and in a
|
|
68
|
+
directory where those writes are safe.
|
|
69
|
+
|
|
70
|
+
## [0.3.1] - 2026-07-16
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- Add `--edit` to open Markdown files as editable UTF-8 source instead of
|
|
75
|
+
automatically entering mdcui mode.
|
|
76
|
+
- Add `--cat` regression coverage for implicit mdcui rendering and explicit
|
|
77
|
+
UTF-8 overrides.
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- Prevent an explicit encoding choice from being replaced by automatic mdcui
|
|
82
|
+
detection.
|
|
83
|
+
- Correct terminal raw-mode handling around log and prompt output.
|
|
84
|
+
|
|
5
85
|
## [0.3.0] - 2026-07-16
|
|
6
86
|
|
|
7
87
|
Initial usable release, based on the bunmicro terminal editor.
|
package/README.md
CHANGED
|
@@ -78,8 +78,11 @@ The command table below uses the cloned-source form. If you use npx, replace
|
|
|
78
78
|
| Command | Result |
|
|
79
79
|
| --- | --- |
|
|
80
80
|
| `bun src/index.js app.md` | Render `app.md` as a read-only terminal UI and write five generated files beside it. |
|
|
81
|
+
| `bun src/index.js --edit app.md` | Open `app.md` as editable UTF-8 source, overriding automatic Markdown UI detection. |
|
|
81
82
|
| `bun src/index.js --cat app.md` | Render the terminal version to stdout, write five generated files beside it, and exit. |
|
|
82
83
|
| `bun src/index.js --testapp.md` | Write the bundled `testapp.md` source to stdout and exit. |
|
|
84
|
+
| `bun src/index.js --demo` | Outputs & overwrites `./testapp.md`, opens it in the terminal UI, and writes 5 generated files beside it. |
|
|
85
|
+
| `bun src/index.js --allow-url URL.md` | Download HTTP(S) Markdown to the current directory, write 5 generated files, and allow its embedded code to run. Only use trusted URLs. |
|
|
83
86
|
| `bun src/index.js --wui` | Use local `testapp.md` when present, otherwise use the bundled demo; write five generated files in the current directory, then print and serve a random URL. |
|
|
84
87
|
| `bun src/index.js --wui app.md` | Write five generated files beside `app.md`, then print and serve a random URL. |
|
|
85
88
|
| `PORT=8080 bun src/index.js --wui app.md` | Start the browser UI on another port. |
|
package/package.json
CHANGED
package/runmd.mjs
CHANGED
package/runtime/help/help.md
CHANGED
|
@@ -78,8 +78,11 @@ The command table below uses the cloned-source form. If you use npx, replace
|
|
|
78
78
|
| Command | Result |
|
|
79
79
|
| --- | --- |
|
|
80
80
|
| `bun src/index.js app.md` | Render `app.md` as a read-only terminal UI and write five generated files beside it. |
|
|
81
|
+
| `bun src/index.js --edit app.md` | Open `app.md` as editable UTF-8 source, overriding automatic Markdown UI detection. |
|
|
81
82
|
| `bun src/index.js --cat app.md` | Render the terminal version to stdout, write five generated files beside it, and exit. |
|
|
82
83
|
| `bun src/index.js --testapp.md` | Write the bundled `testapp.md` source to stdout and exit. |
|
|
84
|
+
| `bun src/index.js --demo` | Outputs & overwrites `./testapp.md`, opens it in the terminal UI, and writes 5 generated files beside it. |
|
|
85
|
+
| `bun src/index.js --allow-url URL.md` | Download HTTP(S) Markdown to the current directory, write 5 generated files, and allow its embedded code to run. Only use trusted URLs. |
|
|
83
86
|
| `bun src/index.js --wui` | Use local `testapp.md` when present, otherwise use the bundled demo; write five generated files in the current directory, then print and serve a random URL. |
|
|
84
87
|
| `bun src/index.js --wui app.md` | Write five generated files beside `app.md`, then print and serve a random URL. |
|
|
85
88
|
| `PORT=8080 bun src/index.js --wui app.md` | Start the browser UI on another port. |
|
package/src/buffer/backup.js
CHANGED
|
@@ -5,9 +5,14 @@ import { existsSync, statSync, unlinkSync } from "node:fs";
|
|
|
5
5
|
import { mkdir, writeFile, readFile, rename } from "node:fs/promises";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
+
import { isMdcuiEncoding } from "../runtime/encodings.js";
|
|
8
9
|
|
|
9
10
|
export const BACKUP_SUFFIX = ".micro-backup";
|
|
10
11
|
|
|
12
|
+
function isMdcuiBuffer(buf) {
|
|
13
|
+
return isMdcuiEncoding(buf?.encoding ?? buf?.Settings?.encoding);
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
export function getBackupDir(buf, configDir) {
|
|
12
17
|
const raw = String(buf?.Settings?.backupdir ?? "");
|
|
13
18
|
if (!raw) return join(configDir, "backups");
|
|
@@ -49,7 +54,7 @@ export function determineBackupPath(backupDirPath, absPath) {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export async function writeBackup(buf, configDir, path = buf?.AbsPath ?? buf?.path, { force = false } = {}) {
|
|
52
|
-
if ((!force && !buf?.Settings?.backup) || !path || buf.type !== "default") return false;
|
|
57
|
+
if (isMdcuiBuffer(buf) || (!force && !buf?.Settings?.backup) || !path || buf.type !== "default") return false;
|
|
53
58
|
const dir = getBackupDir(buf, configDir);
|
|
54
59
|
await mkdir(dir, { recursive: true });
|
|
55
60
|
const { name, resolveName } = determineBackupPath(dir, path);
|
|
@@ -66,7 +71,7 @@ export async function writeBackup(buf, configDir, path = buf?.AbsPath ?? buf?.pa
|
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
export function removeBackup(buf, configDir, path = buf?.AbsPath ?? buf?.path) {
|
|
69
|
-
if (buf?.Settings?.permbackup || buf?._forceKeepBackup) return;
|
|
74
|
+
if (isMdcuiBuffer(buf) || buf?.Settings?.permbackup || buf?._forceKeepBackup) return;
|
|
70
75
|
if (!path || buf.type !== "default") return;
|
|
71
76
|
const dir = getBackupDir(buf, configDir);
|
|
72
77
|
const { name, resolveName } = determineBackupPath(dir, path);
|
|
@@ -77,7 +82,7 @@ export function removeBackup(buf, configDir, path = buf?.AbsPath ?? buf?.path) {
|
|
|
77
82
|
// promptFn(msg) -> Promise<string>
|
|
78
83
|
// Returns { recovered: bool, abort: bool }
|
|
79
84
|
export async function applyBackup(buf, configDir, promptFn) {
|
|
80
|
-
if (!buf?.Settings?.backup || buf?.Settings?.permbackup) return { recovered: false, abort: false };
|
|
85
|
+
if (isMdcuiBuffer(buf) || !buf?.Settings?.backup || buf?.Settings?.permbackup) return { recovered: false, abort: false };
|
|
81
86
|
if (!buf.path || buf.type !== "default") return { recovered: false, abort: false };
|
|
82
87
|
|
|
83
88
|
const dir = getBackupDir(buf, configDir);
|
package/src/config/config.js
CHANGED
|
@@ -27,6 +27,9 @@ export class Config {
|
|
|
27
27
|
const text = await readFile(path, "utf8");
|
|
28
28
|
if (text.trimStart().startsWith("null") || text.trim() === "") return;
|
|
29
29
|
this.parsedSettings = Bun.JSON5.parse(text);
|
|
30
|
+
// jsmdcui chooses the input encoding per invocation/buffer. A persisted
|
|
31
|
+
// global encoding must neither affect startup nor survive the next save.
|
|
32
|
+
delete this.parsedSettings.encoding;
|
|
30
33
|
this.applyParsedSettings(this.parsedSettings);
|
|
31
34
|
}
|
|
32
35
|
|
package/src/config/defaults.js
CHANGED
|
@@ -98,7 +98,7 @@ export const OPTION_CHOICES = {
|
|
|
98
98
|
|
|
99
99
|
// Settings that are buffer-local only and must never be written to the global config file.
|
|
100
100
|
// Mirrors Go micro's config.LocalSettings.
|
|
101
|
-
export const LOCAL_SETTINGS = new Set(["readonly", "filetype", "fileformat"]);
|
|
101
|
+
export const LOCAL_SETTINGS = new Set(["readonly", "filetype", "fileformat", "encoding"]);
|
|
102
102
|
|
|
103
103
|
export function defaultAllSettings() {
|
|
104
104
|
return { ...DEFAULT_COMMON_SETTINGS, ...DEFAULT_GLOBAL_ONLY_SETTINGS };
|
package/src/index.js
CHANGED
|
@@ -304,14 +304,14 @@ function encodeHex3Text(text) {
|
|
|
304
304
|
return decodeBinaryBytes(Buffer.from(text, "latin1"));
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
async function readTextFileWithEncoding(path, encoding = "utf-8") {
|
|
307
|
+
async function readTextFileWithEncoding(path, encoding = "utf-8", inferMdcui = true) {
|
|
308
308
|
const bytes = new Uint8Array(await Bun.file(path).arrayBuffer());
|
|
309
|
-
return decodeAndRenderTextBytes(bytes, encodingForPath(path, encoding), process.stdout.columns || 80, path);
|
|
309
|
+
return decodeAndRenderTextBytes(bytes, encodingForPath(path, encoding, inferMdcui), process.stdout.columns || 80, path);
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
async function fetchTextWithEncoding(url, encoding = "utf-8") {
|
|
312
|
+
async function fetchTextWithEncoding(url, encoding = "utf-8", inferMdcui = true) {
|
|
313
313
|
const bytes = await fetchHttpBytes(url);
|
|
314
|
-
return decodeAndRenderTextBytes(new Uint8Array(bytes), encodingForPath(url, encoding));
|
|
314
|
+
return decodeAndRenderTextBytes(new Uint8Array(bytes), encodingForPath(url, encoding, inferMdcui));
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
function normalizeEncodingLabel(encoding = "utf-8") {
|
|
@@ -321,9 +321,9 @@ function normalizeEncodingLabel(encoding = "utf-8") {
|
|
|
321
321
|
return new TextDecoder(s).encoding;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
function encodingForPath(pathOrUrl, encoding = DEFAULT_SETTINGS.encoding) {
|
|
324
|
+
function encodingForPath(pathOrUrl, encoding = DEFAULT_SETTINGS.encoding, inferMdcui = true) {
|
|
325
325
|
const normalized = normalizeEncodingLabel(encoding);
|
|
326
|
-
if (normalized !== "utf-8") return normalized;
|
|
326
|
+
if (normalized !== "utf-8" || !inferMdcui) return normalized;
|
|
327
327
|
let pathname = String(pathOrUrl ?? "").replace(/[?#].*$/, "");
|
|
328
328
|
try {
|
|
329
329
|
if (isHttpUrl(pathname)) pathname = new URL(pathname).pathname;
|
|
@@ -869,6 +869,8 @@ function parseArgs(argv) {
|
|
|
869
869
|
docs: false,
|
|
870
870
|
changelog: false,
|
|
871
871
|
testapp: false,
|
|
872
|
+
demo: false,
|
|
873
|
+
allowUrl: false,
|
|
872
874
|
buildExe: false,
|
|
873
875
|
buildFor: "",
|
|
874
876
|
configDir: "",
|
|
@@ -901,9 +903,14 @@ function parseArgs(argv) {
|
|
|
901
903
|
else if (arg === "--hex3zst") {
|
|
902
904
|
flags.settings.set("encoding", "hex3zst");
|
|
903
905
|
}
|
|
906
|
+
else if (arg === "--edit") {
|
|
907
|
+
flags.settings.set("encoding", "utf-8");
|
|
908
|
+
}
|
|
904
909
|
else if (arg === "--docs" || arg === "--readme") flags.docs = true;
|
|
905
910
|
else if (arg === "--changelog") flags.changelog = true;
|
|
906
911
|
else if (arg === "--testapp.md") flags.testapp = true;
|
|
912
|
+
else if (arg === "--demo") flags.demo = true;
|
|
913
|
+
else if (arg === "--allow-url") flags.allowUrl = true;
|
|
907
914
|
else if (arg === "--build-exe") flags.buildExe = true;
|
|
908
915
|
else if (arg === "--build-for") flags.buildFor = argv[++i] ?? "";
|
|
909
916
|
else if (arg === "-debug") flags.debug = true;
|
|
@@ -946,6 +953,8 @@ Modes:
|
|
|
946
953
|
--hex3, --hex3gz, --hex3zst
|
|
947
954
|
Set -encoding hex3, hex3gz, or hex3zst for this session
|
|
948
955
|
hex3 shows raw bytes; gz/zst variants compress the same hex3 view
|
|
956
|
+
--edit
|
|
957
|
+
Open files as editable UTF-8 text, overriding .md mdcui detection
|
|
949
958
|
-encoding mdcui
|
|
950
959
|
Render Markdown through runmd.mjs#createTui; .md files use this automatically
|
|
951
960
|
Writes .front.js, .back.js, .html, -rpc.js, and -server.js beside the .md file
|
|
@@ -971,11 +980,19 @@ Information:
|
|
|
971
980
|
Show ${pkg.name}'s README.md & exit
|
|
972
981
|
--changelog
|
|
973
982
|
Show CHANGELOG.md & exit
|
|
974
|
-
--testapp.md
|
|
975
|
-
Write testapp.md to stdout & exit
|
|
976
983
|
-profile, --profile
|
|
977
984
|
Print startup performance profile and exit
|
|
978
985
|
|
|
986
|
+
Demo:
|
|
987
|
+
--testapp.md
|
|
988
|
+
Write the bundled testapp.md to stdout & exit
|
|
989
|
+
--demo
|
|
990
|
+
Outputs & overwrites ./testapp.md, opens it in the TUI, and writes 5 generated files
|
|
991
|
+
|
|
992
|
+
Remote Markdown:
|
|
993
|
+
--allow-url
|
|
994
|
+
Download HTTP(S) Markdown to cwd, write 5 generated files, and allow its code to run
|
|
995
|
+
|
|
979
996
|
Experimental:
|
|
980
997
|
--build-exe Build a Bun single-file executable and exit
|
|
981
998
|
--build-for <target> Build a Bun single-file executable for target`
|
|
@@ -1129,7 +1146,7 @@ class BufferModel {
|
|
|
1129
1146
|
let text = "";
|
|
1130
1147
|
let readonly = false;
|
|
1131
1148
|
let modTimeMs = null;
|
|
1132
|
-
let encoding = context.config?.globalSettings?.encoding ?? DEFAULT_SETTINGS.encoding;
|
|
1149
|
+
let encoding = context.inputEncoding ?? context.config?.globalSettings?.encoding ?? DEFAULT_SETTINGS.encoding;
|
|
1133
1150
|
let ansiStyleLines = null;
|
|
1134
1151
|
let ansiText = null;
|
|
1135
1152
|
let sourceText = null;
|
|
@@ -1140,7 +1157,7 @@ class BufferModel {
|
|
|
1140
1157
|
if (info.isDirectory()) throw new Error(`${path} is a directory`);
|
|
1141
1158
|
readonly = !canWritePath(path);
|
|
1142
1159
|
modTimeMs = info.mtimeMs;
|
|
1143
|
-
const decoded = await readTextFileWithEncoding(path, encoding);
|
|
1160
|
+
const decoded = await readTextFileWithEncoding(path, encoding, !context.encodingExplicit);
|
|
1144
1161
|
text = decoded.text;
|
|
1145
1162
|
encoding = decoded.encoding;
|
|
1146
1163
|
ansiStyleLines = decoded.ansiStyleLines ?? null;
|
|
@@ -1536,7 +1553,7 @@ class BufferModel {
|
|
|
1536
1553
|
async reopen(context = {}) {
|
|
1537
1554
|
if (!this.path) return;
|
|
1538
1555
|
if (isHttpUrl(this.path)) {
|
|
1539
|
-
const decoded = await fetchTextWithEncoding(this.path, this.Settings.encoding ?? this.encoding);
|
|
1556
|
+
const decoded = await fetchTextWithEncoding(this.path, this.Settings.encoding ?? this.encoding, false);
|
|
1540
1557
|
const text = decoded.text;
|
|
1541
1558
|
this.encoding = decoded.encoding;
|
|
1542
1559
|
this.Settings.encoding = decoded.encoding;
|
|
@@ -1568,7 +1585,7 @@ class BufferModel {
|
|
|
1568
1585
|
}
|
|
1569
1586
|
const info = statSync(this.path);
|
|
1570
1587
|
if (info.isDirectory()) throw new Error(`${this.path} is a directory`);
|
|
1571
|
-
const decoded = await readTextFileWithEncoding(this.path, this.Settings.encoding ?? this.encoding);
|
|
1588
|
+
const decoded = await readTextFileWithEncoding(this.path, this.Settings.encoding ?? this.encoding, false);
|
|
1572
1589
|
const text = decoded.text;
|
|
1573
1590
|
this.encoding = decoded.encoding;
|
|
1574
1591
|
this.Settings.encoding = decoded.encoding;
|
|
@@ -4624,7 +4641,7 @@ class App {
|
|
|
4624
4641
|
async openInPane(path) {
|
|
4625
4642
|
try {
|
|
4626
4643
|
const previous = this.pane.buffer;
|
|
4627
|
-
const buffer = await loadBufferForPath(path, this.context);
|
|
4644
|
+
const buffer = await loadBufferForPath(path, this.context, {}, { interactive: true });
|
|
4628
4645
|
this.pane.buffer = buffer;
|
|
4629
4646
|
this.pane.selection = null;
|
|
4630
4647
|
if (previous !== buffer) this._closeBufferIfUnused(previous);
|
|
@@ -4637,7 +4654,7 @@ class App {
|
|
|
4637
4654
|
|
|
4638
4655
|
async openFile(path) {
|
|
4639
4656
|
try {
|
|
4640
|
-
const buffer = await loadBufferForPath(path, this.context);
|
|
4657
|
+
const buffer = await loadBufferForPath(path, this.context, {}, { interactive: true });
|
|
4641
4658
|
if (isEmptyUntitledBuffer(this.buffer)) {
|
|
4642
4659
|
const previous = this.pane.buffer;
|
|
4643
4660
|
this.pane.buffer = buffer;
|
|
@@ -4655,6 +4672,34 @@ class App {
|
|
|
4655
4672
|
}
|
|
4656
4673
|
}
|
|
4657
4674
|
|
|
4675
|
+
reconcileReopenedBuffer(buffer) {
|
|
4676
|
+
if (!buffer || isHttpUrl(buffer.path)) return buffer;
|
|
4677
|
+
const map = this.context?._openBuffers;
|
|
4678
|
+
if (!map) return buffer;
|
|
4679
|
+
const absPath = resolve(buffer.AbsPath || buffer.path);
|
|
4680
|
+
|
|
4681
|
+
if (isMdcuiEncoding(buffer.encoding)) {
|
|
4682
|
+
if (map.get(absPath) === buffer) map.delete(absPath);
|
|
4683
|
+
buffer._openBufferMap = null;
|
|
4684
|
+
return buffer;
|
|
4685
|
+
}
|
|
4686
|
+
|
|
4687
|
+
const existing = map.get(absPath);
|
|
4688
|
+
if (existing && existing !== buffer && !isMdcuiEncoding(existing.encoding)) {
|
|
4689
|
+
for (const tab of this.tabs) {
|
|
4690
|
+
for (const pane of tab.panes()) {
|
|
4691
|
+
if (pane.buffer === buffer) pane.buffer = existing;
|
|
4692
|
+
if (pane.prevBuffer === buffer) pane.prevBuffer = existing;
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
return existing;
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
buffer._openBufferMap = map;
|
|
4699
|
+
map.set(absPath, buffer);
|
|
4700
|
+
return buffer;
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4658
4703
|
async save({ force = false } = {}) {
|
|
4659
4704
|
if (isEditLockedBuffer(this.buffer)) { this.message = "Can't save under readonly mode"; return; }
|
|
4660
4705
|
if (!force && this.buffer?.readonly) { this.message = "Can't save under readonly mode"; return; }
|
|
@@ -5110,9 +5155,10 @@ class App {
|
|
|
5110
5155
|
}
|
|
5111
5156
|
case "reopen": {
|
|
5112
5157
|
if (!buf?.path) { this.message = "No file to reopen"; break; }
|
|
5158
|
+
let requestedEncoding = null;
|
|
5113
5159
|
if (cmdArgs[0]) {
|
|
5114
5160
|
try {
|
|
5115
|
-
|
|
5161
|
+
requestedEncoding = normalizeEncodingLabel(cmdArgs[0]);
|
|
5116
5162
|
} catch (error) {
|
|
5117
5163
|
this.message = String(error.message || error);
|
|
5118
5164
|
break;
|
|
@@ -5120,9 +5166,24 @@ class App {
|
|
|
5120
5166
|
}
|
|
5121
5167
|
const doReopen = async () => {
|
|
5122
5168
|
try {
|
|
5169
|
+
if (isMdcuiEncoding(requestedEncoding)) {
|
|
5170
|
+
const detachedContext = { ...this.context, inputEncoding: "mdcui", encodingExplicit: true };
|
|
5171
|
+
const reopened = await loadBufferForPath(buf.path, detachedContext);
|
|
5172
|
+
const previous = this.pane.buffer;
|
|
5173
|
+
this.pane.buffer = reopened;
|
|
5174
|
+
this.pane.selection = null;
|
|
5175
|
+
if (previous !== reopened) this._closeBufferIfUnused(previous);
|
|
5176
|
+
await this.context.plugins?.run("onBufferOpen", reopened);
|
|
5177
|
+
await this.context.jsPlugins?.run("onBufferOpen", reopened);
|
|
5178
|
+
this.message = `Reopened ${reopened.name} as ${reopened.encoding}`;
|
|
5179
|
+
this.render();
|
|
5180
|
+
return;
|
|
5181
|
+
}
|
|
5182
|
+
if (requestedEncoding) buf.SetOption("encoding", requestedEncoding);
|
|
5123
5183
|
await buf.reopen(this.context);
|
|
5124
|
-
|
|
5125
|
-
this.
|
|
5184
|
+
const reopened = this.reconcileReopenedBuffer(buf);
|
|
5185
|
+
if (this.pane?.buffer === reopened) this.pane.selection = null;
|
|
5186
|
+
this.message = `Reopened ${reopened.name} as ${reopened.encoding}`;
|
|
5126
5187
|
} catch (error) {
|
|
5127
5188
|
this.message = String(error.message || error);
|
|
5128
5189
|
}
|
|
@@ -5228,7 +5289,7 @@ class App {
|
|
|
5228
5289
|
case "vsplit": {
|
|
5229
5290
|
let newBuf;
|
|
5230
5291
|
if (cmdArgs.length > 0) {
|
|
5231
|
-
try { newBuf = await loadBufferForPath(resolve(expandHome(cmdArgs[0])), this.context); }
|
|
5292
|
+
try { newBuf = await loadBufferForPath(resolve(expandHome(cmdArgs[0])), this.context, {}, { interactive: true }); }
|
|
5232
5293
|
catch (err) { this.message = err.message; break; }
|
|
5233
5294
|
} else {
|
|
5234
5295
|
newBuf = new BufferModel({ command: {} });
|
|
@@ -5241,7 +5302,7 @@ class App {
|
|
|
5241
5302
|
case "hsplit": {
|
|
5242
5303
|
let newBuf;
|
|
5243
5304
|
if (cmdArgs.length > 0) {
|
|
5244
|
-
try { newBuf = await loadBufferForPath(resolve(expandHome(cmdArgs[0])), this.context); }
|
|
5305
|
+
try { newBuf = await loadBufferForPath(resolve(expandHome(cmdArgs[0])), this.context, {}, { interactive: true }); }
|
|
5245
5306
|
catch (err) { this.message = err.message; break; }
|
|
5246
5307
|
} else {
|
|
5247
5308
|
newBuf = new BufferModel({ command: {} });
|
|
@@ -6528,11 +6589,25 @@ function commandHasStartupJump(command = {}) {
|
|
|
6528
6589
|
return commandHasStartCursor(command) || Boolean(command.searchRegex);
|
|
6529
6590
|
}
|
|
6530
6591
|
|
|
6531
|
-
async function loadBufferForPath(pathOrUrl, context, command = {}) {
|
|
6592
|
+
async function loadBufferForPath(pathOrUrl, context, command = {}, { interactive = false } = {}) {
|
|
6593
|
+
const loadContext = interactive
|
|
6594
|
+
? { ...context, inputEncoding: defaultAllSettings().encoding, encodingExplicit: false }
|
|
6595
|
+
: context;
|
|
6596
|
+
if (isHttpUrl(pathOrUrl) && loadContext.allowUrl) {
|
|
6597
|
+
const url = new URL(pathOrUrl);
|
|
6598
|
+
let name;
|
|
6599
|
+
try { name = decodeURIComponent(basename(url.pathname)); }
|
|
6600
|
+
catch { name = basename(url.pathname); }
|
|
6601
|
+
if (!name) name = "remote.md";
|
|
6602
|
+
if (!name.toLowerCase().endsWith(".md")) name += ".md";
|
|
6603
|
+
const localPath = resolve(name);
|
|
6604
|
+
await Bun.write(localPath, await fetchHttpBytes(pathOrUrl));
|
|
6605
|
+
return await loadBufferForPath(localPath, loadContext, command, { interactive });
|
|
6606
|
+
}
|
|
6532
6607
|
let buffer;
|
|
6533
6608
|
if (isHttpUrl(pathOrUrl)) {
|
|
6534
|
-
let encoding =
|
|
6535
|
-
const decoded = await fetchTextWithEncoding(pathOrUrl, encoding);
|
|
6609
|
+
let encoding = loadContext.inputEncoding ?? loadContext.config?.globalSettings?.encoding ?? DEFAULT_SETTINGS.encoding;
|
|
6610
|
+
const decoded = await fetchTextWithEncoding(pathOrUrl, encoding, !loadContext.encodingExplicit);
|
|
6536
6611
|
const text = decoded.text;
|
|
6537
6612
|
encoding = decoded.encoding;
|
|
6538
6613
|
const urlPath = pathOrUrl.replace(/[?#].*$/, "");
|
|
@@ -6548,13 +6623,18 @@ async function loadBufferForPath(pathOrUrl, context, command = {}) {
|
|
|
6548
6623
|
mdcuiRenderWidth: decoded.mdcuiRenderWidth ?? 0,
|
|
6549
6624
|
});
|
|
6550
6625
|
buffer._configDir = context?.config?.configDir ?? null;
|
|
6551
|
-
attachSyntax(buffer,
|
|
6626
|
+
attachSyntax(buffer, loadContext, urlPath, text);
|
|
6552
6627
|
} else {
|
|
6553
6628
|
if (!context._openBuffers) context._openBuffers = new Map();
|
|
6554
6629
|
const absPath = resolve(pathOrUrl);
|
|
6555
6630
|
const existing = context._openBuffers.get(absPath);
|
|
6556
|
-
|
|
6557
|
-
|
|
6631
|
+
const requestedEncoding = encodingForPath(
|
|
6632
|
+
absPath,
|
|
6633
|
+
loadContext.inputEncoding ?? loadContext.config?.globalSettings?.encoding ?? DEFAULT_SETTINGS.encoding,
|
|
6634
|
+
!loadContext.encodingExplicit,
|
|
6635
|
+
);
|
|
6636
|
+
if (existing && !isMdcuiEncoding(existing.encoding) && !isMdcuiEncoding(requestedEncoding)) return existing;
|
|
6637
|
+
buffer = await BufferModel.fromFile(absPath, command, loadContext);
|
|
6558
6638
|
// Check for crash-recovery backup before returning the buffer.
|
|
6559
6639
|
const promptFn = context._termPrompt;
|
|
6560
6640
|
if (promptFn && buffer._configDir) {
|
|
@@ -6565,8 +6645,12 @@ async function loadBufferForPath(pathOrUrl, context, command = {}) {
|
|
|
6565
6645
|
attachSyntax(buffer, context, absPath, buffer.lines.join("\n"));
|
|
6566
6646
|
}
|
|
6567
6647
|
}
|
|
6568
|
-
|
|
6569
|
-
|
|
6648
|
+
// mdcui is a derived, read-only view. Keep every view independent and out
|
|
6649
|
+
// of the same-path cache used by normal editable buffers.
|
|
6650
|
+
if (!isMdcuiEncoding(buffer.encoding)) {
|
|
6651
|
+
buffer._openBufferMap = context._openBuffers;
|
|
6652
|
+
context._openBuffers.set(absPath, buffer);
|
|
6653
|
+
}
|
|
6570
6654
|
}
|
|
6571
6655
|
if (DEFAULT_SETTINGS.savecursor && !commandHasStartupJump(command) && context?.cursorStates?.[pathOrUrl]) {
|
|
6572
6656
|
const saved = context.cursorStates[pathOrUrl];
|
|
@@ -7272,8 +7356,11 @@ async function printChangelogDocs() {
|
|
|
7272
7356
|
}
|
|
7273
7357
|
|
|
7274
7358
|
async function printTestappSource() {
|
|
7275
|
-
|
|
7276
|
-
|
|
7359
|
+
process.stdout.write(await bundledTestappSource());
|
|
7360
|
+
}
|
|
7361
|
+
|
|
7362
|
+
async function bundledTestappSource() {
|
|
7363
|
+
return readInternalAssetText("testapp.md") ?? await Bun.file(join(REPO_ROOT, "testapp.md")).text();
|
|
7277
7364
|
}
|
|
7278
7365
|
|
|
7279
7366
|
async function main() {
|
|
@@ -7331,6 +7418,11 @@ async function main() {
|
|
|
7331
7418
|
await printTestappSource();
|
|
7332
7419
|
return;
|
|
7333
7420
|
}
|
|
7421
|
+
if (flags.demo) {
|
|
7422
|
+
const demoPath = resolve("testapp.md");
|
|
7423
|
+
await Bun.write(demoPath, await bundledTestappSource());
|
|
7424
|
+
rawFiles.splice(0, rawFiles.length, demoPath);
|
|
7425
|
+
}
|
|
7334
7426
|
if (flags.options) {
|
|
7335
7427
|
for (const [key, value] of Object.entries(defaultAllSettings()).sort(([a], [b]) => a.localeCompare(b))) {
|
|
7336
7428
|
console.log(`-${key} value`);
|
|
@@ -7341,6 +7433,7 @@ async function main() {
|
|
|
7341
7433
|
addCheckpoint("Config Initialization");
|
|
7342
7434
|
const config = await new Config({ configDir: flags.configDir }).init();
|
|
7343
7435
|
config.applyCliSettings(flags.settings);
|
|
7436
|
+
const encodingExplicit = flags.settings.has("encoding") || Object.hasOwn(config.parsedSettings, "encoding");
|
|
7344
7437
|
syncEditorSettings(config);
|
|
7345
7438
|
|
|
7346
7439
|
addCheckpoint("Runtime Registry Init");
|
|
@@ -7352,7 +7445,7 @@ async function main() {
|
|
|
7352
7445
|
const syntaxDefinitions = await loadSyntaxDefinitions(runtime);
|
|
7353
7446
|
|
|
7354
7447
|
if (flags.cat) {
|
|
7355
|
-
await catFiles(rawFiles, colorscheme, syntaxDefinitions, config.getGlobalOption("encoding"));
|
|
7448
|
+
await catFiles(rawFiles, colorscheme, syntaxDefinitions, config.getGlobalOption("encoding"), !encodingExplicit);
|
|
7356
7449
|
return;
|
|
7357
7450
|
}
|
|
7358
7451
|
|
|
@@ -7410,7 +7503,7 @@ async function main() {
|
|
|
7410
7503
|
|
|
7411
7504
|
const { files, command } = parseInput(rawFiles);
|
|
7412
7505
|
const jsPlugins = new JsPluginManager();
|
|
7413
|
-
const context = { colorscheme, syntaxDefinitions, config, runtime, jsPlugins };
|
|
7506
|
+
const context = { colorscheme, syntaxDefinitions, config, runtime, jsPlugins, encodingExplicit, allowUrl: flags.allowUrl };
|
|
7414
7507
|
jsPlugins.setContext(context);
|
|
7415
7508
|
buildMicroGlobal(jsPlugins); // sets globalThis.micro
|
|
7416
7509
|
|
|
@@ -7922,30 +8015,34 @@ function syncEditorSettings(config) {
|
|
|
7922
8015
|
}
|
|
7923
8016
|
}
|
|
7924
8017
|
|
|
7925
|
-
async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAULT_SETTINGS.encoding) {
|
|
8018
|
+
async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAULT_SETTINGS.encoding, inferMdcui = true) {
|
|
7926
8019
|
const targets = files.length > 0 ? files.map((f) => ({ path: f, stdin: false })) : [{ path: null, stdin: true }];
|
|
7927
8020
|
for (const { path: filePath, stdin } of targets) {
|
|
7928
8021
|
let content;
|
|
7929
8022
|
let ansiContent = null;
|
|
7930
8023
|
let effectivePath = filePath;
|
|
8024
|
+
let effectiveEncoding = normalizeEncodingLabel(encoding);
|
|
7931
8025
|
if (stdin) {
|
|
7932
8026
|
const chunks = [];
|
|
7933
8027
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7934
8028
|
const decoded = await decodeAndRenderTextBytes(Buffer.concat(chunks), encoding);
|
|
7935
8029
|
content = decoded.text;
|
|
7936
8030
|
ansiContent = decoded.ansiText ?? null;
|
|
8031
|
+
effectiveEncoding = decoded.encoding;
|
|
7937
8032
|
} else if (isHttpUrl(filePath)) {
|
|
7938
|
-
const decoded = await fetchTextWithEncoding(filePath, encoding);
|
|
8033
|
+
const decoded = await fetchTextWithEncoding(filePath, encoding, inferMdcui);
|
|
7939
8034
|
content = decoded.text;
|
|
7940
8035
|
ansiContent = decoded.ansiText ?? null;
|
|
8036
|
+
effectiveEncoding = decoded.encoding;
|
|
7941
8037
|
// Use the URL pathname for syntax/md detection (strip query/hash)
|
|
7942
8038
|
try { effectivePath = new URL(filePath).pathname; } catch { effectivePath = filePath; }
|
|
7943
8039
|
} else {
|
|
7944
|
-
const decoded = await readTextFileWithEncoding(filePath, encoding);
|
|
8040
|
+
const decoded = await readTextFileWithEncoding(filePath, encoding, inferMdcui);
|
|
7945
8041
|
content = decoded.text;
|
|
7946
8042
|
ansiContent = decoded.ansiText ?? null;
|
|
8043
|
+
effectiveEncoding = decoded.encoding;
|
|
7947
8044
|
}
|
|
7948
|
-
if (isMdcuiEncoding(
|
|
8045
|
+
if (isMdcuiEncoding(effectiveEncoding)) {
|
|
7949
8046
|
process.stdout.write(ansiContent ?? content);
|
|
7950
8047
|
if (!(ansiContent ?? content).endsWith("\n")) process.stdout.write("\n");
|
|
7951
8048
|
continue;
|
package/tests/backup.test.js
CHANGED
|
@@ -130,4 +130,29 @@ describe("backup lifecycle", () => {
|
|
|
130
130
|
expect(await writeBackup(buf, root, buf.AbsPath, { force: true })).toBe(true);
|
|
131
131
|
expect(existsSync(determineBackupPath(backupDir, buf.AbsPath).name)).toBe(true);
|
|
132
132
|
});
|
|
133
|
+
|
|
134
|
+
test("mdcui buffers never write, recover, or remove backups", async () => {
|
|
135
|
+
const root = await tempDir();
|
|
136
|
+
const backupDir = join(root, "backups");
|
|
137
|
+
await mkdir(backupDir);
|
|
138
|
+
const buf = buffer("/tmp/file.md", "rendered markdown");
|
|
139
|
+
buf.Settings.backupdir = backupDir;
|
|
140
|
+
buf.encoding = "mdcui";
|
|
141
|
+
buf.Settings.encoding = "mdcui";
|
|
142
|
+
const target = determineBackupPath(backupDir, buf.AbsPath);
|
|
143
|
+
|
|
144
|
+
expect(await writeBackup(buf, root)).toBe(false);
|
|
145
|
+
expect(await writeBackup(buf, root, buf.AbsPath, { force: true })).toBe(false);
|
|
146
|
+
expect(existsSync(target.name)).toBe(false);
|
|
147
|
+
|
|
148
|
+
await writeFile(target.name, "utf8 editor recovery");
|
|
149
|
+
let prompted = false;
|
|
150
|
+
expect(await applyBackup(buf, root, async () => { prompted = true; return "recover"; }))
|
|
151
|
+
.toEqual({ recovered: false, abort: false });
|
|
152
|
+
expect(prompted).toBe(false);
|
|
153
|
+
expect(buf.lines).toEqual(["rendered markdown"]);
|
|
154
|
+
|
|
155
|
+
removeBackup(buf, root);
|
|
156
|
+
expect(existsSync(target.name)).toBe(true);
|
|
157
|
+
});
|
|
133
158
|
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const tui = join(import.meta.dir, "..", "tui");
|
|
8
|
+
|
|
9
|
+
test("cat renders .md files once with the implicit mdcui encoding", async () => {
|
|
10
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-cat-"));
|
|
11
|
+
const markdownPath = join(dir, "sample.md");
|
|
12
|
+
await writeFile(markdownPath, "# Heading\n\n- one\n- two\n");
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const implicit = Bun.spawnSync([tui, "-cat", markdownPath], {
|
|
16
|
+
cwd: dir,
|
|
17
|
+
stdout: "pipe",
|
|
18
|
+
stderr: "pipe",
|
|
19
|
+
});
|
|
20
|
+
const explicit = Bun.spawnSync([tui, "-cat", "-encoding", "mdcui", markdownPath], {
|
|
21
|
+
cwd: dir,
|
|
22
|
+
stdout: "pipe",
|
|
23
|
+
stderr: "pipe",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(implicit.exitCode).toBe(0);
|
|
27
|
+
expect(explicit.exitCode).toBe(0);
|
|
28
|
+
expect(implicit.stdout.toString()).toBe(explicit.stdout.toString());
|
|
29
|
+
} finally {
|
|
30
|
+
await rm(dir, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("an explicit utf8 encoding overrides the .md mdcui default", async () => {
|
|
35
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-cat-utf8-"));
|
|
36
|
+
const markdownPath = join(dir, "sample.md");
|
|
37
|
+
await writeFile(markdownPath, "# Heading\n\n```js front\nalert('kept')\n```\n");
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const result = Bun.spawnSync([tui, "-cat", "-encoding", "utf8", markdownPath], {
|
|
41
|
+
cwd: dir,
|
|
42
|
+
stdout: "pipe",
|
|
43
|
+
stderr: "pipe",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(result.exitCode).toBe(0);
|
|
47
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("alert('kept')");
|
|
48
|
+
expect(existsSync(markdownPath + ".front.js")).toBe(false);
|
|
49
|
+
expect(existsSync(markdownPath + ".html")).toBe(false);
|
|
50
|
+
} finally {
|
|
51
|
+
await rm(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("--edit overrides the .md mdcui default with utf8", async () => {
|
|
56
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-cat-edit-"));
|
|
57
|
+
const markdownPath = join(dir, "sample.md");
|
|
58
|
+
await writeFile(markdownPath, "# Heading\n\n```js front\nalert('editable')\n```\n");
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const edit = Bun.spawnSync([tui, "-cat", "--edit", markdownPath], {
|
|
62
|
+
cwd: dir,
|
|
63
|
+
stdout: "pipe",
|
|
64
|
+
stderr: "pipe",
|
|
65
|
+
});
|
|
66
|
+
const utf8 = Bun.spawnSync([tui, "-cat", "-encoding", "utf8", markdownPath], {
|
|
67
|
+
cwd: dir,
|
|
68
|
+
stdout: "pipe",
|
|
69
|
+
stderr: "pipe",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(edit.exitCode).toBe(0);
|
|
73
|
+
expect(edit.stdout.toString()).toBe(utf8.stdout.toString());
|
|
74
|
+
expect(Bun.stripANSI(edit.stdout.toString())).toContain("alert('editable')");
|
|
75
|
+
expect(existsSync(markdownPath + ".front.js")).toBe(false);
|
|
76
|
+
} finally {
|
|
77
|
+
await rm(dir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Config } from "../src/config/config.js";
|
|
6
|
+
|
|
7
|
+
const tempDirs = [];
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
await Promise.all(tempDirs.splice(0).map((path) => rm(path, { recursive: true, force: true })));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
async function loadConfig(settings) {
|
|
14
|
+
const configDir = await mkdtemp(join(tmpdir(), "jsmdcui-config-"));
|
|
15
|
+
tempDirs.push(configDir);
|
|
16
|
+
await writeFile(join(configDir, "settings.json"), JSON.stringify(settings), "utf8");
|
|
17
|
+
return await new Config({ configDir }).init();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("Config.init discards top-level encoding from settings.json in memory", async () => {
|
|
21
|
+
const config = await loadConfig({
|
|
22
|
+
encoding: "big5",
|
|
23
|
+
ruler: false,
|
|
24
|
+
"ft:markdown": { encoding: "big5", tabsize: 2 },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(config.getGlobalOption("encoding")).toBe("utf-8");
|
|
28
|
+
expect(config.parsedSettings).not.toHaveProperty("encoding");
|
|
29
|
+
expect(config.parsedSettings["ft:markdown"].encoding).toBe("big5");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("Config.saveSettings removes the top-level encoding discarded during init", async () => {
|
|
33
|
+
const config = await loadConfig({
|
|
34
|
+
encoding: "big5",
|
|
35
|
+
ruler: false,
|
|
36
|
+
"ft:markdown": { encoding: "big5", tabsize: 2 },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await config.saveSettings();
|
|
40
|
+
const saved = JSON.parse(await readFile(join(config.configDir, "settings.json"), "utf8"));
|
|
41
|
+
expect(saved).toEqual({ ruler: false, "ft:markdown": { encoding: "big5", tabsize: 2 } });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("CLI encoding applies to the current invocation but saveSettings never persists it", async () => {
|
|
45
|
+
const config = await loadConfig({ ruler: false });
|
|
46
|
+
config.applyCliSettings(new Map([["encoding", "big5"]]));
|
|
47
|
+
|
|
48
|
+
expect(config.getGlobalOption("encoding")).toBe("big5");
|
|
49
|
+
await config.saveSettings();
|
|
50
|
+
const saved = JSON.parse(await readFile(join(config.configDir, "settings.json"), "utf8"));
|
|
51
|
+
expect(saved).toEqual({ ruler: false });
|
|
52
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const tui = join(import.meta.dir, "..", "tui");
|
|
7
|
+
|
|
8
|
+
test("--demo writes bundled testapp.md to cwd before opening it", async () => {
|
|
9
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-"));
|
|
10
|
+
try {
|
|
11
|
+
const result = Bun.spawnSync([tui, "--demo", "-cat", "-encoding", "utf8"], {
|
|
12
|
+
cwd: dir,
|
|
13
|
+
stdout: "pipe",
|
|
14
|
+
stderr: "pipe",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
expect(result.exitCode).toBe(0);
|
|
18
|
+
const written = await readFile(join(dir, "testapp.md"), "utf8");
|
|
19
|
+
expect(written).toContain("# jsmdcui");
|
|
20
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("jsmdcui");
|
|
21
|
+
} finally {
|
|
22
|
+
await rm(dir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
});
|