buninu-linux 0.3.0 → 0.4.0

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 (75) hide show
  1. package/LICENSES/Apache-2.0.txt +202 -0
  2. package/LICENSES/BSD-3-Clause-Skia.txt +29 -0
  3. package/LICENSES/Bitstream-Vera.txt +44 -0
  4. package/LICENSES/MIT-xterm.js.txt +21 -0
  5. package/LICENSES/OFL-1.1.txt +91 -0
  6. package/NOTICE.md +18 -1
  7. package/README.md +226 -22
  8. package/fetch-alpine.sh +9 -4
  9. package/graphics.md +213 -0
  10. package/initramfs/bin/bunterm +66 -0
  11. package/initramfs/bin/ip +2 -1
  12. package/initramfs/bin/mount +2 -1
  13. package/initramfs/bin/poweroff +6 -27
  14. package/initramfs/bin/ps +256 -0
  15. package/initramfs/bin/reboot +10 -0
  16. package/initramfs/bin/tar +169 -0
  17. package/initramfs/bin/top +129 -0
  18. package/initramfs/bin/umount +2 -1
  19. package/initramfs/buninu/CHANGELOG.md +40 -0
  20. package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +36 -0
  21. package/initramfs/buninu/apps/bunmsh/README.md +7 -0
  22. package/initramfs/buninu/apps/bunmsh/package.json +1 -1
  23. package/initramfs/buninu/apps/bunmsh/src/fancy-ls.js +29 -14
  24. package/initramfs/buninu/apps/bunmsh/src/line-edit.js +43 -0
  25. package/initramfs/buninu/apps/bunmsh/src/main.js +15 -1
  26. package/initramfs/buninu/apps/bunmsh/src/mouse.js +29 -4
  27. package/initramfs/buninu/apps/bunmsh/src/shell.js +9 -1
  28. package/initramfs/buninu/apps/bunmsh/test/shell1.test.js +70 -0
  29. package/initramfs/buninu/apps/jsgotty/gotty.js +4 -3
  30. package/initramfs/buninu/apps/jsgotty/package.json +1 -1
  31. package/initramfs/buninu/apps/jsmdcui/CHANGELOG.md +10 -0
  32. package/initramfs/buninu/apps/jsmdcui/README.md +2 -0
  33. package/initramfs/buninu/apps/jsmdcui/package.json +1 -1
  34. package/initramfs/buninu/apps/jsmdcui/runtime/help/commands.md +4 -1
  35. package/initramfs/buninu/apps/jsmdcui/runtime/help/help.md +2 -0
  36. package/initramfs/buninu/apps/jsmdcui/src/index.js +40 -13
  37. package/initramfs/buninu/package.json +1 -1
  38. package/initramfs/init.js +6 -0
  39. package/initramfs/lib/bunterm/glyphs.js +446 -0
  40. package/initramfs/lib/bunterm/images.js +225 -0
  41. package/initramfs/lib/bunterm/input.js +140 -0
  42. package/initramfs/lib/bunterm/renderer.js +424 -0
  43. package/initramfs/lib/bunterm/terminal.js +203 -0
  44. package/initramfs/lib/canvas.js +181 -0
  45. package/initramfs/lib/canvaskit/LICENSE +29 -0
  46. package/initramfs/lib/canvaskit/canvaskit.js +264 -0
  47. package/initramfs/lib/canvaskit/canvaskit.wasm +0 -0
  48. package/initramfs/lib/dlopen.js +14 -11
  49. package/initramfs/lib/document.js +6 -0
  50. package/initramfs/lib/fbdev.js +581 -0
  51. package/initramfs/lib/power.js +35 -0
  52. package/initramfs/lib/xterm/LICENSE +21 -0
  53. package/initramfs/lib/xterm/addon-unicode-graphemes.mjs +44 -0
  54. package/initramfs/lib/xterm/xterm-headless.mjs +46 -0
  55. package/initramfs/usr/share/doc/buninu-linux/bunterm.md +83 -0
  56. package/initramfs/usr/share/doc/buninu-linux/canvas.md +91 -0
  57. package/initramfs/usr/share/doc/buninu-linux/fbdev.md +113 -0
  58. package/initramfs/usr/share/doc/buninu-linux/ps.md +94 -0
  59. package/initramfs/usr/share/doc/buninu-linux/reboot.md +15 -0
  60. package/initramfs/usr/share/doc/buninu-linux/tar.md +73 -0
  61. package/initramfs/usr/share/doc/buninu-linux/top.md +23 -0
  62. package/initramfs/usr/share/fonts/DejaVuSansMono-Bold.ttf +0 -0
  63. package/initramfs/usr/share/fonts/DejaVuSansMono.ttf +0 -0
  64. package/initramfs/usr/share/fonts/LICENSE-Apache-2.0.txt +202 -0
  65. package/initramfs/usr/share/fonts/LICENSE-DejaVu.txt +44 -0
  66. package/initramfs/usr/share/fonts/LICENSE-OFL-1.1.txt +91 -0
  67. package/initramfs/usr/share/fonts/NotoColorEmoji.ttf +0 -0
  68. package/initramfs/usr/share/fonts/NotoColorEmojiFlags.ttf +0 -0
  69. package/initramfs/usr/share/fonts/NotoSansCJK-Regular.ttc +0 -0
  70. package/initramfs/usr/share/fonts/NotoSansSymbols-Regular-Subsetted.ttf +0 -0
  71. package/initramfs/usr/share/fonts/NotoSansSymbols-Regular-Subsetted2.ttf +0 -0
  72. package/initramfs/usr/share/fonts/Roboto-Regular.ttf +0 -0
  73. package/initramfs/usr/share/fonts/fonts.json +28 -0
  74. package/package.json +2 -2
  75. package/run-qemu.sh +1 -1
