bunmicro 0.9.22 → 0.9.25
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 +17 -0
- package/package.json +3 -2
- package/src/config/colorscheme.js +10 -20
- package/src/highlight/parser.js +15 -10
- package/src/index.js +251 -66
- package/src/runtime/registry.js +10 -1
- package/tests/pty-demo.js +492 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.25] - 2026-06-10
|
|
4
|
+
- Fixed colorscheme: aligned with go
|
|
5
|
+
- Added colorcolumn taberror showchars
|
|
6
|
+
* e.g. showchars = tab=>,space=.
|
|
7
|
+
- linter underline
|
|
8
|
+
- set hltrailingws on : whitespace
|
|
9
|
+
- Added tests/pty-demo.js
|
|
10
|
+
* Demo of the whole bunmicro App
|
|
11
|
+
* bun tests/pty-demo.js
|
|
12
|
+
|
|
13
|
+
## [0.9.23] - 2026-06-10
|
|
14
|
+
- Fixed mouse close prompt cursor move
|
|
15
|
+
- Fixed set filetype doesn't apply instantly
|
|
16
|
+
- Added syntax highlighting fallback if user ~/.config/micro yaml fails
|
|
17
|
+
- Redetect highlighting syntax when unknown filetype saves
|
|
18
|
+
- Warning for dos(CRLF) shell scripts
|
|
19
|
+
|
|
3
20
|
## [0.9.22] - 2026-06-09
|
|
4
21
|
- Clicking on icons toggles prompts
|
|
5
22
|
- Unsaved star triggers save cmd
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunmicro",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.25",
|
|
4
4
|
"description": "Bun JavaScript rewrite of the micro editor originally in Golang",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"start": "bun ./src/index.js",
|
|
13
|
-
"check": "node --check ./src/index.js"
|
|
13
|
+
"check": "node --check ./src/index.js",
|
|
14
|
+
"demo:pty": "bun ./tests/pty-demo.js"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
17
|
"wasmoon": "^1.16.0"
|
|
@@ -7,17 +7,6 @@ export const DEFAULT_STYLE = {
|
|
|
7
7
|
underline: false,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
// Built-in fallback styles for color groups not defined in a colorscheme.
|
|
11
|
-
// Matches Go micro's default group colours.
|
|
12
|
-
const BUILTIN_GROUPS = {
|
|
13
|
-
"diff-added": { ...DEFAULT_STYLE, fg: "green" },
|
|
14
|
-
"diff-modified": { ...DEFAULT_STYLE, fg: "yellow" },
|
|
15
|
-
"diff-deleted": { ...DEFAULT_STYLE, fg: "red" },
|
|
16
|
-
"gutter-error": { ...DEFAULT_STYLE, fg: "red" },
|
|
17
|
-
"gutter-warning":{ ...DEFAULT_STYLE, fg: "yellow" },
|
|
18
|
-
"gutter-info": { ...DEFAULT_STYLE, fg: "brightblue" },
|
|
19
|
-
};
|
|
20
|
-
|
|
21
10
|
const COLOR_LINK = /color-link\s+(\S*)\s+"(.*)"/;
|
|
22
11
|
const INCLUDE = /include\s+"(.*)"/;
|
|
23
12
|
|
|
@@ -47,10 +36,11 @@ export class Colorscheme {
|
|
|
47
36
|
if (include) {
|
|
48
37
|
const includeName = include[1];
|
|
49
38
|
if (!parsed.has(includeName)) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
const file = this.runtime.find(0, includeName);
|
|
40
|
+
if (!file) throw new Error(`${includeName} is not a valid colorscheme`);
|
|
41
|
+
parsed.add(includeName);
|
|
42
|
+
const includedStyles = await this.parse(includeName, await file.text(), parsed);
|
|
43
|
+
for (const [key, value] of includedStyles) styles.set(key, value);
|
|
54
44
|
}
|
|
55
45
|
continue;
|
|
56
46
|
}
|
|
@@ -76,7 +66,7 @@ export class Colorscheme {
|
|
|
76
66
|
if (this.styles.has(cur)) style = this.styles.get(cur);
|
|
77
67
|
}
|
|
78
68
|
}
|
|
79
|
-
return style ??
|
|
69
|
+
return style ?? stringToStyle(group, this.defaultStyle);
|
|
80
70
|
}
|
|
81
71
|
}
|
|
82
72
|
|
|
@@ -87,10 +77,10 @@ export function stringToStyle(input, base = DEFAULT_STYLE) {
|
|
|
87
77
|
return {
|
|
88
78
|
fg: stringToColor(fgRaw.trim(), base.fg),
|
|
89
79
|
bg: stringToColor(bgRaw.trim(), base.bg),
|
|
90
|
-
bold: text.includes("bold"),
|
|
91
|
-
italic: text.includes("italic"),
|
|
92
|
-
reverse: text.includes("reverse"),
|
|
93
|
-
underline: text.includes("underline"),
|
|
80
|
+
bold: base.bold || text.includes("bold"),
|
|
81
|
+
italic: base.italic || text.includes("italic"),
|
|
82
|
+
reverse: base.reverse || text.includes("reverse"),
|
|
83
|
+
underline: base.underline || text.includes("underline"),
|
|
94
84
|
};
|
|
95
85
|
}
|
|
96
86
|
|
package/src/highlight/parser.js
CHANGED
|
@@ -47,25 +47,30 @@ export async function loadSyntaxDefinitions(runtime) {
|
|
|
47
47
|
const definitions = [];
|
|
48
48
|
for (const file of runtime.list(1)) {
|
|
49
49
|
let text = "";
|
|
50
|
+
let activeFile = file;
|
|
51
|
+
let source = null;
|
|
50
52
|
try {
|
|
51
53
|
text = await file.text();
|
|
54
|
+
source = Bun.YAML.parse(text);
|
|
52
55
|
} catch (e) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
const fallback = file.real ? runtime.fallback?.(1, file.name) : null;
|
|
57
|
+
if (fallback) {
|
|
58
|
+
try {
|
|
59
|
+
text = await fallback.text();
|
|
60
|
+
source = Bun.YAML.parse(text);
|
|
61
|
+
activeFile = fallback;
|
|
62
|
+
console.error("Failed to load user syntax yaml, using built-in fallback:", file.name);
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
source = Bun.YAML.parse(text);
|
|
62
|
-
} catch (e) {
|
|
63
|
-
console.error("Failed to parse syntax yaml:", file.name);
|
|
67
|
+
if (!source) {
|
|
68
|
+
console.error("Failed to load syntax yaml:", file.name);
|
|
64
69
|
console.error(" Will not highlight this kind of file");
|
|
65
70
|
console.error(" @ loadSyntaxDefinitions ");
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
const header = headers.get(
|
|
73
|
+
const header = headers.get(activeFile.name) ?? (source ? parseHeaderYaml(source) : parseHeaderTextFallback(text, activeFile.name));
|
|
69
74
|
definitions.push(new SyntaxDefinition(header, source ?? { rules: [] }, text));
|
|
70
75
|
}
|
|
71
76
|
return definitions;
|
package/src/index.js
CHANGED
|
@@ -101,7 +101,11 @@ const DEFAULT_SETTINGS = {
|
|
|
101
101
|
fileformat: process.platform === "win32" ? "dos" : "unix",
|
|
102
102
|
"comment.type": "",
|
|
103
103
|
commenttype: "",
|
|
104
|
-
|
|
104
|
+
hltrailingws: false,
|
|
105
|
+
hltaberrors: false,
|
|
106
|
+
colorcolumn: 0,
|
|
107
|
+
showchars: "",
|
|
108
|
+
indentchar: " ",
|
|
105
109
|
};
|
|
106
110
|
|
|
107
111
|
const LONG_LINE_REHIGHLIGHT_LIMIT = 300;
|
|
@@ -647,7 +651,11 @@ class BufferModel {
|
|
|
647
651
|
reload: DEFAULT_SETTINGS.reload,
|
|
648
652
|
eofnewline: DEFAULT_SETTINGS.eofnewline,
|
|
649
653
|
fileformat: this.fileformat,
|
|
650
|
-
|
|
654
|
+
hltrailingws: DEFAULT_SETTINGS.hltrailingws,
|
|
655
|
+
hltaberrors: DEFAULT_SETTINGS.hltaberrors,
|
|
656
|
+
colorcolumn: DEFAULT_SETTINGS.colorcolumn,
|
|
657
|
+
showchars: DEFAULT_SETTINGS.showchars,
|
|
658
|
+
indentchar: DEFAULT_SETTINGS.indentchar,
|
|
651
659
|
encoding: this.encoding,
|
|
652
660
|
readonly,
|
|
653
661
|
};
|
|
@@ -1092,6 +1100,7 @@ class BufferModel {
|
|
|
1092
1100
|
}
|
|
1093
1101
|
async save(path = this.path) {
|
|
1094
1102
|
if (!path) throw new Error("No filename");
|
|
1103
|
+
const detectSyntaxAfterSave = this.filetype === "unknown";
|
|
1095
1104
|
let text = this.lines.join("\n");
|
|
1096
1105
|
if (this.encoding === "hex3") {
|
|
1097
1106
|
await Bun.write(path, decodeBinaryBytes(Buffer.from(text, "latin1")));
|
|
@@ -1106,6 +1115,7 @@ class BufferModel {
|
|
|
1106
1115
|
this._savedSerial = this._undoSerial ?? 0;
|
|
1107
1116
|
this.modified = false;
|
|
1108
1117
|
this.message = `Saved ${path}`;
|
|
1118
|
+
if (detectSyntaxAfterSave && this._syntaxContext) attachSyntax(this, this._syntaxContext, path, text);
|
|
1109
1119
|
return;
|
|
1110
1120
|
}
|
|
1111
1121
|
if ((this.Settings.eofnewline ?? DEFAULT_SETTINGS.eofnewline) && !text.endsWith("\n")) text += "\n";
|
|
@@ -1123,6 +1133,7 @@ class BufferModel {
|
|
|
1123
1133
|
this._savedSerial = this._undoSerial ?? 0;
|
|
1124
1134
|
this.modified = false;
|
|
1125
1135
|
this.message = `Saved ${path}`;
|
|
1136
|
+
if (detectSyntaxAfterSave && this._syntaxContext) attachSyntax(this, this._syntaxContext, path, text);
|
|
1126
1137
|
}
|
|
1127
1138
|
|
|
1128
1139
|
// --- Autocomplete (BufferComplete) ---
|
|
@@ -1788,7 +1799,10 @@ class App {
|
|
|
1788
1799
|
const keymenuHeight = this.keymenu ? KEYDISPLAY.length : 0;
|
|
1789
1800
|
const activeSuggestions = this._activeSuggestions();
|
|
1790
1801
|
const activeSuggestionIdx = this._activeSuggestionIdx();
|
|
1791
|
-
const
|
|
1802
|
+
const formatWarning = this.buffer?.filetype === "shell" && this.buffer?.fileformat === "dos"
|
|
1803
|
+
? "dos(CRLF fileformat) invalid for shell scripts!"
|
|
1804
|
+
: "";
|
|
1805
|
+
const activeMessage = this.message || this.buffer?.message || formatWarning;
|
|
1792
1806
|
if (activeSuggestions.length === 0) this._acHScroll = 0;
|
|
1793
1807
|
const suggestionsHeight = activeSuggestions.length > 1 ? 1 : 0;
|
|
1794
1808
|
const messageHeight = suggestionsHeight ? 0 : activeMessage ? 1 : 0;
|
|
@@ -1873,7 +1887,9 @@ class App {
|
|
|
1873
1887
|
const ft = (buf?.filetype && buf.filetype !== "unknown") ? buf.filetype : "?";
|
|
1874
1888
|
const fmt = buf?.fileformat ?? "unix";
|
|
1875
1889
|
const enc = buf?.encoding ?? "utf-8";
|
|
1876
|
-
const baseStatus =
|
|
1890
|
+
const baseStatus = this.context.colorscheme?.styles?.has("statusline")
|
|
1891
|
+
? this.context.colorscheme.get("statusline")
|
|
1892
|
+
: { ...defaultStyle, reverse: true };
|
|
1877
1893
|
const redStatus = { ...baseStatus, fg: "red" };
|
|
1878
1894
|
// Fill entire row with base style first
|
|
1879
1895
|
putText(this.screen, 0, statusRow, " ".repeat(this.cols), baseStatus, this.cols);
|
|
@@ -2004,8 +2020,9 @@ class App {
|
|
|
2004
2020
|
}
|
|
2005
2021
|
|
|
2006
2022
|
renderMessageRow(defaultStyle, row, message) {
|
|
2007
|
-
const
|
|
2008
|
-
|
|
2023
|
+
const style = this.context.colorscheme?.styles?.has("message")
|
|
2024
|
+
? this.context.colorscheme.get("message")
|
|
2025
|
+
: defaultStyle;
|
|
2009
2026
|
putText(this.screen, 0, row, " ".repeat(this.cols), style, this.cols);
|
|
2010
2027
|
this._messageRowY = row;
|
|
2011
2028
|
this._messageRowClickZone = null;
|
|
@@ -2035,15 +2052,15 @@ class App {
|
|
|
2035
2052
|
}
|
|
2036
2053
|
|
|
2037
2054
|
renderSuggestions(defaultStyle, row, suggestions, curIdx) {
|
|
2038
|
-
const
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2055
|
+
const cs = this.context.colorscheme;
|
|
2056
|
+
const baseStyle = cs?.styles?.has("statusline.suggestions")
|
|
2057
|
+
? cs.get("statusline.suggestions")
|
|
2058
|
+
: cs?.styles?.has("statusline")
|
|
2059
|
+
? cs.get("statusline")
|
|
2060
|
+
: { ...defaultStyle, reverse: true };
|
|
2042
2061
|
const selStyle = {
|
|
2043
2062
|
...baseStyle,
|
|
2044
|
-
|
|
2045
|
-
bg: baseStyle.fg === "default" ? "brightwhite" : baseStyle.fg,
|
|
2046
|
-
reverse: false,
|
|
2063
|
+
reverse: true,
|
|
2047
2064
|
};
|
|
2048
2065
|
|
|
2049
2066
|
// Compute each item's position in the virtual (pre-scroll) space.
|
|
@@ -2166,9 +2183,16 @@ class App {
|
|
|
2166
2183
|
const softwrap = buf.Settings?.softwrap ?? false;
|
|
2167
2184
|
const wordwrap = softwrap && (buf.Settings?.wordwrap ?? false);
|
|
2168
2185
|
const tabsize = buf.Settings?.tabsize ?? DEFAULT_SETTINGS.tabsize;
|
|
2169
|
-
const gutterStyle = { ...defaultStyle, fg: "brightblack" };
|
|
2170
|
-
const dirtyGutterStyle = { ...gutterStyle, fg: "red" };
|
|
2171
2186
|
const isActivePane = pane === this.tab.activePane;
|
|
2187
|
+
const gutterStyle = this.context.colorscheme?.styles?.has("line-number")
|
|
2188
|
+
? this.context.colorscheme.get("line-number")
|
|
2189
|
+
: defaultStyle;
|
|
2190
|
+
const cursorlineOn = buf.Settings?.cursorline ?? DEFAULT_SETTINGS.cursorline;
|
|
2191
|
+
const csHasCurNum = this.context.colorscheme?.styles?.has("current-line-number");
|
|
2192
|
+
const curNumStyle = !csHasCurNum
|
|
2193
|
+
? defaultStyle
|
|
2194
|
+
: (cursorlineOn ? this.context.colorscheme.get("current-line-number") : gutterStyle);
|
|
2195
|
+
const dirtyGutterStyle = { ...gutterStyle, fg: "red" };
|
|
2172
2196
|
const useCursorline = (buf.Settings?.cursorline ?? DEFAULT_SETTINGS.cursorline) && isActivePane;
|
|
2173
2197
|
const clBg = (useCursorline && this.context.colorscheme?.styles?.has("cursor-line"))
|
|
2174
2198
|
? (this.context.colorscheme.get("cursor-line")?.fg ?? null)
|
|
@@ -2181,12 +2205,12 @@ class App {
|
|
|
2181
2205
|
const diffCol = (buf.Settings?.diffgutter ?? false) ? 1 : 0;
|
|
2182
2206
|
const lineNumW = gutterW - msgW - diffCol;
|
|
2183
2207
|
const cs = this.context.colorscheme;
|
|
2184
|
-
const diffAddStyle = cs?.
|
|
2185
|
-
const diffModStyle = cs?.
|
|
2186
|
-
const diffDelStyle = cs?.
|
|
2187
|
-
const msgInfoStyle
|
|
2188
|
-
const msgWarnStyle
|
|
2189
|
-
const msgErrStyle
|
|
2208
|
+
const diffAddStyle = cs?.styles?.has("diff-added") ? cs.get("diff-added") : null;
|
|
2209
|
+
const diffModStyle = cs?.styles?.has("diff-modified") ? cs.get("diff-modified") : null;
|
|
2210
|
+
const diffDelStyle = cs?.styles?.has("diff-deleted") ? cs.get("diff-deleted") : null;
|
|
2211
|
+
const msgInfoStyle = cs?.styles?.has("gutter-info") ? cs.get("gutter-info") : defaultStyle;
|
|
2212
|
+
const msgWarnStyle = cs?.styles?.has("gutter-warning") ? cs.get("gutter-warning") : defaultStyle;
|
|
2213
|
+
const msgErrStyle = cs?.styles?.has("gutter-error") ? cs.get("gutter-error") : defaultStyle;
|
|
2190
2214
|
|
|
2191
2215
|
const renderGutter = (lineNo, row, screenRow, subRow = 0) => {
|
|
2192
2216
|
// Message indicator: 2 cols, '> ' with kind-based style (Go: drawGutter)
|
|
@@ -2203,19 +2227,22 @@ class App {
|
|
|
2203
2227
|
}
|
|
2204
2228
|
putText(this.screen, pane.x, screenRow, msgCh + " ", msgSt, 2);
|
|
2205
2229
|
}
|
|
2230
|
+
const isCurrentLine = isActivePane && !pane.selection && lineNo === buf.cursor.y;
|
|
2231
|
+
const lineStyle = isCurrentLine ? curNumStyle : gutterStyle;
|
|
2206
2232
|
if (diffCol > 0) {
|
|
2207
2233
|
const m = diffMarks?.[lineNo] ?? 0;
|
|
2208
|
-
const [ch,
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2234
|
+
const [ch, colorStyle] = m === 1 ? ["▌", diffAddStyle]
|
|
2235
|
+
: m === 2 ? ["▌", diffModStyle]
|
|
2236
|
+
: m === 3 ? ["▔", diffDelStyle]
|
|
2237
|
+
: [" ", null];
|
|
2238
|
+
const st = colorStyle ? { ...lineStyle, fg: colorStyle.fg } : lineStyle;
|
|
2212
2239
|
putText(this.screen, pane.x + msgW, screenRow, subRow === 0 ? ch : " ", st, 1);
|
|
2213
2240
|
}
|
|
2214
2241
|
if (lineNumW > 0) {
|
|
2215
2242
|
const prefix = subRow === 0
|
|
2216
2243
|
? lineNumberText(buf, lineNo, row, lineNumW)
|
|
2217
2244
|
: visualLineNumberText(subRow, lineNumW);
|
|
2218
|
-
putText(this.screen, pane.x + msgW + diffCol, screenRow, prefix, isDirtyLongLine(buf, lineNo) ? dirtyGutterStyle :
|
|
2245
|
+
putText(this.screen, pane.x + msgW + diffCol, screenRow, prefix, isDirtyLongLine(buf, lineNo) ? dirtyGutterStyle : lineStyle, lineNumW);
|
|
2219
2246
|
}
|
|
2220
2247
|
};
|
|
2221
2248
|
|
|
@@ -2299,7 +2326,11 @@ class App {
|
|
|
2299
2326
|
|
|
2300
2327
|
renderDividers(node, defaultStyle) {
|
|
2301
2328
|
if (node instanceof Pane) return;
|
|
2302
|
-
const
|
|
2329
|
+
const divReverse = this.context.config?.globalSettings?.divreverse ?? true;
|
|
2330
|
+
const baseDiv = this.context.colorscheme?.styles?.has("divider")
|
|
2331
|
+
? this.context.colorscheme.get("divider")
|
|
2332
|
+
: defaultStyle;
|
|
2333
|
+
const divStyle = divReverse ? { ...baseDiv, reverse: true } : baseDiv;
|
|
2303
2334
|
for (let i = 0; i < node.children.length - 1; i++) {
|
|
2304
2335
|
const child = node.children[i];
|
|
2305
2336
|
if (node.dir === "h") {
|
|
@@ -2316,21 +2347,37 @@ class App {
|
|
|
2316
2347
|
}
|
|
2317
2348
|
|
|
2318
2349
|
renderTabbar(defaultStyle) {
|
|
2319
|
-
const
|
|
2320
|
-
const
|
|
2350
|
+
const cs = this.context.colorscheme;
|
|
2351
|
+
const gs = this.context.config?.globalSettings;
|
|
2352
|
+
const tabReverse = gs?.tabreverse ?? true;
|
|
2353
|
+
const tabHighlight = gs?.tabhighlight ?? false;
|
|
2354
|
+
const tabCharReverse = (tabReverse || tabHighlight) && !(tabReverse && tabHighlight);
|
|
2355
|
+
|
|
2356
|
+
const stylesFor = (reverse) => {
|
|
2357
|
+
const base = cs?.styles?.has("tabbar")
|
|
2358
|
+
? cs.get("tabbar")
|
|
2359
|
+
: { ...defaultStyle, reverse };
|
|
2360
|
+
const active = cs?.styles?.has("tabbar.active")
|
|
2361
|
+
? cs.get("tabbar.active")
|
|
2362
|
+
: base;
|
|
2363
|
+
return [base, active];
|
|
2364
|
+
};
|
|
2365
|
+
const [sepStyle] = stylesFor(tabReverse);
|
|
2366
|
+
const [charBase, charActive] = stylesFor(tabCharReverse);
|
|
2367
|
+
|
|
2321
2368
|
let x = 0;
|
|
2322
2369
|
for (let i = 0; i < this.tabs.length && x < this.cols; i++) {
|
|
2323
2370
|
const name = this.tabs[i].name || "No name";
|
|
2324
2371
|
const isActive = i === this.activeTabIdx;
|
|
2325
2372
|
const label = isActive ? `[${name}]` : ` ${name} `;
|
|
2326
|
-
const style = isActive ?
|
|
2373
|
+
const style = isActive ? charActive : charBase;
|
|
2327
2374
|
const start = x;
|
|
2328
2375
|
x = putText(this.screen, x, 0, label, style, this.cols - x);
|
|
2329
2376
|
this.tabRects.push({ index: i, start, end: x });
|
|
2330
2377
|
if (i < this.tabs.length - 1 && x < this.cols)
|
|
2331
|
-
x = putText(this.screen, x, 0, " ",
|
|
2378
|
+
x = putText(this.screen, x, 0, " ", sepStyle, this.cols - x);
|
|
2332
2379
|
}
|
|
2333
|
-
if (x < this.cols) putText(this.screen, x, 0, " ".repeat(this.cols - x),
|
|
2380
|
+
if (x < this.cols) putText(this.screen, x, 0, " ".repeat(this.cols - x), sepStyle, this.cols - x);
|
|
2334
2381
|
}
|
|
2335
2382
|
|
|
2336
2383
|
updateScrollForPane(pane) {
|
|
@@ -3517,18 +3564,7 @@ class App {
|
|
|
3517
3564
|
}
|
|
3518
3565
|
break;
|
|
3519
3566
|
case "ft": {
|
|
3520
|
-
|
|
3521
|
-
const filetypes = defs.map(d => d.filetype).filter(Boolean).sort();
|
|
3522
|
-
const ftComplete = (partial) => filetypes.filter(f => f.startsWith(partial));
|
|
3523
|
-
this.openPrompt("Set filetype: ", (value) => {
|
|
3524
|
-
if (!value || !buf) return;
|
|
3525
|
-
buf.filetype = value;
|
|
3526
|
-
buf.Settings.filetype = value;
|
|
3527
|
-
const def = defs.find(d => d.filetype === value);
|
|
3528
|
-
buf.syntaxDefinition = def ?? null;
|
|
3529
|
-
buf.highlighter = def ? new Highlighter(def, defs) : null;
|
|
3530
|
-
buf._highlightCache = null;
|
|
3531
|
-
}, { completer: ftComplete, initial: buf?.filetype ?? "" });
|
|
3567
|
+
this.openCommandMode("set filetype ");
|
|
3532
3568
|
break;
|
|
3533
3569
|
}
|
|
3534
3570
|
case "fmt":
|
|
@@ -3552,9 +3588,11 @@ class App {
|
|
|
3552
3588
|
await this.addTab();
|
|
3553
3589
|
break;
|
|
3554
3590
|
case "cmdmode":
|
|
3591
|
+
if (this.prompt?.type === "Command") this._suppressMouseUntilUp = true;
|
|
3555
3592
|
await this.togglePromptMode("Command");
|
|
3556
3593
|
break;
|
|
3557
3594
|
case "shellmode":
|
|
3595
|
+
if (this.prompt?.type === "Shell") this._suppressMouseUntilUp = true;
|
|
3558
3596
|
await this.togglePromptMode("Shell");
|
|
3559
3597
|
break;
|
|
3560
3598
|
}
|
|
@@ -5149,6 +5187,18 @@ function completeOptionValue(cmd, option, partial, context) {
|
|
|
5149
5187
|
const optVal = allSettings[option];
|
|
5150
5188
|
const suggestions = [];
|
|
5151
5189
|
|
|
5190
|
+
if (option === "filetype") {
|
|
5191
|
+
const filetypes = [
|
|
5192
|
+
"off",
|
|
5193
|
+
"unknown",
|
|
5194
|
+
...(context?.syntaxDefinitions ?? []).map((definition) => definition.filetype),
|
|
5195
|
+
];
|
|
5196
|
+
return [...new Set(filetypes)]
|
|
5197
|
+
.filter((filetype) => filetype && filetype.startsWith(partial))
|
|
5198
|
+
.sort()
|
|
5199
|
+
.map((filetype) => ({ value: `${cmd} ${option} ${filetype}`, label: filetype }));
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5152
5202
|
if (typeof optVal === "boolean") {
|
|
5153
5203
|
if ("on".startsWith(partial)) suggestions.push("on");
|
|
5154
5204
|
else if ("true".startsWith(partial)) suggestions.push("true");
|
|
@@ -6038,22 +6088,75 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
6038
6088
|
// Go: cursor-line bg is skipped when a syntax style already has a non-default background (preservebg)
|
|
6039
6089
|
const defBg = colorscheme?.defaultStyle?.bg ?? "default";
|
|
6040
6090
|
|
|
6041
|
-
const showTrailingWs = buf.Settings?.
|
|
6091
|
+
const showTrailingWs = buf.Settings?.hltrailingws ?? false;
|
|
6042
6092
|
let trailingWsIdx = raw.length;
|
|
6043
6093
|
if (showTrailingWs) {
|
|
6044
6094
|
let k = raw.length - 1;
|
|
6045
6095
|
while (k >= 0 && (raw[k] === " " || raw[k] === "\t")) k--;
|
|
6046
6096
|
trailingWsIdx = k + 1;
|
|
6047
6097
|
}
|
|
6048
|
-
const
|
|
6049
|
-
?
|
|
6098
|
+
const trailingWsColor = showTrailingWs && colorscheme?.styles?.has("trailingws")
|
|
6099
|
+
? colorscheme.get("trailingws")?.fg
|
|
6050
6100
|
: null;
|
|
6051
6101
|
|
|
6102
|
+
// showchars parsing (Go bufwindow.go:455-476)
|
|
6103
|
+
const indentchar = buf.Settings?.indentchar ?? " ";
|
|
6104
|
+
let spacechars = " ";
|
|
6105
|
+
let tabchars = indentchar;
|
|
6106
|
+
let indentspacechars = "";
|
|
6107
|
+
let indenttabchars = "";
|
|
6108
|
+
for (const entry of String(buf.Settings?.showchars ?? "").split(",")) {
|
|
6109
|
+
const eq = entry.indexOf("=");
|
|
6110
|
+
if (eq < 0) continue;
|
|
6111
|
+
const key = entry.slice(0, eq);
|
|
6112
|
+
const val = entry.slice(eq + 1);
|
|
6113
|
+
if (key === "space") spacechars = val;
|
|
6114
|
+
else if (key === "tab") tabchars = val;
|
|
6115
|
+
else if (key === "ispace") indentspacechars = val;
|
|
6116
|
+
else if (key === "itab") indenttabchars = val;
|
|
6117
|
+
}
|
|
6118
|
+
// leadingwsEnd: index of first non-whitespace char (raw code-unit index)
|
|
6119
|
+
let leadingwsEnd = 0;
|
|
6120
|
+
while (leadingwsEnd < raw.length && (raw[leadingwsEnd] === " " || raw[leadingwsEnd] === "\t")) leadingwsEnd++;
|
|
6121
|
+
|
|
6122
|
+
const hltaberrors = buf.Settings?.hltaberrors ?? false;
|
|
6123
|
+
const tabstospaces = buf.Settings?.tabstospaces ?? false;
|
|
6124
|
+
const tabErrorFg = hltaberrors && colorscheme?.styles?.has("tab-error")
|
|
6125
|
+
? colorscheme.get("tab-error")?.fg
|
|
6126
|
+
: null;
|
|
6127
|
+
const indentCharFg = colorscheme?.styles?.has("indent-char")
|
|
6128
|
+
? colorscheme.get("indent-char")?.fg
|
|
6129
|
+
: null;
|
|
6130
|
+
const colorcolumn = Number(buf.Settings?.colorcolumn ?? 0) | 0;
|
|
6131
|
+
const colorColumnBg = colorcolumn > 0 && colorscheme?.styles?.has("color-column")
|
|
6132
|
+
? colorscheme.get("color-column")?.fg
|
|
6133
|
+
: null;
|
|
6134
|
+
const tabsize = buf.Settings?.tabsize ?? DEFAULT_SETTINGS.tabsize;
|
|
6135
|
+
|
|
6136
|
+
// scrollVisualCol: visual column of raw[0..scrollX). Uses displayWidth (tab = full
|
|
6137
|
+
// tabsize, not aligned-to-boundary) to stay consistent with cursor/scroll math.
|
|
6138
|
+
const scrollVisualCol = displayWidth(raw.slice(0, scrollX));
|
|
6139
|
+
|
|
6140
|
+
// Linter messages overlapping this line (Go bufwindow.go:662-668)
|
|
6141
|
+
const lineMessages = (buf.Messages ?? []).filter((m) => {
|
|
6142
|
+
const sy = m.Start?.Y ?? 0, ey = m.End?.Y ?? 0;
|
|
6143
|
+
return sy <= lineNo && ey >= lineNo;
|
|
6144
|
+
});
|
|
6145
|
+
const inMessageAt = (charIdx) => {
|
|
6146
|
+
for (const m of lineMessages) {
|
|
6147
|
+
const sY = m.Start?.Y ?? 0, sX = m.Start?.X ?? 0;
|
|
6148
|
+
const eY = m.End?.Y ?? 0, eX = m.End?.X ?? 0;
|
|
6149
|
+
const ge = lineNo > sY || (lineNo === sY && charIdx >= sX);
|
|
6150
|
+
const lt = lineNo < eY || (lineNo === eY && charIdx < eX);
|
|
6151
|
+
if (ge && lt) return true;
|
|
6152
|
+
}
|
|
6153
|
+
return false;
|
|
6154
|
+
};
|
|
6155
|
+
|
|
6052
6156
|
let changeIndex = 0;
|
|
6053
6157
|
let searchIdx = 0;
|
|
6054
6158
|
let i = scrollX;
|
|
6055
6159
|
while (i < raw.length && width < maxWidth) {
|
|
6056
|
-
const cp = raw.codePointAt(i);
|
|
6057
6160
|
const unit = displayUnitAt(raw, i);
|
|
6058
6161
|
const ch = unit.text;
|
|
6059
6162
|
const charLen = unit.length;
|
|
@@ -6062,16 +6165,30 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
6062
6165
|
while (changeIndex + 1 < changes.length && i >= changes[changeIndex + 1][0]) changeIndex++;
|
|
6063
6166
|
const group = changes[changeIndex]?.[1] ?? "default";
|
|
6064
6167
|
const syntaxStyle = colorscheme?.get(group) ?? colorscheme?.defaultStyle ?? {};
|
|
6065
|
-
|
|
6066
|
-
|
|
6168
|
+
let preservebg = syntaxStyle.bg !== undefined && syntaxStyle.bg !== defBg;
|
|
6169
|
+
let baseStyle = (cursorLineBg && !preservebg) ? { ...syntaxStyle, bg: cursorLineBg } : syntaxStyle;
|
|
6067
6170
|
while (searchIdx < searchRanges.length && searchRanges[searchIdx][1] <= i) searchIdx++;
|
|
6068
6171
|
const inSearch = searchIdx < searchRanges.length && i >= searchRanges[searchIdx][0] && i < searchRanges[searchIdx][1];
|
|
6069
6172
|
const selected = isSelected(selection, lineNo, i, i + charLen);
|
|
6070
6173
|
const braceMatched = braceMatches?.has(String(lineNo) + ":" + String(i));
|
|
6071
|
-
|
|
6174
|
+
|
|
6175
|
+
// tab-error in leading whitespace
|
|
6176
|
+
let style = baseStyle;
|
|
6177
|
+
const inLeading = i < leadingwsEnd;
|
|
6178
|
+
if (tabErrorFg != null && inLeading) {
|
|
6179
|
+
if ((tabstospaces && ch === "\t") || (!tabstospaces && ch === " ")) {
|
|
6180
|
+
style = { ...style, bg: tabErrorFg };
|
|
6181
|
+
preservebg = true;
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
6184
|
+
if (trailingWsColor != null && i >= trailingWsIdx) {
|
|
6185
|
+
style = { ...style, bg: trailingWsColor };
|
|
6186
|
+
preservebg = true;
|
|
6187
|
+
}
|
|
6072
6188
|
if (inSearch) {
|
|
6073
6189
|
const searchStyle = colorscheme?.styles?.has("hlsearch") ? colorscheme.get("hlsearch") : null;
|
|
6074
|
-
style = searchStyle ?? { ...
|
|
6190
|
+
style = searchStyle ?? { ...(colorscheme?.defaultStyle ?? {}), reverse: true };
|
|
6191
|
+
if ((style.bg ?? "default") !== defBg) preservebg = true;
|
|
6075
6192
|
}
|
|
6076
6193
|
if (braceMatched) {
|
|
6077
6194
|
if ((buf.Settings?.matchbracestyle ?? DEFAULT_SETTINGS.matchbracestyle) === "highlight") {
|
|
@@ -6082,22 +6199,73 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
6082
6199
|
}
|
|
6083
6200
|
}
|
|
6084
6201
|
if (selected) {
|
|
6085
|
-
|
|
6202
|
+
const selectionStyle = colorscheme?.styles?.has("selection") ? colorscheme.get("selection") : null;
|
|
6203
|
+
style = selectionStyle ?? { ...(colorscheme?.defaultStyle ?? {}), reverse: true };
|
|
6204
|
+
}
|
|
6205
|
+
if (lineMessages.length > 0 && inMessageAt(i)) {
|
|
6206
|
+
style = { ...style, underline: true };
|
|
6207
|
+
}
|
|
6208
|
+
|
|
6209
|
+
// Visualize whitespace
|
|
6210
|
+
let displayCh = ch;
|
|
6211
|
+
let useIndentCharFg = false;
|
|
6212
|
+
if (ch === " ") {
|
|
6213
|
+
// Go bufwindow.go:554-559: ispace only kicks in at tabsize boundaries (indent guide).
|
|
6214
|
+
const visualCol = scrollVisualCol + width;
|
|
6215
|
+
const useIspace = inLeading && indentspacechars && (visualCol % tabsize === 0);
|
|
6216
|
+
const candidate = useIspace ? indentspacechars : spacechars;
|
|
6217
|
+
if (candidate && candidate !== " ") {
|
|
6218
|
+
displayCh = candidate[0] ?? " ";
|
|
6219
|
+
useIndentCharFg = true;
|
|
6220
|
+
}
|
|
6221
|
+
} else if (ch === "\t") {
|
|
6222
|
+
const candidate = (inLeading && indenttabchars) ? indenttabchars : tabchars;
|
|
6223
|
+
if (candidate && candidate !== " ") {
|
|
6224
|
+
displayCh = candidate[0] ?? " ";
|
|
6225
|
+
useIndentCharFg = true;
|
|
6226
|
+
} else {
|
|
6227
|
+
displayCh = " ";
|
|
6228
|
+
}
|
|
6086
6229
|
}
|
|
6230
|
+
if (useIndentCharFg && indentCharFg != null) {
|
|
6231
|
+
style = { ...style, fg: indentCharFg };
|
|
6232
|
+
}
|
|
6233
|
+
|
|
6234
|
+
const ccAt = (visualCol) => {
|
|
6235
|
+
if (colorColumnBg == null || preservebg) return null;
|
|
6236
|
+
return visualCol === colorcolumn ? colorColumnBg : null;
|
|
6237
|
+
};
|
|
6238
|
+
|
|
6087
6239
|
if (ch === "\t") {
|
|
6088
|
-
const spaces = Math.min(
|
|
6089
|
-
for (let j = 0; j < spaces; j++)
|
|
6090
|
-
|
|
6240
|
+
const spaces = Math.min(tabsize, maxWidth - width);
|
|
6241
|
+
for (let j = 0; j < spaces; j++) {
|
|
6242
|
+
const visualCol = scrollVisualCol + width;
|
|
6243
|
+
const cellCh = j === 0 ? displayCh : " ";
|
|
6244
|
+
const ccBg = ccAt(visualCol);
|
|
6245
|
+
const cellStyle = ccBg != null ? { ...style, bg: ccBg } : style;
|
|
6246
|
+
cells.push({ ch: cellCh, style: cellStyle });
|
|
6247
|
+
width++;
|
|
6248
|
+
}
|
|
6091
6249
|
} else if (w > 0 && width + w <= maxWidth) {
|
|
6092
|
-
|
|
6250
|
+
const visualCol = scrollVisualCol + width;
|
|
6251
|
+
const ccBg = ccAt(visualCol);
|
|
6252
|
+
const cellStyle = ccBg != null ? { ...style, bg: ccBg } : style;
|
|
6253
|
+
cells.push({ ch: displayCh, style: cellStyle, width: w });
|
|
6093
6254
|
width += w;
|
|
6094
6255
|
}
|
|
6095
6256
|
i += charLen;
|
|
6096
6257
|
}
|
|
6097
|
-
//
|
|
6098
|
-
|
|
6099
|
-
const
|
|
6100
|
-
|
|
6258
|
+
// Trailing fill: cursor-line bg and color-column always apply (Go bufwindow.go:807-826)
|
|
6259
|
+
while (width < maxWidth) {
|
|
6260
|
+
const visualCol = scrollVisualCol + width;
|
|
6261
|
+
let padStyle = cursorLineBg
|
|
6262
|
+
? { ...(colorscheme?.defaultStyle ?? {}), bg: cursorLineBg }
|
|
6263
|
+
: (colorscheme?.defaultStyle ?? {});
|
|
6264
|
+
if (colorColumnBg != null && visualCol === colorcolumn) {
|
|
6265
|
+
padStyle = { ...padStyle, bg: colorColumnBg };
|
|
6266
|
+
}
|
|
6267
|
+
cells.push({ ch: " ", style: padStyle });
|
|
6268
|
+
width++;
|
|
6101
6269
|
}
|
|
6102
6270
|
return cells;
|
|
6103
6271
|
}
|
|
@@ -6229,9 +6397,14 @@ async function loadBuffers(files, command) {
|
|
|
6229
6397
|
if (loadBuffers.context) attachSyntax(stdinBuf, loadBuffers.context, "", stdinText);
|
|
6230
6398
|
buffers.push(stdinBuf);
|
|
6231
6399
|
} else {
|
|
6232
|
-
|
|
6400
|
+
const buffer = new BufferModel({ command });
|
|
6401
|
+
if (loadBuffers.context) attachSyntax(buffer, loadBuffers.context, "", "");
|
|
6402
|
+
buffers.push(buffer);
|
|
6233
6403
|
}
|
|
6234
|
-
|
|
6404
|
+
if (buffers.length > 0) return buffers;
|
|
6405
|
+
const buffer = new BufferModel({ command });
|
|
6406
|
+
if (loadBuffers.context) attachSyntax(buffer, loadBuffers.context, "", "");
|
|
6407
|
+
return [buffer];
|
|
6235
6408
|
}
|
|
6236
6409
|
|
|
6237
6410
|
async function printReadmeDocs() {
|
|
@@ -6666,6 +6839,7 @@ function getSelectionText(buf, selection) {
|
|
|
6666
6839
|
}
|
|
6667
6840
|
|
|
6668
6841
|
function attachSyntax(buffer, context, path, text) {
|
|
6842
|
+
buffer._syntaxContext = context;
|
|
6669
6843
|
const def = detectBufferSyntax(context.syntaxDefinitions, path, text);
|
|
6670
6844
|
buffer.syntaxDefinition = def;
|
|
6671
6845
|
buffer.filetype = def?.filetype ?? "unknown";
|
|
@@ -6673,21 +6847,32 @@ function attachSyntax(buffer, context, path, text) {
|
|
|
6673
6847
|
buffer.highlighter = def ? new Highlighter(def, context.syntaxDefinitions ?? []) : null;
|
|
6674
6848
|
buffer._highlightCache = null;
|
|
6675
6849
|
buffer._onOptionChange = (option, oldVal, newVal) => {
|
|
6850
|
+
if (option === "filetype") setBufferFiletype(buffer, context, newVal);
|
|
6676
6851
|
const ba = makeBufferAdapter(buffer);
|
|
6677
6852
|
context.plugins?.run("onBufferOptionChanged", ba, option, oldVal, newVal);
|
|
6678
6853
|
context.jsPlugins?.run("onBufferOptionChanged", ba, option, oldVal, newVal);
|
|
6679
6854
|
};
|
|
6680
6855
|
}
|
|
6681
6856
|
|
|
6857
|
+
function setBufferFiletype(buffer, context, filetype) {
|
|
6858
|
+
const value = String(filetype);
|
|
6859
|
+
const definitions = context?.syntaxDefinitions ?? [];
|
|
6860
|
+
const def = definitions.find((candidate) => candidate.filetype === value) ?? null;
|
|
6861
|
+
buffer.filetype = value;
|
|
6862
|
+
buffer.Settings.filetype = value;
|
|
6863
|
+
buffer.syntaxDefinition = def;
|
|
6864
|
+
buffer.highlighter = def ? new Highlighter(def, definitions) : null;
|
|
6865
|
+
buffer._highlightCache = null;
|
|
6866
|
+
}
|
|
6867
|
+
|
|
6682
6868
|
function detectBufferSyntax(definitions, path, text) {
|
|
6683
6869
|
if (!definitions) return null;
|
|
6684
|
-
const lines =
|
|
6870
|
+
const lines = normalizeBufferText(text).split("\n").slice(0, 50);
|
|
6685
6871
|
return detectSyntax(definitions, { path, firstLine: lines[0] ?? "", lines });
|
|
6686
6872
|
}
|
|
6687
6873
|
|
|
6688
6874
|
function detectBufferFiletype(definitions, path, text) {
|
|
6689
6875
|
if (!definitions) return "unknown";
|
|
6690
|
-
const lines = String(text).split("\n").slice(0, 50);
|
|
6691
6876
|
return detectBufferSyntax(definitions, path, text)?.filetype ?? "unknown";
|
|
6692
6877
|
}
|
|
6693
6878
|
|
|
@@ -6761,7 +6946,7 @@ async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAUL
|
|
|
6761
6946
|
);
|
|
6762
6947
|
continue;
|
|
6763
6948
|
}
|
|
6764
|
-
const lines = content.split("\n");
|
|
6949
|
+
const lines = normalizeBufferText(content).split("\n");
|
|
6765
6950
|
const def = detectSyntax(syntaxDefinitions, {
|
|
6766
6951
|
path: effectivePath ?? "",
|
|
6767
6952
|
firstLine: lines[0] ?? "",
|
package/src/runtime/registry.js
CHANGED
|
@@ -14,11 +14,13 @@ export class RuntimeRegistry {
|
|
|
14
14
|
this.configDir = configDir;
|
|
15
15
|
this.files = [[], [], [], [], []];
|
|
16
16
|
this.realFiles = [[], [], [], [], []];
|
|
17
|
+
this.fallbackFiles = [[], [], [], [], []];
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async init({ user = true } = {}) {
|
|
20
21
|
this.files = [[], [], [], [], []];
|
|
21
22
|
this.realFiles = [[], [], [], [], []];
|
|
23
|
+
this.fallbackFiles = [[], [], [], [], []];
|
|
22
24
|
await this.addRuntimeKind(RTColorscheme, "colorschemes", ".micro", user);
|
|
23
25
|
await this.addRuntimeKind(RTSyntax, "syntax", ".yaml", user);
|
|
24
26
|
await this.addRuntimeKind(RTSyntaxHeader, "syntax", ".hdr", user);
|
|
@@ -36,7 +38,10 @@ export class RuntimeRegistry {
|
|
|
36
38
|
for (const entry of entries) {
|
|
37
39
|
if (entry.isDirectory() || !entry.name.endsWith(extension)) continue;
|
|
38
40
|
const file = new RuntimeFile(join(dir, entry.name), real);
|
|
39
|
-
if (!real && this.realFiles[kind].some((f) => f.name === file.name))
|
|
41
|
+
if (!real && this.realFiles[kind].some((f) => f.name === file.name)) {
|
|
42
|
+
this.fallbackFiles[kind].push(file);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
40
45
|
this.files[kind].push(file);
|
|
41
46
|
if (real) this.realFiles[kind].push(file);
|
|
42
47
|
}
|
|
@@ -53,6 +58,10 @@ export class RuntimeRegistry {
|
|
|
53
58
|
find(kind, name) {
|
|
54
59
|
return this.list(kind).find((file) => file.name === name) ?? null;
|
|
55
60
|
}
|
|
61
|
+
|
|
62
|
+
fallback(kind, name) {
|
|
63
|
+
return this.fallbackFiles[kind]?.find((file) => file.name === name) ?? null;
|
|
64
|
+
}
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
class RuntimeFile {
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Visual PTY demo for bunmicro.
|
|
4
|
+
*
|
|
5
|
+
* Child terminal output is forwarded directly to the current TTY.
|
|
6
|
+
* stdin is not forwarded; press q, Ctrl-Q, or Esc to stop the demo.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bun tests/pty-demo.js
|
|
10
|
+
* bun tests/pty-demo.js --delay 700 --type-delay 45 --term-delay 1800
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
|
|
14
|
+
import { join, resolve,dirname,basename } from "node:path";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
|
|
17
|
+
const isAnci = Bun.env.TMPK_HOME && Bun.which('ldcustom')
|
|
18
|
+
|
|
19
|
+
const ROOT = resolve(import.meta.dir, "..");
|
|
20
|
+
const BUNMICRO = join(ROOT, "src", "index.js");
|
|
21
|
+
|
|
22
|
+
const DEMO_ONE_TEXT = `
|
|
23
|
+
|
|
24
|
+
# BUNMICRO PTY DEMO
|
|
25
|
+
"Use q, Ctrl-Q, or Esc to stop this demo."
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const DEMO_TWO_LONG_LINE = "This deliberately long line demonstrates soft wrapping across the editor viewport without real newline characters. 中文段落用來測試寬字元、游標移動與自動換行是否正確,並確認每一個漢字都能完整顯示。日本語の文章では、ひらがな、カタカナ、漢字を混ぜて、表示幅と折り返し位置を確認します。さらに長い一行を維持したまま、画面端で自然に折り返される動作を丁寧に実演します。中文日本語🚀✨🧪確認。";
|
|
29
|
+
const DEMO_TWO_TEXT = [
|
|
30
|
+
"SECOND TAB",
|
|
31
|
+
DEMO_TWO_LONG_LINE,
|
|
32
|
+
"",
|
|
33
|
+
].join("\n");
|
|
34
|
+
if ([...DEMO_TWO_LONG_LINE].length !== 250) throw new Error("demo two long line must be exactly 250 characters");
|
|
35
|
+
const options = {
|
|
36
|
+
delay: numberArg("--delay", 500),
|
|
37
|
+
typeDelay: numberArg("--type-delay", 100),
|
|
38
|
+
startupDelay: numberArg("--startup-delay", 5000),
|
|
39
|
+
termDelay: numberArg("--term-delay", 1400),
|
|
40
|
+
cols: numberArg("--cols", process.stdout.columns || 100),
|
|
41
|
+
rows: numberArg("--rows", process.stdout.rows || 30),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (process.argv.includes("--help")) {
|
|
45
|
+
console.log(`Usage: bun tests/pty-demo.js [options]
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--delay MS Delay after each action (default: ${options.delay})
|
|
49
|
+
--type-delay MS Delay between typed characters (default: ${options.typeDelay})
|
|
50
|
+
--startup-delay MS Initial editor startup delay (default: ${options.startupDelay})
|
|
51
|
+
--term-delay MS Extra delay for terminal pane startup (default: ${options.termDelay})
|
|
52
|
+
--cols N PTY columns (default: current TTY width)
|
|
53
|
+
--rows N PTY rows (default: current TTY height)
|
|
54
|
+
|
|
55
|
+
stdin is not forwarded to bunmicro. Press q, Ctrl-Q, or Esc to stop.`);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
60
|
+
console.error("pty-demo requires stdin and stdout to be TTYs.");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let stopped = false;
|
|
65
|
+
let resolveStopped;
|
|
66
|
+
const stoppedPromise = new Promise((resolve) => { resolveStopped = resolve; });
|
|
67
|
+
let terminal = null;
|
|
68
|
+
let proc = null;
|
|
69
|
+
let temp = "";
|
|
70
|
+
let originalRaw = false;
|
|
71
|
+
|
|
72
|
+
function requestStop() {
|
|
73
|
+
if (stopped) return;
|
|
74
|
+
stopped = true;
|
|
75
|
+
resolveStopped();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onInput(data) {
|
|
79
|
+
const bytes = Buffer.from(data);
|
|
80
|
+
if (bytes.includes(0x71) || bytes.includes(0x11) || bytes.includes(0x1b)) requestStop();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function onResize() {
|
|
84
|
+
if (!terminal || terminal.closed) return;
|
|
85
|
+
terminal.resize(process.stdout.columns || options.cols, process.stdout.rows || options.rows);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function pause(ms = options.delay) {
|
|
89
|
+
if (stopped) throw new Error("demo stopped");
|
|
90
|
+
await Promise.race([Bun.sleep(ms), stoppedPromise]);
|
|
91
|
+
if (stopped) throw new Error("demo stopped");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function setTitle(title) {
|
|
95
|
+
process.stdout.write(`\x1b]0;bunmicro demo: ${title}\x07`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function send(data, wait = options.delay) {
|
|
99
|
+
if (stopped) throw new Error("demo stopped");
|
|
100
|
+
terminal.write(data);
|
|
101
|
+
await pause(wait);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function type(text, wait = options.delay) {
|
|
105
|
+
for (const ch of text) {
|
|
106
|
+
if (stopped) throw new Error("demo stopped");
|
|
107
|
+
terminal.write(ch);
|
|
108
|
+
await pause(options.typeDelay);
|
|
109
|
+
}
|
|
110
|
+
await pause(wait);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function paste(text, wait = options.delay) {
|
|
114
|
+
await send(`\x1b[200~${text}\x1b[201~`, wait);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function action(title, fn, waitBefore = Math.min(250, options.delay)) {
|
|
118
|
+
setTitle(title);
|
|
119
|
+
await pause(waitBefore);
|
|
120
|
+
await fn();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function command(value, wait = options.delay) {
|
|
124
|
+
await send("\x05", Math.min(350, options.delay)); // Ctrl-E
|
|
125
|
+
await type(`${value}\r`, wait);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function click(x, y, button = 0) {
|
|
129
|
+
return `\x1b[<${button};${x + 1};${y + 1}M\x1b[<${button};${x + 1};${y + 1}m`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function runDemo({ file2, unknownFile, redetectedFile, crlfShell, themeCount }) {
|
|
133
|
+
await action("bracketed paste JavaScript sample", async () => {
|
|
134
|
+
await send("\x1b[F", 150);
|
|
135
|
+
await paste([
|
|
136
|
+
"",
|
|
137
|
+
"alpha one",
|
|
138
|
+
"alpha two",
|
|
139
|
+
"foo bar foo",
|
|
140
|
+
"",
|
|
141
|
+
"async function loadProfile(userId) {",
|
|
142
|
+
" const enabled = true;",
|
|
143
|
+
" const retries = 3;",
|
|
144
|
+
' const greeting = "hello from bunmicro";',
|
|
145
|
+
" const tags = [\"editor\", \"javascript\", \"demo\"];",
|
|
146
|
+
" const profile = { userId, enabled, retries, tags };",
|
|
147
|
+
" await new Promise((resolve) => setTimeout(resolve, 250));",
|
|
148
|
+
" if (!profile.enabled) throw new Error(\"disabled profile\");",
|
|
149
|
+
" return { ...profile, greeting };",
|
|
150
|
+
"}",
|
|
151
|
+
"",
|
|
152
|
+
"loadProfile(42).then(console.log).catch(console.error);",
|
|
153
|
+
"mouse target",
|
|
154
|
+
].join("\n"), 1000);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await action("cursor movement and edit", async () => {
|
|
158
|
+
await send("\x1b[A\x1b[A\x1b[H", 200);
|
|
159
|
+
await type("[edited] ");
|
|
160
|
+
await send("\x1b[F", 150);
|
|
161
|
+
await type(" !");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await action("undo and redo", async () => {
|
|
165
|
+
await send("\x1a", 350);
|
|
166
|
+
await send("\x19", 500);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await action("set filetype applies syntax instantly", async () => {
|
|
170
|
+
await command("set filetype javascript", 1100);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await action("theme picker: preview every theme with Down", async () => {
|
|
174
|
+
await send("\x05", 250); // Ctrl-E
|
|
175
|
+
await type("theme");
|
|
176
|
+
setTitle("theme picker: Space opens theme completions");
|
|
177
|
+
await send(" ", 350);
|
|
178
|
+
setTitle("theme picker: Tab previews first theme");
|
|
179
|
+
await send("\t", 850);
|
|
180
|
+
for (let i = 1; i < themeCount; i++) {
|
|
181
|
+
setTitle(`theme picker: Down ${i}/${themeCount - 1}`);
|
|
182
|
+
await send("\x1b[B", 650);
|
|
183
|
+
}
|
|
184
|
+
setTitle("theme picker: reached final theme");
|
|
185
|
+
await pause(1000);
|
|
186
|
+
await send("\x1b", 500); // Cancel preview and restore original theme
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await action("select fixed theme: dracula-tc", async () => {
|
|
190
|
+
await command("theme darcula", 1000);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await action("find alpha", async () => {
|
|
194
|
+
await send("\x06", 300);
|
|
195
|
+
await type("alpha");
|
|
196
|
+
await send("\r", 500);
|
|
197
|
+
await send("\x0e", 500);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await action("interactive replace foo", async () => {
|
|
201
|
+
await send("\x1bh", 300);
|
|
202
|
+
await type("foo FIRST");
|
|
203
|
+
await send("\r", 700);
|
|
204
|
+
setTitle("interactive replace: accept first match");
|
|
205
|
+
await send("y", 700);
|
|
206
|
+
setTitle("interactive replace: skip second match");
|
|
207
|
+
await send("n", 700);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await action("replace all alpha", async () => {
|
|
211
|
+
await command("replaceall alpha 阿爾法", 650);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await action("show whitespace and trailing spaces", async () => {
|
|
215
|
+
await command("set showchars tab=>,space=.", 450);
|
|
216
|
+
await command("set hltrailingws true", 450);
|
|
217
|
+
await command("set colorcolumn 30", 600);
|
|
218
|
+
await command("set hltaberrors true", 450);
|
|
219
|
+
await command("set tabstospaces true", 850);
|
|
220
|
+
await send("\x1b[F", 120);
|
|
221
|
+
await type(" ");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await action("save", async () => {
|
|
225
|
+
await send("\x13", 650);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await action("unknown filetype saves as .js and redetects", async () => {
|
|
229
|
+
await command(`open ${unknownFile}`, 850);
|
|
230
|
+
await command(`save ${redetectedFile}`, 1200);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await action("select all and eval js", async () => {
|
|
234
|
+
await send("\x01", 900); // Ctrl-A
|
|
235
|
+
|
|
236
|
+
if(isAnci)
|
|
237
|
+
await command("js Object.getOwnPropertyNames(''.__proto__)", 2500);
|
|
238
|
+
else
|
|
239
|
+
await command("eval js", 2500);
|
|
240
|
+
setTitle("eval js result: press Enter to continue");
|
|
241
|
+
await send("\r", 1000);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await action("DOS CRLF shell script warning", async () => {
|
|
245
|
+
await command(`open ${crlfShell}`, 1200);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await action("open second file in another tab", async () => {
|
|
249
|
+
await command(`open ${file2}`, 800);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await action("enable softwrap for long line", async () => {
|
|
253
|
+
await command("set softwrap on", 1200);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await action("previous and next tab", async () => {
|
|
257
|
+
await send("\x1bp", 650);
|
|
258
|
+
await send("\x1bt", 650);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await action("new scratch tab", async () => {
|
|
262
|
+
await send("\x14", 600);
|
|
263
|
+
await type("scratch tab\ncreated by PTY demo");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await action("mouse click unsaved star opens save command", async () => {
|
|
267
|
+
await send(click(9, options.rows - 1), 750);
|
|
268
|
+
await send("\x1b", 500);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
await action("mouse command and shell icons toggle prompts", async () => {
|
|
272
|
+
await send(click(25, options.rows - 1), 650); // € opens command prompt
|
|
273
|
+
await send(click(25, options.rows - 2), 650); // € closes command prompt
|
|
274
|
+
await send(click(32, options.rows - 1), 650); // $ opens shell prompt
|
|
275
|
+
await send(click(32, options.rows - 2), 650); // $ closes shell prompt
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await action("mouse click first tab", async () => {
|
|
279
|
+
await send(click(2, 0), 800);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await action("vertical split and pane switch", async () => {
|
|
283
|
+
await command("vsplit", 800);
|
|
284
|
+
await send("\x17", 650);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await action("mouse click editor pane", async () => {
|
|
288
|
+
await send(click(Math.floor(options.cols * 0.75), 5), 700);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await action("horizontal split and pane switch", async () => {
|
|
292
|
+
await command("hsplit", 800);
|
|
293
|
+
await send("\x17", 650);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await action("Ctrl-B interactive shell", async () => {
|
|
297
|
+
await send("\x02", 350); // Ctrl-B
|
|
298
|
+
await type("echo BUNMICRO_CTRL_B_SHELL");
|
|
299
|
+
await send("\r", options.termDelay);
|
|
300
|
+
setTitle("Ctrl-B shell: press Enter to return");
|
|
301
|
+
await send("\r", 900);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await action("open terminal pane", async () => {
|
|
305
|
+
await command("term", options.termDelay);
|
|
306
|
+
await type("printf 'BUNMICRO_TERM_DEMO\\n'\r", options.termDelay);
|
|
307
|
+
await send("\x1b", 800);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await action("mouse wheel and editor click", async () => {
|
|
311
|
+
await send(click(10, 8, 65), 500);
|
|
312
|
+
await send(click(10, 8, 64), 500);
|
|
313
|
+
await send(click(12, 6), 700);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await action("final tab switching", async () => {
|
|
317
|
+
await send("\x1bp", 600);
|
|
318
|
+
await send("\x1bt", 900);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function cleanup() {
|
|
323
|
+
process.stdin.off("data", onInput);
|
|
324
|
+
process.stdout.off("resize", onResize);
|
|
325
|
+
|
|
326
|
+
if (proc?.exitCode === null) {
|
|
327
|
+
// Let bunmicro restore the terminal itself first. Escape closes a possible
|
|
328
|
+
// terminal pane; repeated Ctrl-Q/n handles panes, tabs, and save prompts.
|
|
329
|
+
terminal?.write("\x1b");
|
|
330
|
+
await Bun.sleep(250);
|
|
331
|
+
for (let i = 0; i < 12 && proc.exitCode === null; i++) {
|
|
332
|
+
terminal?.write("\x11"); // Ctrl-Q
|
|
333
|
+
await Bun.sleep(180);
|
|
334
|
+
terminal?.write("n");
|
|
335
|
+
await Bun.sleep(120);
|
|
336
|
+
}
|
|
337
|
+
await Promise.race([proc.exited, Bun.sleep(700)]);
|
|
338
|
+
}
|
|
339
|
+
if (proc?.exitCode === null) {
|
|
340
|
+
proc.kill();
|
|
341
|
+
await Promise.race([proc.exited, Bun.sleep(500)]);
|
|
342
|
+
}
|
|
343
|
+
if (terminal && !terminal.closed) terminal.close();
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
let udroot="";
|
|
347
|
+
|
|
348
|
+
if(isAnci)
|
|
349
|
+
{
|
|
350
|
+
udroot=join(Bun.env.HOME,'.udocker/containers/alpine-toolbox/ROOT')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (temp) await rm(udroot+temp, { recursive: true, force: true });
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
// Defensive reset in case the child was killed before emitting its teardown.
|
|
357
|
+
process.stdout.write([
|
|
358
|
+
"\x1b[?1000l", // normal mouse tracking
|
|
359
|
+
"\x1b[?1001l", // highlight mouse tracking
|
|
360
|
+
"\x1b[?1002l", // button-event mouse tracking
|
|
361
|
+
"\x1b[?1003l", // any-event mouse tracking
|
|
362
|
+
"\x1b[?1004l", // focus events
|
|
363
|
+
"\x1b[?1005l", // UTF-8 mouse encoding
|
|
364
|
+
"\x1b[?1006l", // SGR mouse encoding
|
|
365
|
+
"\x1b[?1007l", // alternate scroll mode
|
|
366
|
+
"\x1b[?1015l", // urxvt mouse encoding
|
|
367
|
+
"\x1b[?1016l", // SGR pixel mouse encoding
|
|
368
|
+
"\x1b[?2004l", // bracketed paste
|
|
369
|
+
"\x1b[?2026l", // synchronized output
|
|
370
|
+
"\x1b[>4;0m", // xterm modifyOtherKeys
|
|
371
|
+
"\x1b[<u", // pop kitty keyboard protocol
|
|
372
|
+
"\x1b[0m",
|
|
373
|
+
"\x1b[?25h",
|
|
374
|
+
"\x1b[?1049l", // leave alternate screen last
|
|
375
|
+
].join(""));
|
|
376
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(originalRaw);
|
|
377
|
+
process.stdin.pause();
|
|
378
|
+
setTitle(stopped ? "stopped" : "complete");
|
|
379
|
+
process.stdout.write("\n");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function main() {
|
|
383
|
+
|
|
384
|
+
let udroot="";
|
|
385
|
+
if(isAnci)
|
|
386
|
+
{
|
|
387
|
+
await Bun.spawn(['fish','-c echo Using fish shell!'],{env:Bun.env}).exited
|
|
388
|
+
|
|
389
|
+
temp = '/tmp/bunmicro-pty-demo-'+Bun.randomUUIDv7().slice(0,8);
|
|
390
|
+
udroot=join(Bun.env.HOME,'.udocker/containers/alpine-toolbox/ROOT')
|
|
391
|
+
}
|
|
392
|
+
else
|
|
393
|
+
temp = await mkdtemp(join(tmpdir(), "bunmicro-pty-demo-"));
|
|
394
|
+
|
|
395
|
+
const configDir = join(temp, "config");
|
|
396
|
+
const file1 = join(temp, "pty-demo-one.txt");
|
|
397
|
+
const file2 = join(temp, "pty-demo-two.txt");
|
|
398
|
+
const unknownFile = join(temp, "pty-demo-redetect");
|
|
399
|
+
const redetectedFile = join(temp, "pty-demo-redetected.js");
|
|
400
|
+
const crlfShell = join(temp, "pty-demo-crlf.sh");
|
|
401
|
+
const syntaxDir = join(configDir, "syntax");
|
|
402
|
+
const themeCount = (await readdir(join(ROOT, "runtime", "colorschemes")))
|
|
403
|
+
.filter((name) => name.endsWith(".micro")).length;
|
|
404
|
+
await mkdir(udroot+configDir, { recursive: true });
|
|
405
|
+
await mkdir(udroot+syntaxDir, { recursive: true });
|
|
406
|
+
await Bun.write(join(udroot,configDir, "settings.json"), JSON.stringify({
|
|
407
|
+
colorscheme: "default",
|
|
408
|
+
mouse: true,
|
|
409
|
+
savecursor: false,
|
|
410
|
+
savehistory: false,
|
|
411
|
+
}, null, 2));
|
|
412
|
+
|
|
413
|
+
await Bun.write(udroot+file1, DEMO_ONE_TEXT);
|
|
414
|
+
await Bun.write(udroot+file2, DEMO_TWO_TEXT);
|
|
415
|
+
await Bun.write(udroot+unknownFile, `
|
|
416
|
+
async function hello(){
|
|
417
|
+
const redetected = 'yes';
|
|
418
|
+
console.log(
|
|
419
|
+
Object.getOwnPropertyNames(
|
|
420
|
+
redetected.__proto__
|
|
421
|
+
)
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await hello();
|
|
426
|
+
`);
|
|
427
|
+
await Bun.write(udroot+crlfShell, "#!/bin/sh\r\necho CRLF_SHELL_WARNING\r\n");
|
|
428
|
+
await Bun.write(join(udroot,syntaxDir, "javascript.yaml"), "filetype: javascript\nrules: [invalid yaml");
|
|
429
|
+
|
|
430
|
+
console.log([
|
|
431
|
+
"NOTE: The upcoming JavaScript YAML parse failure is intentional.",
|
|
432
|
+
"It demonstrates fallback to bunmicro's built-in JavaScript syntax.",
|
|
433
|
+
].join("\n"));
|
|
434
|
+
|
|
435
|
+
originalRaw = process.stdin.isRaw ?? false;
|
|
436
|
+
process.stdin.setRawMode(true);
|
|
437
|
+
process.stdin.resume();
|
|
438
|
+
process.stdin.on("data", onInput);
|
|
439
|
+
process.stdout.on("resize", onResize);
|
|
440
|
+
|
|
441
|
+
terminal = new Bun.Terminal({
|
|
442
|
+
cols: options.cols,
|
|
443
|
+
rows: options.rows,
|
|
444
|
+
data(_terminal, data) {
|
|
445
|
+
process.stdout.write(data);
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
let bunArr
|
|
450
|
+
if(isAnci)
|
|
451
|
+
{
|
|
452
|
+
bunArr=['fish','-c',`/android/bin/ldcustom --library-path /android/glibc:/android/bun /android/bun/bun-linux-aarch64 $argv`,'--']
|
|
453
|
+
}
|
|
454
|
+
else
|
|
455
|
+
bunArr=['bun'];
|
|
456
|
+
|
|
457
|
+
proc = Bun.spawn({
|
|
458
|
+
cmd: [...bunArr, BUNMICRO, "-config-dir", configDir, file1],
|
|
459
|
+
cwd: ROOT,
|
|
460
|
+
terminal,
|
|
461
|
+
env: {
|
|
462
|
+
...process.env,
|
|
463
|
+
TERM: process.env.TERM || "xterm-256color",
|
|
464
|
+
COLORTERM: process.env.COLORTERM || "truecolor",
|
|
465
|
+
COLUMNS: String(options.cols),
|
|
466
|
+
LINES: String(options.rows),
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
setTitle("startup - press q to stop");
|
|
471
|
+
await pause(options.startupDelay);
|
|
472
|
+
await runDemo({ file2, unknownFile, redetectedFile, crlfShell, themeCount });
|
|
473
|
+
await pause(1000);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
await main();
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (!stopped) throw error;
|
|
480
|
+
} finally {
|
|
481
|
+
await cleanup();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function numberArg(name, fallback) {
|
|
485
|
+
const eqArg = process.argv.find((arg) => arg.startsWith(`${name}=`));
|
|
486
|
+
const index = process.argv.indexOf(name);
|
|
487
|
+
const raw = eqArg?.slice(name.length + 1) ?? (index >= 0 ? process.argv[index + 1] : undefined);
|
|
488
|
+
if (raw === undefined) return fallback;
|
|
489
|
+
const value = Number(raw);
|
|
490
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative number`);
|
|
491
|
+
return Math.floor(value);
|
|
492
|
+
}
|