buninu-linux 0.13.56 → 0.15.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSES/linux-firmware/LICENCE.nvidia +131 -0
  2. package/LICENSES/linux-firmware/LICENSE.amdgpu +51 -0
  3. package/LICENSES/linux-firmware/LICENSE.amdisp +31 -0
  4. package/LICENSES/linux-firmware/LICENSE.i915 +39 -0
  5. package/LICENSES/linux-firmware/LICENSE.radeon +51 -0
  6. package/LICENSES/linux-firmware/LICENSE.xe +39 -0
  7. package/LICENSES/linux-firmware/WHENCE +10280 -0
  8. package/NOTICE.md +64 -46
  9. package/README.md +410 -96
  10. package/README.zh.md +2034 -0
  11. package/build-image.sh +10 -1
  12. package/build-uki.sh +3 -1
  13. package/fetch-alpine.sh +15 -1
  14. package/graphics.md +31 -0
  15. package/index.js +28 -11
  16. package/initramfs/bin/buninu-browser +4 -1
  17. package/initramfs/bin/buninu-land +1 -1
  18. package/initramfs/bin/bunterm +3 -3
  19. package/initramfs/bin/chroot +13 -0
  20. package/initramfs/bin/chvt +11 -0
  21. package/initramfs/bin/dhclient +8 -0
  22. package/initramfs/bin/mount +4 -0
  23. package/initramfs/bin/ping +8 -0
  24. package/initramfs/bin/umount +6 -0
  25. package/initramfs/bin/xdg-open +33 -0
  26. package/initramfs/buninu/CHANGELOG.md +19 -0
  27. package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +4 -0
  28. package/initramfs/buninu/apps/bunmsh/package.json +1 -1
  29. package/initramfs/buninu/apps/bunmsh/src/main.js +1 -1
  30. package/initramfs/buninu/apps/jsgotty/README.en.md +40 -0
  31. package/initramfs/buninu/apps/jsgotty/README.md +40 -0
  32. package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
  33. package/initramfs/buninu/apps/jsgotty/package.json +1 -1
  34. package/initramfs/buninu/apps/jsgotty/static/index.html +1 -0
  35. package/initramfs/buninu/apps/jsgotty/static/js/gotty.js +82 -17
  36. package/initramfs/buninu/apps/jsgotty/static/js/kitty-keyboard.js +193 -0
  37. package/initramfs/buninu/apps/jsgotty/tests/android-alt.test.js +115 -0
  38. package/initramfs/buninu/apps/jsgotty/tests/arrow-modifiers.test.js +80 -0
  39. package/initramfs/buninu/apps/jsgotty/tests/kitty-keyboard-browser.test.js +53 -0
  40. package/initramfs/buninu/apps/jsgotty/tests/kitty-keyboard.test.js +102 -0
  41. package/initramfs/buninu/apps/jsgotty/tests/manual/README.md +20 -0
  42. package/initramfs/buninu/apps/jsgotty/tests/manual/keyboard-collector.cjs +55 -0
  43. package/initramfs/buninu/apps/jsgotty/tests/manual/keyboard-debug.js +74 -0
  44. package/initramfs/buninu/apps/jsgotty/tests/manual/kitty-keyboard.js +80 -0
  45. package/initramfs/buninu/package.json +1 -1
  46. package/initramfs/init.js +27 -10
  47. package/initramfs/lib/bunterm/clipboard.js +14 -0
  48. package/initramfs/lib/bunterm/commands.js +37 -0
  49. package/initramfs/lib/bunterm/fps.js +38 -0
  50. package/initramfs/lib/bunterm/images.js +9 -5
  51. package/initramfs/lib/bunterm/input.js +58 -15
  52. package/initramfs/lib/bunterm/keyboard.js +62 -0
  53. package/initramfs/lib/bunterm/keycodes.js +104 -0
  54. package/initramfs/lib/bunterm/kitty-keyboard.cjs +195 -0
  55. package/initramfs/lib/bunterm/raw-protocol.js +33 -0
  56. package/initramfs/lib/bunterm/renderer.js +90 -21
  57. package/initramfs/lib/bunterm/selection.js +60 -0
  58. package/initramfs/lib/bunterm/terminal.js +147 -12
  59. package/initramfs/lib/bunterm/vt-menu.js +181 -0
  60. package/initramfs/lib/bunterm/vt.js +27 -1
  61. package/initramfs/lib/canvas.js +99 -0
  62. package/initramfs/lib/chvt.js +31 -0
  63. package/initramfs/lib/dhclient.js +135 -0
  64. package/initramfs/lib/dhcp-start.js +85 -0
  65. package/initramfs/lib/dhcp-transport.js +44 -0
  66. package/initramfs/lib/dhcp.js +124 -0
  67. package/initramfs/lib/dlopen.js +58 -11
  68. package/initramfs/lib/fbdev.js +21 -3
  69. package/initramfs/lib/gpu.js +77 -0
  70. package/initramfs/lib/ping.js +110 -0
  71. package/initramfs/lib/raw-image.js +47 -0
  72. package/initramfs/lib/xdg-open.js +42 -0
  73. package/initramfs/usr/share/doc/buninu-linux/bunterm.md +86 -12
  74. package/initramfs/usr/share/doc/buninu-linux/chvt.md +34 -0
  75. package/initramfs/usr/share/doc/buninu-linux/dhclient.md +82 -0
  76. package/initramfs/usr/share/doc/buninu-linux/ping.md +91 -0
  77. package/initramfs/usr/share/doc/buninu-linux/xdg-open.md +43 -0
  78. package/package.json +2 -2
  79. package/scripts/arch.js +7 -5
  80. package/scripts/arch.sh +5 -5
  81. package/scripts/build.js +16 -3
  82. package/scripts/fetch-gpu.js +105 -0
  83. package/scripts/fetch.js +11 -1
  84. package/scripts/gpu-firmware.json +22 -0
  85. package/scripts/modules.js +5 -1
  86. package/scripts/npm-packages.md +17 -17
  87. package/test/bunterm-fps.test.js +17 -0
  88. package/test/bunterm-keyboard.test.js +116 -0
  89. package/test/bunterm-next-vt.test.js +9 -0
  90. package/test/bunterm-raw-protocol.test.js +19 -0
  91. package/test/bunterm-renderer.test.js +158 -0
  92. package/test/bunterm-selection.test.js +38 -0
  93. package/test/bunterm-vt-menu.test.js +178 -0
  94. package/test/chroot-bind.test.js +40 -2
  95. package/test/chvt.test.js +46 -0
  96. package/test/dhcp-start.test.js +67 -0
  97. package/test/dhcp.test.js +99 -0
  98. package/test/fbdev-sync.test.js +33 -0
  99. package/test/fetch-npm.test.js +1 -1
  100. package/test/gpu.test.js +155 -0
  101. package/test/image-size.test.js +17 -0
  102. package/test/libc.test.js +41 -0
  103. package/test/manual-bunterm-image-paths.js +31 -0
  104. package/test/mount-libc-lifetime.test.js +50 -0
  105. package/test/no-gpu.test.js +43 -0
  106. package/test/ping.test.js +80 -0
  107. package/test/readme-cli.test.js +29 -0
  108. package/test/stage-npm.test.js +11 -4
  109. package/test/xdg-open.test.js +55 -0
