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 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.23",
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 child = new Colorscheme(this.runtime);
51
- await child.load(includeName, parsed);
52
- for (const [key, value] of child.styles) styles.set(key, value);
53
- if (child.styles.has("default")) this.defaultStyle = child.styles.get("default");
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 ?? BUILTIN_GROUPS[group] ?? stringToStyle(group, this.defaultStyle);
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