buninu-linux 0.6.0 → 0.7.0

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/README.md CHANGED
@@ -20,7 +20,7 @@
20
20
  * There is no BusyBox and no C bootstrap.
21
21
 
22
22
  - Homepage: <https://buninu.org>
23
- - Souce: [github.com/jjtseng93/buninu-linux](https://github.com/jjtseng93/buninu-linux)
23
+ - Source: [github.com/jjtseng93/buninu-linux](https://github.com/jjtseng93/buninu-linux)
24
24
  - Buninu userspace source: [github.com/jjtseng93/buninu](https://github.com/jjtseng93/buninu)
25
25
  + **BUNinu Is Not Unix** 🐮
26
26
  + **幫你牛** 🐂 ・ **Bunに入魂** 🔥
@@ -33,12 +33,14 @@
33
33
  - Still in the early stages
34
34
  - [Video here](https://www.reddit.com/r/bun/comments/1wkpraj/buninu_linux_a_distro_with_bun_as_pid_1): Booted successfully on real x86-64 UEFI hardware with `--real`:
35
35
  * Bun reaches its interactive REPL
36
- * start() starts the Buninu userspace shell
36
+ * `start` starts the Buninu userspace shell
37
37
  * `cfg.disk` + shell `mount` mounts local disks
38
38
  * `cfg.net` loads common wired NIC and Android USB-tethering drivers
39
39
  * Android phone USB tethering over RNDIS has been tested successfully, allowing Buninu Linux to access the Internet through an Android phone
40
40
  * IP addresses and routes are configured manually because the image does not yet include a DHCP client.
41
- * The bundled `jmi` editor, jsmdcui editor/terminal multitasking, and local JavaScript execution also work.
41
+ * The bundled `jmi` editor, jsmdcui App runtime, and local JavaScript execution also work.
42
+ * Multi-tasking with kernel VT or jsmdcui
43
+ + See [Multitasking modes](#multitasking-modes)
42
44
  * The graphical terminal `bunterm` draws on the framebuffer with CJK, colour emoji and kitty images; see [Showing images](#showing-images)
43
45
  + Also has mouse click, wheel, and cursor drawing
44
46
  + Simplified browser works: @sanohiro/casty in chroot debian
@@ -341,7 +343,29 @@ If `Enter` is not recognized in a particular terminal, try `Ctrl-J` or
341
343
  - `jsmdcui --cdp-maze` runs a self-solving maze game
342
344
  * Run `bunterm` first to show its Emojis
343
345
 
344
- ### Panes, terminals, and tabs
346
+ ### Multitasking modes
347
+
348
+ #### Multiple virtual consoles with bunterm
349
+
350
+ Run a separate `bunterm` on each Linux virtual console. A numeric positional
351
+ argument is a short form of the corresponding device, so `bunterm 2` means
352
+ `bunterm /dev/tty2`:
353
+
354
+ ```sh
355
+ bunterm 2
356
+ bunterm 3 -s 13
357
+ ```
358
+
359
+ When the named VT is not the active one, `bunterm` starts there as a detached
360
+ session, switches the display to it, and immediately returns control to the
361
+ calling shell. Switch among VTs with `Ctrl+Alt+F1` through `Ctrl+Alt+F12`, or
362
+ move to the previous or next VT with `Alt+Left` and `Alt+Right`. Each VT keeps
363
+ its own terminal session; an inactive bunterm continues processing its PTY but
364
+ stops drawing until its VT becomes active again. Only one bunterm can control a
365
+ given VT. If one is already in use, the error identifies its PID and suggests
366
+ another VT number.
367
+
368
+ #### Panes, terminals, and tabs with jsmdcui
345
369
 
346
370
  `jmi` and jsmdcui can keep editors and terminal sessions open together. Press
347
371
  `Ctrl-E`, type `term`, and press `Enter` to open the default Buninu shell in a
@@ -350,7 +374,7 @@ terminal pane. `term COMMAND` starts a specific command instead. Use `vsplit`,
350
374
  editor pane or tab; each command optionally accepts a filename. From an editor
351
375
  pane, `Ctrl-T` is the direct shortcut for a new empty tab.
352
376
 
353
- #### Pane and tab controls
377
+ ##### Pane and tab controls
354
378
 
355
379
  | Input | Result |
356
380
  | --- | --- |
@@ -457,6 +481,7 @@ needs a kernel with a framebuffer: build with `--linux-lts` or `--real`
457
481
 
458
482
  | command | does | manual |
459
483
  | --- | --- | --- |
484
+ | `buninu-linux-help` | render this buninu-linux README from `/usr/share/doc/buninu-linux/README.md` | `buninu-linux-help` |
460
485
  | `mount` | `mount(2)` with type detection, `-o` parsing, `LABEL=`/`UUID=`, bind/move/remount; loads required filesystem and disk modules | `mount --help` → `/usr/share/doc/buninu-linux/mount.md` |
461
486
  | `umount` | `umount2(2)` with `-l`, `-f`, `-R` | `umount --help` |
462
487
  | `ip` | iproute2 grammar over `SIOC*` ioctls and `/proc/net`: `link`, `addr`, `route`, `neigh` | `ip --help` |
@@ -469,7 +494,7 @@ needs a kernel with a framebuffer: build with `--linux-lts` or `--real`
469
494
  | `chroot` | `chroot(2)` into another root directory, mounting `/proc`, `/sys`, `/dev`, `/dev/pts`, `/run`, `/tmp` and `/etc/resolv.conf` for you and unmounting them afterwards | `chroot --help` |
470
495
  | `bunterm` | a graphical terminal on the framebuffer: Skia (CanvasKit) text with CJK, colour emoji, seamless box drawing, kitty graphics images, with the mouse working, wheel scrollback included (`--no-mouse` turns it off); xterm.js's emulator core over Bun's built-in PTY | `bunterm --help` |
471
496
 
472
- Besides `bun` (and `sh`/`node` pointing at it), `/bin` includes eleven commands
497
+ Besides `bun` (and `sh`/`node` pointing at it), `/bin` includes twelve commands
473
498
  implemented as Bun scripts.
474
499
 
475
500
  The small `tar` reads the archive's own headers for links and listing, so
@@ -514,11 +539,13 @@ bun udocker.js create --name db debian:13
514
539
  cd ~/.udocker/containers/db/ROOT
515
540
  chroot . /bin/bash
516
541
  apt update
517
- apt install curl unzip chromium fonts-noto-cjk fonts-noto-color-emoji npm
542
+ apt install --no-install-recommends ca-certificates curl unzip chromium-headless-shell fonts-noto-cjk fonts-noto-color-emoji git
518
543
  curl -fsSL https://bun.sh/install | bash
519
544
  bun x bunmsh
520
545
  # We haven't fully reviewed
521
546
  # @sanohiro/casty's safety
547
+ # below is a fork of mine
548
+ # fixing some issues
522
549
  # !!!Use at your own risk!!!
523
550
  # !!!Use at your own risk!!!
524
551
  # !!!Use at your own risk!!!
@@ -529,9 +556,11 @@ bun x bunmsh
529
556
  # !!!Use at your own risk!!!
530
557
  # !!!Use at your own risk!!!
531
558
  # !!!Use at your own risk!!!
532
- bun i -g @sanohiro/casty
559
+ cd
560
+ git clone https://github.com/jjtseng93/casty
561
+ cd casty/bin
533
562
  # When clicking around, don’t release the mouse button immediately after pressing it, to make sure the mouse-down event is triggered
534
- casty https://github.com/jjtseng93/buninu-linux
563
+ bun casty.js buninu.org
535
564
 
536
565
 
537
566
 
@@ -581,7 +610,7 @@ tar czvf /tmp/buninu.tar.gz README.md package.json
581
610
 
582
611
  # On a virtual console: a graphical terminal with CJK, emoji and images
583
612
  bunterm
584
- bunterm /dev/tty1 --font-size 20 -e bun /buninu/apps/jsmdcui/src/index.js --demo
613
+ bunterm /dev/tty1 --font-size 13 -e bun /buninu/apps/jsmdcui/src/index.js --demo
585
614
  ```
586
615
 
587
616
  The graphics stack behind `bunterm` — the framebuffer module, CanvasKit,
@@ -1101,8 +1130,10 @@ for the userspace session.
1101
1130
  * [Basic configuration](#basic-configuration)
1102
1131
  * [Editor quick start](#editor-quick-start)
1103
1132
  + [jsmdcui](#jsmdcui)
1104
- * [Panes, terminals, and tabs](#panes-terminals-and-tabs)
1105
- + [Pane and tab controls](#pane-and-tab-controls)
1133
+ * [Multitasking modes](#multitasking-modes)
1134
+ + [Multiple virtual consoles with bunterm](#multiple-virtual-consoles-with-bunterm)
1135
+ + [Panes, terminals, and tabs with jsmdcui](#panes-terminals-and-tabs-with-jsmdcui)
1136
+ - [Pane and tab controls](#pane-and-tab-controls)
1106
1137
  * [Using Bun Modern Shell](#using-bun-modern-shell)
1107
1138
  * [Showing images](#showing-images)
1108
1139
  - [Commands inside /bin](#commands-inside-bin)
@@ -0,0 +1,12 @@
1
+ #!/bin/bun
2
+
3
+ import { resolve } from "node:path";
4
+ import { showDocument } from "../lib/document.js";
5
+
6
+ // The packed image has README.md under /usr/share/doc/buninu-linux. During
7
+ // development, address the checkout's canonical copy relative to that same
8
+ // document directory so this command runs as-is and still uses showDocument.
9
+ const installedReadme = resolve(import.meta.dir, "../usr/share/doc/buninu-linux/README.md");
10
+ await showDocument(await Bun.file(installedReadme).exists()
11
+ ? "README"
12
+ : "../../../../../README");
@@ -3,16 +3,22 @@
3
3
 
4
4
  import { showDocument } from "../lib/document.js";
5
5
  import { runTerminal } from "../lib/bunterm/terminal.js";
6
+ import { activeConsole, openConsole } from "../lib/fbdev.js";
7
+ import { virtualConsoleNumber } from "../lib/bunterm/vt.js";
8
+ import { closeSync, openSync, readFileSync } from "node:fs";
9
+
10
+ const CANONICAL_BUNTERM = "/bin/bunterm";
6
11
 
7
12
  const usage = () => {
8
13
  console.error(`Usage:
9
- bunterm [/dev/ttyN] [--font-size N] [--fb /dev/fb0] [--no-mouse] [-e command [argument...]]
14
+ bunterm [N | /dev/ttyN] [-s N | --font-size N] [--fb /dev/fb0] [--no-mouse] [-e command [argument...]]
10
15
  bunterm -h | --help`);
11
16
  };
12
17
 
13
18
  const main = async () => {
14
19
  const options = { argv: [] };
15
20
  const arguments_ = process.argv.slice(2);
21
+ let consoleArgumentIndex = -1;
16
22
  for (let index = 0; index < arguments_.length; index++) {
17
23
  const argument = arguments_[index];
18
24
  if (argument === "-h" || argument === "--help") {
@@ -36,8 +42,10 @@ const main = async () => {
36
42
  }
37
43
  return value;
38
44
  };
39
- if (argument === "--font-size" || argument.startsWith("--font-size=")) {
40
- options.fontSize = Number(valued("--font-size"));
45
+ if (argument === "-s" || argument.startsWith("-s=")
46
+ || argument === "--font-size" || argument.startsWith("--font-size=")) {
47
+ const name = argument === "-s" || argument.startsWith("-s=") ? "-s" : "--font-size";
48
+ options.fontSize = Number(valued(name));
41
49
  if (!Number.isFinite(options.fontSize) || options.fontSize < 6) throw new Error("--font-size must be a number of at least 6");
42
50
  } else if (argument === "--line-height" || argument.startsWith("--line-height=")) {
43
51
  options.lineHeight = Number(valued("--line-height"));
@@ -50,13 +58,64 @@ const main = async () => {
50
58
  options.mouse = true;
51
59
  } else if (argument === "--no-mouse") {
52
60
  options.mouse = false;
53
- } else if (argument.startsWith("/dev/")) {
54
- options.console = argument;
61
+ } else if (/^[1-9][0-9]*$/.test(argument) || argument.startsWith("/dev/")) {
62
+ const number = /^[1-9][0-9]*$/.test(argument) ? Number(argument) : virtualConsoleNumber(argument);
63
+ if (number === null) throw new Error(`expected N or /dev/ttyN, got ${argument}`);
64
+ if (consoleArgumentIndex !== -1) throw new Error("only one virtual console may be specified");
65
+ options.console = `/dev/tty${number}`;
66
+ consoleArgumentIndex = index;
55
67
  } else {
56
68
  usage();
57
69
  throw new Error(`unexpected argument: ${argument}`);
58
70
  }
59
71
  }
72
+
73
+ const probe = openConsole(["/dev/tty0"]);
74
+ let activeConsoleNumber;
75
+ try {
76
+ activeConsoleNumber = activeConsole(probe);
77
+ } finally {
78
+ probe.close();
79
+ }
80
+ if (!options.console) options.console = `/dev/tty${activeConsoleNumber}`;
81
+
82
+ // Every live bunterm converges on one unambiguous /proc/<pid>/cmdline:
83
+ // /bin/bunterm /bin/bunterm /dev/ttyN [options...]
84
+ // execve replaces the original process, so no forwarding parent remains.
85
+ const remaining = arguments_.filter((_argument, index) => index !== consoleArgumentIndex);
86
+ const canonicalArgv = [CANONICAL_BUNTERM, CANONICAL_BUNTERM, options.console, ...remaining];
87
+ const currentArgv = readFileSync("/proc/self/cmdline").toString("utf8").split("\0");
88
+ if (currentArgv.at(-1) === "") currentArgv.pop();
89
+ if (currentArgv.length !== canonicalArgv.length
90
+ || currentArgv.some((argument, index) => argument !== canonicalArgv[index])) {
91
+ const environment = Object.fromEntries(
92
+ Object.entries(process.env).filter((entry) => typeof entry[1] === "string"),
93
+ );
94
+ const targetConsoleNumber = virtualConsoleNumber(options.console);
95
+ if (consoleArgumentIndex !== -1 && targetConsoleNumber !== activeConsoleNumber) {
96
+ const consoleFd = openSync(options.console, "r+");
97
+ try {
98
+ const child = Bun.spawn(
99
+ [process.execPath, CANONICAL_BUNTERM, options.console, ...remaining],
100
+ {
101
+ argv0: CANONICAL_BUNTERM,
102
+ detached: true,
103
+ env: environment,
104
+ stdin: "ignore",
105
+ stdout: consoleFd,
106
+ stderr: consoleFd,
107
+ },
108
+ );
109
+ child.unref();
110
+ } finally {
111
+ closeSync(consoleFd);
112
+ }
113
+ return 0;
114
+ }
115
+ process.execve(process.execPath, canonicalArgv, environment);
116
+ throw new Error("process.execve returned unexpectedly");
117
+ }
118
+
60
119
  return runTerminal(options);
61
120
  };
62
121
 
@@ -20,6 +20,7 @@ import { UnicodeGraphemesAddon } from "../xterm/addon-unicode-graphemes.mjs";
20
20
  import { Renderer, defaultTheme } from "./renderer.js";
21
21
  import { ImageStore } from "./images.js";
22
22
  import { openKeyboard, translateKeys } from "./input.js";
23
+ import { claimVirtualConsole } from "./vt.js";
23
24
 
24
25
  const require = createRequire(import.meta.url);
25
26
 
@@ -81,6 +82,7 @@ export const createSession = ({
81
82
  });
82
83
 
83
84
  let frameTimer = null;
85
+ let active = true;
84
86
  let blinkOn = true;
85
87
  const cursorHidden = () => Boolean(term._core?.coreService?.isCursorHidden);
86
88
  // Only set when a pointer is attached; without one the renderer is called
@@ -89,6 +91,7 @@ export const createSession = ({
89
91
  const paint = () => {
90
92
  if (frameTimer) clearTimeout(frameTimer);
91
93
  frameTimer = null;
94
+ if (!active) return false;
92
95
  const buffer = term.buffer.active;
93
96
  // cursorY counts from the buffer's base, so a scrolled-back viewport
94
97
  // moves the cursor down the screen and eventually off it.
@@ -104,8 +107,20 @@ export const createSession = ({
104
107
  });
105
108
  };
106
109
  const requestFrame = () => {
110
+ if (!active) return;
107
111
  frameTimer ??= setTimeout(paint, 16);
108
112
  };
113
+ const suspend = () => {
114
+ active = false;
115
+ if (frameTimer) clearTimeout(frameTimer);
116
+ frameTimer = null;
117
+ };
118
+ const resume = () => {
119
+ if (active) return;
120
+ active = true;
121
+ renderer.invalidate();
122
+ paint();
123
+ };
109
124
  const blinkTimer = cursorBlink
110
125
  ? setInterval(() => { blinkOn = !blinkOn; requestFrame(); }, 500)
111
126
  : null;
@@ -193,6 +208,9 @@ export const createSession = ({
193
208
  };
194
209
 
195
210
  const reportMouse = (report) => {
211
+ // /dev/input is global rather than routed through the active VT. Do not
212
+ // draw or send mouse input to a terminal whose console is off-screen.
213
+ if (!active) return;
196
214
  pointer = { x: report.x, y: report.y };
197
215
  requestFrame();
198
216
  const service = term._core?.coreMouseService;
@@ -268,7 +286,7 @@ export const createSession = ({
268
286
  paint();
269
287
  return {
270
288
  term, backend, renderer, images, kitty,
271
- paint, requestFrame, settle, input, exited, close, attachPointer, reportMouse, wheelScroll,
289
+ paint, requestFrame, suspend, resume, settle, input, exited, close, attachPointer, reportMouse, wheelScroll,
272
290
  };
273
291
  };
274
292
 
@@ -282,15 +300,13 @@ export const runTerminal = async ({ console: consoleDevice = null, device = "/de
282
300
  const fonts = loadFontSet(CanvasKit, "terminal");
283
301
  const consolePaths = consoleDevice ? [consoleDevice] : undefined;
284
302
 
285
- return runGraphics(async (display, { onRestore, onAcquire }) => {
303
+ return runGraphics(async (display, { onRestore, onRelease, onAcquire }) => {
286
304
  const screen = openCanvas(CanvasKit, display);
287
305
  const session = createSession({ CanvasKit, screen, fonts, ...options });
288
- // Coming back from another console (Ctrl-Alt-Fn): fbcon has drawn over
289
- // the framebuffer, so repaint everything.
290
- onAcquire(() => {
291
- session.renderer.invalidate();
292
- session.paint();
293
- });
306
+ onRelease(session.suspend);
307
+ // Coming back from another console (Ctrl-Alt-Fn): fbcon or another
308
+ // bunterm has drawn over the framebuffer, so repaint everything.
309
+ onAcquire(session.resume);
294
310
  let keyboard = null;
295
311
  try {
296
312
  keyboard = openKeyboard({
@@ -320,5 +336,5 @@ export const runTerminal = async ({ console: consoleDevice = null, device = "/de
320
336
  session.close();
321
337
  screen.close();
322
338
  }
323
- }, { device, console: consolePaths });
339
+ }, { device, console: consolePaths, prepareConsole: claimVirtualConsole });
324
340
  };
@@ -0,0 +1,91 @@
1
+ // Cooperative ownership for one Linux virtual console.
2
+ //
3
+ // Linux VT_SETMODE replaces an existing VT_PROCESS owner rather than failing,
4
+ // and VT_GETMODE does not expose that owner's pid. bunterm therefore publishes
5
+ // a short per-VT task name and verifies that another such process does not also
6
+ // hold the console device open. Both facts live under /proc/<pid> and disappear
7
+ // with the process; there is no persistent pid or lock file to become stale.
8
+
9
+ import { readdirSync, readFileSync, statSync } from "node:fs";
10
+ import { libc, cString, check } from "../dlopen.js";
11
+
12
+ const PR_SET_NAME = 15;
13
+ const PR_GET_NAME = 16;
14
+
15
+ export const virtualConsoleNumber = (path) => {
16
+ const match = /^\/dev\/tty([1-9][0-9]*)$/.exec(path);
17
+ return match ? Number(match[1]) : null;
18
+ };
19
+
20
+ const taskName = (number) => `bunterm:tty${number}`;
21
+
22
+ const getTaskName = () => {
23
+ const name = new Uint8Array(16);
24
+ check("prctl(PR_GET_NAME)", libc.symbols.prctl(PR_GET_NAME, name, 0n, 0n, 0n));
25
+ const zero = name.indexOf(0);
26
+ return new TextDecoder().decode(zero === -1 ? name : name.subarray(0, zero));
27
+ };
28
+
29
+ const setTaskName = (name) => {
30
+ const encoded = cString(name);
31
+ if (encoded.byteLength > 16) throw new Error(`Linux task name is too long: ${name}`);
32
+ check("prctl(PR_SET_NAME)", libc.symbols.prctl(PR_SET_NAME, encoded, 0n, 0n, 0n));
33
+ };
34
+
35
+ const processHoldsDevice = (pid, deviceId) => {
36
+ let descriptors;
37
+ try {
38
+ descriptors = readdirSync(`/proc/${pid}/fd`);
39
+ } catch {
40
+ return false;
41
+ }
42
+ for (const descriptor of descriptors) {
43
+ try {
44
+ if (statSync(`/proc/${pid}/fd/${descriptor}`).rdev === deviceId) return true;
45
+ } catch {
46
+ // The process or descriptor disappeared between readdir and stat.
47
+ }
48
+ }
49
+ return false;
50
+ };
51
+
52
+ export const claimVirtualConsole = (consoleDevice) => {
53
+ const number = virtualConsoleNumber(consoleDevice.path);
54
+ if (number === null) throw new Error(`bunterm needs a canonical virtual console, got ${consoleDevice.path}`);
55
+ const name = taskName(number);
56
+ const previousName = getTaskName();
57
+ const deviceId = statSync(consoleDevice.path).rdev;
58
+
59
+ // Publish before scanning. Two simultaneous starters can then reject each
60
+ // other, but they cannot both scan an unpublished peer and proceed.
61
+ setTaskName(name);
62
+ try {
63
+ const pids = readdirSync("/proc")
64
+ .filter((entry) => /^[0-9]+$/.test(entry) && Number(entry) !== process.pid);
65
+ for (const pid of pids) {
66
+ let otherName;
67
+ try {
68
+ otherName = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
69
+ } catch {
70
+ continue;
71
+ }
72
+ if (otherName !== name || !processHoldsDevice(pid, deviceId)) continue;
73
+
74
+ throw new Error(`
75
+ ${consoleDevice.path} is occupied
76
+ Controlled by bunterm pid: ${pid}
77
+ See --help or try another VT:
78
+ bunterm ${number- -1}
79
+ Switch between TTYs by Ctrl+Alt+F1-12
80
+ or Alt-Left / Alt-Right
81
+ `);
82
+ }
83
+ } catch (error) {
84
+ try { setTaskName(previousName); } catch {}
85
+ throw error;
86
+ }
87
+
88
+ return () => {
89
+ try { setTaskName(previousName); } catch {}
90
+ };
91
+ };
@@ -85,6 +85,10 @@ export const libc = dlopen(libcPath, {
85
85
  args: [FFIType.i32, FFIType.ptr, FFIType.i32],
86
86
  returns: FFIType.i32,
87
87
  },
88
+ prctl: {
89
+ args: [FFIType.i32, FFIType.ptr, FFIType.u64, FFIType.u64, FFIType.u64],
90
+ returns: FFIType.i32,
91
+ },
88
92
  chroot: {
89
93
  args: [FFIType.ptr],
90
94
  returns: FFIType.i32,
@@ -355,8 +355,12 @@ const setConsoleSwitchMode = (consoleDevice, mode) => {
355
355
  // when it comes back (answered with VT_ACKACQ), at which point `onAcquire`
356
356
  // runs so the caller can repaint what fbcon left on the screen. Returns a
357
357
  // function that puts the console back into automatic switching.
358
- export const watchConsoleSwitches = (consoleDevice, onAcquire) => {
358
+ export const watchConsoleSwitches = (consoleDevice, callbacks) => {
359
+ // Keep the original (console, onAcquire) public form working.
360
+ const onRelease = typeof callbacks === "function" ? () => {} : callbacks.onRelease;
361
+ const onAcquire = typeof callbacks === "function" ? callbacks : callbacks.onAcquire;
359
362
  const release = () => {
363
+ try { onRelease(); } catch {}
360
364
  try { ioctlValue(consoleDevice.fd, VT_RELDISP, 1, "VT_RELDISP"); } catch {}
361
365
  };
362
366
  const acquire = () => {
@@ -429,6 +433,7 @@ const defaultTriangle = (display) => [
429
433
  // before text mode returns, such as putting a keyboard back into cooked mode.
430
434
  export const runGraphics = async (draw, {
431
435
  device = "/dev/fb0", console: consolePaths, switchConsole = true,
436
+ prepareConsole = null,
432
437
  } = {}) => {
433
438
  const display = openFramebuffer(device);
434
439
  let consoleDevice;
@@ -442,6 +447,7 @@ export const runGraphics = async (draw, {
442
447
  let previousConsole = null;
443
448
  let unwatch = null;
444
449
  const restoreCallbacks = [];
450
+ const releaseCallbacks = [];
445
451
  const acquireCallbacks = [];
446
452
  const restore = () => {
447
453
  while (restoreCallbacks.length) {
@@ -470,6 +476,10 @@ export const runGraphics = async (draw, {
470
476
  for (const [signal, handler] of signalHandlers) process.on(signal, handler);
471
477
  process.on("exit", restore);
472
478
  try {
479
+ if (prepareConsole) {
480
+ const cleanup = await prepareConsole(consoleDevice);
481
+ if (typeof cleanup === "function") restoreCallbacks.push(cleanup);
482
+ }
473
483
  const target = switchConsole ? consoleNumber(consoleDevice.path) : null;
474
484
  if (target !== null) {
475
485
  const active = activeConsole(consoleDevice);
@@ -480,22 +490,35 @@ export const runGraphics = async (draw, {
480
490
  }
481
491
  setConsoleGraphics(consoleDevice);
482
492
  graphics = true;
493
+ // An async draw callback runs synchronously until its first await. Start
494
+ // it before enabling VT_PROCESS so it can register release/acquire hooks;
495
+ // otherwise a switch in that small window could be acknowledged without
496
+ // giving the renderer a chance to stop.
497
+ const drawing = draw(display, {
498
+ console: consoleDevice,
499
+ onRestore: (callback) => { restoreCallbacks.push(callback); },
500
+ onRelease: (callback) => { releaseCallbacks.push(callback); },
501
+ onAcquire: (callback) => { acquireCallbacks.push(callback); },
502
+ });
483
503
  if (switchConsole) {
484
504
  try {
485
- unwatch = watchConsoleSwitches(consoleDevice, () => {
486
- for (const callback of acquireCallbacks) {
487
- try { callback(); } catch {}
488
- }
505
+ unwatch = watchConsoleSwitches(consoleDevice, {
506
+ onRelease: () => {
507
+ for (const callback of releaseCallbacks) {
508
+ try { callback(); } catch {}
509
+ }
510
+ },
511
+ onAcquire: () => {
512
+ for (const callback of acquireCallbacks) {
513
+ try { callback(); } catch {}
514
+ }
515
+ },
489
516
  });
490
517
  } catch {
491
518
  // Not a virtual console (or no permission): switches stay automatic.
492
519
  }
493
520
  }
494
- return await draw(display, {
495
- console: consoleDevice,
496
- onRestore: (callback) => { restoreCallbacks.push(callback); },
497
- onAcquire: (callback) => { acquireCallbacks.push(callback); },
498
- });
521
+ return await drawing;
499
522
  } finally {
500
523
  restore();
501
524
  process.off("exit", restore);
@@ -7,8 +7,9 @@ box drawing and kitty graphics protocol images — no X11, Wayland or GPU.
7
7
 
8
8
  ```sh
9
9
  bunterm # run /bin/sh on the console you are on
10
- bunterm /dev/tty1 # use virtual console 1 for graphics and keys
11
- bunterm --font-size 20
10
+ bunterm 2 # start a detached bunterm on /dev/tty2
11
+ bunterm /dev/tty1 # the full virtual-console spelling also works
12
+ bunterm -s 20 # short for --font-size 20
12
13
  bunterm -e bun /buninu/apps/jsmdcui/src/index.js --demo
13
14
  bunterm --no-mouse # keyboard only
14
15
  bunterm -h
@@ -19,13 +20,21 @@ The program's `TERM` is `xterm-256color`, `COLORTERM=truecolor`,
19
20
  Markdown images without `--kitty`). When it exits, the console returns to
20
21
  text mode and `bunterm` exits with its status.
21
22
 
23
+ Every live session has a canonical `/bin/bunterm /dev/ttyN ...` command line.
24
+ When no console is named, the currently active VT is used in the foreground.
25
+ Naming a different VT starts bunterm as a detached session and returns
26
+ immediately to the calling shell. Only one bunterm may control a given VT;
27
+ ownership is
28
+ identified from the process task name and its open console descriptor under
29
+ `/proc`, so it disappears automatically when the process exits.
30
+
22
31
  ## Options
23
32
 
24
33
  | Option | Meaning |
25
34
  | --- | --- |
26
- | `/dev/ttyN` | The virtual console to use for graphics and the keyboard. If another console is on screen, the display switches to this one first and back when `bunterm` exits. Without it the process's own console and stdin are used, so run it from a VT (not a serial line or a PTY). |
35
+ | `N`, `/dev/ttyN` | The virtual console to use for graphics and the keyboard; for example, `2` means `/dev/tty2`. Naming a VT other than the active one starts a detached bunterm there, switches the display to it, and immediately returns control to the calling shell. Without this argument, bunterm uses the active VT in the foreground. |
27
36
  | `-e command [argument...]` | The program to run; everything after `-e` belongs to it. Default `/bin/sh`. |
28
- | `--font-size N` | Font size in pixels (default 16). The cell is 0.6 × N wide and 1.16 × N tall with DejaVu Sans Mono, so 1024×768 at 16 px gives 106 × 41 cells. |
37
+ | `-s N`, `--font-size N` | Font size in pixels (default 16). The cell is 0.6 × N wide and 1.16 × N tall with DejaVu Sans Mono, so 1024×768 at 16 px gives 106 × 41 cells. |
29
38
  | `--line-height F` | Multiplies the cell height (default 1). |
30
39
  | `--fb /dev/fbN` | The framebuffer device (default `/dev/fb0`). |
31
40
  | `--no-blink` | A steady cursor. |
@@ -57,7 +66,11 @@ text mode and `bunterm` exits with its status.
57
66
 
58
67
  The console stays in the kernel's translated keyboard mode, so
59
68
  `Ctrl-Alt-F2` and friends still switch consoles; coming back repaints the
60
- screen. A few console-specific
69
+ screen. While its VT is inactive, bunterm continues consuming PTY output and
70
+ updating its scrollback and kitty images, but neither writes the framebuffer
71
+ nor forwards global `/dev/input` mouse events. This allows separate bunterm
72
+ processes on `/dev/tty1`, `/dev/tty2`, and so on to hand the display back and
73
+ forth through the normal Linux VT switch. A few console-specific
61
74
  sequences are rewritten to xterm's (`F1`–`F5`, `Home`, `End`), and arrow keys
62
75
  follow the program's application cursor mode.
63
76
 
@@ -104,7 +117,8 @@ reported and the session continues without it.
104
117
  - Kitty images are drawn above text; `z` ordering below text and animation
105
118
  frames are not implemented, and file transmission (`t=f`/`t=t`) works only
106
119
  for files under `/tmp`, `/dev/shm` or `$TMPDIR`.
107
- - One framebuffer, one session. For panes and tabs run `jsmdcui` inside it.
120
+ - One bunterm per virtual console may use the framebuffer; only the active VT
121
+ draws. For panes and tabs within one VT, run `jsmdcui` inside it.
108
122
 
109
123
  ## See also
110
124
 
@@ -84,7 +84,7 @@ than an X11 `Display`. Colours are `{ r, g, b, a }` objects.
84
84
  | `openConsole`, `setConsoleGraphics`, `setConsoleText` | Explicit Linux virtual-console control for callers that need it. |
85
85
  | `activeConsole(console)`, `activateConsole(console, n)`, `consoleNumber(path)` | `VT_GETSTATE` / `VT_ACTIVATE` + `VT_WAITACTIVE`, and the number in a `/dev/ttyN` path. |
86
86
  | `watchConsoleSwitches(console, onAcquire)` | `VT_SETMODE` with `VT_PROCESS`: answers the kernel's switch-away and switch-back signals and calls `onAcquire` when the display returns. Returns a function restoring automatic switching. |
87
- | `runGraphics(draw, { device, console, switchConsole }?)` | Open the framebuffer, put the console in graphics mode, run `await draw(display, { console, onRestore, onAcquire })`, and restore text mode however the call ends (return, throw, `process.exit()`, `SIGHUP`/`SIGINT`/`SIGQUIT`/`SIGTERM`). `console` lists console devices to try, such as `["/dev/tty1"]`; when the one opened is a `/dev/ttyN` other than the console on screen, the display is switched to it first and back afterwards (`switchConsole: false` disables this). Console switches with Ctrl-Alt-Fn keep working; `onAcquire(callback)` runs when the display comes back so the caller can repaint, and `onRestore(callback)` registers cleanup to run before text mode returns. |
87
+ | `runGraphics(draw, { device, console, switchConsole, prepareConsole }?)` | Open the framebuffer, optionally call `await prepareConsole(console)`, put the console in graphics mode, run `await draw(display, { console, onRestore, onRelease, onAcquire })`, and restore text mode however the call ends (return, throw, `process.exit()`, `SIGHUP`/`SIGINT`/`SIGQUIT`/`SIGTERM`). `console` lists console devices to try, such as `["/dev/tty1"]`; when the one opened is a `/dev/ttyN` other than the console on screen, the display is switched to it first and back afterwards (`switchConsole: false` disables this). Console switches with Ctrl-Alt-Fn keep working: `onRelease(callback)` runs before the switch is acknowledged so drawing can stop, `onAcquire(callback)` runs when the display comes back, and `onRestore(callback)` registers cleanup to run before text mode returns. A cleanup function returned by `prepareConsole` is registered the same way. |
88
88
  | `runDemo({ polygons, device }?)` | Run the CLI demo through `runGraphics()`. Each item in `polygons` is drawn separately. |
89
89
 
90
90
  The module supports 16-, 24-, and 32-bit packed-pixel framebuffers and uses
@@ -0,0 +1,4 @@
1
+ # Licenses in the built image
2
+
3
+ The packaging step copies the project `LICENSE`, `NOTICE.md`, and third-party
4
+ `LICENSES/` directory here.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-linux",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "A Linux distribution running Bun as PID 1 with the Buninu userspace.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -13,7 +13,7 @@
13
13
  "type": "git",
14
14
  "url": "git+https://github.com/jjtseng93/buninu-linux.git"
15
15
  },
16
- "homepage": "https://github.com/jjtseng93/buninu-linux#readme",
16
+ "homepage": "https://buninu.org",
17
17
  "bugs": {
18
18
  "url": "https://github.com/jjtseng93/buninu-linux/issues"
19
19
  },
@@ -22,6 +22,10 @@ trap 'rm -rf "$staging"; rm -f "$archive"' EXIT
22
22
  cp -a --reflink=auto initramfs/. "$staging/"
23
23
  rm -rf "$staging/bin/bun" "$staging/lib/modules"
24
24
  cp -a --reflink=auto "$native_dir/." "$staging/"
25
+ # Keep the project README canonical while making it available to
26
+ # buninu-linux-help from the conventional installed-document path.
27
+ cp README.md "$staging/usr/share/doc/buninu-linux/README.md"
28
+ cp -a LICENSE NOTICE.md LICENSES "$staging/usr/share/licenses/"
25
29
 
26
30
  # fakeroot lets cpio record the character devices without real root. Nothing
27
31
  # has mounted devtmpfs when the kernel execs Bun as PID 1, so every device