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 +117 -22
- package/fetch-alpine.sh +16 -0
- package/graphics.md +23 -7
- package/initramfs/bin/buninu-linux-help +12 -0
- package/initramfs/bin/bunterm +66 -5
- package/initramfs/init.js +26 -0
- package/initramfs/lib/bunterm/clipboard.js +51 -0
- package/initramfs/lib/bunterm/terminal.js +30 -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 +32 -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,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
|
|
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
|
|
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
|
-
*
|
|
233
|
-
|
|
234
|
-
|
|
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`.
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
* [
|
|
1105
|
-
+ [
|
|
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.
|
|
178
|
-
`
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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");
|
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] [--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 === "
|
|
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,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
|
|
54
|
-
options.
|
|
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
|
-
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
};
|
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,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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.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://
|
|
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
|