bunmicro 0.9.23 → 0.9.30
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 +15 -0
- package/README.md +10 -0
- package/package.json +3 -2
- package/src/buffer/backup.js +160 -0
- package/src/config/colorscheme.js +10 -20
- package/src/index.js +396 -68
- package/tests/backup.test.js +133 -0
- package/tests/pty-demo.js +492 -0
- package/todo.txt +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.30] - 2026-06-13
|
|
4
|
+
- Fixed binary edit hex3 regression
|
|
5
|
+
* see readme
|
|
6
|
+
- Added crash backup recovery
|
|
7
|
+
|
|
8
|
+
## [0.9.25] - 2026-06-10
|
|
9
|
+
- Fixed colorscheme: aligned with go
|
|
10
|
+
- Added colorcolumn taberror showchars
|
|
11
|
+
* e.g. showchars = tab=>,space=.
|
|
12
|
+
- linter underline
|
|
13
|
+
- set hltrailingws on : whitespace
|
|
14
|
+
- Added tests/pty-demo.js
|
|
15
|
+
* Demo of the whole bunmicro App
|
|
16
|
+
* bun tests/pty-demo.js
|
|
17
|
+
|
|
3
18
|
## [0.9.23] - 2026-06-10
|
|
4
19
|
- Fixed mouse close prompt cursor move
|
|
5
20
|
- Fixed set filetype doesn't apply instantly
|
package/README.md
CHANGED
|
@@ -35,6 +35,16 @@
|
|
|
35
35
|
- Works like bat ccat glow
|
|
36
36
|
- bunmicro -bat file
|
|
37
37
|
- aliases: --cat --bat --ccat --glow
|
|
38
|
+
## Binary edit by hex3
|
|
39
|
+
- Edit binary files like text files
|
|
40
|
+
- bunmicro libc.so.6
|
|
41
|
+
- Ctrl+E reopen hex3
|
|
42
|
+
- Printable chars stay readable
|
|
43
|
+
* a => a..
|
|
44
|
+
- Non-printable chars become escapes
|
|
45
|
+
* \xff => \ff
|
|
46
|
+
- Search & Edit by plain text
|
|
47
|
+
- And simply save by Ctrl+S
|
|
38
48
|
## Preview color schemes(theme)
|
|
39
49
|
- Ctrl-E theme, then press Tab and use arrow keys to preview
|
|
40
50
|
## Mouse clicks more useful
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunmicro",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.30",
|
|
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"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Crash recovery backup system compatible with Go micro's internal/buffer/backup.go.
|
|
2
|
+
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { existsSync, statSync, unlinkSync } from "node:fs";
|
|
5
|
+
import { mkdir, writeFile, readFile, rename } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
export const BACKUP_SUFFIX = ".micro-backup";
|
|
10
|
+
|
|
11
|
+
export function getBackupDir(buf, configDir) {
|
|
12
|
+
const raw = String(buf?.Settings?.backupdir ?? "");
|
|
13
|
+
if (!raw) return join(configDir, "backups");
|
|
14
|
+
if (raw === "~" || raw.startsWith("~/") || raw.startsWith("~\\")) {
|
|
15
|
+
return raw.replace(/^~/, homedir());
|
|
16
|
+
}
|
|
17
|
+
// Node has no portable equivalent of Go's user.Lookup for ~otheruser.
|
|
18
|
+
if (raw.startsWith("~")) return join(configDir, "backups");
|
|
19
|
+
return raw;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function queryEscapePath(path) {
|
|
23
|
+
return encodeURIComponent(String(path).replaceAll("\\", "/"))
|
|
24
|
+
.replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`)
|
|
25
|
+
.replace(/%20/g, "+");
|
|
26
|
+
} // '
|
|
27
|
+
|
|
28
|
+
function legacyEscapePath(path) {
|
|
29
|
+
let escaped = String(path).replaceAll("\\", "/");
|
|
30
|
+
if (process.platform === "win32") escaped = escaped.replaceAll(":", "%");
|
|
31
|
+
return escaped.replaceAll("/", "%");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function determineBackupPath(backupDirPath, absPath) {
|
|
35
|
+
const urlName = join(backupDirPath, queryEscapePath(absPath));
|
|
36
|
+
if (existsSync(urlName)) return { name: urlName, resolveName: null };
|
|
37
|
+
|
|
38
|
+
const legacyName = join(backupDirPath, legacyEscapePath(absPath));
|
|
39
|
+
if (existsSync(legacyName)) return { name: legacyName, resolveName: null };
|
|
40
|
+
|
|
41
|
+
if (Buffer.byteLength(urlName + BACKUP_SUFFIX) > 255) {
|
|
42
|
+
const hash = createHash("md5").update(absPath).digest("hex");
|
|
43
|
+
return {
|
|
44
|
+
name: join(backupDirPath, hash),
|
|
45
|
+
resolveName: join(backupDirPath, hash + ".path"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { name: urlName, resolveName: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function writeBackup(buf, configDir, path = buf?.AbsPath ?? buf?.path, { force = false } = {}) {
|
|
52
|
+
if ((!force && !buf?.Settings?.backup) || !path || buf.type !== "default") return false;
|
|
53
|
+
const dir = getBackupDir(buf, configDir);
|
|
54
|
+
await mkdir(dir, { recursive: true });
|
|
55
|
+
const { name, resolveName } = determineBackupPath(dir, path);
|
|
56
|
+
const tmp = name + BACKUP_SUFFIX;
|
|
57
|
+
try {
|
|
58
|
+
await writeFile(tmp, buf.lines.join("\n"), "utf8");
|
|
59
|
+
await rename(tmp, name);
|
|
60
|
+
if (resolveName) await writeFile(resolveName, path, "utf8");
|
|
61
|
+
return true;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
try { unlinkSync(tmp); } catch {}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function removeBackup(buf, configDir, path = buf?.AbsPath ?? buf?.path) {
|
|
69
|
+
if (buf?.Settings?.permbackup || buf?._forceKeepBackup) return;
|
|
70
|
+
if (!path || buf.type !== "default") return;
|
|
71
|
+
const dir = getBackupDir(buf, configDir);
|
|
72
|
+
const { name, resolveName } = determineBackupPath(dir, path);
|
|
73
|
+
try { unlinkSync(name); } catch {}
|
|
74
|
+
if (resolveName) try { unlinkSync(resolveName); } catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// promptFn(msg) -> Promise<string>
|
|
78
|
+
// Returns { recovered: bool, abort: bool }
|
|
79
|
+
export async function applyBackup(buf, configDir, promptFn) {
|
|
80
|
+
if (!buf?.Settings?.backup || buf?.Settings?.permbackup) return { recovered: false, abort: false };
|
|
81
|
+
if (!buf.path || buf.type !== "default") return { recovered: false, abort: false };
|
|
82
|
+
|
|
83
|
+
const dir = getBackupDir(buf, configDir);
|
|
84
|
+
const { name: backupFile, resolveName } = determineBackupPath(dir, buf.AbsPath ?? buf.path);
|
|
85
|
+
if (!existsSync(backupFile)) return { recovered: false, abort: false };
|
|
86
|
+
|
|
87
|
+
let info;
|
|
88
|
+
try { info = statSync(backupFile); } catch { return { recovered: false, abort: false }; }
|
|
89
|
+
|
|
90
|
+
const t = info.mtime;
|
|
91
|
+
const dateStr =
|
|
92
|
+
t.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" }) +
|
|
93
|
+
" at " +
|
|
94
|
+
t.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }) +
|
|
95
|
+
", " + t.getFullYear();
|
|
96
|
+
|
|
97
|
+
const ticcc='```';
|
|
98
|
+
|
|
99
|
+
const msg = `
|
|
100
|
+
# Backup detected! ⚠️
|
|
101
|
+
- File path/url:
|
|
102
|
+
${ticcc}
|
|
103
|
+
${buf.path}
|
|
104
|
+
${ticcc}
|
|
105
|
+
- either micro crashed
|
|
106
|
+
- or another micro is running
|
|
107
|
+
- or an error occurred while saving
|
|
108
|
+
- The file may be corrupted
|
|
109
|
+
# Date of backup 🕰
|
|
110
|
+
- ${dateStr}
|
|
111
|
+
# Path of backup
|
|
112
|
+
${ticcc}
|
|
113
|
+
${backupFile}
|
|
114
|
+
${ticcc}
|
|
115
|
+
# Recovery options
|
|
116
|
+
## r = 'recover'
|
|
117
|
+
- Apply the backup
|
|
118
|
+
- as unsaved changes to the current buffer
|
|
119
|
+
- When the buffer is closed, the backup will be removed.
|
|
120
|
+
## i = 'ignore'
|
|
121
|
+
- Ignore & remove the backup
|
|
122
|
+
## a = 'abort'
|
|
123
|
+
- Abort the open operation
|
|
124
|
+
- Open an empty buffer
|
|
125
|
+
- Keep the backup
|
|
126
|
+
### Your choice
|
|
127
|
+
- [r]ecover, [i]gnore, [a]bort
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
const options = ["r", "i", "a", "recover", "ignore", "abort"];
|
|
131
|
+
let choice = -1;
|
|
132
|
+
let prompt = msg;
|
|
133
|
+
while (choice === -1) {
|
|
134
|
+
const resp = await promptFn(prompt);
|
|
135
|
+
const idx = options.indexOf(resp.trim().toLowerCase());
|
|
136
|
+
if (idx !== -1) choice = idx % 3;
|
|
137
|
+
else prompt = "\n#### Invalid choice!";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (choice === 0) {
|
|
141
|
+
try {
|
|
142
|
+
const text = await readFile(backupFile, "utf8");
|
|
143
|
+
buf.lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
144
|
+
if (!buf.lines.length) buf.lines = [""];
|
|
145
|
+
buf._recovered = true;
|
|
146
|
+
buf._savedSerial = -1;
|
|
147
|
+
buf.setModified?.(true);
|
|
148
|
+
if (!buf.setModified) buf.modified = true;
|
|
149
|
+
return { recovered: true, abort: false };
|
|
150
|
+
} catch {
|
|
151
|
+
return { recovered: false, abort: false };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (choice === 1) {
|
|
155
|
+
try { unlinkSync(backupFile); } catch {}
|
|
156
|
+
if (resolveName) try { unlinkSync(resolveName); } catch {}
|
|
157
|
+
return { recovered: false, abort: false };
|
|
158
|
+
}
|
|
159
|
+
return { recovered: false, abort: true };
|
|
160
|
+
}
|
|
@@ -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
|
|