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 +43 -12
- package/initramfs/bin/buninu-linux-help +12 -0
- package/initramfs/bin/bunterm +64 -5
- package/initramfs/lib/bunterm/terminal.js +25 -9
- package/initramfs/lib/bunterm/vt.js +91 -0
- package/initramfs/lib/dlopen.js +4 -0
- package/initramfs/lib/fbdev.js +33 -10
- package/initramfs/usr/share/doc/buninu-linux/bunterm.md +20 -6
- package/initramfs/usr/share/doc/buninu-linux/fbdev.md +1 -1
- package/initramfs/usr/share/licenses/README.md +4 -0
- package/package.json +2 -2
- package/scripts/pack-initramfs.sh +4 -0
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
|
-
-
|
|
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
|
|
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
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
* [
|
|
1105
|
-
+ [
|
|
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");
|
package/initramfs/bin/bunterm
CHANGED
|
@@ -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 === "
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
};
|
package/initramfs/lib/dlopen.js
CHANGED
|
@@ -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,
|
package/initramfs/lib/fbdev.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
487
|
-
|
|
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
|
|
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
|
|
11
|
-
bunterm
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buninu-linux",
|
|
3
|
-
"version": "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://
|
|
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
|