buninu-linux 0.14.57 → 0.15.58

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.
Files changed (94) hide show
  1. package/LICENSES/linux-firmware/LICENCE.nvidia +131 -0
  2. package/LICENSES/linux-firmware/LICENSE.amdgpu +51 -0
  3. package/LICENSES/linux-firmware/LICENSE.amdisp +31 -0
  4. package/LICENSES/linux-firmware/LICENSE.i915 +39 -0
  5. package/LICENSES/linux-firmware/LICENSE.radeon +51 -0
  6. package/LICENSES/linux-firmware/LICENSE.xe +39 -0
  7. package/LICENSES/linux-firmware/WHENCE +10280 -0
  8. package/NOTICE.md +64 -46
  9. package/README.md +371 -87
  10. package/README.zh.md +2034 -0
  11. package/build-image.sh +10 -1
  12. package/build-uki.sh +3 -1
  13. package/fetch-alpine.sh +14 -0
  14. package/graphics.md +31 -0
  15. package/index.js +28 -11
  16. package/initramfs/bin/buninu-browser +4 -1
  17. package/initramfs/bin/buninu-land +1 -1
  18. package/initramfs/bin/bunterm +3 -3
  19. package/initramfs/bin/chroot +13 -0
  20. package/initramfs/bin/chvt +11 -0
  21. package/initramfs/bin/mount +4 -0
  22. package/initramfs/bin/umount +6 -0
  23. package/initramfs/bin/xdg-open +33 -0
  24. package/initramfs/buninu/CHANGELOG.md +19 -0
  25. package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +4 -0
  26. package/initramfs/buninu/apps/bunmsh/package.json +1 -1
  27. package/initramfs/buninu/apps/bunmsh/src/main.js +1 -1
  28. package/initramfs/buninu/apps/jsgotty/README.en.md +40 -0
  29. package/initramfs/buninu/apps/jsgotty/README.md +40 -0
  30. package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
  31. package/initramfs/buninu/apps/jsgotty/package.json +1 -1
  32. package/initramfs/buninu/apps/jsgotty/static/index.html +1 -0
  33. package/initramfs/buninu/apps/jsgotty/static/js/gotty.js +82 -17
  34. package/initramfs/buninu/apps/jsgotty/static/js/kitty-keyboard.js +193 -0
  35. package/initramfs/buninu/apps/jsgotty/tests/android-alt.test.js +115 -0
  36. package/initramfs/buninu/apps/jsgotty/tests/arrow-modifiers.test.js +80 -0
  37. package/initramfs/buninu/apps/jsgotty/tests/kitty-keyboard-browser.test.js +53 -0
  38. package/initramfs/buninu/apps/jsgotty/tests/kitty-keyboard.test.js +102 -0
  39. package/initramfs/buninu/apps/jsgotty/tests/manual/README.md +20 -0
  40. package/initramfs/buninu/apps/jsgotty/tests/manual/keyboard-collector.cjs +55 -0
  41. package/initramfs/buninu/apps/jsgotty/tests/manual/keyboard-debug.js +74 -0
  42. package/initramfs/buninu/apps/jsgotty/tests/manual/kitty-keyboard.js +80 -0
  43. package/initramfs/buninu/package.json +1 -1
  44. package/initramfs/init.js +19 -3
  45. package/initramfs/lib/bunterm/clipboard.js +14 -0
  46. package/initramfs/lib/bunterm/commands.js +37 -0
  47. package/initramfs/lib/bunterm/fps.js +38 -0
  48. package/initramfs/lib/bunterm/images.js +9 -5
  49. package/initramfs/lib/bunterm/input.js +58 -15
  50. package/initramfs/lib/bunterm/keyboard.js +62 -0
  51. package/initramfs/lib/bunterm/keycodes.js +104 -0
  52. package/initramfs/lib/bunterm/kitty-keyboard.cjs +195 -0
  53. package/initramfs/lib/bunterm/raw-protocol.js +33 -0
  54. package/initramfs/lib/bunterm/renderer.js +89 -20
  55. package/initramfs/lib/bunterm/selection.js +60 -0
  56. package/initramfs/lib/bunterm/terminal.js +146 -11
  57. package/initramfs/lib/bunterm/vt-menu.js +181 -0
  58. package/initramfs/lib/bunterm/vt.js +27 -1
  59. package/initramfs/lib/canvas.js +99 -0
  60. package/initramfs/lib/chvt.js +31 -0
  61. package/initramfs/lib/fbdev.js +21 -3
  62. package/initramfs/lib/gpu.js +77 -0
  63. package/initramfs/lib/raw-image.js +47 -0
  64. package/initramfs/lib/xdg-open.js +42 -0
  65. package/initramfs/usr/share/doc/buninu-linux/bunterm.md +85 -11
  66. package/initramfs/usr/share/doc/buninu-linux/chvt.md +34 -0
  67. package/initramfs/usr/share/doc/buninu-linux/xdg-open.md +43 -0
  68. package/package.json +1 -1
  69. package/scripts/arch.js +7 -5
  70. package/scripts/arch.sh +5 -5
  71. package/scripts/build.js +16 -3
  72. package/scripts/fetch-gpu.js +105 -0
  73. package/scripts/fetch.js +11 -1
  74. package/scripts/gpu-firmware.json +22 -0
  75. package/scripts/modules.js +4 -1
  76. package/scripts/npm-packages.md +17 -17
  77. package/test/bunterm-fps.test.js +17 -0
  78. package/test/bunterm-keyboard.test.js +116 -0
  79. package/test/bunterm-next-vt.test.js +9 -0
  80. package/test/bunterm-raw-protocol.test.js +19 -0
  81. package/test/bunterm-renderer.test.js +158 -0
  82. package/test/bunterm-selection.test.js +38 -0
  83. package/test/bunterm-vt-menu.test.js +178 -0
  84. package/test/chroot-bind.test.js +40 -2
  85. package/test/chvt.test.js +46 -0
  86. package/test/fbdev-sync.test.js +33 -0
  87. package/test/gpu.test.js +155 -0
  88. package/test/image-size.test.js +17 -0
  89. package/test/manual-bunterm-image-paths.js +31 -0
  90. package/test/mount-libc-lifetime.test.js +50 -0
  91. package/test/no-gpu.test.js +43 -0
  92. package/test/readme-cli.test.js +29 -0
  93. package/test/stage-npm.test.js +11 -4
  94. package/test/xdg-open.test.js +55 -0
