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
@@ -0,0 +1,105 @@
1
+ // Firmware is image data, not a running service. Only cfg.gpu loads GPU drivers.
2
+ import { createHash } from "node:crypto";
3
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, symlinkSync, statSync, mkdtempSync, renameSync } from "node:fs";
4
+ import { join, dirname, posix } from "node:path";
5
+ import pins from "./gpu-firmware.json" with { type: "json" };
6
+ import { config, runIfMain, withUmask022 } from "./arch.js";
7
+ import { download, readTar } from "./fetch.js";
8
+
9
+ const fingerprint = (cfg) => createHash("sha256").update(JSON.stringify({ revision: 2, arch: cfg.arch, release: cfg.kernelRelease, real: !!cfg.real, noGpu: !!cfg.noGpu, pins })).digest("hex");
10
+ const marker = (cfg) => join(cfg.modulesRoot, "gpu-support.json");
11
+
12
+ export const gpuReady = (cfg) => {
13
+ try {
14
+ const state = JSON.parse(readFileSync(marker(cfg), "utf8"));
15
+ if (cfg.noGpu && existsSync(join(cfg.nativeDir, "lib/firmware"))) return false;
16
+ return state.fingerprint === fingerprint(cfg) && state.files.every(path => statSync(join(cfg.nativeDir, path)).size > 0);
17
+ } catch { return false; }
18
+ };
19
+
20
+ // Validate paths before writing; firmware packages can link across GPU folders.
21
+ export const firmwarePath = (name) => {
22
+ if (!name.startsWith("lib/firmware/") || name.includes("\\") || posix.normalize(name) !== name || name.endsWith("/"))
23
+ throw Error(`Invalid firmware path: ${name}`);
24
+ return name;
25
+ };
26
+ export const firmwareLinkTarget = (name, { target, hard }) => {
27
+ firmwarePath(name);
28
+ if (target.startsWith("/") || target.includes("\\")) throw Error(`Invalid firmware link: ${name} -> ${target}`);
29
+ return firmwarePath(posix.normalize(hard ? target : posix.join(posix.dirname(name), target)));
30
+ };
31
+
32
+ export const extractFirmware = (archives, staging) => {
33
+ const files = new Map(), links = new Map();
34
+ for (const archive of archives) {
35
+ for (const [name, bytes] of archive.entries()) if (name.startsWith("lib/firmware/")) files.set(firmwarePath(name), bytes);
36
+ for (const [name, link] of archive.links()) if (name.startsWith("lib/firmware/")) {
37
+ firmwareLinkTarget(name, link);
38
+ links.set(name, link);
39
+ }
40
+ }
41
+ const directories = new Set();
42
+ for (const name of [...files.keys(), ...links.keys()])
43
+ for (let parent = posix.dirname(name); parent !== "."; parent = posix.dirname(parent)) directories.add(parent);
44
+ // NVIDIA also aliases entire GPU firmware directories. Resolve links in any
45
+ // path component, without following host filesystem links during validation.
46
+ const resolve = (name, seen = new Set()) => {
47
+ if (seen.has(name)) throw Error(`Firmware link cycle: ${name}`);
48
+ seen.add(name);
49
+ for (let prefix = name; prefix !== "."; prefix = posix.dirname(prefix)) {
50
+ const link = links.get(prefix);
51
+ if (link) {
52
+ if (seen.has("link:" + prefix)) throw Error(`Firmware link cycle: ${prefix}`);
53
+ seen.add("link:" + prefix);
54
+ return resolve(firmwareLinkTarget(prefix, link) + name.slice(prefix.length), seen);
55
+ }
56
+ }
57
+ if (files.has(name)) return files.get(name);
58
+ if (directories.has(name)) return null;
59
+ throw Error(`Missing firmware link target: ${name}`);
60
+ };
61
+ for (const name of [...files.keys(), ...links.keys()]) {
62
+ for (let parent = posix.dirname(name); parent !== "."; parent = posix.dirname(parent))
63
+ if (links.has(parent) || files.has(parent)) throw Error(`Firmware parent is not a directory: ${parent}`);
64
+ if (links.has(name)) {
65
+ if (files.has(name)) throw Error(`Duplicate firmware file/link: ${name}`);
66
+ if (resolve(name) === null && links.get(name).hard) throw Error(`Firmware hardlink to directory: ${name}`);
67
+ }
68
+ }
69
+ for (const [name, bytes] of files) {
70
+ const path = join(staging, name); mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, bytes);
71
+ }
72
+ for (const [name, link] of links) {
73
+ const path = join(staging, name); mkdirSync(dirname(path), { recursive: true });
74
+ if (files.has(name)) throw Error(`Duplicate firmware file/link: ${name}`);
75
+ if (link.hard) writeFileSync(path, resolve(name));
76
+ else symlinkSync(link.target, path);
77
+ }
78
+ return [...files.keys(), ...links.keys()];
79
+ };
80
+
81
+ export const fetchGpuFirmware = (cfg = config()) => withUmask022(async () => {
82
+ const dep = readFileSync(join(cfg.modulesRoot, "modules.dep"), "utf8");
83
+ const builtin = readFileSync(join(cfg.modulesRoot, "modules.builtin"), "utf8");
84
+ const pc = !cfg.noGpu && cfg.arch === "x86_64" && /\/(?:i915|xe|amdgpu|radeon|nouveau)\.ko(?:[:\n]|$)/.test(dep + "\n" + builtin);
85
+ // Firmware bytes are architecture-independent; these pins refer to the x86_64 APKs.
86
+ const packages = pc ? pins : [];
87
+ const archives = [];
88
+ for (const pkg of packages) archives.push(readTar(await download(cfg, `${cfg.alpineBase}/${pkg.name}`, pkg.name, pkg.sha256)));
89
+ mkdirSync(join(cfg.nativeDir, "lib"), { recursive: true });
90
+ const staging = mkdtempSync(join(cfg.nativeDir, "lib", ".gpu-firmware-"));
91
+ try {
92
+ const files = extractFirmware(archives, staging);
93
+ const target = join(cfg.nativeDir, "lib/firmware");
94
+ // This generated directory is owned by the firmware fetch stage.
95
+ rmSync(target, { recursive: true, force: true });
96
+ if (files.length) renameSync(join(staging, "lib/firmware"), target);
97
+ writeFileSync(marker(cfg), JSON.stringify({ fingerprint: fingerprint(cfg), packages, files }, null, 2) + "\n");
98
+ console.log(`GPU support: ${packages.length} firmware packages, ${files.length} files/links; drivers stay opt-in via cfg.gpu`);
99
+ } finally { rmSync(staging, { recursive: true, force: true }); }
100
+ });
101
+
102
+ runIfMain(import.meta.url, async () => {
103
+ if (process.argv.includes("--check")) process.exitCode = gpuReady(config()) ? 0 : 1;
104
+ else await fetchGpuFirmware();
105
+ });
package/scripts/fetch.js CHANGED
@@ -73,7 +73,9 @@ export const download = async (cfg, url, name, sha256) => {
73
73
  export const readTar = (path) => {
74
74
  const data = gunzipSync(readFileSync(path));
75
75
  const files = new Map();
76
+ const links = new Map();
76
77
  let longName = null;
78
+ let longLink = null;
77
79
  for (let offset = 0; offset + 512 <= data.length;) {
78
80
  const header = data.subarray(offset, offset + 512);
79
81
  if (header.every((byte) => byte === 0)) { offset += 512; continue; }
@@ -87,19 +89,25 @@ export const readTar = (path) => {
87
89
  for (const record of body.toString("utf8").split("\n")) {
88
90
  const match = /^\d+ path=(.*)$/.exec(record);
89
91
  if (match) longName = match[1];
92
+ const link = /^\d+ linkpath=(.*)$/.exec(record);
93
+ if (link) longLink = link[1];
90
94
  }
91
95
  continue;
92
96
  }
93
97
  if (type === "L") { longName = body.toString("utf8").replace(/\0.*$/s, ""); continue; }
98
+ if (type === "K") { longLink = body.toString("utf8").replace(/\0.*$/s, ""); continue; }
94
99
  if (type === "g") continue;
95
100
  const prefix = field(345, 155);
96
101
  const name = longName ?? (prefix ? `${prefix}/${field(0, 100)}` : field(0, 100));
97
102
  longName = null;
98
103
  if (type === "0" || type === "\0") files.set(name.replace(/^\.\//, ""), body);
104
+ if (type === "1" || type === "2") links.set(name.replace(/^\.\//, ""), { target: longLink ?? field(157, 100), hard: type === "1" });
105
+ longLink = null;
99
106
  }
100
107
  return {
101
108
  // Enumerate regular files without exposing the Map's mutation methods.
102
109
  entries() { return files.entries(); },
110
+ links() { return links.entries(); },
103
111
  get(name) {
104
112
  const bytes = files.get(name);
105
113
  if (!bytes) throw new Error(`${path}: no ${name}`);
@@ -132,7 +140,7 @@ export const fetchAlpine = (cfg = config()) => withUmask022(async () => {
132
140
  const release = `lib/modules/${cfg.kernelRelease}`;
133
141
  const dep = linux.get(`${release}/modules.dep`).toString();
134
142
  const builtin = linux.get(`${release}/modules.builtin`);
135
- const { selected, notes } = select({ dep, builtin: builtin.toString(), packageName: cfg.linuxPackage, arch: cfg.arch, flavor: cfg.flavor, real: cfg.real });
143
+ const { selected, notes } = select({ dep, builtin: builtin.toString(), packageName: cfg.linuxPackage, arch: cfg.arch, flavor: cfg.flavor, real: cfg.real, noGpu: cfg.noGpu });
136
144
  for (const note of notes) console.error(note);
137
145
  rmSync(cfg.modulesRoot, { recursive: true, force: true });
138
146
  writeTo(join(cfg.modulesRoot, "modules.builtin"), builtin);
@@ -142,6 +150,8 @@ export const fetchAlpine = (cfg = config()) => withUmask022(async () => {
142
150
  writeTo(join(cfg.modulesRoot, "modules.dep"), trimDependencies(dep, selected));
143
151
  writeTo(join(cfg.modulesRoot, "modules.alias"), filterAliases(linux.get(`${release}/modules.alias`).toString(), selected));
144
152
 
153
+ await (await import("./fetch-gpu.js")).fetchGpuFirmware(cfg);
154
+
145
155
  chmodSync(join(cfg.nativeDir, "lib", cfg.muslLoader), 0o755);
146
156
  console.log(`Fetched Alpine ${cfg.arch} linux-${cfg.flavor} kernel, ${selected.length} modules, musl, and the ${cfg.efiStub} UKI stub.`);
147
157
  });
@@ -0,0 +1,22 @@
1
+ [
2
+ {
3
+ "name": "linux-firmware-amdgpu-20260519-r0.apk",
4
+ "sha256": "70bc5c972a2da62b84b53e06b6d4c5aec0a8e9d6d280802755c3c8643f3bb8c6"
5
+ },
6
+ {
7
+ "name": "linux-firmware-i915-20260519-r0.apk",
8
+ "sha256": "3946f8b0a8f0dd187bd71d9aa9b3085a10cb1e8cf4700aa876799e6368309fa4"
9
+ },
10
+ {
11
+ "name": "linux-firmware-nvidia-20260519-r0.apk",
12
+ "sha256": "9fd3fa7eff65792ef8558c01a27246862e2701a99ef97dba5111b9f6db2c80ee"
13
+ },
14
+ {
15
+ "name": "linux-firmware-radeon-20260519-r0.apk",
16
+ "sha256": "3b0d6d3982563d17df0c33c2d695a5a547487cb56f2fd9a004ed729fac8f0684"
17
+ },
18
+ {
19
+ "name": "linux-firmware-xe-20260519-r0.apk",
20
+ "sha256": "6ebc37da64ecb48973c1814d1499c2c2f8f38593e8cf7227811d840c8b17172f"
21
+ }
22
+ ]
@@ -16,11 +16,15 @@
16
16
  const required = (names) => names.map((name) => ({ name }));
17
17
  const optional = (names) => names.map((name) => ({ name, optional: true }));
18
18
 
19
- export const requests = ({ arch, flavor, real }) => [
19
+ export const requests = ({ arch, flavor, real, noGpu = false }) => [
20
+ // Pack, but never coldplug at boot or through cfg.all. cfg.gpu matches hardware.
21
+ ...(!noGpu ? optional(["virtio-gpu", "vmwgfx"]) : []),
22
+ ...(!noGpu && arch === "x86_64" ? optional(["i915", "xe", "amdgpu", "radeon", "nouveau"]) : []),
20
23
  ...required([
21
24
  "failover",
22
25
  "net_failover",
23
26
  "virtio_net",
27
+ "af_packet", // DHCP before an interface has an IPv4 address
24
28
  ]),
25
29
  ...optional([
26
30
  "virtio_pci",
@@ -1,24 +1,24 @@
1
1
  # npm packages
2
2
  # Latest versions checked on 2026-10-11; SHA-256 pins verified against registry SHA-512 integrity.
3
- # buninu-browser updated and verified on 2026-10-12.
4
- # casty 1.3.7 and buninu-land 0.1.3 verified on 2026-10-16.
3
+ # buninu-browser 0.3.0, buninu-land 0.2.0, casty 1.4.1, and jspulse 0.2.3 verified on 2026-10-22.
4
+ # jspinyin 0.2.0 verified on 2026-10-22.
5
5
 
6
- "buninu-browser@0.2.3":
7
- - https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.2.3.tgz
8
- - sha256:0f7c9cd9df689d7b0900fdf3172851125ebd8aba12bed555be7e091d427146e8
6
+ "buninu-browser@0.3.0":
7
+ - https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.3.0.tgz
8
+ - sha256:4590c523f76ce7c3a079894b80a541b788c95895036d0094c0e98c2acc6d5179
9
9
 
10
- "buninu-land@0.1.3":
11
- - https://registry.npmjs.org/buninu-land/-/buninu-land-0.1.3.tgz
12
- - sha256:b2c94f7fa0f7275ed739f4949d0be3ef45a99bf0b61e63a71ea9d6459f9f1c2e
10
+ "buninu-land@0.2.0":
11
+ - https://registry.npmjs.org/buninu-land/-/buninu-land-0.2.0.tgz
12
+ - sha256:543098c9f934fe4d723ca65b5a0c8a0bd11a04268228c23c3272073dc4549b56
13
13
 
14
- "@drxiaozhi/casty@1.3.7":
15
- - https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.3.7.tgz
16
- - sha256:acc0641fb68c00222ba1cd2b7520ba99c269b996e145e82f18466be4d126b811
14
+ "@drxiaozhi/casty@1.4.1":
15
+ - https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.4.1.tgz
16
+ - sha256:0179c699a82202dc3d84f8736c8d88b29941d87995d6c6b554fd534daa70d280
17
17
 
18
- "@drxiaozhi/jspulse@0.2.1":
19
- - https://registry.npmjs.org/@drxiaozhi/jspulse/-/jspulse-0.2.1.tgz
20
- - sha256:9a34a76b1edac6767fa90e33cbc4d3a511c082e9ab73fb196390f841aad37839
18
+ "@drxiaozhi/jspulse@0.2.3":
19
+ - https://registry.npmjs.org/@drxiaozhi/jspulse/-/jspulse-0.2.3.tgz
20
+ - sha256:c4a1a7adb559f94096243bebaac0890dc5096d518f7f83ed489c38d5cdfaa263
21
21
 
22
- "@drxiaozhi/jspinyin@0.1.1":
23
- - https://registry.npmjs.org/@drxiaozhi/jspinyin/-/jspinyin-0.1.1.tgz
24
- - sha256:c6d10a40b6c93a942d85dff69ed2e45dacf506b15caab6821688e8e29df810ed
22
+ "@drxiaozhi/jspinyin@0.2.0":
23
+ - https://registry.npmjs.org/@drxiaozhi/jspinyin/-/jspinyin-0.2.0.tgz
24
+ - sha256:b6dfb18f8a8602b9449b75823bce1790fd9682fac68fcbc57c55b4ebd98baa6e
@@ -0,0 +1,17 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {createFpsMeter} from '../initramfs/lib/bunterm/fps.js';
3
+ test('FPS counts new image presentations, ignores redraws, idles at zero and resets on toggle',()=>{
4
+ let time=0;const meter=createFpsMeter(()=>time),old={},a={},b={};
5
+ meter.presented([{image:old}]);expect(meter.enabled).toBe(false);
6
+ expect(meter.toggle([{image:old}])).toBe(true);
7
+ meter.presented([{image:old}]);
8
+ meter.presented([{image:a}]);meter.presented([{image:a}]);
9
+ meter.presented([{image:a},{image:b}]);meter.presented([]);
10
+ time=1000;expect(meter.sample()).toBe(2);
11
+ time=2000;expect(meter.sample()).toBe(0);
12
+ expect(meter.toggle()).toBe(false);
13
+ meter.presented([{image:{}}]);
14
+ time=3000;expect(meter.sample()).toBe(0);
15
+ meter.toggle([{image:b}]);meter.presented([{image:b}]);
16
+ meter.presented([{image:{}}]);time=3500;expect(meter.sample()).toBe(2);
17
+ });
@@ -0,0 +1,116 @@
1
+ import {test,expect} from "bun:test";
2
+ import {Terminal} from "../initramfs/lib/xterm/xterm-headless.mjs";
3
+ import {ConsoleKeys,MediumRawDecoder,legacyKey} from "../initramfs/lib/bunterm/keycodes.js";
4
+ import {attachKeyboard} from "../initramfs/lib/bunterm/keyboard.js";
5
+ const write = (term,text) => new Promise(resolve=>term.write(text,resolve));
6
+ const letters = {16:"q",17:"w",18:"e",19:"r",20:"t",21:"y",22:"u",23:"i",24:"o",25:"p",
7
+ 30:"a",31:"s",32:"d",33:"f",34:"g",35:"h",36:"j",37:"k",38:"l",44:"z",45:"x",46:"c",
8
+ 47:"v",48:"b",49:"n",50:"m"};
9
+ const keys = () => new ConsoleKeys({lookup:(table,n) => {
10
+ const letter = letters[n];
11
+ if (letter) return 0xb00 | (table & 1 ? letter.toUpperCase() : letter).charCodeAt(0);
12
+ return n === 57 ? 32 : n === 53 ? (table & 1 ? 63 : 47) : null;
13
+ }});
14
+ function fixture(extra = {}) {
15
+ const term = new Terminal({allowProposedApi:true}), output = [], modes = [], switches = [];
16
+ const keyboard = attachKeyboard({term,write:text=>output.push(text),
17
+ setEnhanced:v=>modes.push(v),switchVT:n=>switches.push(n),...extra});
18
+ return {term,output,modes,switches,keyboard,close(){keyboard.dispose();term.dispose();}};
19
+ }
20
+ test("mediumraw extended codes survive every possible read boundary", () => {
21
+ const bytes = Buffer.from([30,158,0,129,183,128,129,183]); // F13 = 183
22
+ for (let split=0;split<=bytes.length;split++) {
23
+ const decoder = new MediumRawDecoder(), events=[];
24
+ decoder.feed(bytes.subarray(0,split),(c,d)=>events.push([c,d]));
25
+ decoder.feed(bytes.subarray(split),(c,d)=>events.push([c,d]));
26
+ expect(events).toEqual([[30,true],[30,false],[183,true],[183,false]]);
27
+ }
28
+ });
29
+ test("all Ctrl/Alt/Shift/Super letter combinations stay distinct", async () => {
30
+ const f=fixture();
31
+ try {
32
+ await write(f.term,"\x1b[>9u");
33
+ for (let bits=0;bits<16;bits++) for (const [number,letter] of Object.entries(letters)) {
34
+ const state=keys();
35
+ for (const [mask,code] of [[1,42],[2,56],[4,29],[8,125]]) if(bits&mask) f.keyboard.event(state.event(code,true));
36
+ f.output.length=0;
37
+ f.keyboard.event(state.event(Number(number),true));
38
+ expect(f.output).toEqual([`\x1b[${letter.charCodeAt(0)}${bits?";"+(bits+1):""}u`]);
39
+ f.keyboard.blur();
40
+ }
41
+ } finally {f.close();}
42
+ });
43
+ test("push/pop, repeat/release, recovery keys, independent buffers and reset", async () => {
44
+ const f=fixture(), state=keys();
45
+ try {
46
+ await write(f.term,"\x1b[?u\x1b[>11u");
47
+ expect(f.output).toEqual(["\x1b[?0u"]);
48
+ f.output.length=0;
49
+ f.keyboard.event(state.event(125,true));
50
+ f.keyboard.event(state.event(50,true));
51
+ f.keyboard.event(state.event(50,true));
52
+ f.keyboard.event(state.event(50,false));
53
+ f.keyboard.event(state.event(125,false));
54
+ expect(f.output).toEqual(["\x1b[57444;9u","\x1b[109;9u","\x1b[109;9:2u","\x1b[109;9:3u","\x1b[57444;1:3u"]);
55
+ await write(f.term,"\x1b[>1u");
56
+ f.output.length=0;
57
+ f.keyboard.event(state.event(28,true));
58
+ f.keyboard.event(state.event(28,false));
59
+ f.keyboard.event(state.event(30,true));
60
+ expect(f.output).toEqual(["\r","a"]);
61
+ await write(f.term,"\x1b[<u");
62
+ expect(f.keyboard.flags).toBe(11);
63
+ await write(f.term,"\x1b[?1049h");
64
+ expect(f.keyboard.flags).toBe(0);
65
+ await write(f.term,"\x1b[?1049l");
66
+ expect(f.keyboard.flags).toBe(11);
67
+ await write(f.term,"\x1bc");
68
+ expect(f.keyboard.flags).toBe(0);
69
+ expect(f.modes).toEqual([true,false,true,false]);
70
+ } finally {f.close();}
71
+ });
72
+ test("local menu and reserved VT keys never reach the child", async () => {
73
+ let menu=false;
74
+ const f=fixture({localInput:()=>menu}), state=keys();
75
+ try {
76
+ await write(f.term,"\x1b[>11u");
77
+ f.keyboard.event(state.event(29,true));
78
+ f.keyboard.event(state.event(56,true));
79
+ f.output.length=0;
80
+ f.keyboard.event(state.event(60,true));
81
+ f.keyboard.event(state.event(60,false));
82
+ expect(f.switches).toEqual([2]);
83
+ expect(f.output).toEqual([]);
84
+ f.keyboard.blur(); f.output.length=0;
85
+ menu=true;
86
+ f.keyboard.event(state.event(30,true));
87
+ f.keyboard.event(state.event(30,false));
88
+ expect(f.output).toEqual([]);
89
+ } finally {f.close();}
90
+ });
91
+ test("layout translation, caps, AltGr, keypad and Unicode map", () => {
92
+ const state=keys();
93
+ state.event(58,true); state.event(58,false);
94
+ expect(state.event(30,true).key).toBe("A");
95
+ state.event(42,true);
96
+ expect(state.event(30,true).key).toBe("a");
97
+ expect(state.event(100,true).key).toBe("AltGraph");
98
+ expect(state.event(53,true).getModifierState("AltGraph")).toBe(true);
99
+ const unicode=new ConsoleKeys({lookup:()=>0xf000^0x4e2d});
100
+ expect(unicode.event(30,true).key).toBe("中");
101
+ const num=keys();
102
+ num.event(69,true);
103
+ expect(num.event(79,true).key).toBe("1");
104
+ expect(num.event(79,false).location).toBe(3);
105
+ expect(legacyKey({type:"keydown",key:"/",ctrlKey:true})).toBe("\x1f");
106
+ });
107
+ test("failed console transition resets flags and does not strand the protocol", async () => {
108
+ const errors=[];
109
+ const f=fixture({setEnhanced(){throw new Error("not a VT");},onError:e=>errors.push(e.message)});
110
+ try {
111
+ await write(f.term,"\x1b[>11u\x1b[?u");
112
+ expect(f.keyboard.flags).toBe(0);
113
+ expect(f.output).toEqual(["\x1b[?0u"]);
114
+ expect(errors).toEqual(["not a VT"]);
115
+ } finally {f.close();}
116
+ });
@@ -0,0 +1,9 @@
1
+ import {expect,test} from 'bun:test';
2
+ import {nextVirtualTerminal} from '../initramfs/lib/bunterm/vt.js';
3
+ test('new bunterm appends after known sessions and reserves the initial console',()=>{
4
+ expect(nextVirtualTerminal([])).toBe(2);
5
+ expect(nextVirtualTerminal([{number:1}])).toBe(2);
6
+ expect(nextVirtualTerminal([{number:1},{number:2},{number:6}])).toBe(7);
7
+ expect(nextVirtualTerminal([{number:4}])).toBe(5);
8
+ expect(()=>nextVirtualTerminal([{number:63}])).toThrow('No next tty');
9
+ });
@@ -0,0 +1,19 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {createRequire} from 'node:module';
3
+ import {preferredPixelFormat,bgraParser,BGRA_FORMAT} from '../initramfs/lib/bunterm/raw-protocol.js';
4
+ const {KittyGraphicsParser}=createRequire(import.meta.url)('../initramfs/buninu/apps/jsgotty/gotty.js');
5
+ const field=offset=>({offset,length:8,msbRight:0});
6
+ const fb=(red=16,blue=0)=>({bitsPerPixel:32,fields:{red:field(red),green:field(8),blue:field(blue),transp:{length:0}}});
7
+ test('native preference follows framebuffer bitfields; browser parser remains unchanged',()=>{
8
+ for(const [display,preferred] of [[fb(),'bgra'],[fb(0,16),'rgba'],[{bitsPerPixel:16},'png'],[undefined,'png']]){
9
+ expect(preferredPixelFormat(display)).toBe(preferred);
10
+ const p=new (bgraParser(KittyGraphicsParser,display))({binaryData:true});
11
+ const packet=Buffer.from(`a=q,f=${BGRA_FORMAT},s=1,v=1,i=41;AAAA/w==`);
12
+ expect(p.parsePacket(packet,{}).responseMessage).toBe('OK Buninu1 '+preferred);
13
+ expect(new KittyGraphicsParser().parsePacket(packet,{}).responseMessage).toBe('OK');
14
+ expect(p.images.size).toBe(0);
15
+ expect(p.parsePacket(Buffer.from(`a=q,f=${BGRA_FORMAT},s=2,v=1,i=41;AAAA/w==`),{}).responseMessage).toStartWith('ERROR:');
16
+ }
17
+ const bad=fb();bad.fields.red.msbRight=1;
18
+ expect(preferredPixelFormat(bad)).toBe('png');expect(preferredPixelFormat(fb(),false)).toBe('png');
19
+ });
@@ -0,0 +1,158 @@
1
+ import {test,expect} from "bun:test";
2
+ import {loadCanvasKit,loadFontSet} from "../initramfs/lib/canvas.js";
3
+ import {Renderer} from "../initramfs/lib/bunterm/renderer.js";
4
+ import {Terminal} from "../initramfs/lib/xterm/xterm-headless.mjs";
5
+ import {createVtMenu} from "../initramfs/lib/bunterm/vt-menu.js";
6
+
7
+ test("real CanvasKit redraws resized text and overlay with fresh font metrics", async () => {
8
+ const CanvasKit=await loadCanvasKit(), fonts=loadFontSet(CanvasKit,"terminal");
9
+ const surface=CanvasKit.MakeSurface(800,480);
10
+ const screen={width:800,height:480,canvas:surface.getCanvas(),flush:()=>surface.flush()};
11
+ const renderer=new Renderer({CanvasKit,screen,fonts});
12
+ const term=new Terminal({cols:renderer.columns,rows:renderer.rows,allowProposedApi:true});
13
+ try {
14
+ await new Promise(resolve=>term.write("Hello 中文 👨‍👩‍👧\r\nSelection and font resize",resolve));
15
+ const columns=renderer.columns;
16
+ renderer.render(term);
17
+ renderer.setFontSize(20);
18
+ expect(renderer.columns).toBeLessThan(columns);
19
+ term.resize(renderer.columns,renderer.rows);
20
+ const menu=createVtMenu({list:()=>[{number:1,label:"tty1 — initial console"}],select(){},changed(){renderer.invalidate();},
21
+ width:800,height:480,rowHeight:renderer.cellHeight,
22
+ controls:{fontSize:()=>renderer.fontSize,cellHeight:()=>renderer.cellHeight,execute:()=>""}});
23
+ menu.pointer({x:0,y:0,pressed:["left"],buttons:{left:true},wheel:0});
24
+ renderer.render(term,{overlay:()=>menu.draw(renderer)});
25
+ const image=surface.makeImageSnapshot();
26
+ expect(image.width()).toBe(800);
27
+ image.delete();
28
+ renderer.setFontSize(13);
29
+ expect(renderer.columns).toBe(columns);
30
+ expect(()=>renderer.setFontSize(NaN)).toThrow();
31
+ } finally {term.dispose();renderer.delete();fonts.delete();surface.delete();}
32
+ },20000);
33
+
34
+ test("local pointer restores exact pixels across images, edges, text and font changes", async () => {
35
+ const {openCanvas}=await import('../initramfs/lib/canvas.js');
36
+ const CK=await loadCanvasKit(), fonts=loadFontSet(CK,'terminal');
37
+ const make=()=>{
38
+ const display={width:320,height:200,stride:1296,bitsPerPixel:32,
39
+ fields:{red:{offset:16,length:8},green:{offset:8,length:8},blue:{offset:0,length:8}},pixels:new Uint8Array(1296*200)};
40
+ const screen=openCanvas(CK,display);screen.flush=()=>screen.surface.flush();
41
+ return screen;
42
+ };
43
+ const screen=make(),reference=make();
44
+ reference.drawOpaqueImage = undefined;
45
+ const r=new Renderer({CanvasKit:CK,screen,fonts}), ref=new Renderer({CanvasKit:CK,screen:reference,fonts});
46
+ const term=new Terminal({cols:r.columns,rows:r.rows,allowProposedApi:true});
47
+ const imageSurface=CK.MakeSurface(37,29);imageSurface.getCanvas().clear(CK.Color(30,90,170));
48
+ const image=imageSurface.makeImageSnapshot(), images=[{image,rect:[0,20,320,200]}];
49
+ let imageDraws=0;const draw=r.drawImages.bind(r);r.drawImages=(...args)=>{imageDraws++;return draw(...args);};
50
+ const check=(pointer,cursor={x:1,y:0,visible:true})=>{
51
+ const options={pointer,cursor,images};r.render(term,options);ref.invalidate();ref.render(term,options);
52
+ expect(screen.saveRegion(0,0,320,200).pixels).toEqual(reference.saveRegion(0,0,320,200).pixels);
53
+ };
54
+ try {
55
+ await new Promise(resolve=>term.write('Hello world',resolve));
56
+ check({x:100,y:100});const initial=imageDraws;
57
+ for(const point of [{x:101,y:101},{x:0,y:0},{x:319,y:199},{x:150.5,y:78.5},null])check(point);
58
+ expect(imageDraws).toBe(initial);
59
+ check({x:20,y:20});
60
+ await new Promise(resolve=>term.write('\rChanged',resolve));check({x:20,y:20});
61
+ check({x:50,y:50},{x:2,y:0,visible:false});
62
+ r.setFontSize(24);ref.setFontSize(24);term.resize(r.columns,r.rows);check({x:318,y:198});
63
+ check(null);
64
+ } finally {term.dispose();r.delete();ref.delete();image.delete();imageSurface.delete();screen.close();reference.close();fonts.delete();}
65
+ },20000);
66
+
67
+ test('opaque copies match Skia for clipped crops and dirty bands; transparency and scaling fall back', async()=>{
68
+ const {openCanvas}=await import('../initramfs/lib/canvas.js');
69
+ const CK=await loadCanvasKit();
70
+ const make=()=>openCanvas(CK,{width:64,height:48,stride:272,bitsPerPixel:32,
71
+ fields:{red:{offset:16,length:8},green:{offset:8,length:8},blue:{offset:0,length:8}},pixels:new Uint8Array(272*48)});
72
+ const a=make(),b=make();
73
+ const data=new Uint8Array(32*24*4);for(let i=0;i<data.length;i+=4){data[i]=i%251;data[i+1]=(i*7)%253;data[i+2]=(i*13)%255;data[i+3]=255;}
74
+ const info={width:32,height:24,colorType:CK.ColorType.RGBA_8888,alphaType:CK.AlphaType.Unpremul,colorSpace:CK.ColorSpace.SRGB};
75
+ const image=CK.MakeImage(info,data,128);
76
+ const bands=[[0,9],[12,35]];
77
+ try {
78
+ for(const rect of [[5,4,37,28],[-5,-3,27,21],[55,39,87,63]]){
79
+ a.canvas.clear(CK.Color(7,11,13));b.canvas.clear(CK.Color(7,11,13));
80
+ expect(a.drawOpaqueImage(image,rect,[0,0,32,24],bands)).toBe(true);
81
+ for(const [y0,y1] of bands){b.canvas.save();b.canvas.clipRect(CK.LTRBRect(0,y0,64,y1),CK.ClipOp.Intersect,false);b.canvas.drawImageRectOptions(image,CK.LTRBRect(0,0,32,24),CK.LTRBRect(...rect),CK.FilterMode.Linear,CK.MipmapMode.Linear,null);b.canvas.restore();}
82
+ expect(a.saveRegion(0,0,64,48).pixels).toEqual(b.saveRegion(0,0,64,48).pixels);
83
+ }
84
+ expect(a.drawOpaqueImage(image,[0,0,64,48],[0,0,32,24],bands)).toBe(false);
85
+ expect(a.drawOpaqueImage(image,[0.5,0,32.5,24],[0,0,32,24],bands)).toBe(false);
86
+ data[3]=100;const alpha=CK.MakeImage(info,data,128);
87
+ try{expect(a.drawOpaqueImage(alpha,[0,0,32,24],[0,0,32,24],bands)).toBe(false);}finally{alpha.delete();}
88
+ }finally{image.delete();a.close();b.close();}
89
+ },20000);
90
+
91
+ test('opaque image readback reuses WASM destination and falls back when native BGRA is rejected',async()=>{
92
+ const {openCanvas}=await import('../initramfs/lib/canvas.js');const CK=await loadCanvasKit();
93
+ const screen=openCanvas(CK,{width:8,height:8,stride:32,bitsPerPixel:32,
94
+ fields:{red:{offset:16,length:8},green:{offset:8,length:8},blue:{offset:0,length:8}},pixels:new Uint8Array(256)});
95
+ const info={width:8,height:8,colorType:CK.ColorType.RGBA_8888,alphaType:CK.AlphaType.Unpremul,colorSpace:CK.ColorSpace.SRGB};
96
+ const bytes=new Uint8Array(256);for(let i=0;i<256;i+=4)bytes.set([201,71,19,255],i);
97
+ const images=[],destinations=[];
98
+ try{
99
+ for(const fallback of [false,true]){
100
+ const image=CK.MakeImage(info,bytes,32);images.push(image);
101
+ const read=image.readPixels.bind(image);let reads=0;
102
+ image.readPixels=(x,y,format,destination,stride)=>{reads++;expect(destination).toBeTruthy();destinations.push(destination);
103
+ if(fallback && format.colorType===CK.ColorType.BGRA_8888)return null;
104
+ return read(x,y,format,destination,stride);};
105
+ for(let n=0;n<2;n++)expect(screen.drawOpaqueImage(image,[0,0,8,8],[0,0,8,8],[[0,8]])).toBe(true);
106
+ expect(reads).toBe(fallback?2:1);
107
+ const actual=screen.saveRegion(0,0,8,8).pixels;
108
+ for(let i=0;i<256;i+=4)expect([...actual.subarray(i,i+4)]).toEqual([19,71,201,255]);
109
+ }
110
+ expect(destinations.every(d=>d===destinations[0])).toBe(true);
111
+ }finally{images.forEach(i=>i.delete());screen.close();}
112
+ },20000);
113
+
114
+ test('raw images bypass Skia allocation/readback; covered text, removal, crops and fallback match full repaint', async()=>{
115
+ const {openCanvas}=await import('../initramfs/lib/canvas.js');
116
+ const {decodeImage}=await import('../initramfs/lib/bunterm/images.js');
117
+ const {skiaImage,rawImageData}=await import('../initramfs/lib/raw-image.js');
118
+ const CK=await loadCanvasKit(),fonts=loadFontSet(CK,'terminal');
119
+ const make=()=>{const s=openCanvas(CK,{width:320,height:200,stride:1296,bitsPerPixel:32,
120
+ fields:{red:{offset:16,length:8},green:{offset:8,length:8},blue:{offset:0,length:8}},pixels:new Uint8Array(1296*200)});s.flush=()=>s.surface.flush();return s;};
121
+ const screen=make(),reference=make();reference.drawOpaqueImage=undefined;reference.canDrawRawImage=undefined;
122
+ const r=new Renderer({CanvasKit:CK,screen,fonts}),ref=new Renderer({CanvasKit:CK,screen:reference,fonts});
123
+ const term=new Terminal({cols:r.columns,rows:r.rows,allowProposedApi:true});
124
+ const handles=[];let allocations=0,draws=0;
125
+ const makeImage=CK.MakeImage.bind(CK),drawRow=r.drawRow.bind(r);
126
+ const proxy=Object.create(CK);proxy.MakeImage=(...args)=>{allocations++;return makeImage(...args);};
127
+ r.drawRow=(...args)=>{draws++;return drawRow(...args);};
128
+ const create=async(format=0x42475241,alpha=255)=>{
129
+ const bytes=new Uint8Array(320*200*4);
130
+ for(let i=0;i<bytes.length;i+=4)bytes.set([i%251,83,211,alpha],i);
131
+ const handle=await decodeImage(proxy,{format,width:320,height:200,data:bytes},{deferRaw:true});handles.push(handle);
132
+ return handle;
133
+ };
134
+ const check=(image,rect=[0,0,320,200],source=[0,0,320,200],pointer=null,invalidate=true)=>{
135
+ const images=image?[{image,rect,source}]:[];
136
+ if(invalidate)r.invalidate();r.render(term,{images,pointer,cursor:{x:1,y:1,visible:true}});
137
+ const before=allocations;
138
+ ref.invalidate();ref.render(term,{images:images.map(p=>({...p,image:skiaImage(p.image)})),pointer,cursor:{x:1,y:1,visible:true}});
139
+ expect(screen.saveRegion(0,0,320,200).pixels).toEqual(reference.saveRegion(0,0,320,200).pixels);
140
+ return before;
141
+ };
142
+ try{
143
+ await new Promise(resolve=>term.write('Hello 中文\r\nHidden text',resolve));
144
+ const raw=await create();
145
+ expect(allocations).toBe(0);expect(check(raw)).toBe(0);expect(draws).toBe(0);
146
+ // Editing covered text must be visible once the image disappears.
147
+ await new Promise(resolve=>term.write('\rChanged underlying text',resolve));
148
+ check(raw,[0,0,320,200],[0,0,320,200],{x:100,y:100});
149
+ check(null,undefined,undefined,null,false);expect(draws).toBeGreaterThan(0);
150
+ check(raw,[-5,11,310,171],[5,20,320,180]);
151
+ check(raw,[0,0,160,100]); // scaling materializes lazily
152
+ check(await create(32)); // channel mismatch falls back
153
+ check(await create(0x42475241,100)); // transparency must paint underlying text
154
+ check(raw,[0.5,0,320.5,200]);
155
+ r.setFontSize(20);ref.setFontSize(20);term.resize(r.columns,r.rows);check(raw);check(null);
156
+ raw.delete();expect(rawImageData(raw)).toBeUndefined();raw.delete();
157
+ }finally{handles.forEach(i=>i.delete());term.dispose();r.delete();ref.delete();screen.close();reference.close();fonts.delete();}
158
+ },20000);
@@ -0,0 +1,38 @@
1
+ import {test,expect} from "bun:test";
2
+ import {Terminal} from "../initramfs/lib/xterm/xterm-headless.mjs";
3
+ import {createSelection} from "../initramfs/lib/bunterm/selection.js";
4
+ import {pasteText} from "../initramfs/lib/bunterm/clipboard.js";
5
+ const write = (term,text) => new Promise(resolve=>term.write(text,resolve));
6
+ const point = (x,y,pressed=[],released=[]) => ({x,y,pressed,released,moved:true});
7
+ test("selection joins wraps, includes wide graphemes and works backwards", async () => {
8
+ const term=new Terminal({cols:8,rows:4,allowProposedApi:true});
9
+ try {
10
+ await write(term,"ab中文efghij\r\nlast");
11
+ const s=createSelection(term,{offsetX:0,offsetY:0,cellWidth:1,cellHeight:1},()=>{});
12
+ s.pointer(point(3,0,["left"]));
13
+ s.pointer(point(1,1,[],["left"]));
14
+ expect(s.text()).toBe("中文efgh");
15
+ s.pointer(point(1,1,["left"]));
16
+ s.pointer(point(3,0,[],["left"]));
17
+ expect(s.text()).toBe("中文efgh");
18
+ s.pointer(point(0,1,["left"]));
19
+ s.pointer(point(3,2,[],["left"]));
20
+ expect(s.text()).toBe("ghij\nlast");
21
+ await write(term,"\x1b[?1000h");
22
+ expect(s.pointer(point(0,0,["left"]))).toBe(false);
23
+ expect(s.active).toBe(false);
24
+ } finally {term.dispose();}
25
+ });
26
+ test("paste uses one negotiated bracketed write, strips embedded ESC, normalizes LF", async () => {
27
+ const term=new Terminal({allowProposedApi:true}),out=[];
28
+ try {
29
+ pasteText(term,"a\nb\r\nc",s=>out.push(s));
30
+ expect(out.pop()).toBe("a\rb\rc");
31
+ await write(term,"\x1b[?2004h");
32
+ pasteText(term,"a\n\x1b[201~b",s=>out.push(s));
33
+ expect(out.pop()).toBe("\x1b[200~a\r[201~b\x1b[201~");
34
+ await write(term,"\x1b[?2004l");
35
+ pasteText(term,"text",s=>out.push(s));
36
+ expect(out).toEqual(["text"]);
37
+ } finally {term.dispose();}
38
+ });