buninu-linux 0.6.0 → 0.7.1

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,16 +33,20 @@
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
+ * `cfg.sound` loads every packaged ALSA sound module
39
40
  * Android phone USB tethering over RNDIS has been tested successfully, allowing Buninu Linux to access the Internet through an Android phone
40
41
  * 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.
42
+ * The bundled `jmi` editor, jsmdcui App runtime, and local JavaScript execution also work.
43
+ * Multi-tasking with kernel VT or jsmdcui
44
+ + See [Multitasking modes](#multitasking-modes)
42
45
  * The graphical terminal `bunterm` draws on the framebuffer with CJK, colour emoji and kitty images; see [Showing images](#showing-images)
43
46
  + Also has mouse click, wheel, and cursor drawing
44
47
  + Simplified browser works: @sanohiro/casty in chroot debian
45
48
  + (Use at your own risk: We haven't fully examined its code)
49
+ + Playing YouTube with sound(experimental)
46
50
  + See [Commands inside `/bin`](#commands-inside-bin) for browser usage instructions
47
51
 
48
52
  ---
@@ -140,7 +144,7 @@ display and USB keyboard with a welcome ending like this:
140
144
  Welcome to Buninu Linux!
141
145
  Bun 1.4.2 is now PID 1
142
146
  Type start() to run buninu --local
143
- Configuration getters: cfg.all, cfg.disk, cfg.net, cfg.power
147
+ Configuration getters: cfg.all, cfg.disk, cfg.net, cfg.power, cfg.sound
144
148
 
145
149
  bun-repl>
146
150
  ```
@@ -155,6 +159,7 @@ Load only the subsystem needed, then enter the Buninu shell:
155
159
  cfg.net // wired NIC and Android USB-tethering drivers
156
160
  cfg.disk // SATA/NVMe/USB storage drivers and detected block devices
157
161
  cfg.power // battery, AC, button and thermal drivers
162
+ cfg.sound // all packaged ALSA sound modules and detected sound devices
158
163
  start()
159
164
  ```
160
165
 
@@ -206,6 +211,8 @@ bun ./index.js -fbr
206
211
  The first boot line from `run-qemu.sh` names the guest, machine, accelerator
207
212
  and firmware it picked. A bare `-r` boots whichever architecture `vda.img`
208
213
  was last built for; `-br --arch arm64` is the aarch64 edit-and-boot loop.
214
+ To run `bunterm` in a QEMU window on the aarch64 guest, see
215
+ [What differs on aarch64](#what-differs-on-aarch64).
209
216
 
210
217
  #### amd64 Linux
211
218
 
@@ -229,9 +236,32 @@ qemu-efi-aarch64` to run.
229
236
  Alpine's aarch64 packages and `bun-linux-aarch64-musl`.
230
237
  * The serial console is `ttyAMA0` (QEMU `virt`'s PL011) instead of `ttyS0`.
231
238
  * `--real` is x86_64 only: its module set is PC hardware.
232
- * The current aarch64 QEMU configuration supplies no framebuffer or PS/2
233
- devices, so `bunterm` has nothing to draw on yet; the serial REPL and the
234
- Buninu shell work as on x86_64.
239
+ * QEMU's `virt` machine has no display or PS/2 devices, so by default
240
+ `bunterm` has nothing to draw on; the serial REPL and the Buninu shell work
241
+ as on x86_64. To run `bunterm`, build with `--linux-lts` and give QEMU a
242
+ `ramfb` display and virtio input devices. On a Mac, `-display cocoa` opens
243
+ the window:
244
+
245
+ ```sh
246
+ bun ./index.js -fbr --arch arm64 --linux-lts -- \
247
+ -device ramfb -device virtio-keyboard-pci -device virtio-mouse-pci -display cocoa
248
+ ```
249
+
250
+ Once that image is built, boot it again without rebuilding (and without
251
+ Docker). `--linux-lts` belongs to the build, so leave it off `-r`:
252
+
253
+ ```sh
254
+ bun ./index.js -r --arch arm64 -- \
255
+ -device ramfb -device virtio-keyboard-pci -device virtio-mouse-pci -display cocoa
256
+ ```
257
+
258
+ `start()` in the serial REPL, then `bunterm 2` opens bunterm on VT 2 in the
259
+ QEMU window and returns to the serial shell; type into the window. The
260
+ serial console is not a VT, so a bare `bunterm` there takes the active VT 1
261
+ and holds the serial shell until it exits; naming another VT starts it
262
+ detached (see [Multiple virtual consoles with bunterm](#multiple-virtual-consoles-with-bunterm)). The
263
+ firmware sets up ramfb at 800×600, and `init.js` loads `simpledrm` so it
264
+ becomes `/dev/fb0` (see [graphics.md](graphics.md#trying-it)).
235
265
  * `--export` writes `buninu-linux-<version>-aarch64.img`.
236
266
  * Before the kernel starts, Homebrew's aarch64 EDK2 prints a few
237
267
  `Error: Image at … start failed` lines (its own drivers for hardware `virt`
@@ -281,6 +311,7 @@ Load only the hardware subsystem you need:
281
311
  cfg.net
282
312
  cfg.disk
283
313
  cfg.power
314
+ cfg.sound
284
315
  ```
285
316
 
286
317
  `cfg.net` loads the packaged wired-network and Android USB-tethering drivers and prints detected
@@ -296,7 +327,10 @@ ip route add default via 192.168.1.1
296
327
  The real image initializes `/etc/resolv.conf` with `1.1.1.1` and `8.8.8.8`;
297
328
  replace them if your network requires different DNS servers. `cfg.disk` loads
298
329
  common SATA/PATA/SCSI, NVMe/VMD and USB-storage drivers so detected disks and
299
- partitions appear in `/dev`. To inspect a filesystem without mounting it:
330
+ partitions appear in `/dev`. `cfg.sound` unconditionally loads every sound
331
+ module packaged in the image, then lists the resulting `/dev/snd` devices; it
332
+ does not probe the hardware before loading modules. To inspect a filesystem
333
+ without mounting it:
300
334
 
301
335
  ```sh
302
336
  mount -fv /dev/sda1 /mnt
@@ -341,7 +375,29 @@ If `Enter` is not recognized in a particular terminal, try `Ctrl-J` or
341
375
  - `jsmdcui --cdp-maze` runs a self-solving maze game
342
376
  * Run `bunterm` first to show its Emojis
343
377
 
344
- ### Panes, terminals, and tabs
378
+ ### Multitasking modes
379
+
380
+ #### Multiple virtual consoles with bunterm
381
+
382
+ Run a separate `bunterm` on each Linux virtual console. A numeric positional
383
+ argument is a short form of the corresponding device, so `bunterm 2` means
384
+ `bunterm /dev/tty2`:
385
+
386
+ ```sh
387
+ bunterm 2
388
+ bunterm 3 -s 13
389
+ ```
390
+
391
+ When the named VT is not the active one, `bunterm` starts there as a detached
392
+ session, switches the display to it, and immediately returns control to the
393
+ calling shell. Switch among VTs with `Ctrl+Alt+F1` through `Ctrl+Alt+F12`, or
394
+ move to the previous or next VT with `Alt+Left` and `Alt+Right`. Each VT keeps
395
+ its own terminal session; an inactive bunterm continues processing its PTY but
396
+ stops drawing until its VT becomes active again. Only one bunterm can control a
397
+ given VT. If one is already in use, the error identifies its PID and suggests
398
+ another VT number.
399
+
400
+ #### Panes, terminals, and tabs with jsmdcui
345
401
 
346
402
  `jmi` and jsmdcui can keep editors and terminal sessions open together. Press
347
403
  `Ctrl-E`, type `term`, and press `Enter` to open the default Buninu shell in a
@@ -350,7 +406,7 @@ terminal pane. `term COMMAND` starts a specific command instead. Use `vsplit`,
350
406
  editor pane or tab; each command optionally accepts a filename. From an editor
351
407
  pane, `Ctrl-T` is the direct shortcut for a new empty tab.
352
408
 
353
- #### Pane and tab controls
409
+ ##### Pane and tab controls
354
410
 
355
411
  | Input | Result |
356
412
  | --- | --- |
@@ -451,12 +507,38 @@ below a shown image, and images scroll with the text. When the program you
451
507
  started exits, the console returns to text mode. `bunterm --help` has the
452
508
  options; [graphics.md](graphics.md) describes how it is built. The image
453
509
  needs a kernel with a framebuffer: build with `--linux-lts` or `--real`
454
- (the default `linux-virt` builds one only as modules the image omits).
510
+ (the default `linux-virt` builds one only as modules the image omits). The
511
+ aarch64 guest also needs a QEMU display; see
512
+ [What differs on aarch64](#what-differs-on-aarch64).
513
+
514
+ ### Clipboard
515
+
516
+ `bunterm` supports copy and paste through OSC 52, so programs such as
517
+ `casty` and `jsmdcui` can use the clipboard. bunterm keeps nothing itself; it
518
+ passes every copy and paste to Buninu's `xclip -selection clipboard`. For
519
+ now that clipboard is a file, `$HOME/.xclip.clipboard`, read and written in
520
+ the ramdisk, so all bunterm sessions share it and it is gone after a reboot.
521
+ `bunterm --no-clipboard` turns OSC 52 off.
522
+
523
+ For Chinese input, run the pinyin input method
524
+ [jspinyin](https://www.npmjs.com/package/@drxiaozhi/jspinyin) in a bunterm
525
+ that is not inside a chroot, so it uses Buninu's `xclip`:
526
+
527
+ ```sh
528
+ bun x @drxiaozhi/jspinyin
529
+ ```
530
+
531
+ - jspinyin → casty: press `Ctrl-C` in jspinyin to copy its result box, then
532
+ `Ctrl-V` in casty pastes it through OSC 52.
533
+ - casty → jsmdcui / jspinyin: `Alt-C` in casty copies the selection through
534
+ OSC 52, and `Ctrl-V` pastes it in jsmdcui or jspinyin, since jsmdcui reads
535
+ from `xclip` first by default.
455
536
 
456
537
  ## Commands inside /bin
457
538
 
458
539
  | command | does | manual |
459
540
  | --- | --- | --- |
541
+ | `buninu-linux-help` | render this buninu-linux README from `/usr/share/doc/buninu-linux/README.md` | `buninu-linux-help` |
460
542
  | `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
543
  | `umount` | `umount2(2)` with `-l`, `-f`, `-R` | `umount --help` |
462
544
  | `ip` | iproute2 grammar over `SIOC*` ioctls and `/proc/net`: `link`, `addr`, `route`, `neigh` | `ip --help` |
@@ -469,7 +551,7 @@ needs a kernel with a framebuffer: build with `--linux-lts` or `--real`
469
551
  | `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
552
  | `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
553
 
472
- Besides `bun` (and `sh`/`node` pointing at it), `/bin` includes eleven commands
554
+ Besides `bun` (and `sh`/`node` pointing at it), `/bin` includes twelve commands
473
555
  implemented as Bun scripts.
474
556
 
475
557
  The small `tar` reads the archive's own headers for links and listing, so
@@ -514,11 +596,13 @@ bun udocker.js create --name db debian:13
514
596
  cd ~/.udocker/containers/db/ROOT
515
597
  chroot . /bin/bash
516
598
  apt update
517
- apt install curl unzip chromium fonts-noto-cjk fonts-noto-color-emoji npm
599
+ apt install --no-install-recommends ca-certificates curl unzip chromium-headless-shell fonts-noto-cjk fonts-noto-color-emoji git
518
600
  curl -fsSL https://bun.sh/install | bash
519
601
  bun x bunmsh
520
602
  # We haven't fully reviewed
521
603
  # @sanohiro/casty's safety
604
+ # below is a fork of mine
605
+ # fixing some issues
522
606
  # !!!Use at your own risk!!!
523
607
  # !!!Use at your own risk!!!
524
608
  # !!!Use at your own risk!!!
@@ -529,10 +613,15 @@ bun x bunmsh
529
613
  # !!!Use at your own risk!!!
530
614
  # !!!Use at your own risk!!!
531
615
  # !!!Use at your own risk!!!
532
- bun i -g @sanohiro/casty
616
+ cd
617
+ git clone https://github.com/jjtseng93/casty
618
+ cd casty/bin
533
619
  # 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
535
-
620
+ bun casty.js buninu.org
621
+ # Playing YouTube with sound(experimental)
622
+ # make sure you have run `cfg.sound` in bun-repl
623
+ # follow the dumped instructions
624
+ bun x bunproot --setup.pulse
536
625
 
537
626
 
538
627
  # Download and enter an x64 Alpine minirootfs
@@ -581,7 +670,7 @@ tar czvf /tmp/buninu.tar.gz README.md package.json
581
670
 
582
671
  # On a virtual console: a graphical terminal with CJK, emoji and images
583
672
  bunterm
584
- bunterm /dev/tty1 --font-size 20 -e bun /buninu/apps/jsmdcui/src/index.js --demo
673
+ bunterm /dev/tty1 --font-size 13 -e bun /buninu/apps/jsmdcui/src/index.js --demo
585
674
  ```
586
675
 
587
676
  The graphics stack behind `bunterm` — the framebuffer module, CanvasKit,
@@ -724,7 +813,7 @@ its own.
724
813
  ```text
725
814
  index.js entry point: -f / -b / -r / --arch / --docker / --export / --linux-lts / --real
726
815
  Dockerfile the Debian 13 build toolchain for --docker (and macOS)
727
- fetch-alpine.sh [fetch] kernel, EFI stub, musl, network/input modules
816
+ fetch-alpine.sh [fetch] kernel, EFI stub, musl, network/input/display modules
728
817
  fetch-bun.sh [fetch] Bun, libstdc++, libgcc
729
818
  build-uki.sh [build] UKI; calls scripts/pack-initramfs.sh
730
819
  build-image.sh [build] GPT disk image; shared with the hello-world EFI
@@ -1060,7 +1149,7 @@ until `cfg.net` loads the packaged drivers and the user assigns an address and
1060
1149
  route.
1061
1150
 
1062
1151
  On a `--real` image the REPL also offers the getter-based `cfg` namespace:
1063
- `cfg.net`, `cfg.disk`, `cfg.power` and `cfg.all` run as soon as the property is read, without
1152
+ `cfg.net`, `cfg.disk`, `cfg.power`, `cfg.sound` and `cfg.all` run as soon as the property is read, without
1064
1153
  parentheses. `cfg.net` loads every packaged network module (including PHY and bus support),
1065
1154
  then reads the `modalias` of every PCI and USB network device, matches it
1066
1155
  against the `pci:`/`usb:` lines
@@ -1072,7 +1161,10 @@ declared dependencies. `--real` ships Intel `e1000`/`e1000e`/`igb`/`igc`,
1072
1161
  Realtek `r8169`, Atheros `alx`, Broadcom `tg3`, common USB dongles, and
1073
1162
  `rndis_host`/`cdc_ether`/`cdc_ncm`/`cdc_eem`/`cdc_subset`/`zaurus` for Android
1074
1163
  USB tethering. `cfg.net` unconditionally attempts all of these packaged
1075
- network modules. `cfg.power` loads the ACPI
1164
+ network modules. `cfg.sound` likewise unconditionally attempts every packaged
1165
+ module below `kernel/sound/`, then lists `/dev/snd`; it does no hardware probe.
1166
+ The packaged sound set covers QEMU ES1371/HDA and common PC HDA/HDMI and USB
1167
+ audio where the selected kernel flavor provides them. `cfg.power` loads the ACPI
1076
1168
  `battery`, `ac`, `button` and `thermal` modules through `/lib/modprobe.js`
1077
1169
  and lists `/sys/class/power_supply` (a desktop without a battery simply
1078
1170
  reports nothing registered). Nothing loads them at boot. PID 1 also installs
@@ -1101,10 +1193,13 @@ for the userspace session.
1101
1193
  * [Basic configuration](#basic-configuration)
1102
1194
  * [Editor quick start](#editor-quick-start)
1103
1195
  + [jsmdcui](#jsmdcui)
1104
- * [Panes, terminals, and tabs](#panes-terminals-and-tabs)
1105
- + [Pane and tab controls](#pane-and-tab-controls)
1196
+ * [Multitasking modes](#multitasking-modes)
1197
+ + [Multiple virtual consoles with bunterm](#multiple-virtual-consoles-with-bunterm)
1198
+ + [Panes, terminals, and tabs with jsmdcui](#panes-terminals-and-tabs-with-jsmdcui)
1199
+ - [Pane and tab controls](#pane-and-tab-controls)
1106
1200
  * [Using Bun Modern Shell](#using-bun-modern-shell)
1107
1201
  * [Showing images](#showing-images)
1202
+ * [Clipboard](#clipboard)
1108
1203
  - [Commands inside /bin](#commands-inside-bin)
1109
1204
  - [Environment and dependencies](#environment-and-dependencies)
1110
1205
  * [Build environment](#build-environment)
package/fetch-alpine.sh CHANGED
@@ -81,6 +81,22 @@ if [ "$arch" = x86_64 ]; then
81
81
  else
82
82
  select_module virtio_input
83
83
  fi
84
+ # The framebuffer for bunterm. Both lts kernels set CONFIG_SYSFB_SIMPLEFB, so
85
+ # the UEFI GOP (QEMU's ramfb on aarch64 virt) becomes a simple-framebuffer
86
+ # device that only simpledrm drives: x86_64 lts builds it in, aarch64 lts has
87
+ # it as a module. init.js loads it at boot, which is what makes /dev/fb0.
88
+ if [ "$linux_flavor" = lts ]; then
89
+ select_optional_module simpledrm
90
+ fi
91
+
92
+ # Sound: a compact set covering QEMU's emulated AC97/HDA devices and the
93
+ # usual PC HDA/HDMI and USB-audio paths. Dependencies pull in the ALSA core,
94
+ # PCM, timer, codec and USB support needed by each available leaf driver.
95
+ # Some kernel flavors build fewer of these, so keep every leaf optional.
96
+ for module in snd-ens1371 snd-hda-intel snd-hda-codec-generic \
97
+ snd-hda-codec-hdmi snd-usb-audio; do
98
+ select_optional_module "$module"
99
+ done
84
100
 
85
101
  if [ "${REAL_MACHINE:-}" = 1 ]; then
86
102
  # USB keyboard for the console; common storage paths (SATA/AHCI HDD and
package/graphics.md CHANGED
@@ -174,13 +174,22 @@ needs about 1 GB.
174
174
 
175
175
  ## Trying it
176
176
 
177
- The framebuffer needs a kernel that provides one. Alpine's `linux-lts` has
178
- `CONFIG_FB`, `CONFIG_FB_EFI` and `CONFIG_DRM_SIMPLEDRM` built in; the default
179
- `linux-virt` builds them as modules, and the image ships only the network,
180
- storage, USB and HID modules, so there `/dev/fb0` never appears. Build with
181
- `./index.js -b --linux-lts` (or `--real`); under QEMU the lts kernel gets a
182
- 1280×800 `simpledrm` framebuffer from the UEFI GOP even with `-display none`,
183
- and the monitor's `screendump` shows it. From a virtual console in the guest:
177
+ The framebuffer needs a kernel that provides one. Both `linux-lts` kernels
178
+ set `CONFIG_SYSFB_SIMPLEFB`, so the UEFI GOP becomes a `simple-framebuffer`
179
+ device, which only `simpledrm` drives (`efifb` never sees it). The x86_64
180
+ `linux-lts` builds `simpledrm` in. The aarch64 one has it as a module:
181
+ `fetch-alpine.sh` ships it and its DRM dependencies for lts builds, and
182
+ `init.js` loads it at boot. The default `linux-virt` builds the framebuffer
183
+ drivers as modules, and the image ships only the network, storage, USB and
184
+ HID modules, so there `/dev/fb0` never appears. Build with
185
+ `./index.js -b --linux-lts` (or `--real` on x86_64). Under QEMU, the x86_64
186
+ lts kernel gets a 1280×800 `simpledrm` framebuffer from the UEFI GOP even
187
+ with `-display none`, and the monitor's `screendump` shows it. QEMU's aarch64
188
+ `virt` machine has no display until you add `-device ramfb`; EDK2 then
189
+ provides an 800×600 GOP, and `virtio-keyboard-pci` gives the VTs a keyboard
190
+ (see the README's
191
+ [What differs on aarch64](README.md#what-differs-on-aarch64)). From a virtual
192
+ console in the guest:
184
193
 
185
194
  ```sh
186
195
  bunterm # /bin/sh on this console
@@ -206,6 +215,13 @@ host, `jsmdcui --tui` on a Markdown file with a JPEG (bunterm sets
206
215
  `JSMDCUI_KITTY_MODE=extended` for the program it runs) places the image
207
216
  through its transmit / `a=p` / `a=d` sequence exactly as in the browser.
208
217
 
218
+ Verified on aarch64 (lts kernel, Hypervisor.framework, `-device ramfb
219
+ -device virtio-keyboard-pci -device virtio-mouse-pci`, `-display none`):
220
+ `/proc/fb` lists `simpledrmdrmfb`; `bunterm 2` from the serial shell returns
221
+ at once and draws on VT 2; and keys sent with the monitor's `sendkey` reach
222
+ it: `showimg /buninu/icon.png` places the kitty image, and the prompt's
223
+ colour emoji and the pointer render, all checked with `screendump`.
224
+
209
225
  ## Not done yet
210
226
 
211
227
  - Selection and scrollback viewing in `bunterm` (the pointer itself works,
@@ -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] [--no-clipboard] [-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,66 @@ 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 (argument === "--no-clipboard") {
62
+ options.clipboard = false;
63
+ } else if (/^[1-9][0-9]*$/.test(argument) || argument.startsWith("/dev/")) {
64
+ const number = /^[1-9][0-9]*$/.test(argument) ? Number(argument) : virtualConsoleNumber(argument);
65
+ if (number === null) throw new Error(`expected N or /dev/ttyN, got ${argument}`);
66
+ if (consoleArgumentIndex !== -1) throw new Error("only one virtual console may be specified");
67
+ options.console = `/dev/tty${number}`;
68
+ consoleArgumentIndex = index;
55
69
  } else {
56
70
  usage();
57
71
  throw new Error(`unexpected argument: ${argument}`);
58
72
  }
59
73
  }
74
+
75
+ const probe = openConsole(["/dev/tty0"]);
76
+ let activeConsoleNumber;
77
+ try {
78
+ activeConsoleNumber = activeConsole(probe);
79
+ } finally {
80
+ probe.close();
81
+ }
82
+ if (!options.console) options.console = `/dev/tty${activeConsoleNumber}`;
83
+
84
+ // Every live bunterm converges on one unambiguous /proc/<pid>/cmdline:
85
+ // /bin/bunterm /bin/bunterm /dev/ttyN [options...]
86
+ // execve replaces the original process, so no forwarding parent remains.
87
+ const remaining = arguments_.filter((_argument, index) => index !== consoleArgumentIndex);
88
+ const canonicalArgv = [CANONICAL_BUNTERM, CANONICAL_BUNTERM, options.console, ...remaining];
89
+ const currentArgv = readFileSync("/proc/self/cmdline").toString("utf8").split("\0");
90
+ if (currentArgv.at(-1) === "") currentArgv.pop();
91
+ if (currentArgv.length !== canonicalArgv.length
92
+ || currentArgv.some((argument, index) => argument !== canonicalArgv[index])) {
93
+ const environment = Object.fromEntries(
94
+ Object.entries(process.env).filter((entry) => typeof entry[1] === "string"),
95
+ );
96
+ const targetConsoleNumber = virtualConsoleNumber(options.console);
97
+ if (consoleArgumentIndex !== -1 && targetConsoleNumber !== activeConsoleNumber) {
98
+ const consoleFd = openSync(options.console, "r+");
99
+ try {
100
+ const child = Bun.spawn(
101
+ [process.execPath, CANONICAL_BUNTERM, options.console, ...remaining],
102
+ {
103
+ argv0: CANONICAL_BUNTERM,
104
+ detached: true,
105
+ env: environment,
106
+ stdin: "ignore",
107
+ stdout: consoleFd,
108
+ stderr: consoleFd,
109
+ },
110
+ );
111
+ child.unref();
112
+ } finally {
113
+ closeSync(consoleFd);
114
+ }
115
+ return 0;
116
+ }
117
+ process.execve(process.execPath, canonicalArgv, environment);
118
+ throw new Error("process.execve returned unexpectedly");
119
+ }
120
+
60
121
  return runTerminal(options);
61
122
  };
62
123
 
package/initramfs/init.js CHANGED
@@ -179,6 +179,16 @@ try {
179
179
  console.error(`init.js: input modules: ${error?.message ?? error}`);
180
180
  }
181
181
 
182
+ // The framebuffer for bunterm: the firmware's GOP is a simple-framebuffer
183
+ // device, and simpledrm turns it into /dev/fb0. x86_64 linux-lts builds it
184
+ // in and linux-virt images do not ship it; tryModprobe skips both.
185
+ try {
186
+ const { tryModprobe } = await import("/lib/modprobe.js");
187
+ if (tryModprobe("simpledrm")?.length) console.log("init.js: loaded simpledrm");
188
+ } catch (error) {
189
+ console.error(`init.js: simpledrm: ${error?.message ?? error}`);
190
+ }
191
+
182
192
  const { default: repl } = await import("node:repl");
183
193
  const { mkdirSync, writeFileSync } = await import("node:fs");
184
194
 
@@ -549,11 +559,26 @@ const configureDisks = () => {
549
559
  return devices;
550
560
  };
551
561
 
562
+ // Load the complete sound stack packaged in the image. Like cfg.net this is
563
+ // deliberately unconditional: it does not probe modaliases first, so codec
564
+ // and helper modules without their own discoverable device are loaded too.
565
+ const configureSound = () => {
566
+ const { modprobeAll } = require("/lib/modprobe.js");
567
+ const { readdirSync } = require("node:fs");
568
+ const report = modprobeAll({ accept: (_module, path) => path.startsWith("kernel/sound/") });
569
+ console.log(`sound: loaded all packaged sound modules (${report.inserted.length} newly loaded, ${report.errors.length} failed)`);
570
+ for (const { module, error } of report.errors) console.log(`sound: ${module}: ${error}`);
571
+ const devices = (() => { try { return readdirSync("/dev/snd"); } catch { return []; } })();
572
+ console.log(`sound: ${devices.length ? devices.map((name) => `/dev/snd/${name}`).join(" ") : "no sound devices registered"}`);
573
+ return devices;
574
+ };
575
+
552
576
  globalThis.cfg = Object.freeze(Object.defineProperties({}, {
553
577
  all: { enumerable: true, get: configureModules },
554
578
  disk: { enumerable: true, get: configureDisks },
555
579
  net: { enumerable: true, get: configureEthernet },
556
580
  power: { enumerable: true, get: configureBattery },
581
+ sound: { enumerable: true, get: configureSound },
557
582
  }));
558
583
 
559
584
  // Compatibility aliases for images and notes that used the original API.
@@ -562,6 +587,7 @@ globalThis.cfgNet = configureEthernet;
562
587
  globalThis.cfgDisk = configureDisks;
563
588
  globalThis.cfgMod = configureModules;
564
589
  globalThis.cfgBat = configureBattery;
590
+ globalThis.cfgSound = configureSound;
565
591
 
566
592
  const showWelcome = () => {
567
593
  console.log(`
@@ -0,0 +1,51 @@
1
+ // /lib/bunterm/clipboard.js — OSC 52 clipboard for bunterm, backed by xclip.
2
+ //
3
+ // program ── ESC ] 52 ; c ; <base64> BEL ──▶ xclip -selection clipboard
4
+ // program ── ESC ] 52 ; c ; ? BEL ─────────▶ xclip -selection clipboard -o
5
+ // ◀── ESC ] 52 ; c ; <base64> BEL ──
6
+ //
7
+ // bunterm keeps nothing itself: every copy and paste goes through xclip, so
8
+ // all bunterm sessions (and anything else using xclip) share one clipboard.
9
+ // Buninu's xclip prints status lines on stderr, which must not reach the
10
+ // console under the framebuffer, so stderr is discarded.
11
+
12
+ const runXclip = async (args, input) => {
13
+ const command = ["xclip", "-selection", "clipboard", ...args];
14
+ const proc = Bun.spawn(command, {
15
+ stdin: input === undefined ? "ignore" : input,
16
+ stdout: input === undefined ? "pipe" : "ignore",
17
+ stderr: "ignore",
18
+ });
19
+ const [output, exitCode] = await Promise.all([
20
+ input === undefined ? new Response(proc.stdout).arrayBuffer() : null,
21
+ proc.exited,
22
+ ]);
23
+ if (exitCode !== 0) throw new Error(`${command.join(" ")} exited with ${exitCode}`);
24
+ return output;
25
+ };
26
+
27
+ // Registers the OSC 52 handler on an xterm.js terminal. `reply` sends bytes
28
+ // back to the program, as the terminal's other replies do. A read the xclip
29
+ // call cannot answer is still answered, with empty contents, so the program
30
+ // does not wait out its own timeout.
31
+ export const attachClipboard = ({ term, reply, onError = () => {} }) =>
32
+ term.parser.registerOscHandler(52, (data) => {
33
+ const separator = data.indexOf(";");
34
+ if (separator < 0) return true;
35
+ const target = data.slice(0, separator);
36
+ const payload = data.slice(separator + 1);
37
+
38
+ if (payload === "?") {
39
+ // The reply is sent after xclip returns, when the program may have exited
40
+ // and the PTY closed; writing then throws, and an unhandled rejection
41
+ // would end bunterm before it restores the console.
42
+ runXclip(["-o"])
43
+ .catch((error) => { onError(error); return new ArrayBuffer(0); })
44
+ .then((bytes) => reply(`\u001b]52;${target};${Buffer.from(bytes).toString("base64")}\u0007`))
45
+ .catch(onError);
46
+ return true;
47
+ }
48
+
49
+ runXclip([], Buffer.from(payload, "base64")).catch(onError);
50
+ return true;
51
+ });
@@ -20,6 +20,8 @@ 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";
24
+ import { attachClipboard } from "./clipboard.js";
23
25
 
24
26
  const require = createRequire(import.meta.url);
25
27
 
@@ -51,6 +53,7 @@ export const createSession = ({
51
53
  scrollback = 1000,
52
54
  cursorBlink = true,
53
55
  mouse = true,
56
+ clipboard = true,
54
57
  env = {},
55
58
  onError = (error) => console.error(`bunterm: ${error?.stack ?? error}`),
56
59
  }) => {
@@ -81,6 +84,7 @@ export const createSession = ({
81
84
  });
82
85
 
83
86
  let frameTimer = null;
87
+ let active = true;
84
88
  let blinkOn = true;
85
89
  const cursorHidden = () => Boolean(term._core?.coreService?.isCursorHidden);
86
90
  // Only set when a pointer is attached; without one the renderer is called
@@ -89,6 +93,7 @@ export const createSession = ({
89
93
  const paint = () => {
90
94
  if (frameTimer) clearTimeout(frameTimer);
91
95
  frameTimer = null;
96
+ if (!active) return false;
92
97
  const buffer = term.buffer.active;
93
98
  // cursorY counts from the buffer's base, so a scrolled-back viewport
94
99
  // moves the cursor down the screen and eventually off it.
@@ -104,8 +109,20 @@ export const createSession = ({
104
109
  });
105
110
  };
106
111
  const requestFrame = () => {
112
+ if (!active) return;
107
113
  frameTimer ??= setTimeout(paint, 16);
108
114
  };
115
+ const suspend = () => {
116
+ active = false;
117
+ if (frameTimer) clearTimeout(frameTimer);
118
+ frameTimer = null;
119
+ };
120
+ const resume = () => {
121
+ if (active) return;
122
+ active = true;
123
+ renderer.invalidate();
124
+ paint();
125
+ };
109
126
  const blinkTimer = cursorBlink
110
127
  ? setInterval(() => { blinkOn = !blinkOn; requestFrame(); }, 500)
111
128
  : null;
@@ -143,6 +160,9 @@ export const createSession = ({
143
160
  });
144
161
  backend.onData(pump);
145
162
  term.onData((data) => backend.write(Buffer.from(data, "utf8")));
163
+ // OSC 52 copy and paste go straight to xclip; replies return to the program.
164
+ // Without it (--no-clipboard) xterm.js ignores OSC 52 and nothing reaches xclip.
165
+ if (clipboard) attachClipboard({ term, reply: (text) => backend.write(Buffer.from(text, "utf8")), onError });
146
166
 
147
167
  // Keyboard bytes from the console, rewritten for the program's key mode.
148
168
  const input = (bytes) => {
@@ -193,6 +213,9 @@ export const createSession = ({
193
213
  };
194
214
 
195
215
  const reportMouse = (report) => {
216
+ // /dev/input is global rather than routed through the active VT. Do not
217
+ // draw or send mouse input to a terminal whose console is off-screen.
218
+ if (!active) return;
196
219
  pointer = { x: report.x, y: report.y };
197
220
  requestFrame();
198
221
  const service = term._core?.coreMouseService;
@@ -268,7 +291,7 @@ export const createSession = ({
268
291
  paint();
269
292
  return {
270
293
  term, backend, renderer, images, kitty,
271
- paint, requestFrame, settle, input, exited, close, attachPointer, reportMouse, wheelScroll,
294
+ paint, requestFrame, suspend, resume, settle, input, exited, close, attachPointer, reportMouse, wheelScroll,
272
295
  };
273
296
  };
274
297
 
@@ -282,15 +305,13 @@ export const runTerminal = async ({ console: consoleDevice = null, device = "/de
282
305
  const fonts = loadFontSet(CanvasKit, "terminal");
283
306
  const consolePaths = consoleDevice ? [consoleDevice] : undefined;
284
307
 
285
- return runGraphics(async (display, { onRestore, onAcquire }) => {
308
+ return runGraphics(async (display, { onRestore, onRelease, onAcquire }) => {
286
309
  const screen = openCanvas(CanvasKit, display);
287
310
  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
- });
311
+ onRelease(session.suspend);
312
+ // Coming back from another console (Ctrl-Alt-Fn): fbcon or another
313
+ // bunterm has drawn over the framebuffer, so repaint everything.
314
+ onAcquire(session.resume);
294
315
  let keyboard = null;
295
316
  try {
296
317
  keyboard = openKeyboard({
@@ -320,5 +341,5 @@ export const runTerminal = async ({ console: consoleDevice = null, device = "/de
320
341
  session.close();
321
342
  screen.close();
322
343
  }
323
- }, { device, console: consolePaths });
344
+ }, { device, console: consolePaths, prepareConsole: claimVirtualConsole });
324
345
  };
@@ -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,10 +7,12 @@ 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
15
+ bunterm --no-clipboard # ignore OSC 52 copy and paste
14
16
  bunterm -h
15
17
  ```
16
18
 
@@ -19,17 +21,26 @@ The program's `TERM` is `xterm-256color`, `COLORTERM=truecolor`,
19
21
  Markdown images without `--kitty`). When it exits, the console returns to
20
22
  text mode and `bunterm` exits with its status.
21
23
 
24
+ 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
27
+ immediately to the calling shell. Only one bunterm may control a given VT;
28
+ ownership is
29
+ identified from the process task name and its open console descriptor under
30
+ `/proc`, so it disappears automatically when the process exits.
31
+
22
32
  ## Options
23
33
 
24
34
  | Option | Meaning |
25
35
  | --- | --- |
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). |
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. |
27
37
  | `-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. |
38
+ | `-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
39
  | `--line-height F` | Multiplies the cell height (default 1). |
30
40
  | `--fb /dev/fbN` | The framebuffer device (default `/dev/fb0`). |
31
41
  | `--no-blink` | A steady cursor. |
32
42
  | `--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. |
33
44
 
34
45
  ## What is drawn
35
46
 
@@ -57,7 +68,11 @@ text mode and `bunterm` exits with its status.
57
68
 
58
69
  The console stays in the kernel's translated keyboard mode, so
59
70
  `Ctrl-Alt-F2` and friends still switch consoles; coming back repaints the
60
- screen. A few console-specific
71
+ screen. While its VT is inactive, bunterm continues consuming PTY output and
72
+ updating its scrollback and kitty images, but neither writes the framebuffer
73
+ nor forwards global `/dev/input` mouse events. This allows separate bunterm
74
+ processes on `/dev/tty1`, `/dev/tty2`, and so on to hand the display back and
75
+ forth through the normal Linux VT switch. A few console-specific
61
76
  sequences are rewritten to xterm's (`F1`–`F5`, `Home`, `End`), and arrow keys
62
77
  follow the program's application cursor mode.
63
78
 
@@ -88,6 +103,16 @@ mouse also needs the `usbhid` stack, which a `--real` image carries and
88
103
  ones (a tablet or touchscreen) alike; a device that cannot be opened is
89
104
  reported and the session continues without it.
90
105
 
106
+ ## Clipboard
107
+
108
+ Programs copy and paste through OSC 52, as `casty` and `jsmdcui` do.
109
+ bunterm keeps no clipboard of its own: `ESC ] 52 ; c ; <base64>` is passed to
110
+ `xclip -selection clipboard`, and a query (`ESC ] 52 ; c ; ?`) is answered
111
+ with the output of `xclip -selection clipboard -o`, so every bunterm session
112
+ shares the same clipboard as `xclip`. A query xclip cannot answer is replied
113
+ to with empty contents. `--no-clipboard` turns this off, so a program can
114
+ neither set nor read the clipboard through the terminal.
115
+
91
116
  ## Limits
92
117
 
93
118
  - Needs a framebuffer: `linux-lts` has one built in, while the default
@@ -104,7 +129,8 @@ reported and the session continues without it.
104
129
  - Kitty images are drawn above text; `z` ordering below text and animation
105
130
  frames are not implemented, and file transmission (`t=f`/`t=t`) works only
106
131
  for files under `/tmp`, `/dev/shm` or `$TMPDIR`.
107
- - One framebuffer, one session. For panes and tabs run `jsmdcui` inside it.
132
+ - One bunterm per virtual console may use the framebuffer; only the active VT
133
+ draws. For panes and tabs within one VT, run `jsmdcui` inside it.
108
134
 
109
135
  ## See also
110
136
 
@@ -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.1",
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