@@ -261,6 +261,26 @@ export const clear = (display, color = { r: 0, g: 0, b: 0, a: 255 }) => {
261
261
  }
262
262
  };
263
263
 
264
+ // msync is a file writeback operation, not a universal fbdev present API.
265
+ // DRM mappings may have no backing fsync operation (vfs_fsync_range then
266
+ // returns EINVAL). Keep working drivers' deferred-I/O sync, but probe unsupported
267
+ // mappings only once. The address is the original mmap base, and flags are fixed.
268
+ export const syncFramebuffer = (display, native = {
269
+ msync: (...args) => libc.symbols.msync(...args), errno, strerror,
270
+ }) => {
271
+ if (display.msyncUnsupported) return;
272
+ for (;;) {
273
+ if (native.msync(display.address, display.memoryLength, MS_SYNC) !== -1) return;
274
+ const code = native.errno();
275
+ if (code === 4) continue; // EINTR
276
+ if (code === 22 || code === 95) { // EINVAL / EOPNOTSUPP on this device mapping
277
+ display.msyncUnsupported = true;
278
+ return;
279
+ }
280
+ throw new Error(`msync(${display.path}): ${native.strerror(code)}`);
281
+ }
282
+ };
283
+
264
284
  export const flush = (display) => {
265
285
  const visibleBytes = display.width * display.bytesPerPixel;
266
286
  for (let y = 0; y < display.height; y++) {
@@ -269,9 +289,7 @@ export const flush = (display) => {
269
289
  + display.xOffset * display.bytesPerPixel;
270
290
  display.memory.set(row, position);
271
291
  }
272
- if (libc.symbols.msync(display.address, display.memoryLength, MS_SYNC) === -1) {
273
- fail(`msync(${display.path})`);
274
- }
292
+ syncFramebuffer(display);
275
293
  };
276
294
 
277
295
  // Copies an RGBA ImageData (HTML canvas layout, 4 bytes per pixel, rows of
@@ -0,0 +1,77 @@
1
+ // Explicit one-shot GPU coldplug. Importing this module never loads a driver.
2
+ import * as fs from "node:fs";
3
+ import { join, basename } from "node:path";
4
+
5
+ const drivers = new Set(["i915", "xe", "amdgpu", "radeon", "nouveau", "virtio_gpu", "vmwgfx"]);
6
+ export const isGpuPath = (_module, path) => path.startsWith("kernel/drivers/gpu/");
7
+ export const isGpuDriver = (module, path) => isGpuPath(module, path) && drivers.has(module.replaceAll("-", "_"));
8
+
9
+ export const framebufferUsers = ({ root = "/", io = fs } = {}) => {
10
+ const users = [];
11
+ for (const pid of io.readdirSync(join(root, "proc")).filter(name => /^\d+$/.test(name))) {
12
+ try {
13
+ const dir = join(root, "proc", pid, "fd");
14
+ let busy = false;
15
+ for (const fd of io.readdirSync(dir)) {
16
+ try { if (/^\/dev\/fb\d+(?: \(deleted\))?$/.test(io.readlinkSync(join(dir, fd)))) busy = true; }
17
+ catch (error) { if (error.code !== "ENOENT") throw error; }
18
+ }
19
+ // An mmap can outlive its fd.
20
+ if (/\/dev\/fb\d+(?:\s|$)/m.test(io.readFileSync(join(root, "proc", pid, "maps"), "utf8"))) busy = true;
21
+ if (busy) users.push(pid);
22
+ } catch (error) {
23
+ if (error.code !== "ENOENT" && error.code !== "ESRCH") throw error;
24
+ }
25
+ }
26
+ return users;
27
+ };
28
+
29
+ export const configureGpu = ({ autoload, tryModprobe, root = "/", io = fs, env = process.env, log = console.log } = {}) => {
30
+ const result = { devices: [], renderNodes: [], framebuffers: [], errors: [], blocked: false };
31
+ const say = text => { try { log(`[gpu] ${text}`); } catch {} };
32
+ try {
33
+ const gid = env.BUNINU_RENDER_GID === undefined ? 0 : Number(env.BUNINU_RENDER_GID);
34
+ if (!Number.isInteger(gid) || gid < 0 || gid >= 0xffffffff || env.BUNINU_RENDER_GID === "")
35
+ throw Error("BUNINU_RENDER_GID must be a numeric group ID");
36
+ const users = framebufferUsers({ root, io });
37
+ if (users.length) {
38
+ result.blocked = true;
39
+ say(`not loaded: framebuffer in use by PID ${users.join(", ")}; exit bunterm/fbdev and run cfg.gpu before reopening it`);
40
+ return result;
41
+ }
42
+ // A virtio GPU may not appear on the virtio bus until its PCI transport loads.
43
+ try { tryModprobe("virtio_pci"); } catch (error) { result.errors.push(`virtio_pci: ${error?.message ?? error}`); }
44
+ result.devices = autoload({ buses: ["pci", "virtio"], acceptModule: isGpuDriver });
45
+ for (const device of result.devices) {
46
+ if (device.error) result.errors.push(`${device.bus}/${device.device}: ${device.error}`);
47
+ say(`${device.bus}/${device.device} ${device.modules.join(" ")}: ${device.error ?? (device.loaded.length ? "loaded " + device.loaded.join(" ") : "already loaded/probed")}`);
48
+ }
49
+ const list = path => { try { return io.readdirSync(join(root, path)); } catch (error) { if (error.code === "ENOENT") return []; throw error; } };
50
+ for (const name of list("dev/dri").filter(name => /^renderD\d+$/.test(name)).sort()) {
51
+ const path = join(root, "dev/dri", name);
52
+ try {
53
+ if (!io.statSync(path).isCharacterDevice()) continue;
54
+ io.chownSync(path, 0, gid);
55
+ io.chmodSync(path, 0o660);
56
+ let driver = "unknown";
57
+ try {
58
+ const device = join(root, "sys/class/drm", name, "device");
59
+ driver = basename(io.readlinkSync(join(device, "driver")));
60
+ // virtio DRM nodes can point at the parent PCI transport device.
61
+ if (driver === "virtio-pci") for (const child of io.readdirSync(device).filter(n => /^virtio\d+$/.test(n))) {
62
+ const candidate = basename(io.readlinkSync(join(device, child, "driver")));
63
+ if (candidate === "virtio_gpu" || candidate === "virtio-gpu") { driver = candidate; break; }
64
+ }
65
+ } catch {}
66
+ result.renderNodes.push({ path: `/dev/dri/${name}`, driver, gid });
67
+ say(`/dev/dri/${name}: driver=${driver}, root:${gid} mode=0660`);
68
+ } catch (error) { result.errors.push(`${name}: ${error?.message ?? error}`); }
69
+ }
70
+ result.framebuffers = list("dev").filter(name => /^fb\d+$/.test(name)).sort().map(name => `/dev/${name}`);
71
+ say(`framebuffer: ${result.framebuffers.join(", ") || "none"}; reopen framebuffer clients after GPU handoff`);
72
+ if (!result.renderNodes.length) say("no accessible render node yet; check kernel firmware/driver messages and GPU support, then retry cfg.gpu after probing finishes");
73
+ say("clients still need their own Mesa/Vulkan driver; non-root clients need the render node's numeric group");
74
+ } catch (error) { result.errors.push(String(error?.message ?? error)); }
75
+ for (const error of result.errors) say(`failed: ${error}; init continues`);
76
+ return result;
77
+ };
@@ -0,0 +1,47 @@
1
+ // Retain immutable raw pixels until an operation actually needs a Skia Image.
2
+ // The native image store owns these handles; encoded images remain ordinary Skia images.
3
+ const rawImages = new WeakMap();
4
+ export const rawImageData = image => rawImages.get(image);
5
+ export const skiaImage = image => rawImages.get(image)?.materialize() ?? image;
6
+
7
+ export function createRawImage(CanvasKit, pixels, width, height, colorType) {
8
+ let decoded = null, closed = false;
9
+ const data = {
10
+ pixels, width, height, colorType,
11
+ opaque: undefined,
12
+ materialize() {
13
+ if (closed) throw Error('raw image is closed');
14
+ if (!decoded) {
15
+ decoded = CanvasKit.MakeImage({width, height, colorType,
16
+ alphaType: CanvasKit.AlphaType.Unpremul, colorSpace: CanvasKit.ColorSpace.SRGB}, pixels, width * 4);
17
+ if (!decoded) throw Error('raw image rejected');
18
+ }
19
+ return decoded;
20
+ },
21
+ };
22
+ const image = {
23
+ width: () => width,
24
+ height: () => height,
25
+ delete() {
26
+ if (closed) return;
27
+ closed = true;
28
+ decoded?.delete(); decoded = null;
29
+ data.pixels = pixels = null;
30
+ rawImages.delete(image);
31
+ },
32
+ };
33
+ rawImages.set(image, data);
34
+ return image;
35
+ }
36
+
37
+ // Check actual bytes once; raw protocol support does not imply opacity.
38
+ export function rawImageOpaque(data) {
39
+ if (data.opaque === undefined) {
40
+ const pixels = data.pixels;
41
+ data.opaque = true;
42
+ for (let i = 3; i < pixels.length; i += 4) {
43
+ if (pixels[i] !== 255) { data.opaque = false; break; }
44
+ }
45
+ }
46
+ return data.opaque;
47
+ }
@@ -0,0 +1,42 @@
1
+ import { realpathSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+
5
+ export function browserTarget(target, cwd = process.cwd()) {
6
+ if (/^about:/i.test(target)) return target;
7
+ if (/^https?:\/\//i.test(target)) {
8
+ // Reject malformed URLs before starting a new terminal.
9
+ try { return new URL(target).href; }
10
+ catch { throw openError("Invalid HTTP/HTTPS URL", 1); }
11
+ }
12
+ const fileUrl = /^file:/i.test(target);
13
+ if (!fileUrl && /^[a-z][a-z0-9+.-]*:/i.test(target)) {
14
+ throw openError("Unsupported URL scheme; use http://, https://, about: or a local file", 4);
15
+ }
16
+ const path = realpathSync(fileUrl ? fileURLToPath(target) : resolve(cwd, target));
17
+ if (!statSync(path).isFile()) throw openError("Expected a regular file", 4);
18
+ // Encode spaces, #, %, and non-ASCII characters as URL path components.
19
+ return pathToFileURL(path).href;
20
+ }
21
+
22
+ export function parseOpenArgs(args) {
23
+ if (args.length === 1 && ["-h", "--help", "--manual"].includes(args[0])) return { help: true };
24
+ if (args.length === 1 && args[0] === "--version") return { version: true };
25
+ if (args.length !== 1 || !args[0]) throw openError("Usage: xdg-open URL | FILE (see --help)", 1);
26
+ if (args[0].startsWith("-")) throw openError("Use ./ before a filename beginning with '-'", 1);
27
+ return { target: args[0] };
28
+ }
29
+
30
+ export function openError(message, exitCode) {
31
+ return Object.assign(new Error(message), { exitCode });
32
+ }
33
+
34
+ export function openExitCode(error) {
35
+ return error.exitCode ?? (["ENOENT", "ENOTDIR"].includes(error.code) ? 2 : 4);
36
+ }
37
+
38
+ export function releaseVersion(text) {
39
+ const value = /^VERSION_ID=(.*)$/m.exec(text)?.[1]?.trim();
40
+ if (!value) throw new Error("VERSION_ID missing from os-release");
41
+ return value.replace(/^["']|["']$/g, "");
42
+ }
@@ -8,7 +8,7 @@ box drawing and kitty graphics protocol images — no X11, Wayland or GPU.
8
8
  ```sh
9
9
  bunterm 2 # start a detached bunterm on /dev/tty2
10
10
  bunterm /dev/tty2 # the full virtual-console spelling also works
11
- bunterm # take over the console you are on (on tty1 that hides the bun-repl)
11
+ bunterm # open a new VT after the last session; first is tty2
12
12
  bunterm -s 20 # short for --font-size 20
13
13
  bunterm -e bun /buninu/apps/jsmdcui/src/index.js --demo
14
14
  bunterm --no-mouse # keyboard only
@@ -16,14 +16,19 @@ bunterm --no-clipboard # ignore OSC 52 copy and paste
16
16
  bunterm -h
17
17
  ```
18
18
 
19
+ Use `chvt 1` inside bunterm to return to tty1 without stopping it, and
20
+ `chvt 2` from a shell to return to a bunterm started on tty2. See `chvt --help`
21
+ for use from the init REPL.
22
+
19
23
  The program's `TERM` is `xterm-256color`, `COLORTERM=truecolor`,
20
24
  `TERM_PROGRAM=bunterm` and `JSMDCUI_KITTY_MODE=extended` (so `jsmdcui` shows
21
25
  Markdown images without `--kitty`). When it exits, the console returns to
22
26
  text mode and `bunterm` exits with its status.
23
27
 
24
28
  Every live session has a canonical `/bin/bunterm /dev/ttyN ...` command line.
25
- When no console is named, the currently active VT is used in the foreground.
26
- Naming a different VT starts bunterm as a detached session and returns
29
+ When no console is named, a new VT after the last known session is selected,
30
+ starting at tty2 and keeping tty1 for hardware setup.
31
+ Using a VT other than the active one starts bunterm as a detached session and returns
27
32
  immediately to the calling shell. Only one bunterm may control a given VT;
28
33
  ownership is
29
34
  identified from the process task name and its open console descriptor under
@@ -33,14 +38,14 @@ identified from the process task name and its open console descriptor under
33
38
 
34
39
  | Option | Meaning |
35
40
  | --- | --- |
36
- | `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. |
41
+ | `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 selects the next VT after the last known session (tty2 initially). |
37
42
  | `-e command [argument...]` | The program to run; everything after `-e` belongs to it. Default `/bin/sh`. |
38
43
  | `-s N`, `--font-size N` | Font size in pixels (default 13). 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. |
39
44
  | `--line-height F` | Multiplies the cell height (default 1). |
40
45
  | `--fb /dev/fbN` | The framebuffer device (default `/dev/fb0`). |
41
46
  | `--no-blink` | A steady cursor. |
42
47
  | `--no-mouse` | Do not attach a pointing device; nothing then reads `/dev/input`. |
43
- | `--no-clipboard` | Ignore OSC 52: programs can neither copy to nor read from the clipboard, and `xclip` is never run. |
48
+ | `--no-clipboard` | Disable both OSC 52 and menu copy/paste; `xclip` is never run. |
44
49
 
45
50
  ## What is drawn
46
51
 
@@ -66,7 +71,7 @@ identified from the process task name and its open console descriptor under
66
71
 
67
72
  ## Keyboard
68
73
 
69
- The console stays in the kernel's translated keyboard mode, so
74
+ Without Kitty keyboard negotiation, the console stays in the kernel's translated keyboard mode, so
70
75
  `Ctrl-Alt-F2` and friends still switch consoles; coming back repaints the
71
76
  screen. While its VT is inactive, bunterm continues consuming PTY output and
72
77
  updating its scrollback and kitty images, but neither writes the framebuffer
@@ -76,6 +81,28 @@ forth through the normal Linux VT switch. A few console-specific
76
81
  sequences are rewritten to xterm's (`F1`–`F5`, `Home`, `End`), and arrow keys
77
82
  follow the program's application cursor mode.
78
83
 
84
+ ### Kitty keyboard
85
+
86
+ The Kitty keyboard protocol uses the same encoder as jsgotty 1.2.0:
87
+ flags **1** (disambiguation), **2** (press/repeat/release), and **8** (all keys).
88
+ Applications enable it with `CSI > flags u`, query with `CSI ? u`, and restore
89
+ with `CSI < u`; normal and alternate buffers have separate stacks. Flags 4
90
+ (alternate keys) and 16 (associated text) are not advertised.
91
+
92
+ Only while an application enables Kitty, the active console uses Linux
93
+ `K_MEDIUMRAW` to distinguish Ctrl, Alt, Shift, Super, keypad and key releases.
94
+ Bunterm restores the original mode on pop/reset or exit. Ctrl+Alt+F1–F12 and
95
+ Alt+Left/Right remain reserved for VT switching. The kernel keymap supplies
96
+ printable characters, including Shift/Caps Lock/AltGr. Kernel dead-key/Compose
97
+ sequences are not implemented in this Kitty path; the ordinary translated
98
+ console path retains its existing behavior. No global evdev keyboard grab is
99
+ needed. A mode transition discards pending console typeahead so raw keycodes
100
+ cannot leak into the child as text.
101
+
102
+ 中文:Kitty 模式與 jsgotty 支援相同的 flags 1、2、8,可區分 Super、
103
+ Ctrl/Alt/Shift 組合及按下/重複/放開。程式退出模式後恢復原有 console
104
+ 輸入;Ctrl+Alt+Fn、Alt+左右保留為切換 VT。Kitty 模式尚不處理 dead key/Compose。
105
+
79
106
  ## Mouse
80
107
 
81
108
  Every pointing device under `/dev/input/event*` is opened and an arrow is
@@ -94,6 +121,13 @@ arrow keys on the alternate screen, which has no scrollback and is what a
94
121
  pager expects. Typing returns to the live screen, and the text cursor is
95
122
  hidden while it is scrolled out of view.
96
123
 
124
+ When mouse reporting is off, left-drag selects terminal text. Wide characters
125
+ and graphemes are selected whole; wrapped lines copy without extra newlines.
126
+ Use **Copy** in the top-left menu to send the selection to xclip. Selection is
127
+ cleared when new PTY text arrives or the font size changes, so it cannot refer
128
+ to replaced cells. Applications requesting mouse reporting receive the drag
129
+ instead; selection never overrides their mouse mode.
130
+
97
131
  `--no-mouse` turns all of this off: the terminal then never looks at
98
132
  `/dev/input` and never loads the mouse module.
99
133
 
@@ -114,7 +148,12 @@ bunterm keeps no clipboard of its own: `ESC ] 52 ; c ; <base64>` is passed to
114
148
  with the output of `xclip -selection clipboard -o`, so every bunterm session
115
149
  shares the same clipboard as `xclip`. A query xclip cannot answer is replied
116
150
  to with empty contents. `--no-clipboard` turns this off, so a program can
117
- neither set nor read the clipboard through the terminal.
151
+ neither set nor read the clipboard through the terminal; menu copy/paste is
152
+ also disabled. The menu's **Paste** reads xclip and sends one paste to the PTY,
153
+ wrapped in `CSI 200~` / `CSI 201~` when the application enabled bracketed paste
154
+ (`DECSET 2004`). Programs that have not requested it receive plain text.
155
+ Newlines are normalized to CR; embedded ESC bytes are stripped in bracketed
156
+ mode so clipboard text cannot close the paste early.
118
157
 
119
158
  ## Limits
120
159
 
@@ -123,10 +162,8 @@ neither set nor read the clipboard through the terminal.
123
162
  `--linux-lts` or `--real` (efifb/simpledrm from UEFI).
124
163
  - The aarch64 image has no framebuffer yet: `run-qemu.sh` gives QEMU's
125
164
  `virt` machine no display device, so there is no `/dev/fb0` to draw on.
126
- - No selection, and no keyboard way to scroll back: the wheel is it. The
127
- buffer keeps 1000 lines. A program that does not enable mouse reporting
128
- sees no clicks — the arrow still moves, so the pointer is visibly alive.
129
- `--no-mouse` removes the pointer entirely.
165
+ - Scrollback uses the wheel; the buffer keeps 1000 lines.
166
+ `--no-mouse` removes the pointer and local selection.
130
167
  - A program that prints a terminal reply while the tty echoes (that is, one
131
168
  not in raw mode) will see the reply echoed, as on any terminal.
132
169
  - Kitty images are drawn above text; `z` ordering below text and animation
@@ -143,3 +180,40 @@ neither set nor read the clipboard through the terminal.
143
180
  - `/lib/fbdev.js` — the framebuffer itself (`fbdev.md`)
144
181
  - `/buninu/apps/jsgotty/gotty.js` — the PTY backend and kitty packet parser
145
182
  that are reused
183
+
184
+ ## Local menu and commands
185
+
186
+ Left-click the top-left **2×2 physical pixels** to open a text menu of live
187
+ bunterm VTs and the initial tty (normally tty1). Click an entry to switch, or use
188
+ Up/Down and Enter. The active VT is marked `(current)`.
189
+ The wheel scrolls the selection; Escape, right-click, or a click
190
+ outside the entries closes the menu. Menu input is consumed locally, including
191
+ when a terminal application has enabled mouse reporting. Requires mouse support.
192
+ The menu switches existing sessions; it does not create a new bunterm.
193
+
194
+ - **Font − / +** changes the current session's font size and resizes the PTY.
195
+ - **Copy / Paste** uses the same xclip clipboard as OSC 52.
196
+ - Click the light **command input field**, or press Tab, to type a local command.
197
+ Enter executes it, Tab returns to menu navigation, Esc closes the menu.
198
+ Backspace/Delete, Left/Right, Home/End and Ctrl+U edit the field.
199
+
200
+ Commands: `font 16`, `font +`, `font -`, `copy`, `paste`, `vt 2`, `help`.
201
+ Font commands accept integer sizes 6–96; changes last for this session.
202
+ The command registry is `/lib/bunterm/commands.js`; new commands can be added
203
+ there without changing the input widget. These are bunterm commands, not shell
204
+ commands. Clipboard failures and command errors appear in the menu footer.
205
+
206
+ 中文:左鍵點擊螢幕最左上角 2×2 像素,會列出正在運行的 bunterm 和初始 tty。
207
+ 目前顯示的 VT 會標示 `(current)`。
208
+ 可以點選或用上下鍵、Enter 切換;Esc、右鍵或點擊選項外關閉。不會把選單操作
209
+ 送進底下的 shell/應用程式,也不會關閉原本的 bunterm。
210
+ 選單還有字體大小減/加、複製/貼上,以及自由輸入的命令框(點選或按 Tab)。
211
+ 可輸入 `font 16`、`copy`、`paste`、`vt 2`、`help`;Enter 執行。
212
+ 複製前用左鍵拖曳選字,僅在程式未開啟滑鼠回報時生效。貼上沿用 xclip,
213
+ 程式啟用 bracketed paste 時會加上貼上邊界。
214
+
215
+ On GPU-provided fbdev devices, a shared framebuffer mapping may not support
216
+ `msync`. bunterm probes it once: `EINVAL`/`EOPNOTSUPP` disables further msync
217
+ calls for that mapping while pixel writes continue normally. Drivers that
218
+ support msync retain it; other errors still propagate. This is not a vsync or
219
+ GPU-acceleration mechanism.
@@ -0,0 +1,34 @@
1
+ # chvt — switch Linux virtual terminals
2
+
3
+ ```sh
4
+ chvt 1 # switch to /dev/tty1 (normally the init REPL)
5
+ chvt 2 # switch to /dev/tty2
6
+ chvt -h # also --help
7
+ ```
8
+
9
+ Accepts one VT number, 1–63. Requires access to a real Linux virtual console
10
+ and permission to switch it (normally root). A PTY alone, including a remote
11
+ terminal or Android terminal, is not a Linux VT.
12
+
13
+ With bunterm, for example:
14
+
15
+ ```sh
16
+ bunterm 2 # start bunterm on tty2
17
+ chvt 1 # from its shell, return to tty1 without stopping bunterm
18
+ ```
19
+
20
+ Run `chvt 2` from a shell to return, or use Alt+F2 / Ctrl+Alt+F2 as appropriate.
21
+ At the init JavaScript REPL, run a command with
22
+ `Bun.spawnSync(["/bin/chvt", "2"], {stdio: ["inherit", "inherit", "inherit"]})`.
23
+ The command switches the existing VT; it does not start a shell or bunterm on an
24
+ empty VT. bunterm retains ownership of its VT, pauses drawing when switched away,
25
+ and redraws when switched back. chvt does not change keyboard or graphics modes.
26
+
27
+ Like the traditional command, chvt waits for the target VT to become active,
28
+ then exits. If its current owner refuses to release the VT, the wait can persist. No daemon is created.
29
+
30
+ 中文:`chvt 1` 切至 tty1,`chvt 2` 切回 tty2 的 bunterm;不會結束 bunterm,
31
+ 也不會替空白 VT 啟動 shell。切出/切回使用 bunterm 原有的釋放與重繪機制。
32
+
33
+ Uses Linux VT_ACTIVATE and VT_WAITACTIVE:
34
+ https://man7.org/linux/man-pages/man2/ioctl_vt.2.html
@@ -0,0 +1,43 @@
1
+ # xdg-open — open in a new browser terminal / 開啟瀏覽器
2
+
3
+ ```sh
4
+ xdg-open https://buninu.org
5
+ xdg-open about:audio
6
+ xdg-open ./page.html
7
+ xdg-open "./file with spaces.md"
8
+ xdg-open file:///buninu/page.html
9
+ ```
10
+
11
+ Runs `bunterm -e buninu-browser URL` on a new virtual console. HTTP, HTTPS,
12
+ and `about:` URLs go to the browser. Local files must exist: symbolic links
13
+ are resolved and the real path is encoded as a `file://` URL, including spaces,
14
+ Chinese characters, `#`, and `%`. Markdown also opens in the browser.
15
+
16
+ 透過 bunterm 在新 tty 啟動瀏覽器。本地檔案先確認存在、解析實際路徑,再轉成
17
+ `file://` 網址;Markdown 同樣使用瀏覽器。檔名若以 `-` 開頭,請加上 `./`。
18
+
19
+ Directories and other schemes (including `data:`, `mailto:` and `ftp:`) are
20
+ not supported. This is Buninu's lightweight implementation, not xdg-utils;
21
+ it does not consult MIME associations or desktop application registrations.
22
+ 目前不支援目錄與其他協定,也不查詢 MIME 預設程式關聯。
23
+
24
+ ## Options / 選項
25
+
26
+ | Option | Meaning |
27
+ | --- | --- |
28
+ | `-h`, `--help`, `--manual` | Show this Markdown manual / 顯示本手冊 |
29
+ | `--version` | Show the Buninu Linux version from the image's os-release / 顯示映像檔的發行版版本 |
30
+
31
+ ## Exit codes / 退出碼
32
+
33
+ | Code | Meaning |
34
+ | --- | --- |
35
+ | 0 | New terminal launch succeeded / 新終端啟動成功 |
36
+ | 1 | Invalid arguments or URL / 參數或網址格式錯誤 |
37
+ | 2 | Local file does not exist / 本地檔案不存在 |
38
+ | 3 | Required tool not found / 缺少必要工具 |
39
+ | 4 | Opening failed or target unsupported / 開啟失敗或目標不支援 |
40
+
41
+ bunterm detaches when opening a new VT. Exit code 0 confirms the launcher
42
+ succeeded, not that the browser subsequently loaded the page.
43
+ 新 tty 以背景工作階段啟動,因此成功退出不代表後續網頁一定載入成功。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-linux",
3
- "version": "0.14.57",
3
+ "version": "0.15.58",
4
4
  "description": "A Linux distribution running Bun as PID 1 with the Buninu userspace.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/scripts/arch.js CHANGED
@@ -16,8 +16,8 @@ export const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
16
16
 
17
17
  const pins = {
18
18
  x86_64: {
19
- linuxVirtSha256: "414aa2572ef88164b1af9905fa6dfbe863bf50f6130d895d57282dea3e15f7b9",
20
- linuxLtsSha256: "7614556471859956a436b95813daf409b444aa849ec899178b404e828c5a9d98",
19
+ linuxVirtSha256: "1eec55004cf7167c914c8839f46691d2bc82767e20466bff51eb45d06f8e90cc",
20
+ linuxLtsSha256: "af399fba75cafb2940fc2853eb73ba2289aa463228f33541cd3010334a30992b",
21
21
  muslSha256: "573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71",
22
22
  stubSha256: "8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9",
23
23
  libstdcppSha256: "14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6",
@@ -32,8 +32,8 @@ const pins = {
32
32
  serialMajor: 4,
33
33
  },
34
34
  aarch64: {
35
- linuxVirtSha256: "b3820fee22e0366a15f7c139e5bc00348e333f3e7f77802271df7ded795aee4f",
36
- linuxLtsSha256: "67b8f61c5cfb6ac9b5a65cb1610b67d467e6c4ffc17e7d29dbea4bd8c447ff32",
35
+ linuxVirtSha256: "b46c51ddf572d85b2ba7b9adfaa64f4c38fc490c93d97a20ffd2418d51284085",
36
+ linuxLtsSha256: "65ab581dd6773e2b1b6831dac50142518b2f2083668f164f68ebfcd00d1763c4",
37
37
  muslSha256: "5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e",
38
38
  stubSha256: "a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38",
39
39
  libstdcppSha256: "2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7",
@@ -58,6 +58,7 @@ export const config = ({
58
58
  arch = process.env.BUNINU_ARCH || "x86_64",
59
59
  flavor = process.env.LINUX_FLAVOR || "virt",
60
60
  real = process.env.REAL_MACHINE === "1",
61
+ noGpu = process.env.BUNINU_NO_GPU === "1",
61
62
  mirror = process.env.ALPINE_MIRROR || "https://dl-cdn.alpinelinux.org/alpine",
62
63
  } = {}) => {
63
64
  arch = normalizeArch(arch);
@@ -67,7 +68,7 @@ export const config = ({
67
68
  // The Alpine repository keeps only the newest build of a package, so a pin
68
69
  // stops downloading when Alpine bumps it; the hashes then fail loudly.
69
70
  const alpineBase = `${mirror}/v3.24/main/${arch}`;
70
- const kernelVersion = "6.18.57";
71
+ const kernelVersion = "6.18.58";
71
72
  const kernelRelease = `${kernelVersion}-0-${flavor}`;
72
73
  const bunVersion = "1.4.2";
73
74
  const downloadsDir = join(rootDir, "downloads", arch);
@@ -77,6 +78,7 @@ export const config = ({
77
78
  arch,
78
79
  flavor,
79
80
  real,
81
+ noGpu,
80
82
  rootDir,
81
83
  alpineBase,
82
84
  kernelVersion,
package/scripts/arch.sh CHANGED
@@ -44,7 +44,7 @@ fi
44
44
  alpine_mirror=${ALPINE_MIRROR:-https://dl-cdn.alpinelinux.org/alpine}
45
45
  alpine_base="$alpine_mirror/v3.24/main/$arch"
46
46
 
47
- kernel_version=6.18.57
47
+ kernel_version=6.18.58
48
48
  kernel_release="$kernel_version-0-$linux_flavor"
49
49
  linux_package="linux-$linux_flavor-$kernel_version-r0.apk"
50
50
  musl_package=musl-1.2.6-r2.apk
@@ -63,8 +63,8 @@ musl_libc="libc.musl-$arch.so.1"
63
63
 
64
64
  case $arch in
65
65
  x86_64)
66
- linux_virt_sha256=414aa2572ef88164b1af9905fa6dfbe863bf50f6130d895d57282dea3e15f7b9
67
- linux_lts_sha256=7614556471859956a436b95813daf409b444aa849ec899178b404e828c5a9d98
66
+ linux_virt_sha256=1eec55004cf7167c914c8839f46691d2bc82767e20466bff51eb45d06f8e90cc
67
+ linux_lts_sha256=af399fba75cafb2940fc2853eb73ba2289aa463228f33541cd3010334a30992b
68
68
  musl_sha256=573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71
69
69
  stub_sha256=8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9
70
70
  libstdcpp_sha256=14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6
@@ -82,8 +82,8 @@ case $arch in
82
82
  serial_major=4
83
83
  ;;
84
84
  aarch64)
85
- linux_virt_sha256=b3820fee22e0366a15f7c139e5bc00348e333f3e7f77802271df7ded795aee4f
86
- linux_lts_sha256=67b8f61c5cfb6ac9b5a65cb1610b67d467e6c4ffc17e7d29dbea4bd8c447ff32
85
+ linux_virt_sha256=b46c51ddf572d85b2ba7b9adfaa64f4c38fc490c93d97a20ffd2418d51284085
86
+ linux_lts_sha256=65ab581dd6773e2b1b6831dac50142518b2f2083668f164f68ebfcd00d1763c4
87
87
  musl_sha256=5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e
88
88
  stub_sha256=a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38
89
89
  libstdcpp_sha256=2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7
package/scripts/build.js CHANGED
@@ -28,6 +28,7 @@ import { format, openVolume } from "../initramfs/lib/fat.js";
28
28
  import { TYPES, randomGuid, writeGpt } from "../initramfs/lib/gpt.js";
29
29
  import { config, jsRuntime, rootDir, runIfMain } from "./arch.js";
30
30
  import { fetchAlpine } from "./fetch.js";
31
+ import { gpuReady } from "./fetch-gpu.js";
31
32
  import { pack } from "./pack.js";
32
33
 
33
34
  const nonEmpty = (path) => existsSync(path) && statSync(path).size > 0;
@@ -64,6 +65,7 @@ export const uki = async (cfg = config()) => {
64
65
  const missing = !nonEmpty(cfg.kernelImage)
65
66
  || !nonEmpty(join(cfg.kernelDir, cfg.efiStub))
66
67
  || !nonEmpty(join(cfg.modulesRoot, "modules.dep"))
68
+ || !gpuReady(cfg)
67
69
  || (cfg.real && (
68
70
  !nonEmpty(join(cfg.modulesRoot, "kernel/drivers/hid/usbhid/usbhid.ko"))
69
71
  || !nonEmpty(join(cfg.modulesRoot, "kernel/drivers/acpi/battery.ko"))
@@ -94,11 +96,22 @@ export const uki = async (cfg = config()) => {
94
96
  console.log(`Built ${cfg.arch} UKI with Alpine linux-${cfg.flavor} at vda/EFI/BOOT/${cfg.efiBootName}`);
95
97
  };
96
98
 
97
- /** build-image.sh: vda.img, 128 MiB, GPT with the ESP from sector 2048 to the last usable one. */
99
+ // Keep small images at 128 MiB. Reserve 1/16 payload size plus 4 MiB for
100
+ // FAT metadata, directories, GPT/alignment and free space, rounded to 64 MiB.
101
+ // Keep this calculation aligned with build-image.sh.
102
+ export const imageBytes = (payloadBytes) => {
103
+ if (!Number.isSafeInteger(payloadBytes) || payloadBytes < 0 || payloadBytes > 0xffffffff)
104
+ throw new Error("EFI payload exceeds the FAT32 single-file limit or has an invalid size");
105
+ const unit = 64 * 1024 * 1024;
106
+ return Math.max(128 * 1024 * 1024,
107
+ Math.ceil((payloadBytes + Math.ceil(payloadBytes / 16) + 4 * 1024 * 1024) / unit) * unit);
108
+ };
109
+
110
+ /** build-image.sh: dynamically sized GPT disk with one FAT32 ESP. */
98
111
  export const image = (cfg = config()) => {
99
112
  const efi = join(rootDir, "vda/EFI/BOOT", cfg.efiBootName);
100
113
  if (!nonEmpty(efi)) throw new Error(`no ${cfg.arch} EFI payload ${efi} yet; build the UKI first`);
101
- const diskSectors = (128 * 1024 * 1024) / 512;
114
+ const diskSectors = imageBytes(statSync(efi).size) / 512;
102
115
  const espStart = 2048;
103
116
  const espEnd = diskSectors - 34;
104
117
  const path = join(rootDir, "vda.img");
@@ -134,7 +147,7 @@ export const image = (cfg = config()) => {
134
147
  } finally {
135
148
  volume.close();
136
149
  }
137
- console.log(`Packed vda/EFI/BOOT/${cfg.efiBootName} into vda.img`);
150
+ console.log(`Packed vda/EFI/BOOT/${cfg.efiBootName} into vda.img (${diskSectors * 512 / 1024 / 1024} MiB)`);
138
151
  };
139
152
 
140
153
  export const buildAll = async (cfg = config()) => {