package/build-image.sh CHANGED
@@ -25,7 +25,16 @@ done
25
25
  temporary_esp="$(mktemp --tmpdir=. .esp.XXXXXX.img)"
26
26
  trap 'rm -f "$temporary_esp"' EXIT
27
27
 
28
- disk_sectors=$((128 * 1024 * 1024 / 512))
28
+ # Match scripts/build.js: payload + 1/16 + 4 MiB, rounded to 64 MiB.
29
+ payload_bytes=$(stat -c %s "vda/EFI/BOOT/$efi_boot_name")
30
+ if ((payload_bytes > 4294967295)); then
31
+ echo "error: EFI payload exceeds the FAT32 single-file limit" >&2
32
+ exit 1
33
+ fi
34
+ unit=$((64 * 1024 * 1024))
35
+ disk_bytes=$(((payload_bytes + (payload_bytes + 15) / 16 + 4 * 1024 * 1024 + unit - 1) / unit * unit))
36
+ ((disk_bytes >= 128 * 1024 * 1024)) || disk_bytes=$((128 * 1024 * 1024))
37
+ disk_sectors=$((disk_bytes / 512))
29
38
  esp_start=2048
30
39
  esp_end=$((disk_sectors - 34))
31
40
  esp_sectors=$((esp_end - esp_start + 1))
package/build-uki.sh CHANGED
@@ -5,7 +5,9 @@ cd "$(dirname "$0")"
5
5
 
6
6
  . ./scripts/arch.sh
7
7
 
8
- if [ ! -s "$kernel_image" ] || [ ! -s "$kernel_dir/$efi_stub" ] || [ ! -s "$modules_root/modules.dep" ] || \
8
+ gpu_js=$(command -v bun || command -v node)
9
+
10
+ if [ ! -s "$kernel_image" ] || [ ! -s "$kernel_dir/$efi_stub" ] || [ ! -s "$modules_root/modules.dep" ] || ! "$gpu_js" scripts/fetch-gpu.js --check || \
9
11
  { [ "${REAL_MACHINE:-}" = 1 ] && \
10
12
  { [ ! -s "$modules_root/kernel/drivers/hid/usbhid/usbhid.ko" ] || \
11
13
  [ ! -s "$modules_root/kernel/drivers/acpi/battery.ko" ]; }; }; then
package/fetch-alpine.sh CHANGED
@@ -33,6 +33,9 @@ depfile="$(mktemp)"
33
33
  selected="$(mktemp)"
34
34
  trap 'rm -f "$depfile" "$selected" "$selected.names"' EXIT
35
35
  extract "$linux_package" "lib/modules/$kernel_release/modules.dep" > "$depfile"
36
+ # Recreate the generated module tree so removed selections cannot linger.
37
+ rm -rf "$modules_root"
38
+ mkdir -p "$modules_root"
36
39
  extract "$linux_package" "lib/modules/$kernel_release/modules.builtin" > "$modules_root/modules.builtin"
37
40
 
