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.
@@ -0,0 +1,133 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { createHash } from "node:crypto";
7
+ import {
8
+ BACKUP_SUFFIX,
9
+ applyBackup,
10
+ determineBackupPath,
11
+ removeBackup,
12
+ writeBackup,
13
+ } from "../src/buffer/backup.js";
14
+
15
+ const cleanup = [];
16
+
17
+ afterEach(async () => {
18
+ await Promise.all(cleanup.splice(0).map((path) => rm(path, { recursive: true, force: true })));
19
+ });
20
+
21
+ async function tempDir() {
22
+ const path = await mkdtemp(join(tmpdir(), "bunmicro-backup-"));
23
+ cleanup.push(path);
24
+ return path;
25
+ }
26
+
27
+ function buffer(path, text = "changed") {
28
+ return {
29
+ path,
30
+ AbsPath: path,
31
+ type: "default",
32
+ lines: text.split("\n"),
33
+ Settings: { backup: true, backupdir: "", permbackup: false },
34
+ _savedSerial: 0,
35
+ setModified(value) { this.modified = Boolean(value); },
36
+ };
37
+ }
38
+
39
+ describe("go-micro compatible backup paths", () => {
40
+ test("uses Go url.QueryEscape-compatible names", async () => {
41
+ const dir = await tempDir();
42
+ const result = determineBackupPath(dir, "/tmp/a b%~!'()*.txt");
43
+ expect(result).toEqual({
44
+ name: join(dir, "%2Ftmp%2Fa+b%25~%21%27%28%29%2A.txt"),
45
+ resolveName: null,
46
+ });
47
+ });
48
+
49
+ test("prefers an existing legacy name", async () => {
50
+ const dir = await tempDir();
51
+ const legacy = join(dir, "%tmp%legacy.txt");
52
+ await writeFile(legacy, "legacy");
53
+ expect(determineBackupPath(dir, "/tmp/legacy.txt").name).toBe(legacy);
54
+ });
55
+
56
+ test("uses Go's full MD5 hash and .path sidecar for long names", async () => {
57
+ const dir = await tempDir();
58
+ const path = "/" + "x".repeat(300);
59
+ const hash = createHash("md5").update(path).digest("hex");
60
+ expect(determineBackupPath(dir, path)).toEqual({
61
+ name: join(dir, hash),
62
+ resolveName: join(dir, hash + ".path"),
63
+ });
64
+ });
65
+ });
66
+
67
+ describe("backup lifecycle", () => {
68
+ test("writes atomically using the Go backup suffix", async () => {
69
+ const root = await tempDir();
70
+ const backupDir = join(root, "backups");
71
+ const buf = buffer("/tmp/file.txt", "one\ntwo");
72
+ buf.Settings.backupdir = backupDir;
73
+ const target = determineBackupPath(backupDir, buf.AbsPath);
74
+
75
+ expect(await writeBackup(buf, root)).toBe(true);
76
+ expect(await readFile(target.name, "utf8")).toBe("one\ntwo");
77
+ expect(existsSync(target.name + BACKUP_SUFFIX)).toBe(false);
78
+ });
79
+
80
+ test("recover keeps the backup and marks a distinct dirty baseline", async () => {
81
+ const root = await tempDir();
82
+ const backupDir = join(root, "backups");
83
+ await mkdir(backupDir);
84
+ const buf = buffer("/tmp/file.txt", "disk");
85
+ buf.Settings.backupdir = backupDir;
86
+ const target = determineBackupPath(backupDir, buf.AbsPath);
87
+ await writeFile(target.name, "recovered");
88
+
89
+ expect(await applyBackup(buf, root, async () => "recover")).toEqual({ recovered: true, abort: false });
90
+ expect(buf.lines).toEqual(["recovered"]);
91
+ expect(buf.modified).toBe(true);
92
+ expect(buf._savedSerial).toBe(-1);
93
+ expect(existsSync(target.name)).toBe(true);
94
+ });
95
+
96
+ test("ignore removes the backup", async () => {
97
+ const root = await tempDir();
98
+ const backupDir = join(root, "backups");
99
+ await mkdir(backupDir);
100
+ const buf = buffer("/tmp/file.txt");
101
+ buf.Settings.backupdir = backupDir;
102
+ const target = determineBackupPath(backupDir, buf.AbsPath);
103
+ await writeFile(target.name, "ignored");
104
+
105
+ expect(await applyBackup(buf, root, async () => "ignore")).toEqual({ recovered: false, abort: false });
106
+ expect(existsSync(target.name)).toBe(false);
107
+ });
108
+
109
+ test("permanent backups survive removal", async () => {
110
+ const root = await tempDir();
111
+ const backupDir = join(root, "backups");
112
+ const buf = buffer("/tmp/file.txt");
113
+ buf.Settings.backupdir = backupDir;
114
+ buf.Settings.permbackup = true;
115
+ await writeBackup(buf, root);
116
+ const target = determineBackupPath(backupDir, buf.AbsPath);
117
+
118
+ removeBackup(buf, root);
119
+ expect(existsSync(target.name)).toBe(true);
120
+ });
121
+
122
+ test("forced safe-write backups work when periodic backups are disabled", async () => {
123
+ const root = await tempDir();
124
+ const backupDir = join(root, "backups");
125
+ const buf = buffer("/tmp/file.txt");
126
+ buf.Settings.backupdir = backupDir;
127
+ buf.Settings.backup = false;
128
+
129
+ expect(await writeBackup(buf, root)).toBe(false);
130
+ expect(await writeBackup(buf, root, buf.AbsPath, { force: true })).toBe(true);
131
+ expect(existsSync(determineBackupPath(backupDir, buf.AbsPath).name)).toBe(true);
132
+ });
133
+ });
@@ -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
+ }
package/todo.txt CHANGED
@@ -116,7 +116,12 @@ Buffer / editing model
116
116
  Done: saving a non-UTF-8-decoded buffer prompts "Save in UTF-8?(y,n)" before converting the buffer to UTF-8 on disk.
117
117
  Done: hex3 encoding supports binary edit/open/save paths without UTF-8 conversion prompt.
118
118
  Remaining: non-UTF-8 save/encode, rmtrailingws, mkparents, autosu/sucmd, full fileformat behavior parity.
119
- [ ] Implement backup recovery and permbackup behavior.
119
+ [x] Implement backup recovery and permbackup behavior.
120
+ - src/buffer/backup.js: writeBackup/removeBackup/applyBackup; path-escaped flat filename in configDir/backups/ (>200 bytes falls back to SHA-256 hash + .resolve sidecar).
121
+ - Periodic backup timer (10s) in App.start() writes all modified default buffers.
122
+ - applyBackup called in loadBufferForPath (covers open/vsplit/hsplit/startup) via context._termPrompt; pre-TUI uses readline/promises, in-TUI uses screen fini/init wrapper.
123
+ - removeBackup called on save, closePane, closeCurrentTab, and stop (all buffers).
124
+ - backup/backupdir/permbackup settings wired through DEFAULT_SETTINGS → buf.Settings → syncEditorSettings.
120
125
  [~] Implement savecursor and saveundo serialization.
121
126
  Done: savecursor saves cursor position to configDir/buffers/cursor_state.json on Ctrl-S and on quit; position is restored when re-opening the same file; syncs from Go micro settings.json.
122
127
  Done: savecursor restore now vertically centers the viewport on the restored cursor position (deferred via _pendingCenterScroll flag, resolved on first render after layout is computed; handles softwrap/non-softwrap via _ttsScrollToCenter).