@@ -0,0 +1,225 @@
1
+ // /lib/bunterm/images.js — kitty graphics protocol placements on the cell
2
+ // grid.
3
+ //
4
+ // jsgotty's KittyGraphicsParser has already split the APC packets out of the
5
+ // PTY stream and assembled chunked transfers; this module turns its
6
+ // "placement" / "delete" events into Skia images anchored to terminal lines.
7
+ // A placement is tied to an xterm.js marker, so it scrolls with the text and
8
+ // disappears when its line leaves the scrollback.
9
+
10
+ const number = (value, fallback = 0) => {
11
+ const parsed = Number.parseInt(value ?? "", 10);
12
+ return Number.isFinite(parsed) ? parsed : fallback;
13
+ };
14
+
15
+ // Decodes a transferred image into a Skia Image. Format 100 is PNG (or any
16
+ // encoded format Skia knows); 24 and 32 are raw RGB / RGBA. Bun.Image, when
17
+ // present, transcodes encodings Skia cannot read on its own.
18
+ export const decodeImage = async (CanvasKit, image) => {
19
+ const bytes = Buffer.from(image.data, "base64");
20
+ if (image.format === 24 || image.format === 32) {
21
+ const { width, height } = image;
22
+ if (!width || !height) throw new Error("raw image without dimensions");
23
+ let pixels = bytes;
24
+ if (image.format === 24) {
25
+ pixels = Buffer.alloc(width * height * 4, 255);
26
+ for (let i = 0, o = 0; i < bytes.length; i += 3, o += 4) {
27
+ pixels[o] = bytes[i];
28
+ pixels[o + 1] = bytes[i + 1];
29
+ pixels[o + 2] = bytes[i + 2];
30
+ }
31
+ }
32
+ const decoded = CanvasKit.MakeImage({
33
+ width, height,
34
+ colorType: CanvasKit.ColorType.RGBA_8888,
35
+ alphaType: CanvasKit.AlphaType.Unpremul,
36
+ colorSpace: CanvasKit.ColorSpace.SRGB,
37
+ }, pixels, width * 4);
38
+ if (!decoded) throw new Error("raw image rejected");
39
+ return decoded;
40
+ }
41
+ let decoded = CanvasKit.MakeImageFromEncoded(bytes);
42
+ if (!decoded && typeof Bun !== "undefined" && Bun.Image) {
43
+ const png = await new Bun.Image(bytes).png().toBuffer();
44
+ decoded = CanvasKit.MakeImageFromEncoded(png);
45
+ }
46
+ if (!decoded) throw new Error("undecodable image data");
47
+ return decoded;
48
+ };
49
+
50
+ export class ImageStore {
51
+ constructor({ CanvasKit, term, renderer }) {
52
+ this.CanvasKit = CanvasKit;
53
+ this.term = term;
54
+ this.renderer = renderer;
55
+ this.images = new Map(); // image id → Skia Image
56
+ this.placements = new Map(); // placement key → placement
57
+ this.counter = 0;
58
+ this.coveredKey = "";
59
+ this.coveredRows = new Set();
60
+ }
61
+
62
+ // Handles one parsed graphic. Returns the control sequence that moves the
63
+ // cursor past a placement (to be written to the terminal), or "".
64
+ async handle(graphic) {
65
+ if (!graphic) return "";
66
+ if (graphic.kind === "delete") {
67
+ this.remove(graphic.delete);
68
+ return "";
69
+ }
70
+ if (graphic.kind !== "placement") return "";
71
+ const { control, image } = graphic;
72
+ let decoded = this.images.get(image.id);
73
+ if (!decoded) {
74
+ try {
75
+ decoded = await decodeImage(this.CanvasKit, image);
76
+ } catch (error) {
77
+ graphic.responseMessage = `EBADPNG:${error.message}`;
78
+ return "";
79
+ }
80
+ this.images.set(image.id, decoded);
81
+ }
82
+ const { cellWidth, cellHeight } = this.renderer;
83
+ const buffer = this.term.buffer.active;
84
+ const sourceX = number(control.x);
85
+ const sourceY = number(control.y);
86
+ const sourceWidth = number(control.w) || decoded.width() - sourceX;
87
+ const sourceHeight = number(control.h) || decoded.height() - sourceY;
88
+ const columns = number(control.c) || Math.max(1, Math.ceil(sourceWidth / cellWidth));
89
+ const rows = number(control.r) || Math.max(1, Math.ceil(sourceHeight / cellHeight));
90
+ // xterm.js only issues markers in the normal buffer. On the alternate
91
+ // screen (full-screen programs) there is no scrollback, so the placement
92
+ // is anchored to a fixed row instead and dropped when that screen ends.
93
+ const marker = buffer.type === "normal" ? this.term.registerMarker(0) : null;
94
+ const placementId = control.p ?? null;
95
+ const key = `${image.id}:${placementId ?? `auto${++this.counter}`}`;
96
+ const existing = this.placements.get(key);
97
+ if (existing) this.dispose(existing);
98
+ const placement = {
99
+ key,
100
+ imageId: image.id,
101
+ placementId,
102
+ marker,
103
+ bufferType: buffer.type,
104
+ fixedLine: buffer.baseY + buffer.cursorY,
105
+ get line() { return this.marker ? this.marker.line : this.fixedLine; },
106
+ get isDisposed() {
107
+ return this.marker ? this.marker.isDisposed : this.bufferType !== this.term.buffer.active.type;
108
+ },
109
+ term: this.term,
110
+ column: buffer.cursorX,
111
+ columns,
112
+ rows,
113
+ offsetX: number(control.X),
114
+ offsetY: number(control.Y),
115
+ source: [sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight],
116
+ zIndex: number(control.z),
117
+ };
118
+ this.placements.set(key, placement);
119
+ this.invalidate(placement);
120
+ if (control.C === "1") return "";
121
+ // Move the cursor below the image, the way jsgotty does for its browser
122
+ // terminal: one line feed per image row (so the screen scrolls when the
123
+ // image runs past the bottom, and the placement's marker scrolls with
124
+ // it), then to the column after the image.
125
+ return `${"\r\n".repeat(rows)}\u001b[${buffer.cursorX + columns + 1}G`;
126
+ }
127
+
128
+ invalidate(placement) {
129
+ const top = placement.line - this.term.buffer.active.viewportY;
130
+ this.renderer.invalidateRows(top, top + placement.rows);
131
+ }
132
+
133
+ dispose(placement) {
134
+ this.invalidate(placement);
135
+ placement.marker?.dispose();
136
+ this.placements.delete(placement.key);
137
+ }
138
+
139
+ remove({ scope, imageId, placementId, cursor }) {
140
+ const wipe = scope === scope.toUpperCase();
141
+ const targets = [];
142
+ for (const placement of this.placements.values()) {
143
+ switch (scope.toLowerCase()) {
144
+ case "a":
145
+ targets.push(placement);
146
+ break;
147
+ case "i":
148
+ if (placement.imageId === imageId && (!placementId || placement.placementId === placementId)) {
149
+ targets.push(placement);
150
+ }
151
+ break;
152
+ case "c": {
153
+ const row = placement.line - this.term.buffer.active.baseY + 1;
154
+ const column = placement.column + 1;
155
+ if (cursor && cursor.row >= row && cursor.row < row + placement.rows
156
+ && cursor.col >= column && cursor.col < column + placement.columns) {
157
+ targets.push(placement);
158
+ }
159
+ break;
160
+ }
161
+ default:
162
+ break;
163
+ }
164
+ }
165
+ for (const placement of targets) this.dispose(placement);
166
+ if (wipe) {
167
+ const used = new Set([...this.placements.values()].map((placement) => placement.imageId));
168
+ for (const [id, image] of this.images) {
169
+ if (!used.has(id)) {
170
+ image.delete();
171
+ this.images.delete(id);
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ // Placements currently on screen, as { image, source, rect } in pixels.
178
+ visible() {
179
+ const { renderer } = this;
180
+ const { cellWidth, cellHeight, offsetX, offsetY } = renderer;
181
+ const viewportY = this.term.buffer.active.viewportY;
182
+ const result = [];
183
+ for (const placement of [...this.placements.values()]) {
184
+ if (placement.isDisposed) {
185
+ this.placements.delete(placement.key);
186
+ continue;
187
+ }
188
+ const top = placement.line - viewportY;
189
+ if (top + placement.rows <= 0 || top >= renderer.rows) continue;
190
+ const image = this.images.get(placement.imageId);
191
+ if (!image) continue;
192
+ const left = offsetX + placement.column * cellWidth + placement.offsetX;
193
+ const y = offsetY + top * cellHeight + placement.offsetY;
194
+ result.push({
195
+ image,
196
+ source: placement.source,
197
+ rect: [left, y, left + placement.columns * cellWidth, y + placement.rows * cellHeight],
198
+ zIndex: placement.zIndex,
199
+ });
200
+ }
201
+ // The renderer's row hashes only see text, so when placements move or
202
+ // vanish every row they covered before or cover now is repainted.
203
+ const covered = new Set();
204
+ for (const { rect } of result) {
205
+ const first = Math.floor((rect[1] - offsetY) / cellHeight);
206
+ const last = Math.ceil((rect[3] - offsetY) / cellHeight);
207
+ for (let row = first; row < last; row++) covered.add(row);
208
+ }
209
+ const key = [...covered].sort((a, b) => a - b).join(",");
210
+ if (key !== this.coveredKey) {
211
+ for (const row of this.coveredRows ?? []) renderer.invalidateRows(row, row + 1);
212
+ for (const row of covered) renderer.invalidateRows(row, row + 1);
213
+ this.coveredKey = key;
214
+ this.coveredRows = covered;
215
+ }
216
+ return result.sort((a, b) => a.zIndex - b.zIndex);
217
+ }
218
+
219
+ delete() {
220
+ for (const placement of [...this.placements.values()]) placement.marker?.dispose();
221
+ this.placements.clear();
222
+ for (const image of this.images.values()) image.delete();
223
+ this.images.clear();
224
+ }
225
+ }
@@ -0,0 +1,140 @@
1
+ // /lib/bunterm/input.js — keyboard bytes from a Linux virtual console.
2
+ //
3
+ // The console is read in raw mode so every key reaches the terminal
4
+ // emulator unchanged (Ctrl-C included; the program in the PTY decides what
5
+ // it means). The kernel keeps translating keys to bytes (K_XLATE), and its
6
+ // "linux" console sequences for a few keys are rewritten into the xterm
7
+ // forms that programs seeing TERM=xterm-256color expect.
8
+
9
+ import { closeSync, openSync, read } from "node:fs";
10
+ import { libc, ptr, errno, strerror } from "../dlopen.js";
11
+
12
+ const TCGETS = 0x5401;
13
+ const TCSETS = 0x5402;
14
+ // struct termios (kernel ABI): 4 x u32 flags, c_line, c_cc[19].
15
+ const TERMIOS_LENGTH = 36;
16
+ const IFLAG = 0;
17
+ const OFLAG = 4;
18
+ const CFLAG = 8;
19
+ const LFLAG = 12;
20
+ const CC = 17;
21
+ const VTIME = 5;
22
+ const VMIN = 6;
23
+
24
+ const IGNBRK = 0o1, BRKINT = 0o2, PARMRK = 0o10, ISTRIP = 0o40, INLCR = 0o100,
25
+ IGNCR = 0o200, ICRNL = 0o400, IXON = 0o2000;
26
+ const OPOST = 0o1;
27
+ const CSIZE = 0o60, CS8 = 0o60, PARENB = 0o400;
28
+ const ISIG = 0o1, ICANON = 0o2, ECHO = 0o10, ECHONL = 0o100, IEXTEN = 0o100000;
29
+
30
+ const fail = (operation) => {
31
+ const code = errno();
32
+ throw new Error(`${operation}: ${strerror(code)}`);
33
+ };
34
+
35
+ const getTermios = (fd) => {
36
+ const buffer = new Uint8Array(TERMIOS_LENGTH);
37
+ if (libc.symbols.ioctl(fd, TCGETS, ptr(buffer)) === -1) fail("TCGETS");
38
+ return buffer;
39
+ };
40
+
41
+ const setTermios = (fd, buffer) => {
42
+ if (libc.symbols.ioctl(fd, TCSETS, ptr(buffer)) === -1) fail("TCSETS");
43
+ };
44
+
45
+ // cfmakeraw() on a copy of the current settings.
46
+ const rawTermios = (current) => {
47
+ const raw = new Uint8Array(current);
48
+ const view = new DataView(raw.buffer);
49
+ const clear = (offset, mask) => view.setUint32(offset, view.getUint32(offset, true) & ~mask, true);
50
+ clear(IFLAG, IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
51
+ clear(OFLAG, OPOST);
52
+ clear(LFLAG, ECHO | ECHONL | ICANON | ISIG | IEXTEN);
53
+ clear(CFLAG, CSIZE | PARENB);
54
+ view.setUint32(CFLAG, view.getUint32(CFLAG, true) | CS8, true);
55
+ raw[CC + VMIN] = 1;
56
+ raw[CC + VTIME] = 0;
57
+ return raw;
58
+ };
59
+
60
+ // Linux console sequences → xterm sequences (keys programs commonly bind).
61
+ const consoleToXterm = new Map([
62
+ ["\u001b[[A", "\u001bOP"], ["\u001b[[B", "\u001bOQ"], ["\u001b[[C", "\u001bOR"],
63
+ ["\u001b[[D", "\u001bOS"], ["\u001b[[E", "\u001b[15~"],
64
+ ["\u001b[1~", "\u001b[H"], ["\u001b[4~", "\u001b[F"],
65
+ ]);
66
+
67
+ const cursorKeys = new Map([
68
+ ["\u001b[A", "\u001bOA"], ["\u001b[B", "\u001bOB"], ["\u001b[C", "\u001bOC"], ["\u001b[D", "\u001bOD"],
69
+ ["\u001b[H", "\u001bOH"], ["\u001b[F", "\u001bOF"],
70
+ ]);
71
+
72
+ // Rewrites console key sequences in `text`. With `applicationCursorKeys`
73
+ // (DECCKM set by the program) the arrow keys use the SS3 forms.
74
+ export const translateKeys = (text, { applicationCursorKeys = false } = {}) => {
75
+ let output = "";
76
+ let index = 0;
77
+ while (index < text.length) {
78
+ if (text[index] !== "\u001b") {
79
+ output += text[index++];
80
+ continue;
81
+ }
82
+ let matched = false;
83
+ for (const [from, to] of consoleToXterm) {
84
+ if (text.startsWith(from, index)) {
85
+ output += applicationCursorKeys ? (cursorKeys.get(to) ?? to) : to;
86
+ index += from.length;
87
+ matched = true;
88
+ break;
89
+ }
90
+ }
91
+ if (matched) continue;
92
+ if (applicationCursorKeys) {
93
+ for (const [from, to] of cursorKeys) {
94
+ if (text.startsWith(from, index)) {
95
+ output += to;
96
+ index += from.length;
97
+ matched = true;
98
+ break;
99
+ }
100
+ }
101
+ if (matched) continue;
102
+ }
103
+ output += text[index++];
104
+ }
105
+ return output;
106
+ };
107
+
108
+ // Opens the keyboard: `device` names a console such as /dev/tty1; without
109
+ // it the process's own stdin is used (it must be a terminal). `onData`
110
+ // receives Buffers. close() restores the previous terminal settings.
111
+ export const openKeyboard = ({ device = null, onData, onError = () => {} }) => {
112
+ const fd = device ? openSync(device, "r+") : 0;
113
+ const saved = getTermios(fd);
114
+ setTermios(fd, rawTermios(saved));
115
+ let open = true;
116
+ const chunk = Buffer.alloc(4096);
117
+ const pump = () => {
118
+ if (!open) return;
119
+ read(fd, chunk, 0, chunk.length, null, (error, count) => {
120
+ if (!open) return;
121
+ if (error) {
122
+ if (error.code === "EAGAIN") { setTimeout(pump, 10); return; }
123
+ onError(error);
124
+ return;
125
+ }
126
+ if (count > 0) onData(Buffer.from(chunk.subarray(0, count)));
127
+ pump();
128
+ });
129
+ };
130
+ pump();
131
+ return {
132
+ fd,
133
+ close() {
134
+ if (!open) return;
135
+ open = false;
136
+ try { setTermios(fd, saved); } catch {}
137
+ if (device) closeSync(fd);
138
+ },
139
+ };
140
+ };