38
41
  select_module() {
@@ -63,8 +66,15 @@ select_optional_module() {
63
66
  # Network: virtio-net for QEMU (init.js loads these at boot). virtio_pci is
64
67
  # the PCI transport for every virtio device, disk included; linux-virt builds
65
68
  # it in, linux-lts has it as a module that nothing in modules.dep pulls in.
66
- for module in failover net_failover virtio_net; do select_module "$module"; done
69
+ for module in failover net_failover virtio_net af_packet; do select_module "$module"; done
67
70
  select_optional_module virtio_pci
71
+ # GPU modules are packaged only; init and cfg.all leave them unloaded.
72
+ if [ "${BUNINU_NO_GPU:-}" != 1 ]; then
73
+ for module in virtio-gpu vmwgfx; do select_optional_module "$module"; done
74
+ if [ "$arch" = x86_64 ]; then
75
+ for module in i915 xe amdgpu radeon nouveau; do select_optional_module "$module"; done
76
+ fi
77
+ fi
68
78
  # Storage: what /bin/mount loads on demand. ext4 brings jbd2, mbcache and
69
79
  # crc16; FAT, exFAT and NTFS ask the kernel for the NLS tables at mount
70
80
  # time, which has no /sbin/modprobe to answer, so mount preloads them.
@@ -145,4 +155,8 @@ extract "$linux_package" "lib/modules/$kernel_release/modules.alias" \
145
155
 
146
156
  chmod 0755 "$native_dir/lib/$musl_loader"
147
157
 
158
+ # Share the verified firmware extraction and cache marker with the JS build.
159
+ js=$(command -v bun || command -v node)
160
+ "$js" scripts/fetch-gpu.js
161
+
148
162
  echo "Fetched Alpine $arch linux-$linux_flavor kernel, $(wc -l < "$selected") modules, musl, and the $efi_stub UKI stub."
package/graphics.md CHANGED
@@ -158,6 +158,28 @@ screen and xterm.js's `CoreMouseService` → PTY. Terminal
158
158
  replies (cursor position, device attributes, kitty acks) go from the
159
159
  emulator straight to the PTY.
160
160
 
161
+ ## Raw image fast path
162
+
163
+ Native bunterm can negotiate BGRA (`f=1111970369`, a private Buninu format) or
164
+ standard RGBA (`f=32`) with Casty. `images.js` retains the received raw bytes;
165
+ `raw-image.js` creates a Skia image only when a fallback operation needs one.
166
+ For an opaque, integer-aligned 1:1 placement matching the framebuffer channel
167
+ order, `canvas.js` copies these bytes into the existing raster back buffer.
168
+ This avoids `MakeImage` followed by `readPixels`, but still performs the raster
169
+ copy and the final framebuffer flush. Opacity is checked from the actual alpha
170
+ bytes once per image, not assumed from its format or filename.
171
+
172
+ Rows completely covered by such an image skip text hashing and painting.
173
+ Partially covered rows keep normal rendering, and uncovering a row forces its
174
+ current text to repaint. Pointer and menu overlays are still drawn afterwards.
175
+ Scaling, transparency, other channel orders and 16-bit framebuffers retain
176
+ CanvasKit rendering. Encoded PNG/JPEG handling is unchanged.
177
+
178
+ 原始像素只有在通道排列一致、1:1 整數座標且經檢查確定不透明時,才直接複製到
179
+ 繪圖緩衝區,省去先建立 Skia Image 再讀回像素的步驟。完全被圖片覆蓋的文字列
180
+ 略過雜湊與重畫;圖片移開後會重畫當下的文字。縮放、透明、格式不符時仍使用
181
+ CanvasKit,最後寫入 framebuffer 的複製也仍然存在。
182
+
161
183
  ## Sizes
162
184
 
163
185
  | Component | Raw | In the gzip initramfs |
@@ -222,6 +244,15 @@ at once and draws on VT 2; and keys sent with the monitor's `sendkey` reach
222
244
  it: `showimg /buninu/icon.png` places the kitty image, and the prompt's
223
245
  colour emoji and the pointer render, all checked with `screendump`.
224
246
 
247
+ ## Optional client GPU acceleration
248
+
249
+ Client GPU rendering can be enabled separately with the PID 1 REPL's `cfg.gpu`,
250
+ before opening framebuffer clients. It loads matched GPU drivers and exposes
251
+ render nodes, while this CanvasKit/bunterm stack remains software-rendered.
252
+ Boot and `cfg.all` do not load those GPU drivers. See the README's
253
+ [optional client GPU acceleration](README.md#optional-client-gpu-acceleration-cfggpu)
254
+ for firmware, device permissions and framebuffer handoff constraints.
255
+
225
256
  ## Not done yet
226
257
 
227
258
  - Selection and scrollback viewing in `bunterm` (the pointer itself works,
package/index.js CHANGED
@@ -79,7 +79,7 @@ function usage() {
79
79
 
80
80
  Usage:
81
81
  ${pkg.name} [-f] [-b] [-r] [--arch ARCH] [--docker] [--export] [--linux-lts]
82
- [--real] [-- qemu arguments]
82
+ [--real] [--no-gpu] [-- qemu arguments]
83
83
  ${pkg.name} --check [FILE...]
84
84
 
85
85
  Stages (always run in this order, whichever you pick):
@@ -107,6 +107,8 @@ Stages (always run in this order, whichever you pick):
107
107
  primary console, and include USB xHCI/HID keyboard modules
108
108
  (x86_64 only)
109
109
 
110
+ --no-gpu omit the added GPU modules/firmware; retain the original framebuffer path
111
+
110
112
  --check [FILE...]
111
113
  print content checksums of the build outputs that ignore
112
114
  mtimes, timestamps and GUIDs, and exit (check_build.js; the
@@ -116,6 +118,8 @@ Stages (always run in this order, whichever you pick):
116
118
  -h, --help show this
117
119
  -V, --version show name, version, runtime and platform
118
120
  --readme render README.md in the terminal and exit
121
+ --readme-zh, --docs
122
+ render README.zh.md in the terminal and exit
119
123
 
120
124
  Short flags combine: -fbr is the whole pipeline, -br is what an edit to
121
125
  initramfs/init.js needs, -r just boots what is already there. Anything after
@@ -134,11 +138,12 @@ reported before anything starts.
134
138
  `;
135
139
  }
136
140
 
137
- function parse(argv) {
141
+ async function parse(argv) {
138
142
  const selected = new Set();
139
143
  const passthrough = [];
140
144
  let linuxLts = false;
141
145
  let real = false;
146
+ let noGpu = false;
142
147
  let exportImage = false;
143
148
  let docker = false;
144
149
  // BUNINU_ARCH is what scripts/arch.sh reads; --arch overrides it.
@@ -165,12 +170,17 @@ function parse(argv) {
165
170
  printVersion();
166
171
  process.exit(0);
167
172
  } else if (argument === "--readme") {
168
- printReadme();
173
+ await printReadme();
174
+ process.exit(0);
175
+ } else if (argument === "--readme-zh" || argument === "--docs") {
176
+ await printReadme("README.zh.md");
169
177
  process.exit(0);
170
178
  } else if (argument === "--check") {
171
179
  runCheck(argv.slice(index + 1));
172
180
  } else if (argument === "--linux-lts") {
173
181
  linuxLts = true;
182
+ } else if (argument === "--no-gpu") {
183
+ noGpu = true;
174
184
  } else if (argument === "--real") {
175
185
  real = true;
176
186
  } else if (argument === "--export") {
@@ -197,8 +207,8 @@ function parse(argv) {
197
207
  if (passthrough.length > 0 && !selected.has(stages[2])) {
198
208
  fail("arguments after -- only make sense with -r/--run");
199
209
  }
200
- if ((linuxLts || real) && !selected.has(stages[0]) && !selected.has(stages[1])) {
201
- fail("--linux-lts/--real only make sense with -f/--fetch or -b/--build");
210
+ if ((linuxLts || real || noGpu) && !selected.has(stages[0]) && !selected.has(stages[1])) {
211
+ fail("--linux-lts/--real/--no-gpu only make sense with -f/--fetch or -b/--build");
202
212
  }
203
213
  if (exportImage && !selected.has(stages[1])) {
204
214
  fail("--export requires -b/--build (use -fb --export for a clean build)");
@@ -206,7 +216,7 @@ function parse(argv) {
206
216
  if (real && arch === "aarch64") {
207
217
  fail("--real builds an x86_64 PC image; it cannot be combined with --arch aarch64");
208
218
  }
209
- return { selected, passthrough, linuxLts, real, exportImage, docker, arch };
219
+ return { selected, passthrough, linuxLts, real, noGpu, exportImage, docker, arch };
210
220
  }
211
221
 
212
222
  // Same shape as Buninu's --version. The runtime line tells you whether this
@@ -222,8 +232,8 @@ function printVersion() {
222
232
 
223
233
  // Same as Buninu's --readme: Bun renders the Markdown to ANSI with clickable
224
234
  // links. Under node there is no renderer, so the file is printed as-is.
225
- function printReadme() {
226
- const path = resolve(rootDir, "README.md");
235
+ function printReadme(filename = "README.md") {
236
+ const path = resolve(rootDir, filename);
227
237
  let markdown;
228
238
  try {
229
239
  markdown = readFileSync(path, "utf8");
@@ -234,7 +244,12 @@ function printReadme() {
234
244
  typeof Bun !== "undefined" && typeof Bun.markdown?.ansi === "function"
235
245
  ? Bun.markdown.ansi(markdown, { hyperlinks: true })
236
246
  : markdown;
237
- process.stdout.write(rendered.endsWith("\n") ? rendered : rendered + "\n");
247
+ // Wait for the whole document to flush before parse() exits. Large README
248
+ // output can otherwise be truncated when stdout is a pipe.
249
+ return new Promise((resolve, reject) => {
250
+ process.stdout.write(rendered.endsWith("\n") ? rendered : rendered + "\n",
251
+ (error) => error ? reject(error) : resolve());
252
+ });
238
253
  }
239
254
 
240
255
  // --check hands everything after it to check_build.js, under the same
@@ -316,7 +331,7 @@ function dockerImage() {
316
331
  // into the bind-mounted checkout stays owned by that user; fakeroot provides
317
332
  // the root ownership the initramfs records.
318
333
  function dockerRun(script, env) {
319
- const passEnv = ["BUNINU_ARCH", "LINUX_FLAVOR", "REAL_MACHINE", "ALPINE_MIRROR", "BUNINU_JS_UKI", "BUNINU_JS_CPIO"]
334
+ const passEnv = ["BUNINU_ARCH", "LINUX_FLAVOR", "REAL_MACHINE", "BUNINU_NO_GPU", "ALPINE_MIRROR", "BUNINU_JS_UKI", "BUNINU_JS_CPIO"]
320
335
  .filter((name) => env[name] !== undefined)
321
336
  .flatMap((name) => ["--env", `${name}=${env[name]}`]);
322
337
  const user = typeof process.getuid === "function"
@@ -344,7 +359,7 @@ function runScript(stage, script, args = [], env = process.env) {
344
359
  }
345
360
  }
346
361
 
347
- const { selected, passthrough, linuxLts, real, exportImage, docker, arch } = parse(process.argv.slice(2));
362
+ const { selected, passthrough, linuxLts, real, noGpu, exportImage, docker, arch } = await parse(process.argv.slice(2));
348
363
  const ordered = stages.filter((stage) => selected.has(stage));
349
364
  const useDocker = docker || process.platform === "darwin";
350
365
  const buildArch = arch ?? "x86_64";
@@ -357,6 +372,7 @@ const stageEnv = {
357
372
  ...(arch && { BUNINU_ARCH: arch }),
358
373
  ...((linuxLts || real) && { LINUX_FLAVOR: "lts" }),
359
374
  ...(real && { REAL_MACHINE: "1" }),
375
+ ...(noGpu && { BUNINU_NO_GPU: "1" }),
360
376
  };
361
377
 
362
378
  // The arch.js config for the JS stages, from the same settings as stageEnv.
@@ -364,6 +380,7 @@ const jsConfig = async (env) => (await import("./scripts/arch.js")).config({
364
380
  arch: env.BUNINU_ARCH,
365
381
  flavor: env.LINUX_FLAVOR,
366
382
  real: env.REAL_MACHINE === "1",
383
+ noGpu: env.BUNINU_NO_GPU === "1",
367
384
  mirror: env.ALPINE_MIRROR,
368
385
  });
369
386
 
@@ -14,9 +14,12 @@ const { chooseFrontend } = await import(pathToFileURL(entry).href);
14
14
  const args = process.argv.slice(2);
15
15
  const choice = chooseFrontend(args);
16
16
  const casty = choice.frontend === "casty";
17
+ const forwarded = casty ? choice.args : args;
18
+ // Preserve Casty's separator when Bun consumes the first one after the script.
19
+ const runtimeArgs = forwarded[0] === "--" ? ["--", ...forwarded] : forwarded;
17
20
  const child = Bun.spawn({
18
21
  cmd: [process.execPath, casty ? join(modules, "@drxiaozhi/casty/index.js") : entry,
19
- ...(casty ? choice.args : args)],
22
+ ...runtimeArgs],
20
23
  env: casty ? { ...process.env, CASTY_BROWSER: join(browser, "buninu-browser.js") } : process.env,
21
24
  stdio: ["inherit", "inherit", "inherit"],
22
25
  });
@@ -14,7 +14,7 @@ const { chooseFrontend } = await import(pathToFileURL(entry).href);
14
14
  const args = process.argv.slice(2);
15
15
  const choice = chooseFrontend(args);
16
16
  const casty = choice.frontend === "casty";
17
- const forwarded = casty ? choice.args : args;
17
+ const forwarded = casty ? ["--no-shortcuts", ...choice.args] : args;
18
18
  // Bun consumes a leading separator after the script path; preserve Casty's
19
19
  // own separator when --casty is followed directly by -- engine flags.
20
20
  const runtimeArgs = forwarded[0] === "--" ? ["--", ...forwarded] : forwarded;
@@ -4,7 +4,7 @@
4
4
  import { showDocument } from "../lib/document.js";
5
5
  import { runTerminal } from "../lib/bunterm/terminal.js";
6
6
  import { activeConsole, openConsole } from "../lib/fbdev.js";
7
- import { virtualConsoleNumber } from "../lib/bunterm/vt.js";
7
+ import { virtualConsoleNumber, nextVirtualTerminal } from "../lib/bunterm/vt.js";
8
8
  import { closeSync, openSync, readFileSync } from "node:fs";
9
9
 
10
10
  const CANONICAL_BUNTERM = "/bin/bunterm";
@@ -79,7 +79,7 @@ const main = async () => {
79
79
  } finally {
80
80
  probe.close();
81
81
  }
82
- if (!options.console) options.console = `/dev/tty${activeConsoleNumber}`;
82
+ if (!options.console) options.console = `/dev/tty${nextVirtualTerminal()}`;
83
83
 
84
84
  // Every live bunterm converges on one unambiguous /proc/<pid>/cmdline:
85
85
  // /bin/bunterm /bin/bunterm /dev/ttyN [options...]
@@ -94,7 +94,7 @@ const main = async () => {
94
94
  Object.entries(process.env).filter((entry) => typeof entry[1] === "string"),
95
95
  );
96
96
  const targetConsoleNumber = virtualConsoleNumber(options.console);
97
- if (consoleArgumentIndex !== -1 && targetConsoleNumber !== activeConsoleNumber) {
97
+ if (targetConsoleNumber !== activeConsoleNumber) {
98
98
  const consoleFd = openSync(options.console, "r+");
99
99
  try {
100
100
  const child = Bun.spawn(
@@ -53,6 +53,19 @@ try { bindings = resolveBindings(options.binds); }
53
53
  catch (error) { fail(`invalid bind: ${error.message}`); }
54
54
 
55
55
 
56
+ // dlopen.js resolves each symbol lazily. Bind everything needed after mounts
57
+ // or chroot before the host libc path can disappear from this process's root.
58
+ // Even --keep-mounts needs umount2 when setup fails and must be rolled back.
59
+ try {
60
+ void libc.symbols.chroot;
61
+ if (options.mount || bindings.length) {
62
+ void libc.symbols.mount;
63
+ void libc.symbols.umount2;
64
+ }
65
+ errno();
66
+ void libc.symbols.strerror;
67
+ } catch (error) { fail(error.message); }
68
+
56
69
  const report = (message) => { if (options.verbose) console.error(`chroot: ${message}`); };
57
70
 
58
71
  // ---------------------------------------------------------------- mounts --
@@ -0,0 +1,11 @@
1
+ #!/bin/bun
2
+ import { parseChvt, switchVt } from "../lib/chvt.js";
3
+ import { showDocument } from "../lib/document.js";
4
+ try {
5
+ const options = parseChvt(process.argv.slice(2));
6
+ if (options.help) await showDocument("chvt");
7
+ else await switchVt(options.number);
8
+ } catch (error) {
9
+ console.error(`chvt: ${error.message}`);
10
+ process.exitCode = 1;
11
+ }
@@ -0,0 +1,8 @@
1
+ #!/bin/bun
2
+ import {parseDhclient,runDhclient} from '../lib/dhclient.js';
3
+ import {showDocument} from '../lib/document.js';
4
+ try {
5
+ const options=parseDhclient(Bun.argv.slice(2));
6
+ if(options.help)await showDocument('dhclient');
7
+ else process.exitCode=await runDhclient(options);
8
+ }catch(error){console.error(`dhclient: ${error.message}`);process.exitCode=1;}
@@ -325,6 +325,10 @@ const doMount = (source, target, type, flags, data, { verbose, fake }) => {
325
325
  console.log(`mount(${source}, ${target}, ${type ?? "(none)"}, [${words.join(",")}], "${data}")`);
326
326
  }
327
327
  if (fake) return;
328
+ // A bind over /lib can hide libc before the read-only remount pass fails.
329
+ void libc.symbols.mount;
330
+ errno();
331
+ void libc.symbols.strerror;
328
332
  const dataBytes = data ? cString(data) : null;
329
333
  const typeBytes = type ? cString(type) : null;
330
334
  const result = libc.symbols.mount(
@@ -0,0 +1,8 @@
1
+ #!/bin/bun
2
+ import {parsePing,runPing} from '../lib/ping.js';
3
+ import {showDocument} from '../lib/document.js';
4
+ try {
5
+ const options=parsePing(Bun.argv.slice(2));
6
+ if(options.help)await showDocument('ping');
7
+ else process.exitCode=await runPing(options);
8
+ } catch(error) {console.error(`ping: ${error.message}`);process.exitCode=2;}
@@ -58,6 +58,12 @@ const unmount = (target) => {
58
58
  return false;
59
59
  };
60
60
 
61
+ // A previous target may contain libc; later failures still need strerror.
62
+ if (targets.length) {
63
+ try { void libc.symbols.umount2; errno(); void libc.symbols.strerror; }
64
+ catch (error) { console.error(`umount: ${error.message}`); process.exit(1); }
65
+ }
66
+
61
67
  let status = 0;
62
68
  for (const target of targets) {
63
69
  const path = target.length > 1 ? target.replace(/\/+$/, "") : target;
@@ -0,0 +1,33 @@
1
+ #!/bin/bun
2
+ import { browserTarget, parseOpenArgs, openError, openExitCode, releaseVersion } from "../lib/xdg-open.js";
3
+ import { showDocument } from "../lib/document.js";
4
+
5
+ try {
6
+ const options = parseOpenArgs(process.argv.slice(2));
7
+ if (options.help) {
8
+ await showDocument("xdg-open");
9
+ } else if (options.version) {
10
+ // Both image builders already generate this from buninu-linux's version.
11
+ const release = new URL("../usr/lib/os-release", import.meta.url);
12
+ let version;
13
+ if (await Bun.file(release).exists()) version = releaseVersion(await Bun.file(release).text());
14
+ else version = (await Bun.file(new URL("../../package.json", import.meta.url)).json()).version;
15
+ console.log(`xdg-open (Buninu Linux) ${version}`);
16
+ } else {
17
+ const url = browserTarget(options.target);
18
+ for (const tool of ["bunterm", "buninu-browser"])
19
+ if (!Bun.which(tool)) throw openError(`Required tool not found: ${tool}`, 3);
20
+ let child;
21
+ try {
22
+ child = Bun.spawn(["bunterm", "-e", "buninu-browser", url], {
23
+ stdio: ["inherit", "inherit", "inherit"],
24
+ });
25
+ } catch (error) {
26
+ throw openError(error.message, error.code === "ENOENT" ? 3 : 4);
27
+ }
28
+ process.exitCode = await child.exited === 0 ? 0 : 4;
29
+ }
30
+ } catch (error) {
31
+ console.error(`xdg-open: ${error.message}`);
32
+ process.exitCode = openExitCode(error);
33
+ }
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 - 2026-10-21
4
+
5
+ - Sync jsgotty to 1.2.0:
6
+ * Add opt-in Kitty keyboard support for disambiguated shortcuts, including
7
+ Super, and press/repeat/release events. Support flags 1, 2 and 8, mode
8
+ queries, and independent mode stacks for normal and alternate screens;
9
+ legacy keyboard encoding remains the default.
10
+ * Recover Super state on Android WebViews that deliver Meta key events but
11
+ omit the modifier state. Handle Android Alt shortcuts without accent
12
+ dead-key conversion, including recognizable keyup-only events.
13
+ * Preserve Alt on arrow, Insert, PageUp and PageDown shortcuts, and encode
14
+ Ctrl+/ as the conventional US control character.
15
+ * Add a manual keyboard tester with readable, colored shortcut names and
16
+ raw sequences; restore keyboard and raw modes when it exits.
17
+ * Allow local Kitty graphics consumers to request binary image data,
18
+ avoiding a base64 conversion; browser clients retain base64 by default.
19
+ - Sync bunmsh to 0.4.1: use the existing linker-aware shell launcher for the
20
+ awk fallback's subprocess commands instead of bypassing linker handling.
21
+
3
22
  ## 0.4.21 - 2026-10-15
4
23
 
5
24
  - Sync jsgotty to 1.1.16: Turn on pixel report
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable user-visible changes to bunmsh are documented here.
4
4
 
5
+ ## [0.4.1] - 2026-10-13
6
+
7
+ - Fixed awk bypassing linker protection
8
+
5
9
  ## [0.4.0] - 2026-10-13
6
10
 
7
11
  ### Added and fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunmsh",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Cross-platform command shell for Bun JavaScript",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -302,7 +302,7 @@ async function interactive(state) {
302
302
  const timer = setTimeout(() => {
303
303
  if (pendingCursorQuery?.resolve === resolve) pendingCursorQuery = null;
304
304
  resolve(null);
305
- }, 60);
305
+ }, 400);
306
306
  pendingCursorQuery = {
307
307
  resolve(position) {
308
308
  clearTimeout(timer);
@@ -3,6 +3,11 @@
3
3
  [中文 README](README.md)
4
4
 
5
5
  ## News
6
+
7
+ ### 2026/10/21
8
+
9
+ - Added [Kitty keyboard](#kitty-keyboard) support for distinct modifier combinations, including Super, and press/repeat/release events.
10
+
6
11
  ### 2026/08/16
7
12
  - Fixed the disconnected-screen `Reconnect` button not being clickable in WebGL mode.
8
13
  - The `Reconnect` UI now stays above the WebGL canvas and receives mouse and touch events correctly; Kitty image handling no longer intercepts right-click / long-press for entering a reconnect token or PID.
@@ -238,6 +243,41 @@ The `jsgotty` server has two connection modes:
238
243
  - On disconnect, the overlay provides a `Reconnect` button for immediate manual retry.
239
244
  - Right-click / long-press on that button lets you enter a custom reconnect token or PID.
240
245
 
246
+ ## Kitty keyboard
247
+
248
+ The web terminal supports the shortcut-related parts of the [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), separately from Kitty graphics. Legacy keyboard encoding remains the default. A terminal application can opt in:
249
+
250
+ ```sh
251
+ printf '\033[>11u' # 1: disambiguation + 2: repeat/release + 8: all keys
252
+ printf '\033[<u' # restore the previous mode when the application exits
253
+ ```
254
+
255
+ The receiving application must decode these sequences and restore the mode in its exit/finally handler; do not leave an ordinary shell in this mode. Negotiation supports `CSI ? u`, `CSI = flags ; mode u`, push and pop. Normal and alternate screens have independent stacks, limited to 32 entries. Reconnection, full reset and soft reset clear the state. Use flag `1` for unambiguous shortcuts, or `11` for modifier keys and press/repeat/release reporting.
256
+
257
+ DOM Meta maps to Kitty **Super (bit 8)**, not the separate Meta bit (32). Super+X is `CSI 120;9u`; Ctrl+Shift+Super+X is `CSI 120;14u`. Release events add `:3` when enabled.
258
+
259
+ Supported flags are **1, 2 and 8**; alternate keys (4) and associated text (16) are not implemented or reflected in the negotiated flags. IME composition/text commits and paste retain their existing paths; physical keys and releases cannot be inferred from committed text. Letters, digits, punctuation, navigation, Tab/Enter, function keys, keypad keys and standard Kitty media keys retain distinct modifiers. OS/browser-reserved shortcuts may never reach the page. Keys without standard Kitty codes, such as brightness and microphone mute, are not assigned private encodings.
260
+
261
+ This implements **jsgotty → PTY** encoding. Controlling Hyprland/Omarchy still requires Casty or another receiver to enable/decode Kitty keyboard and forward Super and release events. It does not complete the remote desktop input chain or change the mouse protocol.
262
+
263
+ Reload the jsgotty page after updating. Run the manual tester inside its terminal:
264
+
265
+ ```sh
266
+ bun tests/manual/kitty-keyboard.js
267
+ ```
268
+
269
+ It prints negotiated flags, raw sequences, modifiers and event types, exits after 60 seconds or unmodified Escape/q/Ctrl-C, and restores the previous keyboard/raw modes. An optional real bundled-xterm test uses an already installed browser:
270
+
271
+ ```sh
272
+ CHROMIUM_HEADLESS=/path/to/chromium-headless-shell node --test tests/kitty-keyboard-browser.test.js
273
+ ```
274
+
275
+ ## Android Keyboards
276
+
277
+ For Android WebViews that deliver Meta down/up but lose `metaKey`, jsgotty tracks both Meta keys inside its own input handler. Blur or page hiding clears held state and sends releases for reported keys when event reporting is enabled.
278
+
279
+ In the Android terminal, Alt with letters, digits, punctuation, or space sends terminal Alt sequences based on physical key positions (US layout, including Shift), bypassing accent dead-key handling. Alt-E/I/N/U therefore no longer compose characters such as é, ê, ñ, and ü; input methods and paste remain available. When WebView omits `code`, recognized legacy `keyCode` values provide a fallback. Dead keys delivered only as `keyup` send the shortcut on release, without duplicating a previously handled `keydown`. Active composition, Ctrl, Meta, AltGr, and unidentified keys retain their existing handling. This applies only inside the jsgotty terminal and does not modify Android system keyboard settings.
280
+
241
281
  ## Tested platforms
242
282
  - Windows x64: Windows 11
243
283
  - Linux x64: CachyOS
@@ -3,6 +3,11 @@
3
3
  [English README](README.en.md)
4
4
 
5
5
  ## News
6
+
7
+ ### 2026/10/21
8
+
9
+ - 新增 [Kitty keyboard](#kitty-keyboard) 支援,可區分 Super 等修飾鍵組合,並回報按下、重複與放開事件。
10
+
6
11
  ### 2026/08/16
7
12
  - 修正 WebGL 模式下斷線畫面的 `Reconnect` 按鈕無法點擊。
8
13
  - `Reconnect` 介面現在會正確位於 WebGL canvas 上方並接收滑鼠與觸控事件;右鍵/長按輸入 reconnect token 或 PID 也不會被 Kitty 圖片事件攔截。
@@ -238,6 +243,41 @@ bun gotty.js --client -k <token|pid> [target]
238
243
  - 斷線時會提供 `Reconnect` 按鈕,可手動立即重試。
239
244
  - 該按鈕右鍵/長按可輸入自訂 reconnect token 或 PID。
240
245
 
246
+ ## Kitty keyboard
247
+
248
+ 網頁終端現在支援 [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) 的快捷鍵相關功能,與 Kitty 圖片協定分開。預設仍是原本的鍵盤編碼;由終端內的程式選擇開啟:
249
+
250
+ ```sh
251
+ printf '\033[>11u' # 1:消除歧義 + 2:重複/放開事件 + 8:回報所有按鍵
252
+ printf '\033[<u' # 程式退出時還原上一層模式
253
+ ```
254
+
255
+ 程式必須能解析新編碼,並在 finally/退出處理中還原模式,不建議直接在一般 shell 長期開啟。支援 `CSI ? u` 查詢、`CSI = flags ; mode u` 設定/加上/移除旗標、`CSI > flags u` push 與 `CSI < count u` pop。主畫面與 alternate screen 各有獨立堆疊(上限 32 層);重連、完整重設或 soft reset 會清除狀態。只需要無歧義快捷鍵時可用旗標 `1`;需要獨立修飾鍵及所有按鍵的 down/repeat/up 時使用 `11`。
256
+
257
+ 網頁的 Meta 對應 Kitty 的 **Super(bit 8)**,不是協定中的另一個 Meta(bit 32)。例如 Super+X 是 `CSI 120;9u`,Ctrl+Shift+Super+X 是 `CSI 120;14u`;啟用事件回報後,放開會帶 `:3`。
258
+
259
+ 目前支援旗標 **1、2、8**;尚未實作替代鍵位(4)與附帶文字(16),查詢只回報實際啟用的旗標。IME 組字/文字提交與貼上保留既有路徑,不保證能回報實體按鍵或放開事件。字母、數字、標點、方向鍵、Tab、Enter、功能鍵、數字鍵盤與有標準 Kitty 編碼的媒體鍵可區分修飾組合;系統/瀏覽器先攔截的快捷鍵不會到達網頁,亮度、麥克風靜音等沒有標準 Kitty 鍵碼的鍵也不會擅自編成私有序列。
260
+
261
+ 這完成的是 **jsgotty → PTY** 的鍵盤編碼。要操作 Hyprland/Omarchy,Casty 或其他接收程式仍須啟用、解析 Kitty keyboard 並轉送 Super 與放開事件;此項改動不代表整條遠端桌面輸入鏈已完成,也不改變滑鼠協定。
262
+
263
+ 在 jsgotty 終端內手動確認(60 秒後自動退出,或按未修飾的 Esc/q/Ctrl+C):
264
+
265
+ ```sh
266
+ bun tests/manual/kitty-keyboard.js
267
+ ```
268
+
269
+ 測試會印出協商結果、原始序列、修飾鍵及 press/repeat/release;結束時還原鍵盤模式與 raw mode。更新後請重新載入 jsgotty 網頁。可用既有 Chromium 執行真正 xterm 前端測試,不會自動安裝瀏覽器:
270
+
271
+ ```sh
272
+ CHROMIUM_HEADLESS=/path/to/chromium-headless-shell node --test tests/kitty-keyboard-browser.test.js
273
+ ```
274
+
275
+ ## Android 鍵盤
276
+
277
+ Android WebView 若有 `Meta` down/up 卻漏掉 `metaKey`,jsgotty 會在自己的輸入處理中追蹤左右 Meta 並還原 Super。失焦或頁面隱藏時清除按住狀態;已送出的按鍵在事件回報模式下會補送放開。
278
+
279
+ Android 終端的 Alt+字母、數字、標點及空白會依實體按鍵位置(美式配置,支援 Shift)送出終端 Alt 序列,不進入重音死鍵流程。因此 Alt-E/I/N/U 等組合不再用來輸入 é、ê、ñ、ü;仍可使用輸入法或貼上。若 WebView 省略 `code`,會回退使用可辨識的 `keyCode`;若死鍵只送出 `keyup`,則在放開時補送一次,避免與已處理的 `keydown` 重複。正在組字、Ctrl、Meta、AltGr,以及無法辨識按鍵的事件維持原本處理。此規則只作用於 jsgotty 終端,不修改 Android 的系統鍵盤配置。
280
+
241
281
  ## Tested platforms 已測試作業系統
242
282
  - Windows x64: Windows 11
243
283
  - Linux x64: CachyOS
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "jsgotty",
3
- "version": "1.1.16",
3
+ "version": "1.2.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "jsgotty",
9
- "version": "1.1.16",
9
+ "version": "1.2.0",
10
10
  "dependencies": {
11
11
  "node-addon-api": "^7.1.1",
12
12
  "ws": "^8.18.3"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsgotty",
3
- "version": "1.1.16",
3
+ "version": "1.2.0",
4
4
  "description": "Share your terminal as a web application: A JavaScript rewrite of GoTTY",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -30,6 +30,7 @@
30
30
  <script src="./js/windows_bridge.js"></script>
31
31
  <script src="./js/zmodem.js"></script>
32
32
  <script src="./js/xterm-addon-unicode11.js"></script>
33
+ <script src="./js/kitty-keyboard.js"></script>
33
34
  <script src="./js/gotty.js"></script>
34
35
 
35
36