bunmicro 0.9.0 → 0.9.5
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 +9 -0
- package/README.md +4 -0
- package/package.json +1 -1
- package/src/buffer/fixed3-codec.js +140 -0
- package/src/index.js +171 -23
- package/src/screen/vt100.js +112 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.5] - 2026-06-03
|
|
4
|
+
- Added encoding hex3 for binary edit
|
|
5
|
+
- term better supports fish
|
|
6
|
+
- enter to close term
|
|
7
|
+
- bat-like highlighting supports URL
|
|
8
|
+
|
|
9
|
+
## [0.9.1] - 2026-06-03
|
|
10
|
+
- Upgrade method explanation
|
|
11
|
+
|
|
3
12
|
## [0.9.0] - 2026-06-03
|
|
4
13
|
- Added more alt- key bindings
|
|
5
14
|
- Alt-s for Selection mode
|
package/README.md
CHANGED
|
@@ -77,6 +77,8 @@ npm install -g bun
|
|
|
77
77
|
|
|
78
78
|
# Run bunmicro(stable)
|
|
79
79
|
npx bunmicro
|
|
80
|
+
# npx bunmicro@latest to upgrade to new version
|
|
81
|
+
|
|
80
82
|
# npx bunmicro [options] [file1] [file2] ...
|
|
81
83
|
# alternative: bun bunmicro/src/index.js [options] [file1] [file2] ...
|
|
82
84
|
# if npx is not available, use npm x -- bunmicro
|
|
@@ -93,6 +95,8 @@ npx bunmicro
|
|
|
93
95
|
|
|
94
96
|
```sh
|
|
95
97
|
bun x bunmicro
|
|
98
|
+
# bun x bunmicro@latest to upgrade to new version
|
|
99
|
+
|
|
96
100
|
# bun x bunmicro [options] [file1] [file2] ...
|
|
97
101
|
# alternative: bun bunmicro/src/index.js [options] [file1] [file2] ...
|
|
98
102
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const HEX = Array.from({ length: 256 }, (_, i) =>
|
|
2
|
+
i.toString(16).padStart(2, "0"),
|
|
3
|
+
);
|
|
4
|
+
const ENCODE_TABLE = new Uint8Array(256 * 3);
|
|
5
|
+
const DECODE_HEX = new Int16Array(128);
|
|
6
|
+
|
|
7
|
+
DECODE_HEX.fill(-1);
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < 10; i++) {
|
|
10
|
+
DECODE_HEX[0x30 + i] = i;
|
|
11
|
+
}
|
|
12
|
+
for (let i = 0; i < 6; i++) {
|
|
13
|
+
DECODE_HEX[0x41 + i] = 10 + i;
|
|
14
|
+
DECODE_HEX[0x61 + i] = 10 + i;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (let byte = 0; byte < 256; byte++) {
|
|
18
|
+
const offset = byte * 3;
|
|
19
|
+
|
|
20
|
+
if (byte >= 0x20 && byte <= 0x7e) {
|
|
21
|
+
ENCODE_TABLE[offset] = byte;
|
|
22
|
+
ENCODE_TABLE[offset + 1] = 0x2e;
|
|
23
|
+
ENCODE_TABLE[offset + 2] = 0x2e;
|
|
24
|
+
} else {
|
|
25
|
+
ENCODE_TABLE[offset] = 0x5c;
|
|
26
|
+
ENCODE_TABLE[offset + 1] = HEX[byte].charCodeAt(0);
|
|
27
|
+
ENCODE_TABLE[offset + 2] = HEX[byte].charCodeAt(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class Fixed3DecodeError extends SyntaxError {
|
|
32
|
+
constructor(message, position) {
|
|
33
|
+
super(`${message} at text offset ${position}`);
|
|
34
|
+
this.name = "Fixed3DecodeError";
|
|
35
|
+
this.position = position;
|
|
36
|
+
this.byteOffset = Math.floor(position / 3);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function encodeBinary(input) {
|
|
41
|
+
return encodeBinaryToBuffer(input).toString("latin1");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function encodeBinaryToBuffer(input) {
|
|
45
|
+
const bytes = input instanceof Uint8Array ? input : Buffer.from(input);
|
|
46
|
+
const out = Buffer.allocUnsafe(bytes.byteLength * 3);
|
|
47
|
+
let j = 0;
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
50
|
+
const offset = bytes[i] * 3;
|
|
51
|
+
out[j++] = ENCODE_TABLE[offset];
|
|
52
|
+
out[j++] = ENCODE_TABLE[offset + 1];
|
|
53
|
+
out[j++] = ENCODE_TABLE[offset + 2];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function decodeBinary(text) {
|
|
60
|
+
if (typeof text !== "string") {
|
|
61
|
+
throw new TypeError("decodeBinary() expects a string");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return decodeBinaryBytes(Buffer.from(text, "latin1"));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function decodeBinaryBytes(input) {
|
|
68
|
+
const bytes = input instanceof Uint8Array ? input : Buffer.from(input);
|
|
69
|
+
const out = Buffer.allocUnsafe(Math.floor(bytes.byteLength / 3));
|
|
70
|
+
let j = 0;
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < bytes.byteLength; i += 3) {
|
|
73
|
+
const a = bytes[i];
|
|
74
|
+
const b = bytes[i + 1];
|
|
75
|
+
const c = bytes[i + 2];
|
|
76
|
+
|
|
77
|
+
if (b === 0x2e && c === 0x2e) {
|
|
78
|
+
out[j++] = a;
|
|
79
|
+
} else {
|
|
80
|
+
out[j++] = (DECODE_HEX[b] << 4) | DECODE_HEX[c];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function decodeBinaryStrict(text) {
|
|
88
|
+
if (typeof text !== "string") {
|
|
89
|
+
throw new TypeError("decodeBinary() expects a string");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (text.length % 3 !== 0) {
|
|
93
|
+
throw new Fixed3DecodeError("input length is not a multiple of 3", text.length);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const out = Buffer.allocUnsafe(text.length / 3);
|
|
97
|
+
let j = 0;
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < text.length; i += 3) {
|
|
100
|
+
const a = text.charCodeAt(i);
|
|
101
|
+
const b = text.charCodeAt(i + 1);
|
|
102
|
+
const c = text.charCodeAt(i + 2);
|
|
103
|
+
|
|
104
|
+
if (b === 0x2e && c === 0x2e) {
|
|
105
|
+
if (a < 0x20 || a > 0x7e) {
|
|
106
|
+
throw new Fixed3DecodeError("printable cell has non-printable byte", i);
|
|
107
|
+
}
|
|
108
|
+
out[j++] = a;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (a !== 0x5c) {
|
|
113
|
+
throw new Fixed3DecodeError("escaped cell must start with backslash", i);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const hi = b < 128 ? DECODE_HEX[b] : -1;
|
|
117
|
+
const lo = c < 128 ? DECODE_HEX[c] : -1;
|
|
118
|
+
if (hi < 0 || lo < 0) {
|
|
119
|
+
throw new Fixed3DecodeError("escaped cell must contain two hex digits", i);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const byte = (hi << 4) | lo;
|
|
123
|
+
if (byte >= 0x20 && byte <= 0x7e) {
|
|
124
|
+
throw new Fixed3DecodeError("printable byte must use c.. form", i);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
out[j++] = byte;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default {
|
|
134
|
+
encodeBinary,
|
|
135
|
+
encodeBinaryToBuffer,
|
|
136
|
+
decodeBinary,
|
|
137
|
+
decodeBinaryBytes,
|
|
138
|
+
decodeBinaryStrict,
|
|
139
|
+
Fixed3DecodeError,
|
|
140
|
+
};
|
package/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { ClipboardManager } from "./platform/clipboard.js";
|
|
|
21
21
|
import { platformId, run as runCommand, runSync, fetchHttp, fetchHttpBytes, detectHttpBackend } from "./platform/commands.js";
|
|
22
22
|
import { shellSplit } from "./shell/shell.js";
|
|
23
23
|
import { styleToAnsi } from "./display/ansi-style.js";
|
|
24
|
+
import { encodeBinaryToBuffer, decodeBinaryBytes } from "./buffer/fixed3-codec.js";
|
|
24
25
|
|
|
25
26
|
import pkg from "../package.json" with { type: "json" };
|
|
26
27
|
|
|
@@ -134,18 +135,27 @@ function isHttpUrl(value) {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
async function readTextFileWithEncoding(path, encoding = "utf-8") {
|
|
137
|
-
const decoder = new TextDecoder(normalizeEncodingLabel(encoding));
|
|
138
138
|
const bytes = new Uint8Array(await Bun.file(path).arrayBuffer());
|
|
139
|
+
if (normalizeEncodingLabel(encoding) === "hex3") {
|
|
140
|
+
return { text: encodeBinaryToBuffer(bytes).toString("latin1"), encoding: "hex3" };
|
|
141
|
+
}
|
|
142
|
+
const decoder = new TextDecoder(normalizeEncodingLabel(encoding));
|
|
139
143
|
return { text: decoder.decode(bytes), encoding: decoder.encoding };
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
async function fetchTextWithEncoding(url, encoding = "utf-8") {
|
|
147
|
+
const bytes = await fetchHttpBytes(url);
|
|
148
|
+
if (normalizeEncodingLabel(encoding) === "hex3") {
|
|
149
|
+
return { text: encodeBinaryToBuffer(new Uint8Array(bytes)).toString("latin1"), encoding: "hex3" };
|
|
150
|
+
}
|
|
143
151
|
const decoder = new TextDecoder(normalizeEncodingLabel(encoding));
|
|
144
|
-
return { text: decoder.decode(
|
|
152
|
+
return { text: decoder.decode(bytes), encoding: decoder.encoding };
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
function normalizeEncodingLabel(encoding = "utf-8") {
|
|
148
|
-
|
|
156
|
+
const s = String(encoding || "utf-8");
|
|
157
|
+
if (s === "hex3") return "hex3";
|
|
158
|
+
return new TextDecoder(s).encoding;
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
function isReadonlyBuffer(buf) {
|
|
@@ -1043,6 +1053,21 @@ class BufferModel {
|
|
|
1043
1053
|
async save(path = this.path) {
|
|
1044
1054
|
if (!path) throw new Error("No filename");
|
|
1045
1055
|
let text = this.lines.join("\n");
|
|
1056
|
+
if (this.encoding === "hex3") {
|
|
1057
|
+
await Bun.write(path, decodeBinaryBytes(Buffer.from(text, "latin1")));
|
|
1058
|
+
this.path = path;
|
|
1059
|
+
this.Path = path;
|
|
1060
|
+
this.AbsPath = path;
|
|
1061
|
+
this.name = basename(path);
|
|
1062
|
+
this.updateModTime();
|
|
1063
|
+
this.readonly = !canWritePath(path);
|
|
1064
|
+
this.Settings.readonly = this.readonly;
|
|
1065
|
+
this.Type.Readonly = this.readonly;
|
|
1066
|
+
this._savedSerial = this._undoSerial ?? 0;
|
|
1067
|
+
this.modified = false;
|
|
1068
|
+
this.message = `Saved ${path}`;
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1046
1071
|
if (DEFAULT_SETTINGS.eofnewline && !text.endsWith("\n")) text += "\n";
|
|
1047
1072
|
await Bun.write(path, text);
|
|
1048
1073
|
this.encoding = "utf-8";
|
|
@@ -1324,6 +1349,7 @@ class TerminalPane {
|
|
|
1324
1349
|
this.proc = null;
|
|
1325
1350
|
this.vt = null;
|
|
1326
1351
|
this.decoder = new TextDecoder();
|
|
1352
|
+
this.exited = false;
|
|
1327
1353
|
}
|
|
1328
1354
|
|
|
1329
1355
|
open(cols, rows) {
|
|
@@ -1335,6 +1361,7 @@ class TerminalPane {
|
|
|
1335
1361
|
cols = Math.max(10, cols ?? this.app.cols);
|
|
1336
1362
|
rows = Math.max(4, rows ?? Math.floor(this.app.rows / 2));
|
|
1337
1363
|
this.vt = new VT100(cols, rows);
|
|
1364
|
+
this.exited = false;
|
|
1338
1365
|
this.proc = Bun.spawn([shell], {
|
|
1339
1366
|
env: { ...process.env, TERM: "xterm-256color", COLUMNS: String(cols), LINES: String(rows) },
|
|
1340
1367
|
terminal: {
|
|
@@ -1348,7 +1375,8 @@ class TerminalPane {
|
|
|
1348
1375
|
this.app.render();
|
|
1349
1376
|
},
|
|
1350
1377
|
exit: () => {
|
|
1351
|
-
this.
|
|
1378
|
+
this.exited = true;
|
|
1379
|
+
this.vt.feed("\r\n[process exited]\r\nPress enter to close\r\n");
|
|
1352
1380
|
this.app.render();
|
|
1353
1381
|
},
|
|
1354
1382
|
},
|
|
@@ -1356,9 +1384,14 @@ class TerminalPane {
|
|
|
1356
1384
|
}
|
|
1357
1385
|
|
|
1358
1386
|
write(data) {
|
|
1387
|
+
if (this.exited) return;
|
|
1359
1388
|
this.proc?.terminal?.write(data);
|
|
1360
1389
|
}
|
|
1361
1390
|
|
|
1391
|
+
writeInput(data) {
|
|
1392
|
+
this.write(encodeTerminalInput(data, this.vt));
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1362
1395
|
resize(cols, rows) {
|
|
1363
1396
|
rows = Math.max(4, rows);
|
|
1364
1397
|
this.vt?.resize(cols, rows);
|
|
@@ -1367,13 +1400,102 @@ class TerminalPane {
|
|
|
1367
1400
|
|
|
1368
1401
|
close() {
|
|
1369
1402
|
try {
|
|
1370
|
-
this.proc?.kill();
|
|
1403
|
+
if (!this.exited) this.proc?.kill();
|
|
1371
1404
|
this.proc?.terminal?.close();
|
|
1372
1405
|
} catch {
|
|
1373
1406
|
// PTY may already be closed.
|
|
1374
1407
|
}
|
|
1375
1408
|
this.proc = null;
|
|
1409
|
+
this.exited = true;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function encodeTerminalInput(data, vt) {
|
|
1414
|
+
if (!vt) return data;
|
|
1415
|
+
const flags = vt.keyboardProtocolFlags ?? 0;
|
|
1416
|
+
const wantsKitty = flags !== 0;
|
|
1417
|
+
const wantsXterm = (vt.modifyOtherKeys ?? 0) > 0 || (vt.formatOtherKeys ?? 0) > 0;
|
|
1418
|
+
if (!wantsKitty && !wantsXterm) return data;
|
|
1419
|
+
|
|
1420
|
+
const text = data instanceof Uint8Array ? decoder.decode(data) : String(data);
|
|
1421
|
+
const events = parseInputEvents(text);
|
|
1422
|
+
if (events.length === 0 || events.some(e => e.type !== "key")) return data;
|
|
1423
|
+
|
|
1424
|
+
const encoded = [];
|
|
1425
|
+
for (const event of events) {
|
|
1426
|
+
const seq = encodeKeyEventForTerminal(event, flags, wantsXterm);
|
|
1427
|
+
if (!seq) return data;
|
|
1428
|
+
encoded.push(seq);
|
|
1429
|
+
}
|
|
1430
|
+
return encoded.join("");
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
function encodeKeyEventForTerminal(event, flags, wantsXterm) {
|
|
1434
|
+
const key = event.key;
|
|
1435
|
+
const raw = event.raw ?? "";
|
|
1436
|
+
const reportAll = (flags & 8) !== 0;
|
|
1437
|
+
const disambiguate = (flags & 1) !== 0 || reportAll || wantsXterm;
|
|
1438
|
+
|
|
1439
|
+
if (!reportAll && isPlainTextKey(raw, key)) return raw;
|
|
1440
|
+
const parsed = keyToKittyCode(key, raw);
|
|
1441
|
+
if (!parsed) return raw;
|
|
1442
|
+
const { code, modifiers } = parsed;
|
|
1443
|
+
if (!reportAll && !disambiguate && modifiers <= 1) return raw;
|
|
1444
|
+
if (!reportAll && modifiers <= 1 && !needsDisambiguation(key)) return raw;
|
|
1445
|
+
return `\x1b[${code};${modifiers}u`;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function isPlainTextKey(raw, key) {
|
|
1449
|
+
return raw && raw === key && !raw.includes("\x1b") && !/^(?:ctrl|alt|shift)-/.test(key) && !KEY_CODEPOINTS[key];
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function needsDisambiguation(key) {
|
|
1453
|
+
return key === "escape" || key.startsWith("ctrl-") || key.startsWith("alt-") || key.includes("shift-");
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
const NAMED_KEY_CODEPOINTS = {
|
|
1457
|
+
escape: 27,
|
|
1458
|
+
enter: 13,
|
|
1459
|
+
tab: 9,
|
|
1460
|
+
backspace: 127,
|
|
1461
|
+
delete: 57362,
|
|
1462
|
+
insert: 57363,
|
|
1463
|
+
left: 57364,
|
|
1464
|
+
right: 57365,
|
|
1465
|
+
up: 57366,
|
|
1466
|
+
down: 57367,
|
|
1467
|
+
pageup: 57368,
|
|
1468
|
+
pagedown: 57369,
|
|
1469
|
+
home: 57370,
|
|
1470
|
+
end: 57371,
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
const KEY_CODEPOINTS = NAMED_KEY_CODEPOINTS;
|
|
1474
|
+
|
|
1475
|
+
function keyToKittyCode(key, raw) {
|
|
1476
|
+
let rest = key;
|
|
1477
|
+
let mods = 1;
|
|
1478
|
+
let changed = true;
|
|
1479
|
+
while (changed) {
|
|
1480
|
+
changed = false;
|
|
1481
|
+
for (const [prefix, bit] of [["shift-", 1], ["alt-", 2], ["ctrl-", 4]]) {
|
|
1482
|
+
if (rest.startsWith(prefix)) {
|
|
1483
|
+
mods += bit;
|
|
1484
|
+
rest = rest.slice(prefix.length);
|
|
1485
|
+
changed = true;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1376
1488
|
}
|
|
1489
|
+
|
|
1490
|
+
if (rest === "space") return { code: 32, modifiers: mods };
|
|
1491
|
+
if (KEY_CODEPOINTS[rest]) return { code: KEY_CODEPOINTS[rest], modifiers: mods };
|
|
1492
|
+
if (rest.length === 1) return { code: rest.toLowerCase().codePointAt(0), modifiers: mods };
|
|
1493
|
+
|
|
1494
|
+
if (raw.length === 1 && raw >= " " && raw !== "\x7f") return { code: raw.toLowerCase().codePointAt(0), modifiers: mods };
|
|
1495
|
+
if (raw.length === 1 && raw.charCodeAt(0) >= 1 && raw.charCodeAt(0) <= 26) {
|
|
1496
|
+
return { code: raw.charCodeAt(0) + 96, modifiers: mods | 4 };
|
|
1497
|
+
}
|
|
1498
|
+
return null;
|
|
1377
1499
|
}
|
|
1378
1500
|
|
|
1379
1501
|
// ─── Pane / split layout ────────────────────────────────────────────────────
|
|
@@ -2325,17 +2447,15 @@ class App {
|
|
|
2325
2447
|
return;
|
|
2326
2448
|
}
|
|
2327
2449
|
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
this.closePane(activePaneObj);
|
|
2338
|
-
}
|
|
2450
|
+
if (activePaneObj.terminal.exited && events.some((event) => event.type === "key" && event.key === "enter")) {
|
|
2451
|
+
this.closeTermPane(activePaneObj);
|
|
2452
|
+
this.render();
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
// Escape alone: close pane in legacy mode; protocol-aware shells need it as input.
|
|
2457
|
+
if (text === "\x1b" && !(activePaneObj.terminal?.vt?.keyboardProtocolFlags || activePaneObj.terminal?.vt?.modifyOtherKeys)) {
|
|
2458
|
+
this.closeTermPane(activePaneObj);
|
|
2339
2459
|
this.render();
|
|
2340
2460
|
return;
|
|
2341
2461
|
}
|
|
@@ -2352,7 +2472,7 @@ class App {
|
|
|
2352
2472
|
|
|
2353
2473
|
// Any other key input: reset scroll to live view, then forward
|
|
2354
2474
|
if (activePaneObj.terminal?.vt) activePaneObj.terminal.vt.scrollOffset = 0;
|
|
2355
|
-
activePaneObj.terminal.
|
|
2475
|
+
activePaneObj.terminal.writeInput(data);
|
|
2356
2476
|
return;
|
|
2357
2477
|
}
|
|
2358
2478
|
|
|
@@ -3384,7 +3504,8 @@ class App {
|
|
|
3384
3504
|
async save({ force = false } = {}) {
|
|
3385
3505
|
if (!force && this.buffer?.readonly) { this.message = "Can't save under readonly mode"; return; }
|
|
3386
3506
|
try {
|
|
3387
|
-
|
|
3507
|
+
const enc = normalizeEncodingLabel(this.buffer?.encoding);
|
|
3508
|
+
if (enc !== "utf-8" && enc !== "hex3") {
|
|
3388
3509
|
this.openYNPrompt("Save in UTF-8?(y,n)", async (answer) => {
|
|
3389
3510
|
if (answer === "y") await this.saveUtf8();
|
|
3390
3511
|
});
|
|
@@ -3536,6 +3657,18 @@ class App {
|
|
|
3536
3657
|
this.message = "No previous diff";
|
|
3537
3658
|
}
|
|
3538
3659
|
|
|
3660
|
+
closeTermPane(pane) {
|
|
3661
|
+
pane.terminal?.close();
|
|
3662
|
+
pane.terminal = null;
|
|
3663
|
+
if (pane.prevBuffer) {
|
|
3664
|
+
pane.type = "editor";
|
|
3665
|
+
pane.buffer = pane.prevBuffer;
|
|
3666
|
+
pane.prevBuffer = null;
|
|
3667
|
+
} else {
|
|
3668
|
+
this.closePane(pane);
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3539
3672
|
closePane(pane) {
|
|
3540
3673
|
pane.terminal?.close();
|
|
3541
3674
|
const tab = this.tab;
|
|
@@ -3774,7 +3907,7 @@ class App {
|
|
|
3774
3907
|
const saveArgs = [...cmdArgs];
|
|
3775
3908
|
const saveForce = saveArgs[0] === "-f" && (saveArgs.shift(), true);
|
|
3776
3909
|
if (!saveForce && buf?.readonly) { this.message = "Can't save under readonly mode"; break; }
|
|
3777
|
-
if (saveArgs.length > 0 && normalizeEncodingLabel(buf?.encoding) !== "utf-8") {
|
|
3910
|
+
if (saveArgs.length > 0 && normalizeEncodingLabel(buf?.encoding) !== "utf-8" && normalizeEncodingLabel(buf?.encoding) !== "hex3") {
|
|
3778
3911
|
const target = resolve(expandHome(saveArgs[0]));
|
|
3779
3912
|
this.openYNPrompt("Save in UTF-8?(y,n)", async (answer) => {
|
|
3780
3913
|
if (answer === "y") {
|
|
@@ -4546,12 +4679,14 @@ const COMMAND_NAMES = [
|
|
|
4546
4679
|
];
|
|
4547
4680
|
|
|
4548
4681
|
const SUPPORTED_ENCODING_LABELS = [
|
|
4682
|
+
"hex3",
|
|
4549
4683
|
"utf-8", "utf-16le", "utf-16be",
|
|
4550
4684
|
"windows-1252", "iso-8859-1", "latin1",
|
|
4551
4685
|
"big5", "gbk", "gb18030",
|
|
4552
4686
|
"shift_jis", "sjis", "euc-jp", "iso-2022-jp",
|
|
4553
4687
|
"euc-kr", "ks_c_5601-1987",
|
|
4554
4688
|
].filter((encoding) => {
|
|
4689
|
+
if (encoding === "hex3") return true;
|
|
4555
4690
|
try { new TextDecoder(encoding); return true; }
|
|
4556
4691
|
catch { return false; }
|
|
4557
4692
|
});
|
|
@@ -5562,7 +5697,10 @@ async function loadBuffers(files, command) {
|
|
|
5562
5697
|
} else if (!process.stdin.isTTY) {
|
|
5563
5698
|
const chunks = [];
|
|
5564
5699
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
5565
|
-
|
|
5700
|
+
const stdinText = Buffer.concat(chunks).toString("utf8");
|
|
5701
|
+
const stdinBuf = new BufferModel({ text: stdinText, type: process.stdout.isTTY ? "default" : "stdout", command });
|
|
5702
|
+
if (loadBuffers.context) attachSyntax(stdinBuf, loadBuffers.context, "", stdinText);
|
|
5703
|
+
buffers.push(stdinBuf);
|
|
5566
5704
|
} else {
|
|
5567
5705
|
buffers.push(new BufferModel({ command }));
|
|
5568
5706
|
}
|
|
@@ -6032,8 +6170,18 @@ function syncEditorSettings(config) {
|
|
|
6032
6170
|
async function catFiles(files, colorscheme, syntaxDefinitions) {
|
|
6033
6171
|
const targets = files.length > 0 ? files.map((f) => ({ path: f, stdin: false })) : [{ path: null, stdin: true }];
|
|
6034
6172
|
for (const { path: filePath, stdin } of targets) {
|
|
6035
|
-
|
|
6036
|
-
|
|
6173
|
+
let content;
|
|
6174
|
+
let effectivePath = filePath;
|
|
6175
|
+
if (stdin) {
|
|
6176
|
+
content = await Bun.stdin.text();
|
|
6177
|
+
} else if (isHttpUrl(filePath)) {
|
|
6178
|
+
content = await fetchHttp(filePath);
|
|
6179
|
+
// Use the URL pathname for syntax/md detection (strip query/hash)
|
|
6180
|
+
try { effectivePath = new URL(filePath).pathname; } catch { effectivePath = filePath; }
|
|
6181
|
+
} else {
|
|
6182
|
+
content = await Bun.file(filePath).text();
|
|
6183
|
+
}
|
|
6184
|
+
if (effectivePath && /\.md$/i.test(effectivePath)) {
|
|
6037
6185
|
process.stdout.write(
|
|
6038
6186
|
Bun.markdown.ansi(content,{
|
|
6039
6187
|
hyperlinks:true
|
|
@@ -6043,7 +6191,7 @@ async function catFiles(files, colorscheme, syntaxDefinitions) {
|
|
|
6043
6191
|
}
|
|
6044
6192
|
const lines = content.split("\n");
|
|
6045
6193
|
const def = detectSyntax(syntaxDefinitions, {
|
|
6046
|
-
path:
|
|
6194
|
+
path: effectivePath ?? "",
|
|
6047
6195
|
firstLine: lines[0] ?? "",
|
|
6048
6196
|
lines: lines.slice(0, 50),
|
|
6049
6197
|
});
|
package/src/screen/vt100.js
CHANGED
|
@@ -77,6 +77,7 @@ export class VT100 {
|
|
|
77
77
|
this.cells = [];
|
|
78
78
|
this.cx = 0;
|
|
79
79
|
this.cy = 0;
|
|
80
|
+
this.wrapPending = false;
|
|
80
81
|
this.savedCursor = { x: 0, y: 0 };
|
|
81
82
|
this.scrollTop = 0;
|
|
82
83
|
this.scrollBottom = this.rows - 1;
|
|
@@ -88,6 +89,10 @@ export class VT100 {
|
|
|
88
89
|
this.scrollOffset = 0; // 0 = live view; n = n rows scrolled back into history
|
|
89
90
|
// mouse reporting: set by the application via ?1000h / ?1002h / ?1003h
|
|
90
91
|
this.mouseMode = false;
|
|
92
|
+
this.keyboardProtocolFlags = 0;
|
|
93
|
+
this.keyboardProtocolStack = [];
|
|
94
|
+
this.modifyOtherKeys = 0;
|
|
95
|
+
this.formatOtherKeys = 0;
|
|
91
96
|
this._initCells();
|
|
92
97
|
}
|
|
93
98
|
|
|
@@ -205,6 +210,7 @@ export class VT100 {
|
|
|
205
210
|
}
|
|
206
211
|
|
|
207
212
|
_lineFeed() {
|
|
213
|
+
this.wrapPending = false;
|
|
208
214
|
if (this.cy < this.scrollBottom) {
|
|
209
215
|
this.cy++;
|
|
210
216
|
} else {
|
|
@@ -212,6 +218,12 @@ export class VT100 {
|
|
|
212
218
|
}
|
|
213
219
|
}
|
|
214
220
|
|
|
221
|
+
_moveCursor(x, y) {
|
|
222
|
+
this.cx = Math.min(this.cols - 1, Math.max(0, x));
|
|
223
|
+
this.cy = Math.min(this.rows - 1, Math.max(0, y));
|
|
224
|
+
this.wrapPending = false;
|
|
225
|
+
}
|
|
226
|
+
|
|
215
227
|
// Feed a chunk of terminal output. Returns array of response strings to send back.
|
|
216
228
|
feed(text) {
|
|
217
229
|
const data = this.pending + text;
|
|
@@ -248,17 +260,18 @@ export class VT100 {
|
|
|
248
260
|
} else if (next === "7") {
|
|
249
261
|
this.savedCursor = { x: this.cx, y: this.cy }; i += 2;
|
|
250
262
|
} else if (next === "8") {
|
|
251
|
-
this.
|
|
263
|
+
this._moveCursor(this.savedCursor.x, this.savedCursor.y); i += 2;
|
|
252
264
|
} else if (next === "M") {
|
|
253
265
|
// Reverse index
|
|
254
266
|
if (this.cy === this.scrollTop) this._scrollDown(1);
|
|
255
|
-
else this.
|
|
267
|
+
else this._moveCursor(this.cx, this.cy - 1);
|
|
256
268
|
i += 2;
|
|
257
269
|
} else if (next === "(" || next === ")" || next === "*" || next === "+") {
|
|
258
270
|
i += 3; // charset designation, skip designator
|
|
259
271
|
} else if (next === "c") {
|
|
260
272
|
// Full reset
|
|
261
273
|
this._initCells(); this.cx = this.cy = 0;
|
|
274
|
+
this.wrapPending = false;
|
|
262
275
|
this.sgr = { fg: "default", bg: "default", bold: false, italic: false, underline: false, reverse: false };
|
|
263
276
|
this.scrollTop = 0; this.scrollBottom = this.rows - 1;
|
|
264
277
|
i += 2;
|
|
@@ -271,13 +284,15 @@ export class VT100 {
|
|
|
271
284
|
}
|
|
272
285
|
|
|
273
286
|
} else if (ch === "\r") {
|
|
274
|
-
this.cx = 0; i++;
|
|
287
|
+
this.cx = 0; this.wrapPending = false; i++;
|
|
275
288
|
} else if (ch === "\n" || ch === "\x0b" || ch === "\x0c") {
|
|
276
289
|
this._lineFeed(); i++;
|
|
277
290
|
} else if (ch === "\b") {
|
|
278
|
-
if (this.
|
|
291
|
+
if (this.wrapPending) this.wrapPending = false;
|
|
292
|
+
else if (this.cx > 0) this.cx--;
|
|
293
|
+
i++;
|
|
279
294
|
} else if (ch === "\t") {
|
|
280
|
-
this.cx = Math.min(this.cols - 1, (Math.floor(this.cx / 8) + 1) * 8); i++;
|
|
295
|
+
this.cx = Math.min(this.cols - 1, (Math.floor(this.cx / 8) + 1) * 8); this.wrapPending = false; i++;
|
|
281
296
|
} else if (ch === "\x07") {
|
|
282
297
|
i++; // Bell: ignore
|
|
283
298
|
} else if (ch === "\x0e" || ch === "\x0f") {
|
|
@@ -287,10 +302,20 @@ export class VT100 {
|
|
|
287
302
|
const cp = data.codePointAt(i);
|
|
288
303
|
const rune = String.fromCodePoint(cp);
|
|
289
304
|
const width = charWidth(rune);
|
|
305
|
+
if (width > 0 && this.wrapPending) {
|
|
306
|
+
this.cx = 0;
|
|
307
|
+
this._lineFeed();
|
|
308
|
+
}
|
|
290
309
|
this._setCell(this.cx, this.cy, rune, width);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
310
|
+
if (width > 0) {
|
|
311
|
+
const nextX = this.cx + width;
|
|
312
|
+
if (nextX >= this.cols) {
|
|
313
|
+
this.cx = this.cols - 1;
|
|
314
|
+
this.wrapPending = true;
|
|
315
|
+
} else {
|
|
316
|
+
this.cx = nextX;
|
|
317
|
+
this.wrapPending = false;
|
|
318
|
+
}
|
|
294
319
|
}
|
|
295
320
|
i += cp > 0xFFFF ? 2 : 1;
|
|
296
321
|
} else {
|
|
@@ -302,6 +327,10 @@ export class VT100 {
|
|
|
302
327
|
}
|
|
303
328
|
|
|
304
329
|
_handleCSI(params, final) {
|
|
330
|
+
if (final === "u" && /^(?:[?<>]=?|=)/.test(params)) {
|
|
331
|
+
return this._handleKeyboardProtocol(params);
|
|
332
|
+
}
|
|
333
|
+
|
|
305
334
|
// Check for private mode prefix
|
|
306
335
|
const isPrivate = params.startsWith("?");
|
|
307
336
|
const raw = isPrivate ? params.slice(1) : params;
|
|
@@ -310,17 +339,15 @@ export class VT100 {
|
|
|
310
339
|
const p2 = parts[1] ?? 0;
|
|
311
340
|
|
|
312
341
|
switch (final) {
|
|
313
|
-
case "A": this.
|
|
314
|
-
case "B": this.
|
|
315
|
-
case "C": this.
|
|
316
|
-
case "D": this.
|
|
317
|
-
case "E": this.
|
|
318
|
-
case "F": this.
|
|
319
|
-
case "G": this.
|
|
342
|
+
case "A": this._moveCursor(this.cx, Math.max(this.scrollTop, this.cy - Math.max(1, p1))); break;
|
|
343
|
+
case "B": this._moveCursor(this.cx, Math.min(this.scrollBottom, this.cy + Math.max(1, p1))); break;
|
|
344
|
+
case "C": this._moveCursor(Math.min(this.cols - 1, this.cx + Math.max(1, p1)), this.cy); break;
|
|
345
|
+
case "D": this._moveCursor(Math.max(0, this.cx - Math.max(1, p1)), this.cy); break;
|
|
346
|
+
case "E": this._moveCursor(0, Math.min(this.rows - 1, this.cy + Math.max(1, p1))); break;
|
|
347
|
+
case "F": this._moveCursor(0, Math.max(0, this.cy - Math.max(1, p1))); break;
|
|
348
|
+
case "G": this._moveCursor(Math.min(this.cols - 1, Math.max(0, Math.max(1, p1) - 1)), this.cy); break;
|
|
320
349
|
case "H":
|
|
321
|
-
|
|
322
|
-
this.cy = Math.min(this.rows - 1, Math.max(0, Math.max(1, p1) - 1));
|
|
323
|
-
this.cx = Math.min(this.cols - 1, Math.max(0, Math.max(1, p2) - 1));
|
|
350
|
+
this._moveCursor(Math.max(0, Math.max(1, p2) - 1), Math.max(0, Math.max(1, p1) - 1));
|
|
324
351
|
break;
|
|
325
352
|
case "J":
|
|
326
353
|
if (p1 === 0) {
|
|
@@ -365,8 +392,17 @@ export class VT100 {
|
|
|
365
392
|
for (let x = this.cx; x < Math.min(this.cols, this.cx + n); x++) this._clearCell(x, this.cy);
|
|
366
393
|
break;
|
|
367
394
|
}
|
|
368
|
-
case "d": this.
|
|
369
|
-
case "
|
|
395
|
+
case "d": this._moveCursor(this.cx, Math.min(this.rows - 1, Math.max(0, Math.max(1, p1) - 1))); break;
|
|
396
|
+
case "f":
|
|
397
|
+
if (params.startsWith(">")) this._handleXtermKeyFormat(raw);
|
|
398
|
+
else {
|
|
399
|
+
this._moveCursor(Math.max(0, Math.max(1, p2) - 1), Math.max(0, Math.max(1, p1) - 1));
|
|
400
|
+
}
|
|
401
|
+
break;
|
|
402
|
+
case "m":
|
|
403
|
+
if (params.startsWith(">")) this._handleXtermKeyModifier(raw);
|
|
404
|
+
else this._handleSGR(parts);
|
|
405
|
+
break;
|
|
370
406
|
case "n":
|
|
371
407
|
if (p1 === 6) return `\x1b[${this.cy + 1};${this.cx + 1}R`; // CPR
|
|
372
408
|
if (p1 === 5) return "\x1b[0n"; // device status OK
|
|
@@ -377,7 +413,14 @@ export class VT100 {
|
|
|
377
413
|
if (this.scrollTop >= this.scrollBottom) { this.scrollTop = 0; this.scrollBottom = this.rows - 1; }
|
|
378
414
|
break;
|
|
379
415
|
case "s": this.savedCursor = { x: this.cx, y: this.cy }; break;
|
|
380
|
-
case "u":
|
|
416
|
+
case "u":
|
|
417
|
+
if (params === "") {
|
|
418
|
+
this._moveCursor(this.savedCursor.x, this.savedCursor.y);
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
case "c":
|
|
422
|
+
if (params === "" || p1 === 0) return "\x1b[?1;2c"; // primary device attributes
|
|
423
|
+
break;
|
|
381
424
|
case "h":
|
|
382
425
|
if (isPrivate) {
|
|
383
426
|
for (const n of parts) {
|
|
@@ -396,6 +439,53 @@ export class VT100 {
|
|
|
396
439
|
return null;
|
|
397
440
|
}
|
|
398
441
|
|
|
442
|
+
_handleKeyboardProtocol(params) {
|
|
443
|
+
const parseNum = (value, fallback = 0) => {
|
|
444
|
+
const n = Number(String(value ?? "").split(":")[0]);
|
|
445
|
+
return Number.isFinite(n) ? n : fallback;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
if (params === "?") return `\x1b[?${this.keyboardProtocolFlags}u`;
|
|
449
|
+
if (params.startsWith("=")) {
|
|
450
|
+
const parts = params.slice(1).split(";");
|
|
451
|
+
const flags = parseNum(parts[0], 0);
|
|
452
|
+
const mode = parseNum(parts[1], 1);
|
|
453
|
+
if (mode === 2) this.keyboardProtocolFlags |= flags;
|
|
454
|
+
else if (mode === 3) this.keyboardProtocolFlags &= ~flags;
|
|
455
|
+
else this.keyboardProtocolFlags = flags;
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
if (params.startsWith(">")) {
|
|
459
|
+
const flags = parseNum(params.slice(1), 0);
|
|
460
|
+
this.keyboardProtocolStack.push(this.keyboardProtocolFlags);
|
|
461
|
+
if (this.keyboardProtocolStack.length > 32) this.keyboardProtocolStack.shift();
|
|
462
|
+
this.keyboardProtocolFlags = flags;
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
if (params.startsWith("<")) {
|
|
466
|
+
const count = Math.max(1, parseNum(params.slice(1), 1));
|
|
467
|
+
for (let i = 0; i < count; i++) {
|
|
468
|
+
this.keyboardProtocolFlags = this.keyboardProtocolStack.length > 0 ? this.keyboardProtocolStack.pop() : 0;
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_handleXtermKeyFormat(raw) {
|
|
476
|
+
const parts = raw.slice(1).split(";").map(p => Number(p));
|
|
477
|
+
const id = parts[0];
|
|
478
|
+
const value = parts[1];
|
|
479
|
+
if (id === 4) this.formatOtherKeys = Number.isFinite(value) ? value : 0;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
_handleXtermKeyModifier(raw) {
|
|
483
|
+
const parts = raw.slice(1).split(";").map(p => Number(p));
|
|
484
|
+
const id = parts[0];
|
|
485
|
+
const value = parts[1];
|
|
486
|
+
if (id === 4) this.modifyOtherKeys = Number.isFinite(value) ? value : 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
399
489
|
_handleSGR(parts) {
|
|
400
490
|
if (parts.length === 0 || (parts.length === 1 && parts[0] === 0)) {
|
|
401
491
|
this.sgr = { fg: "default", bg: "default", bold: false, italic: false, underline: false, reverse: false };
|
|
@@ -459,6 +549,7 @@ export class VT100 {
|
|
|
459
549
|
this.cells = newCells;
|
|
460
550
|
this.cx = Math.min(this.cx, cols - 1);
|
|
461
551
|
this.cy = Math.min(this.cy, rows - 1);
|
|
552
|
+
this.wrapPending = false;
|
|
462
553
|
this.scrollTop = 0;
|
|
463
554
|
this.scrollBottom = rows - 1;
|
|
464
555
|
}
|