bunmicro 0.9.23 → 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 +10 -0
- package/package.json +3 -2
- package/src/config/colorscheme.js +10 -20
- package/src/index.js +207 -48
- package/tests/pty-demo.js +492 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## [0.9.23] - 2026-06-10
|
|
4
14
|
- Fixed mouse close prompt cursor move
|
|
5
15
|
- Fixed set filetype doesn't apply instantly
|
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/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
|
};
|
|
@@ -1879,7 +1887,9 @@ class App {
|
|
|
1879
1887
|
const ft = (buf?.filetype && buf.filetype !== "unknown") ? buf.filetype : "?";
|
|
1880
1888
|
const fmt = buf?.fileformat ?? "unix";
|
|
1881
1889
|
const enc = buf?.encoding ?? "utf-8";
|
|
1882
|
-
const baseStatus =
|
|
1890
|
+
const baseStatus = this.context.colorscheme?.styles?.has("statusline")
|
|
1891
|
+
? this.context.colorscheme.get("statusline")
|
|
1892
|
+
: { ...defaultStyle, reverse: true };
|
|
1883
1893
|
const redStatus = { ...baseStatus, fg: "red" };
|
|
1884
1894
|
// Fill entire row with base style first
|
|
1885
1895
|
putText(this.screen, 0, statusRow, " ".repeat(this.cols), baseStatus, this.cols);
|
|
@@ -2010,8 +2020,9 @@ class App {
|
|
|
2010
2020
|
}
|
|
2011
2021
|
|
|
2012
2022
|
renderMessageRow(defaultStyle, row, message) {
|
|
2013
|
-
const
|
|
2014
|
-
|
|
2023
|
+
const style = this.context.colorscheme?.styles?.has("message")
|
|
2024
|
+
? this.context.colorscheme.get("message")
|
|
2025
|
+
: defaultStyle;
|
|
2015
2026
|
putText(this.screen, 0, row, " ".repeat(this.cols), style, this.cols);
|
|
2016
2027
|
this._messageRowY = row;
|
|
2017
2028
|
this._messageRowClickZone = null;
|
|
@@ -2041,15 +2052,15 @@ class App {
|
|
|
2041
2052
|
}
|
|
2042
2053
|
|
|
2043
2054
|
renderSuggestions(defaultStyle, row, suggestions, curIdx) {
|
|
2044
|
-
const
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
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 };
|
|
2048
2061
|
const selStyle = {
|
|
2049
2062
|
...baseStyle,
|
|
2050
|
-
|
|
2051
|
-
bg: baseStyle.fg === "default" ? "brightwhite" : baseStyle.fg,
|
|
2052
|
-
reverse: false,
|
|
2063
|
+
reverse: true,
|
|
2053
2064
|
};
|
|
2054
2065
|
|
|
2055
2066
|
// Compute each item's position in the virtual (pre-scroll) space.
|
|
@@ -2172,9 +2183,16 @@ class App {
|
|
|
2172
2183
|
const softwrap = buf.Settings?.softwrap ?? false;
|
|
2173
2184
|
const wordwrap = softwrap && (buf.Settings?.wordwrap ?? false);
|
|
2174
2185
|
const tabsize = buf.Settings?.tabsize ?? DEFAULT_SETTINGS.tabsize;
|
|
2175
|
-
const gutterStyle = { ...defaultStyle, fg: "brightblack" };
|
|
2176
|
-
const dirtyGutterStyle = { ...gutterStyle, fg: "red" };
|
|
2177
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" };
|
|
2178
2196
|
const useCursorline = (buf.Settings?.cursorline ?? DEFAULT_SETTINGS.cursorline) && isActivePane;
|
|
2179
2197
|
const clBg = (useCursorline && this.context.colorscheme?.styles?.has("cursor-line"))
|
|
2180
2198
|
? (this.context.colorscheme.get("cursor-line")?.fg ?? null)
|
|
@@ -2187,12 +2205,12 @@ class App {
|
|
|
2187
2205
|
const diffCol = (buf.Settings?.diffgutter ?? false) ? 1 : 0;
|
|
2188
2206
|
const lineNumW = gutterW - msgW - diffCol;
|
|
2189
2207
|
const cs = this.context.colorscheme;
|
|
2190
|
-
const diffAddStyle = cs?.
|
|
2191
|
-
const diffModStyle = cs?.
|
|
2192
|
-
const diffDelStyle = cs?.
|
|
2193
|
-
const msgInfoStyle
|
|
2194
|
-
const msgWarnStyle
|
|
2195
|
-
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;
|
|
2196
2214
|
|
|
2197
2215
|
const renderGutter = (lineNo, row, screenRow, subRow = 0) => {
|
|
2198
2216
|
// Message indicator: 2 cols, '> ' with kind-based style (Go: drawGutter)
|
|
@@ -2209,19 +2227,22 @@ class App {
|
|
|
2209
2227
|
}
|
|
2210
2228
|
putText(this.screen, pane.x, screenRow, msgCh + " ", msgSt, 2);
|
|
2211
2229
|
}
|
|
2230
|
+
const isCurrentLine = isActivePane && !pane.selection && lineNo === buf.cursor.y;
|
|
2231
|
+
const lineStyle = isCurrentLine ? curNumStyle : gutterStyle;
|
|
2212
2232
|
if (diffCol > 0) {
|
|
2213
2233
|
const m = diffMarks?.[lineNo] ?? 0;
|
|
2214
|
-
const [ch,
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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;
|
|
2218
2239
|
putText(this.screen, pane.x + msgW, screenRow, subRow === 0 ? ch : " ", st, 1);
|
|
2219
2240
|
}
|
|
2220
2241
|
if (lineNumW > 0) {
|
|
2221
2242
|
const prefix = subRow === 0
|
|
2222
2243
|
? lineNumberText(buf, lineNo, row, lineNumW)
|
|
2223
2244
|
: visualLineNumberText(subRow, lineNumW);
|
|
2224
|
-
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);
|
|
2225
2246
|
}
|
|
2226
2247
|
};
|
|
2227
2248
|
|
|
@@ -2305,7 +2326,11 @@ class App {
|
|
|
2305
2326
|
|
|
2306
2327
|
renderDividers(node, defaultStyle) {
|
|
2307
2328
|
if (node instanceof Pane) return;
|
|
2308
|
-
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;
|
|
2309
2334
|
for (let i = 0; i < node.children.length - 1; i++) {
|
|
2310
2335
|
const child = node.children[i];
|
|
2311
2336
|
if (node.dir === "h") {
|
|
@@ -2322,21 +2347,37 @@ class App {
|
|
|
2322
2347
|
}
|
|
2323
2348
|
|
|
2324
2349
|
renderTabbar(defaultStyle) {
|
|
2325
|
-
const
|
|
2326
|
-
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
|
+
|
|
2327
2368
|
let x = 0;
|
|
2328
2369
|
for (let i = 0; i < this.tabs.length && x < this.cols; i++) {
|
|
2329
2370
|
const name = this.tabs[i].name || "No name";
|
|
2330
2371
|
const isActive = i === this.activeTabIdx;
|
|
2331
2372
|
const label = isActive ? `[${name}]` : ` ${name} `;
|
|
2332
|
-
const style = isActive ?
|
|
2373
|
+
const style = isActive ? charActive : charBase;
|
|
2333
2374
|
const start = x;
|
|
2334
2375
|
x = putText(this.screen, x, 0, label, style, this.cols - x);
|
|
2335
2376
|
this.tabRects.push({ index: i, start, end: x });
|
|
2336
2377
|
if (i < this.tabs.length - 1 && x < this.cols)
|
|
2337
|
-
x = putText(this.screen, x, 0, " ",
|
|
2378
|
+
x = putText(this.screen, x, 0, " ", sepStyle, this.cols - x);
|
|
2338
2379
|
}
|
|
2339
|
-
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);
|
|
2340
2381
|
}
|
|
2341
2382
|
|
|
2342
2383
|
updateScrollForPane(pane) {
|
|
@@ -6047,22 +6088,75 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
6047
6088
|
// Go: cursor-line bg is skipped when a syntax style already has a non-default background (preservebg)
|
|
6048
6089
|
const defBg = colorscheme?.defaultStyle?.bg ?? "default";
|
|
6049
6090
|
|
|
6050
|
-
const showTrailingWs = buf.Settings?.
|
|
6091
|
+
const showTrailingWs = buf.Settings?.hltrailingws ?? false;
|
|
6051
6092
|
let trailingWsIdx = raw.length;
|
|
6052
6093
|
if (showTrailingWs) {
|
|
6053
6094
|
let k = raw.length - 1;
|
|
6054
6095
|
while (k >= 0 && (raw[k] === " " || raw[k] === "\t")) k--;
|
|
6055
6096
|
trailingWsIdx = k + 1;
|
|
6056
6097
|
}
|
|
6057
|
-
const
|
|
6058
|
-
?
|
|
6098
|
+
const trailingWsColor = showTrailingWs && colorscheme?.styles?.has("trailingws")
|
|
6099
|
+
? colorscheme.get("trailingws")?.fg
|
|
6100
|
+
: null;
|
|
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
|
|
6059
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
|
+
};
|
|
6060
6155
|
|
|
6061
6156
|
let changeIndex = 0;
|
|
6062
6157
|
let searchIdx = 0;
|
|
6063
6158
|
let i = scrollX;
|
|
6064
6159
|
while (i < raw.length && width < maxWidth) {
|
|
6065
|
-
const cp = raw.codePointAt(i);
|
|
6066
6160
|
const unit = displayUnitAt(raw, i);
|
|
6067
6161
|
const ch = unit.text;
|
|
6068
6162
|
const charLen = unit.length;
|
|
@@ -6071,16 +6165,30 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
6071
6165
|
while (changeIndex + 1 < changes.length && i >= changes[changeIndex + 1][0]) changeIndex++;
|
|
6072
6166
|
const group = changes[changeIndex]?.[1] ?? "default";
|
|
6073
6167
|
const syntaxStyle = colorscheme?.get(group) ?? colorscheme?.defaultStyle ?? {};
|
|
6074
|
-
|
|
6075
|
-
|
|
6168
|
+
let preservebg = syntaxStyle.bg !== undefined && syntaxStyle.bg !== defBg;
|
|
6169
|
+
let baseStyle = (cursorLineBg && !preservebg) ? { ...syntaxStyle, bg: cursorLineBg } : syntaxStyle;
|
|
6076
6170
|
while (searchIdx < searchRanges.length && searchRanges[searchIdx][1] <= i) searchIdx++;
|
|
6077
6171
|
const inSearch = searchIdx < searchRanges.length && i >= searchRanges[searchIdx][0] && i < searchRanges[searchIdx][1];
|
|
6078
6172
|
const selected = isSelected(selection, lineNo, i, i + charLen);
|
|
6079
6173
|
const braceMatched = braceMatches?.has(String(lineNo) + ":" + String(i));
|
|
6080
|
-
|
|
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
|
+
}
|
|
6081
6188
|
if (inSearch) {
|
|
6082
6189
|
const searchStyle = colorscheme?.styles?.has("hlsearch") ? colorscheme.get("hlsearch") : null;
|
|
6083
|
-
style = searchStyle ?? { ...
|
|
6190
|
+
style = searchStyle ?? { ...(colorscheme?.defaultStyle ?? {}), reverse: true };
|
|
6191
|
+
if ((style.bg ?? "default") !== defBg) preservebg = true;
|
|
6084
6192
|
}
|
|
6085
6193
|
if (braceMatched) {
|
|
6086
6194
|
if ((buf.Settings?.matchbracestyle ?? DEFAULT_SETTINGS.matchbracestyle) === "highlight") {
|
|
@@ -6091,22 +6199,73 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
6091
6199
|
}
|
|
6092
6200
|
}
|
|
6093
6201
|
if (selected) {
|
|
6094
|
-
|
|
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
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
if (useIndentCharFg && indentCharFg != null) {
|
|
6231
|
+
style = { ...style, fg: indentCharFg };
|
|
6095
6232
|
}
|
|
6233
|
+
|
|
6234
|
+
const ccAt = (visualCol) => {
|
|
6235
|
+
if (colorColumnBg == null || preservebg) return null;
|
|
6236
|
+
return visualCol === colorcolumn ? colorColumnBg : null;
|
|
6237
|
+
};
|
|
6238
|
+
|
|
6096
6239
|
if (ch === "\t") {
|
|
6097
|
-
const spaces = Math.min(
|
|
6098
|
-
for (let j = 0; j < spaces; j++)
|
|
6099
|
-
|
|
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
|
+
}
|
|
6100
6249
|
} else if (w > 0 && width + w <= maxWidth) {
|
|
6101
|
-
|
|
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 });
|
|
6102
6254
|
width += w;
|
|
6103
6255
|
}
|
|
6104
6256
|
i += charLen;
|
|
6105
6257
|
}
|
|
6106
|
-
//
|
|
6107
|
-
|
|
6108
|
-
const
|
|
6109
|
-
|
|
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++;
|
|
6110
6269
|
}
|
|
6111
6270
|
return cells;
|
|
6112
6271
|
}
|
|
@@ -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
|
+
}
|