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,581 @@
1
+ #!/bin/bun
2
+ // /lib/fbdev.js — small Linux framebuffer drawing API and executable demo.
3
+
4
+ import { resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { libc, cString, ptr, errno, strerror, toArrayBuffer } from "./dlopen.js";
7
+ import { showDocument } from "./document.js";
8
+
9
+ const O_RDWR = 2;
10
+ const FBIOGET_VSCREENINFO = 0x4600;
11
+ const FBIOGET_FSCREENINFO = 0x4602;
12
+ const FB_TYPE_PACKED_PIXELS = 0;
13
+ const KDSETMODE = 0x4b3a;
14
+ const KD_TEXT = 0;
15
+ const KD_GRAPHICS = 1;
16
+ const VT_SETMODE = 0x5602;
17
+ const VT_GETSTATE = 0x5603;
18
+ const VT_RELDISP = 0x5605;
19
+ const VT_ACTIVATE = 0x5606;
20
+ const VT_WAITACTIVE = 0x5607;
21
+ const VT_AUTO = 0;
22
+ const VT_PROCESS = 1;
23
+ const VT_ACKACQ = 2;
24
+ const SIGUSR1 = 10;
25
+ const SIGUSR2 = 12;
26
+ const PROT_READ = 1;
27
+ const PROT_WRITE = 2;
28
+ const MAP_SHARED = 1;
29
+ const MS_SYNC = 4;
30
+ const MAP_FAILED = 0xffffffffffffffffn;
31
+
32
+ const fail = (operation) => {
33
+ const code = errno();
34
+ throw new Error(`${operation}: ${strerror(code)}`);
35
+ };
36
+
37
+ const openDevice = (path) => {
38
+ const fd = libc.symbols.open(cString(path), O_RDWR, 0);
39
+ if (fd === -1) fail(`open(${path})`);
40
+ return fd;
41
+ };
42
+
43
+ const ioctlBuffer = (fd, request, buffer, operation) => {
44
+ if (libc.symbols.ioctl(fd, request, ptr(buffer)) === -1) fail(operation);
45
+ };
46
+
47
+ const ioctlValue = (fd, request, value, operation) => {
48
+ if (libc.symbols.ioctl(fd, request, value) === -1) fail(operation);
49
+ };
50
+
51
+ const u32 = (view, offset) => view.getUint32(offset, true);
52
+
53
+ const bitfield = (view, offset) => ({
54
+ offset: u32(view, offset),
55
+ length: u32(view, offset + 4),
56
+ msbRight: u32(view, offset + 8),
57
+ });
58
+
59
+ const channelBits = (value, field) => {
60
+ if (!field.length) return 0;
61
+ const maximum = 2 ** field.length - 1;
62
+ const scaled = Math.round(Math.max(0, Math.min(255, value)) * maximum / 255);
63
+ return scaled * 2 ** field.offset;
64
+ };
65
+
66
+ const encodePixel = (color, fields) => {
67
+ const r = Number(color?.r ?? 255);
68
+ const g = Number(color?.g ?? 255);
69
+ const b = Number(color?.b ?? 255);
70
+ const a = Number(color?.a ?? 255);
71
+ return channelBits(r, fields.red)
72
+ + channelBits(g, fields.green)
73
+ + channelBits(b, fields.blue)
74
+ + channelBits(a, fields.transp);
75
+ };
76
+
77
+ const point = (value) => {
78
+ if (Array.isArray(value)) return { x: Number(value[0]), y: Number(value[1]) };
79
+ return { x: Number(value?.x), y: Number(value?.y) };
80
+ };
81
+
82
+ export const normalizePoints = (values) => {
83
+ if (!Array.isArray(values)) return [];
84
+ if (values.every((value) => typeof value === "number")) {
85
+ const evenLength = values.length - values.length % 2;
86
+ const output = [];
87
+ for (let index = 0; index < evenLength; index += 2) {
88
+ output.push({ x: values[index], y: values[index + 1] });
89
+ }
90
+ return output.filter(({ x, y }) => Number.isFinite(x) && Number.isFinite(y));
91
+ }
92
+ return values.map(point)
93
+ .filter(({ x, y }) => Number.isFinite(x) && Number.isFinite(y));
94
+ };
95
+
96
+ export const parsePoints = (values) => {
97
+ const numbers = values.flatMap((value) => String(value).split(/[\s,]+/u))
98
+ .filter(Boolean)
99
+ .map((value) => {
100
+ const number = Number(value);
101
+ if (!Number.isFinite(number)) throw new Error(`invalid coordinate: ${value}`);
102
+ return number;
103
+ });
104
+ return normalizePoints(numbers);
105
+ };
106
+
107
+ export class Framebuffer {
108
+ constructor(path = "/dev/fb0") {
109
+ this.path = path;
110
+ this.fd = openDevice(path);
111
+ try {
112
+ const variable = new Uint8Array(160);
113
+ const fixed = new Uint8Array(80);
114
+ ioctlBuffer(this.fd, FBIOGET_VSCREENINFO, variable, "FBIOGET_VSCREENINFO");
115
+ ioctlBuffer(this.fd, FBIOGET_FSCREENINFO, fixed, "FBIOGET_FSCREENINFO");
116
+ const variableView = new DataView(variable.buffer);
117
+ const fixedView = new DataView(fixed.buffer);
118
+
119
+ this.width = u32(variableView, 0);
120
+ this.height = u32(variableView, 4);
121
+ this.virtualWidth = u32(variableView, 8);
122
+ this.virtualHeight = u32(variableView, 12);
123
+ this.xOffset = u32(variableView, 16);
124
+ this.yOffset = u32(variableView, 20);
125
+ this.bitsPerPixel = u32(variableView, 24);
126
+ this.bytesPerPixel = Math.ceil(this.bitsPerPixel / 8);
127
+ this.memoryLength = u32(fixedView, 24);
128
+ this.type = u32(fixedView, 28);
129
+ this.stride = u32(fixedView, 48)
130
+ || Math.ceil(this.virtualWidth * this.bitsPerPixel / 8);
131
+ this.fields = {
132
+ red: bitfield(variableView, 32),
133
+ green: bitfield(variableView, 44),
134
+ blue: bitfield(variableView, 56),
135
+ transp: bitfield(variableView, 68),
136
+ };
137
+
138
+ if (this.type !== FB_TYPE_PACKED_PIXELS) {
139
+ throw new Error(`unsupported framebuffer type ${this.type}; packed pixels required`);
140
+ }
141
+ if (![16, 24, 32].includes(this.bitsPerPixel)) {
142
+ throw new Error(`unsupported framebuffer depth ${this.bitsPerPixel} bpp`);
143
+ }
144
+ if (!this.width || !this.height || !this.stride) {
145
+ throw new Error("framebuffer reports an empty display");
146
+ }
147
+ const minimumLength = this.stride * (this.yOffset + this.height);
148
+ if (this.memoryLength < minimumLength) {
149
+ throw new Error(`framebuffer memory is ${this.memoryLength} bytes; ${minimumLength} required`);
150
+ }
151
+ this.pixels = new Uint8Array(this.stride * this.height);
152
+ this.address = libc.symbols.mmap(
153
+ null,
154
+ this.memoryLength,
155
+ PROT_READ | PROT_WRITE,
156
+ MAP_SHARED,
157
+ this.fd,
158
+ 0,
159
+ );
160
+ if (this.address === null || this.address === 0 || this.address === 0n
161
+ || this.address === MAP_FAILED) {
162
+ fail(`mmap(${path})`);
163
+ }
164
+ this.memory = new Uint8Array(toArrayBuffer(this.address, 0, this.memoryLength));
165
+ } catch (error) {
166
+ if (this.address) libc.symbols.munmap(this.address, this.memoryLength);
167
+ libc.symbols.close(this.fd);
168
+ this.fd = -1;
169
+ throw error;
170
+ }
171
+ }
172
+
173
+ close() {
174
+ if (this.fd < 0) return;
175
+ if (this.address) {
176
+ libc.symbols.munmap(this.address, this.memoryLength);
177
+ this.address = null;
178
+ this.memory = null;
179
+ }
180
+ libc.symbols.close(this.fd);
181
+ this.fd = -1;
182
+ }
183
+ }
184
+
185
+ export const drawPoint = (display, x, y, color = {}) => {
186
+ x = Math.round(Number(x));
187
+ y = Math.round(Number(y));
188
+ if (x < 0 || y < 0 || x >= display.width || y >= display.height) return;
189
+ let pixel = encodePixel(color, display.fields);
190
+ const offset = y * display.stride + x * display.bytesPerPixel;
191
+ for (let byte = 0; byte < display.bytesPerPixel; byte++) {
192
+ display.pixels[offset + byte] = pixel % 256;
193
+ pixel = Math.floor(pixel / 256);
194
+ }
195
+ };
196
+
197
+ export const drawLine = (display, x1, y1, x2, y2, color = {}) => {
198
+ x1 = Math.round(Number(x1));
199
+ y1 = Math.round(Number(y1));
200
+ x2 = Math.round(Number(x2));
201
+ y2 = Math.round(Number(y2));
202
+ const dx = Math.abs(x2 - x1);
203
+ const sx = x1 < x2 ? 1 : -1;
204
+ const dy = -Math.abs(y2 - y1);
205
+ const sy = y1 < y2 ? 1 : -1;
206
+ let error = dx + dy;
207
+ while (true) {
208
+ drawPoint(display, x1, y1, color);
209
+ if (x1 === x2 && y1 === y2) break;
210
+ const twice = 2 * error;
211
+ if (twice >= dy) { error += dy; x1 += sx; }
212
+ if (twice <= dx) { error += dx; y1 += sy; }
213
+ }
214
+ };
215
+
216
+ export const drawLines = (display, values, color = {}, close = false) => {
217
+ const points = normalizePoints(values);
218
+ for (let index = 1; index < points.length; index++) {
219
+ drawLine(display, points[index - 1].x, points[index - 1].y,
220
+ points[index].x, points[index].y, color);
221
+ }
222
+ if (close && points.length > 2) {
223
+ drawLine(display, points.at(-1).x, points.at(-1).y,
224
+ points[0].x, points[0].y, color);
225
+ }
226
+ };
227
+
228
+ export const fillPolygon = (display, values, color = {}) => {
229
+ const points = normalizePoints(values);
230
+ if (points.length < 3) return;
231
+ const minimumY = Math.max(0, Math.floor(Math.min(...points.map(({ y }) => y))));
232
+ const maximumY = Math.min(display.height - 1,
233
+ Math.ceil(Math.max(...points.map(({ y }) => y))));
234
+ for (let y = minimumY; y <= maximumY; y++) {
235
+ const scanY = y + 0.5;
236
+ const intersections = [];
237
+ for (let index = 0; index < points.length; index++) {
238
+ const a = points[index];
239
+ const b = points[(index + 1) % points.length];
240
+ if ((a.y <= scanY && b.y > scanY) || (b.y <= scanY && a.y > scanY)) {
241
+ intersections.push(a.x + (scanY - a.y) * (b.x - a.x) / (b.y - a.y));
242
+ }
243
+ }
244
+ intersections.sort((a, b) => a - b);
245
+ for (let index = 0; index + 1 < intersections.length; index += 2) {
246
+ const from = Math.max(0, Math.ceil(intersections[index]));
247
+ const to = Math.min(display.width - 1, Math.floor(intersections[index + 1]));
248
+ for (let x = from; x <= to; x++) drawPoint(display, x, y, color);
249
+ }
250
+ }
251
+ };
252
+
253
+ export const clear = (display, color = { r: 0, g: 0, b: 0, a: 255 }) => {
254
+ if (!color.r && !color.g && !color.b && (color.a ?? 255) === 255
255
+ && display.fields.transp.length === 0) {
256
+ display.pixels.fill(0);
257
+ return;
258
+ }
259
+ for (let y = 0; y < display.height; y++) {
260
+ for (let x = 0; x < display.width; x++) drawPoint(display, x, y, color);
261
+ }
262
+ };
263
+
264
+ export const flush = (display) => {
265
+ const visibleBytes = display.width * display.bytesPerPixel;
266
+ for (let y = 0; y < display.height; y++) {
267
+ const row = display.pixels.subarray(y * display.stride, y * display.stride + visibleBytes);
268
+ const position = (y + display.yOffset) * display.stride
269
+ + display.xOffset * display.bytesPerPixel;
270
+ display.memory.set(row, position);
271
+ }
272
+ if (libc.symbols.msync(display.address, display.memoryLength, MS_SYNC) === -1) {
273
+ fail(`msync(${display.path})`);
274
+ }
275
+ };
276
+
277
+ // Copies an RGBA ImageData (HTML canvas layout, 4 bytes per pixel, rows of
278
+ // `width` pixels) into the in-memory frame at (x, y), converting to the
279
+ // framebuffer's own pixel format and clipping at the edges.
280
+ export const blitImageData = (display, imageData, x = 0, y = 0) => {
281
+ const { width, height, data } = imageData;
282
+ x = Math.round(Number(x));
283
+ y = Math.round(Number(y));
284
+ const fromX = Math.max(0, -x);
285
+ const fromY = Math.max(0, -y);
286
+ const toX = Math.min(width, display.width - x);
287
+ const toY = Math.min(height, display.height - y);
288
+ const color = { r: 0, g: 0, b: 0, a: 255 };
289
+ for (let row = fromY; row < toY; row++) {
290
+ let source = (row * width + fromX) * 4;
291
+ for (let column = fromX; column < toX; column++, source += 4) {
292
+ color.r = data[source];
293
+ color.g = data[source + 1];
294
+ color.b = data[source + 2];
295
+ color.a = data[source + 3];
296
+ drawPoint(display, x + column, y + row, color);
297
+ }
298
+ }
299
+ };
300
+
301
+ export const openFramebuffer = (path) => new Framebuffer(path);
302
+
303
+ export const openConsole = (paths = ["/dev/tty", "/dev/console", "/dev/tty0"]) => {
304
+ const failures = [];
305
+ for (const path of paths) {
306
+ const fd = libc.symbols.open(cString(path), O_RDWR, 0);
307
+ if (fd !== -1) return {
308
+ fd,
309
+ path,
310
+ close() {
311
+ if (this.fd < 0) return;
312
+ libc.symbols.close(this.fd);
313
+ this.fd = -1;
314
+ },
315
+ };
316
+ failures.push(`${path}: ${strerror(errno())}`);
317
+ }
318
+ throw new Error(`cannot open a Linux console (${failures.join("; ")})`);
319
+ };
320
+
321
+ export const setConsoleGraphics = (consoleDevice) =>
322
+ ioctlValue(consoleDevice.fd, KDSETMODE, KD_GRAPHICS, "KDSETMODE(KD_GRAPHICS)");
323
+
324
+ export const setConsoleText = (consoleDevice) =>
325
+ ioctlValue(consoleDevice.fd, KDSETMODE, KD_TEXT, "KDSETMODE(KD_TEXT)");
326
+
327
+ // The number of the virtual console currently shown (struct vt_stat.v_active).
328
+ export const activeConsole = (consoleDevice) => {
329
+ const state = new Uint8Array(6);
330
+ ioctlBuffer(consoleDevice.fd, VT_GETSTATE, state, "VT_GETSTATE");
331
+ return new DataView(state.buffer).getUint16(0, true);
332
+ };
333
+
334
+ // Switches the display and keyboard to virtual console `number` and waits
335
+ // until the kernel has completed the switch.
336
+ export const activateConsole = (consoleDevice, number) => {
337
+ ioctlValue(consoleDevice.fd, VT_ACTIVATE, number, `VT_ACTIVATE(${number})`);
338
+ ioctlValue(consoleDevice.fd, VT_WAITACTIVE, number, `VT_WAITACTIVE(${number})`);
339
+ };
340
+
341
+ // struct vt_mode { char mode; char waitv; short relsig, acqsig, frsig; }.
342
+ const setConsoleSwitchMode = (consoleDevice, mode) => {
343
+ const buffer = new Uint8Array(8);
344
+ const view = new DataView(buffer.buffer);
345
+ buffer[0] = mode;
346
+ if (mode === VT_PROCESS) {
347
+ view.setInt16(2, SIGUSR1, true);
348
+ view.setInt16(4, SIGUSR2, true);
349
+ }
350
+ ioctlBuffer(consoleDevice.fd, VT_SETMODE, buffer, "VT_SETMODE");
351
+ };
352
+
353
+ // Asks the kernel to tell this process about console switches: SIGUSR1
354
+ // before the display is taken away (answered with VT_RELDISP 1), SIGUSR2
355
+ // when it comes back (answered with VT_ACKACQ), at which point `onAcquire`
356
+ // runs so the caller can repaint what fbcon left on the screen. Returns a
357
+ // function that puts the console back into automatic switching.
358
+ export const watchConsoleSwitches = (consoleDevice, onAcquire) => {
359
+ const release = () => {
360
+ try { ioctlValue(consoleDevice.fd, VT_RELDISP, 1, "VT_RELDISP"); } catch {}
361
+ };
362
+ const acquire = () => {
363
+ try { ioctlValue(consoleDevice.fd, VT_RELDISP, VT_ACKACQ, "VT_RELDISP(VT_ACKACQ)"); } catch {}
364
+ try { onAcquire(); } catch {}
365
+ };
366
+ process.on("SIGUSR1", release);
367
+ process.on("SIGUSR2", acquire);
368
+ setConsoleSwitchMode(consoleDevice, VT_PROCESS);
369
+ return () => {
370
+ process.off("SIGUSR1", release);
371
+ process.off("SIGUSR2", acquire);
372
+ try { setConsoleSwitchMode(consoleDevice, VT_AUTO); } catch {}
373
+ };
374
+ };
375
+
376
+ export const consoleNumber = (path) => {
377
+ const match = /^\/dev\/tty(\d+)$/.exec(path);
378
+ return match && Number(match[1]) > 0 ? Number(match[1]) : null;
379
+ };
380
+
381
+ const waitForKey = async (setRestore) => {
382
+ const input = process.stdin;
383
+ if (!input.isTTY) throw new Error("interactive demo requires a TTY on stdin");
384
+ const wasRaw = Boolean(input.isRaw);
385
+ let restored = false;
386
+ const restore = () => {
387
+ if (restored) return;
388
+ restored = true;
389
+ input.setRawMode?.(wasRaw);
390
+ if (!wasRaw) input.pause();
391
+ };
392
+ setRestore(restore);
393
+ input.setRawMode?.(true);
394
+ input.resume();
395
+ try {
396
+ await new Promise((resolve, reject) => {
397
+ const onData = () => { cleanup(); resolve(); };
398
+ const onError = (error) => { cleanup(); reject(error); };
399
+ const cleanup = () => {
400
+ input.off("data", onData);
401
+ input.off("error", onError);
402
+ };
403
+ input.once("data", onData);
404
+ input.once("error", onError);
405
+ });
406
+ } finally {
407
+ restore();
408
+ setRestore(() => {});
409
+ }
410
+ };
411
+
412
+ const defaultTriangle = (display) => [
413
+ { x: display.width / 2, y: display.height / 6 },
414
+ { x: display.width / 6, y: display.height * 5 / 6 },
415
+ { x: display.width * 5 / 6, y: display.height * 5 / 6 },
416
+ ];
417
+
418
+ // Runs `draw(display, context)` with the framebuffer open and the virtual
419
+ // console in graphics mode, restoring text mode however the call ends:
420
+ // normally, by throwing, by process.exit(), or on SIGHUP/SIGINT/SIGQUIT/
421
+ // SIGTERM. `console` names the console device(s) to use; the default list
422
+ // resolves to the console the process is running on. When it names a
423
+ // specific /dev/ttyN that is not the one on screen, the display is switched
424
+ // to it first (and back afterwards), the way X servers do; `switchConsole:
425
+ // false` disables that. Console switches away and back (Ctrl-Alt-Fn) are
426
+ // allowed while drawing; `context.onAcquire(callback)` runs when the display
427
+ // returns so the caller can repaint. `context` also offers `console` (the
428
+ // open console device) and `onRestore(callback)` for cleanup that must run
429
+ // before text mode returns, such as putting a keyboard back into cooked mode.
430
+ export const runGraphics = async (draw, {
431
+ device = "/dev/fb0", console: consolePaths, switchConsole = true,
432
+ } = {}) => {
433
+ const display = openFramebuffer(device);
434
+ let consoleDevice;
435
+ try {
436
+ consoleDevice = openConsole(consolePaths);
437
+ } catch (error) {
438
+ display.close();
439
+ throw error;
440
+ }
441
+ let graphics = false;
442
+ let previousConsole = null;
443
+ let unwatch = null;
444
+ const restoreCallbacks = [];
445
+ const acquireCallbacks = [];
446
+ const restore = () => {
447
+ while (restoreCallbacks.length) {
448
+ try { restoreCallbacks.pop()(); } catch {}
449
+ }
450
+ if (unwatch) {
451
+ unwatch();
452
+ unwatch = null;
453
+ }
454
+ if (graphics) {
455
+ try { setConsoleText(consoleDevice); } catch {}
456
+ graphics = false;
457
+ }
458
+ if (previousConsole !== null) {
459
+ try { activateConsole(consoleDevice, previousConsole); } catch {}
460
+ previousConsole = null;
461
+ }
462
+ };
463
+ const signalStatuses = new Map([
464
+ ["SIGHUP", 129], ["SIGINT", 130], ["SIGQUIT", 131], ["SIGTERM", 143],
465
+ ]);
466
+ const signalHandlers = new Map([...signalStatuses].map(([signal, status]) => [
467
+ signal,
468
+ () => { restore(); process.exit(status); },
469
+ ]));
470
+ for (const [signal, handler] of signalHandlers) process.on(signal, handler);
471
+ process.on("exit", restore);
472
+ try {
473
+ const target = switchConsole ? consoleNumber(consoleDevice.path) : null;
474
+ if (target !== null) {
475
+ const active = activeConsole(consoleDevice);
476
+ if (active !== target) {
477
+ activateConsole(consoleDevice, target);
478
+ previousConsole = active;
479
+ }
480
+ }
481
+ setConsoleGraphics(consoleDevice);
482
+ graphics = true;
483
+ if (switchConsole) {
484
+ try {
485
+ unwatch = watchConsoleSwitches(consoleDevice, () => {
486
+ for (const callback of acquireCallbacks) {
487
+ try { callback(); } catch {}
488
+ }
489
+ });
490
+ } catch {
491
+ // Not a virtual console (or no permission): switches stay automatic.
492
+ }
493
+ }
494
+ return await draw(display, {
495
+ console: consoleDevice,
496
+ onRestore: (callback) => { restoreCallbacks.push(callback); },
497
+ onAcquire: (callback) => { acquireCallbacks.push(callback); },
498
+ });
499
+ } finally {
500
+ restore();
501
+ process.off("exit", restore);
502
+ for (const [signal, handler] of signalHandlers) process.off(signal, handler);
503
+ display.close();
504
+ consoleDevice.close();
505
+ }
506
+ };
507
+
508
+ export const runDemo = async ({ polygons = [], points, device = "/dev/fb0" } = {}) => {
509
+ if (!process.stdin.isTTY) throw new Error("interactive demo requires a TTY on stdin");
510
+ const shapes = polygons.map(normalizePoints);
511
+ if (points !== undefined) shapes.unshift(normalizePoints(points));
512
+ const drawableShapes = shapes.filter((polygon) => polygon.length > 0);
513
+ for (const polygon of drawableShapes) {
514
+ if (polygon.length < 3) {
515
+ throw new Error("each --points/-p polygon needs at least three complete x,y pairs");
516
+ }
517
+ }
518
+ console.error("fbdev: entering graphics mode; press any key to return");
519
+ await runGraphics(async (display, { onRestore }) => {
520
+ clear(display);
521
+ const drawing = drawableShapes.length ? drawableShapes : [defaultTriangle(display)];
522
+ const colors = [
523
+ { r: 64, g: 200, b: 255, a: 255 },
524
+ { r: 255, g: 96, b: 128, a: 255 },
525
+ { r: 128, g: 224, b: 96, a: 255 },
526
+ { r: 255, g: 192, b: 64, a: 255 },
527
+ ];
528
+ drawing.forEach((polygon, index) =>
529
+ fillPolygon(display, polygon, colors[index % colors.length]));
530
+ flush(display);
531
+ await waitForKey(onRestore);
532
+ }, { device });
533
+ };
534
+
535
+ const usage = () => {
536
+ console.error(`Usage:
537
+ bun /lib/fbdev.js
538
+ bun /lib/fbdev.js --points 'x1,y1 x2,y2 x3,y3' [--points '...']
539
+ bun /lib/fbdev.js -p 'x1,y1 x2,y2 x3,y3' [-p '...']
540
+ bun /lib/fbdev.js -h | --help`);
541
+ };
542
+
543
+ const main = async () => {
544
+ const arguments_ = process.argv.slice(2);
545
+ if (arguments_.length === 1 && (arguments_[0] === "-h" || arguments_[0] === "--help")) {
546
+ await showDocument("fbdev");
547
+ return;
548
+ }
549
+ const polygons = [];
550
+ for (let index = 0; index < arguments_.length; index++) {
551
+ const argument = arguments_[index];
552
+ if (argument === "-p" || argument === "--points") {
553
+ const value = arguments_[++index];
554
+ if (value === undefined) {
555
+ usage();
556
+ throw new Error(`${argument} needs one polygon coordinate list`);
557
+ }
558
+ polygons.push(parsePoints([value]));
559
+ } else if (argument.startsWith("--points=")) {
560
+ polygons.push(parsePoints([argument.slice(9)]));
561
+ } else if (argument.startsWith("-p=")) {
562
+ polygons.push(parsePoints([argument.slice(3)]));
563
+ } else {
564
+ usage();
565
+ throw new Error(`unexpected argument: ${argument}`);
566
+ }
567
+ }
568
+ await runDemo({ polygons });
569
+ };
570
+
571
+ const isDirect = process.argv[1]
572
+ && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
573
+
574
+ if (isDirect) {
575
+ try {
576
+ await main();
577
+ } catch (error) {
578
+ console.error(`fbdev: ${error?.message ?? error}`);
579
+ process.exit(1);
580
+ }
581
+ }
@@ -0,0 +1,35 @@
1
+ // Shared implementation for /bin/poweroff and /bin/reboot.
2
+
3
+ import { libc, check } from "./dlopen.js";
4
+ import { showDocument } from "./document.js";
5
+
6
+ export const LINUX_REBOOT_CMD_RESTART = 0x01234567;
7
+ export const LINUX_REBOOT_CMD_POWER_OFF = 0x4321fedc;
8
+
9
+ export const runPowerCommand = async ({ name, command, action }) => {
10
+ const arguments_ = process.argv.slice(2);
11
+
12
+ if (arguments_.length === 1 && (arguments_[0] === "-h" || arguments_[0] === "--help")) {
13
+ await showDocument(name);
14
+ return;
15
+ }
16
+
17
+ if (arguments_.length !== 0) {
18
+ console.error(`Usage: ${name} [-h | --help]`);
19
+ process.exit(1);
20
+ }
21
+
22
+ console.log(`${name}: syncing filesystems`);
23
+ libc.symbols.sync();
24
+ console.log(`${name}: ${action}`);
25
+ try {
26
+ check(`reboot(${command.name})`, libc.symbols.reboot(command.value));
27
+ } catch (error) {
28
+ console.error(`${name}: ${error.message}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ // A successful reboot(2) never returns.
33
+ console.error(`${name}: reboot returned without ${action}`);
34
+ process.exit(1);
35
+ };
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
2
+ Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
3
+ Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.