buninu-linux 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -10
- package/fetch-alpine.sh +6 -0
- package/graphics.md +7 -2
- package/initramfs/bin/bunterm +5 -1
- package/initramfs/bin/chroot +213 -0
- package/initramfs/bin/stripansi +27 -0
- package/initramfs/bin/tar +189 -14
- package/initramfs/init.js +74 -1
- package/initramfs/lib/bunterm/mouse.js +222 -0
- package/initramfs/lib/bunterm/renderer.js +48 -3
- package/initramfs/lib/bunterm/terminal.js +123 -2
- package/initramfs/lib/dlopen.js +4 -0
- package/initramfs/lib/document.js +5 -1
- package/initramfs/usr/share/doc/buninu-linux/bunterm.md +31 -2
- package/initramfs/usr/share/doc/buninu-linux/chroot.md +105 -0
- package/initramfs/usr/share/doc/buninu-linux/tar.md +30 -6
- package/package.json +2 -2
package/initramfs/init.js
CHANGED
|
@@ -94,6 +94,79 @@ const mountFilesystems = () => {
|
|
|
94
94
|
mountFilesystems();
|
|
95
95
|
console.log("init.js: mounted /proc, /sys, /dev, /tmp, /dev/pts");
|
|
96
96
|
|
|
97
|
+
// Num Lock. The keypad's lock state lives per virtual console in the
|
|
98
|
+
// kernel's keyboard table, and the kernel starts every console with it off,
|
|
99
|
+
// so the keypad types arrows instead of digits. KDSKBLED carries the current
|
|
100
|
+
// flags in its low nibble and the ones a console is reset to in its high
|
|
101
|
+
// nibble; KDSETLED with a value above 7 puts the physical LED back to
|
|
102
|
+
// following those flags. A console allocated later (Ctrl-Alt-F3 for the
|
|
103
|
+
// first time) starts from the kernel default again, so numlock() is left in
|
|
104
|
+
// the REPL for that case.
|
|
105
|
+
const KDSETLED = 0x4b32;
|
|
106
|
+
const KDGKBLED = 0x4b64;
|
|
107
|
+
const KDSKBLED = 0x4b65;
|
|
108
|
+
const LED_NUM = 0x02;
|
|
109
|
+
|
|
110
|
+
// Nothing here is essential to booting, so a console that is not a virtual
|
|
111
|
+
// terminal, a missing device node or a rejected ioctl is skipped rather than
|
|
112
|
+
// reported: the remaining consoles are still set, and the system comes up
|
|
113
|
+
// either way.
|
|
114
|
+
const setNumLock = (enabled = true) => {
|
|
115
|
+
const consoles = [];
|
|
116
|
+
const paths = ["/dev/tty0", "/dev/console", "/dev/tty"];
|
|
117
|
+
for (let index = 1; index <= 12; index++) paths.push(`/dev/tty${index}`);
|
|
118
|
+
for (const path of paths) {
|
|
119
|
+
let fd = -1;
|
|
120
|
+
try {
|
|
121
|
+
fd = libc.symbols.open(cString(path), 2, 0);
|
|
122
|
+
if (fd < 0) continue;
|
|
123
|
+
const state = new Uint8Array(1);
|
|
124
|
+
if (libc.symbols.ioctl(fd, KDGKBLED, ptr(state)) !== 0) continue;
|
|
125
|
+
const current = enabled ? state[0] | LED_NUM : state[0] & ~LED_NUM;
|
|
126
|
+
const fallback = enabled ? (state[0] >> 4) | LED_NUM : (state[0] >> 4) & ~LED_NUM;
|
|
127
|
+
const flags = (current & 7) | ((fallback & 7) << 4);
|
|
128
|
+
if (libc.symbols.ioctl(fd, KDSKBLED, flags) !== 0) continue;
|
|
129
|
+
libc.symbols.ioctl(fd, KDSETLED, 0x80);
|
|
130
|
+
consoles.push(path);
|
|
131
|
+
} catch {
|
|
132
|
+
// This console keeps whatever state it had; the others still get set.
|
|
133
|
+
} finally {
|
|
134
|
+
if (fd >= 0) {
|
|
135
|
+
try { libc.symbols.close(fd); } catch {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return consoles;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
globalThis.numlock = setNumLock;
|
|
143
|
+
try {
|
|
144
|
+
const consoles = setNumLock(true);
|
|
145
|
+
console.log(consoles.length
|
|
146
|
+
? `init.js: num lock on (${consoles.join(" ")})`
|
|
147
|
+
: "init.js: no virtual console accepted a num lock setting");
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error(`init.js: num lock: ${error?.message ?? error}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The input layer's character devices, so a pointing device has a
|
|
153
|
+
// /dev/input/event* node for `bunterm --mouse` to read. Loading them only
|
|
154
|
+
// creates those nodes: the console keyboard is unaffected, and nothing reads
|
|
155
|
+
// a pointer unless it is asked to. A USB mouse also needs the usbhid stack,
|
|
156
|
+
// which `cfg.all` or a --real boot brings in.
|
|
157
|
+
try {
|
|
158
|
+
const { tryModprobe } = await import("/lib/modprobe.js");
|
|
159
|
+
const loaded = ["evdev", "psmouse"].filter((module) => tryModprobe(module) !== null);
|
|
160
|
+
const { readdirSync } = await import("node:fs");
|
|
161
|
+
const devices = (() => {
|
|
162
|
+
try { return readdirSync("/dev/input").filter((name) => name.startsWith("event")); } catch { return []; }
|
|
163
|
+
})();
|
|
164
|
+
console.log(`init.js: input modules ${loaded.length ? loaded.join(" ") : "none"}`
|
|
165
|
+
+ ` (${devices.length} event device${devices.length === 1 ? "" : "s"})`);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`init.js: input modules: ${error?.message ?? error}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
97
170
|
const { default: repl } = await import("node:repl");
|
|
98
171
|
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
99
172
|
|
|
@@ -354,7 +427,7 @@ Type bunterm in bunmsh: Graphical Terminal
|
|
|
354
427
|
], {
|
|
355
428
|
env: {
|
|
356
429
|
...process.env,
|
|
357
|
-
PATH:"/bin:/usr/bin:/buninu/bin",
|
|
430
|
+
PATH:"/bin:/sbin:/usr/bin:/usr/sbin:/buninu/.bun/bin:/buninu/bin",
|
|
358
431
|
HOME:"/buninu"
|
|
359
432
|
},
|
|
360
433
|
stdin: "inherit",
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// /lib/bunterm/mouse.js — pointing devices through the Linux input layer.
|
|
2
|
+
//
|
|
3
|
+
// Read from /dev/input/event*, where the kernel reports one 24-byte
|
|
4
|
+
// input_event per axis, button or wheel notch, ending each batch with an
|
|
5
|
+
// EV_SYN. Relative devices (a mouse) move the pointer by their deltas;
|
|
6
|
+
// absolute ones (QEMU's USB tablet, a touchscreen) place it directly, which
|
|
7
|
+
// needs the axis range from EVIOCGABS. This module is only loaded when
|
|
8
|
+
// bunterm is asked for a mouse.
|
|
9
|
+
|
|
10
|
+
import { closeSync, existsSync, openSync, read, readFileSync, readdirSync } from "node:fs";
|
|
11
|
+
import { libc, ptr } from "../dlopen.js";
|
|
12
|
+
|
|
13
|
+
const EVENT_SIZE = 24;
|
|
14
|
+
const EV_SYN = 0x00;
|
|
15
|
+
const EV_KEY = 0x01;
|
|
16
|
+
const EV_REL = 0x02;
|
|
17
|
+
const EV_ABS = 0x03;
|
|
18
|
+
const REL_X = 0x00;
|
|
19
|
+
const REL_Y = 0x01;
|
|
20
|
+
const REL_WHEEL = 0x08;
|
|
21
|
+
const REL_HWHEEL = 0x06;
|
|
22
|
+
const ABS_X = 0x00;
|
|
23
|
+
const ABS_Y = 0x01;
|
|
24
|
+
const BTN_LEFT = 0x110;
|
|
25
|
+
const BTN_RIGHT = 0x111;
|
|
26
|
+
const BTN_MIDDLE = 0x112;
|
|
27
|
+
const BTN_TOUCH = 0x14a;
|
|
28
|
+
|
|
29
|
+
// struct input_absinfo { s32 value, minimum, maximum, fuzz, flat, resolution }
|
|
30
|
+
const EVIOCGABS = (axis) => 0x80184540 + axis;
|
|
31
|
+
|
|
32
|
+
const buttonNames = {
|
|
33
|
+
[BTN_LEFT]: "left",
|
|
34
|
+
[BTN_RIGHT]: "right",
|
|
35
|
+
[BTN_MIDDLE]: "middle",
|
|
36
|
+
[BTN_TOUCH]: "left",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const capability = (directory, name) => {
|
|
40
|
+
try {
|
|
41
|
+
// A capability file is a space-separated list of 64-bit hex words, most
|
|
42
|
+
// significant first, so the last word holds bits 0-63.
|
|
43
|
+
const words = readFileSync(`${directory}/${name}`, "utf8").trim().split(/\s+/u).reverse();
|
|
44
|
+
return (bit) => {
|
|
45
|
+
const word = words[Math.floor(bit / 64)];
|
|
46
|
+
return word ? (BigInt(`0x${word}`) >> BigInt(bit % 64)) & 1n ? true : false : false;
|
|
47
|
+
};
|
|
48
|
+
} catch {
|
|
49
|
+
return () => false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Every /dev/input/event* that looks like a pointer, with how it reports
|
|
54
|
+
// position. A device with both relative and absolute axes counts as absolute.
|
|
55
|
+
export const findPointers = (directory = "/dev/input") => {
|
|
56
|
+
const found = [];
|
|
57
|
+
let names = [];
|
|
58
|
+
try {
|
|
59
|
+
names = readdirSync(directory).filter((name) => /^event\d+$/u.test(name));
|
|
60
|
+
} catch {
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
63
|
+
for (const name of names.sort()) {
|
|
64
|
+
const sysfs = `/sys/class/input/${name}/device/capabilities`;
|
|
65
|
+
if (!existsSync(sysfs)) continue;
|
|
66
|
+
const relative = capability(sysfs, "rel");
|
|
67
|
+
const absolute = capability(sysfs, "abs");
|
|
68
|
+
const keys = capability(sysfs, "key");
|
|
69
|
+
const hasRelative = relative(REL_X) && relative(REL_Y);
|
|
70
|
+
const hasAbsolute = absolute(ABS_X) && absolute(ABS_Y);
|
|
71
|
+
const hasButton = keys(BTN_LEFT) || keys(BTN_TOUCH);
|
|
72
|
+
if (!hasButton || (!hasRelative && !hasAbsolute)) continue;
|
|
73
|
+
let label = name;
|
|
74
|
+
try {
|
|
75
|
+
label = readFileSync(`/sys/class/input/${name}/device/name`, "utf8").trim() || name;
|
|
76
|
+
} catch {}
|
|
77
|
+
found.push({
|
|
78
|
+
path: `${directory}/${name}`,
|
|
79
|
+
name: label,
|
|
80
|
+
kind: hasAbsolute ? "absolute" : "relative",
|
|
81
|
+
wheel: relative(REL_WHEEL),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return found;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const absoluteRange = (fd, axis) => {
|
|
88
|
+
const info = new Int32Array(6);
|
|
89
|
+
if (libc.symbols.ioctl(fd, EVIOCGABS(axis), ptr(info)) === -1) return null;
|
|
90
|
+
const [, minimum, maximum] = info;
|
|
91
|
+
return maximum > minimum ? { minimum, maximum } : null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Opens every pointer in `devices` and reports the pointer state after each
|
|
95
|
+
// EV_SYN: { x, y, buttons: { left, middle, right }, wheel, hwheel, moved,
|
|
96
|
+
// pressed, released }. Coordinates are pixels clamped to width x height.
|
|
97
|
+
// `speed` scales relative motion. close() releases the devices.
|
|
98
|
+
export const openPointer = ({
|
|
99
|
+
devices = findPointers(),
|
|
100
|
+
width,
|
|
101
|
+
height,
|
|
102
|
+
speed = 1,
|
|
103
|
+
x = Math.floor(width / 2),
|
|
104
|
+
y = Math.floor(height / 2),
|
|
105
|
+
onSync,
|
|
106
|
+
onError = () => {},
|
|
107
|
+
}) => {
|
|
108
|
+
const state = {
|
|
109
|
+
x, y,
|
|
110
|
+
buttons: { left: false, middle: false, right: false },
|
|
111
|
+
wheel: 0,
|
|
112
|
+
hwheel: 0,
|
|
113
|
+
moved: false,
|
|
114
|
+
pressed: [],
|
|
115
|
+
released: [],
|
|
116
|
+
};
|
|
117
|
+
const open = [];
|
|
118
|
+
|
|
119
|
+
for (const device of devices) {
|
|
120
|
+
let fd = -1;
|
|
121
|
+
try {
|
|
122
|
+
fd = openSync(device.path, "r");
|
|
123
|
+
const ranges = device.kind === "absolute"
|
|
124
|
+
? { x: absoluteRange(fd, ABS_X), y: absoluteRange(fd, ABS_Y) }
|
|
125
|
+
: null;
|
|
126
|
+
// An absolute device without a usable range is read as relative.
|
|
127
|
+
const kind = ranges?.x && ranges?.y ? "absolute" : "relative";
|
|
128
|
+
open.push({ ...device, fd, kind, ranges, buffer: Buffer.alloc(EVENT_SIZE * 64), reading: true });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (fd >= 0) {
|
|
131
|
+
try { closeSync(fd); } catch {}
|
|
132
|
+
}
|
|
133
|
+
onError(new Error(`${device.path}: ${error.message}`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const clamp = (value, maximum) => Math.max(0, Math.min(maximum - 1, value));
|
|
138
|
+
|
|
139
|
+
const apply = (entry, type, code, value) => {
|
|
140
|
+
if (type === EV_REL) {
|
|
141
|
+
if (code === REL_X) { state.x = clamp(Math.round(state.x + value * speed), width); state.moved = true; }
|
|
142
|
+
else if (code === REL_Y) { state.y = clamp(Math.round(state.y + value * speed), height); state.moved = true; }
|
|
143
|
+
else if (code === REL_WHEEL) state.wheel += value;
|
|
144
|
+
else if (code === REL_HWHEEL) state.hwheel += value;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (type === EV_ABS && entry.kind === "absolute") {
|
|
148
|
+
const range = code === ABS_X ? entry.ranges.x : code === ABS_Y ? entry.ranges.y : null;
|
|
149
|
+
if (!range) return;
|
|
150
|
+
const span = range.maximum - range.minimum;
|
|
151
|
+
const fraction = (value - range.minimum) / span;
|
|
152
|
+
if (code === ABS_X) state.x = clamp(Math.round(fraction * width), width);
|
|
153
|
+
else state.y = clamp(Math.round(fraction * height), height);
|
|
154
|
+
state.moved = true;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (type === EV_KEY) {
|
|
158
|
+
const button = buttonNames[code];
|
|
159
|
+
if (!button) return;
|
|
160
|
+
const down = value !== 0;
|
|
161
|
+
if (state.buttons[button] === down) return; // key repeat
|
|
162
|
+
state.buttons[button] = down;
|
|
163
|
+
(down ? state.pressed : state.released).push(button);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const flush = () => {
|
|
168
|
+
const report = {
|
|
169
|
+
x: state.x,
|
|
170
|
+
y: state.y,
|
|
171
|
+
buttons: { ...state.buttons },
|
|
172
|
+
wheel: state.wheel,
|
|
173
|
+
hwheel: state.hwheel,
|
|
174
|
+
moved: state.moved,
|
|
175
|
+
pressed: state.pressed,
|
|
176
|
+
released: state.released,
|
|
177
|
+
};
|
|
178
|
+
state.wheel = 0;
|
|
179
|
+
state.hwheel = 0;
|
|
180
|
+
state.moved = false;
|
|
181
|
+
state.pressed = [];
|
|
182
|
+
state.released = [];
|
|
183
|
+
if (report.moved || report.wheel || report.hwheel || report.pressed.length || report.released.length) {
|
|
184
|
+
try { onSync(report); } catch (error) { onError(error); }
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const pump = (entry) => {
|
|
189
|
+
if (!entry.reading) return;
|
|
190
|
+
read(entry.fd, entry.buffer, 0, entry.buffer.length, null, (error, count) => {
|
|
191
|
+
if (!entry.reading) return;
|
|
192
|
+
if (error) {
|
|
193
|
+
if (error.code === "EAGAIN") { setTimeout(() => pump(entry), 10); return; }
|
|
194
|
+
entry.reading = false;
|
|
195
|
+
onError(new Error(`${entry.path}: ${error.message}`));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
for (let offset = 0; offset + EVENT_SIZE <= count; offset += EVENT_SIZE) {
|
|
199
|
+
const type = entry.buffer.readUInt16LE(offset + 16);
|
|
200
|
+
const code = entry.buffer.readUInt16LE(offset + 18);
|
|
201
|
+
const value = entry.buffer.readInt32LE(offset + 20);
|
|
202
|
+
if (type === EV_SYN) flush();
|
|
203
|
+
else apply(entry, type, code, value);
|
|
204
|
+
}
|
|
205
|
+
pump(entry);
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
for (const entry of open) pump(entry);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
devices: open.map(({ path, name, kind }) => ({ path, name, kind })),
|
|
213
|
+
state,
|
|
214
|
+
close() {
|
|
215
|
+
for (const entry of open) {
|
|
216
|
+
entry.reading = false;
|
|
217
|
+
try { closeSync(entry.fd); } catch {}
|
|
218
|
+
}
|
|
219
|
+
open.length = 0;
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
};
|
|
@@ -105,6 +105,7 @@ export class Renderer {
|
|
|
105
105
|
this.forcedRows = new Set();
|
|
106
106
|
this.dirtyAll = true;
|
|
107
107
|
this.lastCursor = { x: -1, y: -1, visible: false };
|
|
108
|
+
this.lastPointer = null;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
fontFor(typeface, size, { bold = false, italic = false } = {}) {
|
|
@@ -355,11 +356,46 @@ export class Renderer {
|
|
|
355
356
|
}
|
|
356
357
|
}
|
|
357
358
|
|
|
359
|
+
// Draws the mouse pointer at (x, y) in pixels: an arrow outlined in the
|
|
360
|
+
// background colour so it stays visible over any cell.
|
|
361
|
+
drawPointer({ x, y }) {
|
|
362
|
+
const { CanvasKit, canvas } = this;
|
|
363
|
+
const size = Math.max(10, Math.round(this.fontSize * 0.9));
|
|
364
|
+
const builder = new CanvasKit.PathBuilder();
|
|
365
|
+
builder.moveTo(x, y);
|
|
366
|
+
builder.lineTo(x, y + size);
|
|
367
|
+
builder.lineTo(x + size * 0.28, y + size * 0.74);
|
|
368
|
+
builder.lineTo(x + size * 0.46, y + size * 1.08);
|
|
369
|
+
builder.lineTo(x + size * 0.63, y + size * 1.0);
|
|
370
|
+
builder.lineTo(x + size * 0.45, y + size * 0.66);
|
|
371
|
+
builder.lineTo(x + size * 0.72, y + size * 0.66);
|
|
372
|
+
builder.close();
|
|
373
|
+
const path = builder.detach();
|
|
374
|
+
builder.delete();
|
|
375
|
+
const outline = this.paintFor(this.background).copy();
|
|
376
|
+
outline.setStyle(CanvasKit.PaintStyle.Stroke);
|
|
377
|
+
outline.setStrokeWidth(Math.max(1, size / 8));
|
|
378
|
+
canvas.drawPath(path, outline);
|
|
379
|
+
canvas.drawPath(path, this.paintFor(this.cursorColor));
|
|
380
|
+
outline.delete();
|
|
381
|
+
path.delete();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// The rows an arrow at (x, y) covers, so they are repainted when it moves.
|
|
385
|
+
pointerRows({ x, y }) {
|
|
386
|
+
void x;
|
|
387
|
+
const size = Math.max(10, Math.round(this.fontSize * 0.9));
|
|
388
|
+
const first = Math.floor((y - this.offsetY) / this.cellHeight);
|
|
389
|
+
const last = Math.ceil((y + size * 1.1 - this.offsetY) / this.cellHeight);
|
|
390
|
+
return [first, last];
|
|
391
|
+
}
|
|
392
|
+
|
|
358
393
|
// Repaints what changed and flushes to the framebuffer. `cursor` is
|
|
359
394
|
// { x, y, visible }; `images` is a list of { image, rect } placements in
|
|
360
|
-
// screen pixels, from images.js, drawn over the text
|
|
361
|
-
// anything was
|
|
362
|
-
|
|
395
|
+
// screen pixels, from images.js, drawn over the text; `pointer` is the
|
|
396
|
+
// mouse position in pixels, drawn last. Returns true when anything was
|
|
397
|
+
// painted.
|
|
398
|
+
render(term, { cursor, images = [], pointer = null } = {}) {
|
|
363
399
|
const buffer = term.buffer.active;
|
|
364
400
|
const top = buffer.viewportY;
|
|
365
401
|
const dirty = [];
|
|
@@ -375,6 +411,13 @@ export class Renderer {
|
|
|
375
411
|
const cursorRows = new Set();
|
|
376
412
|
if (this.lastCursor.visible) cursorRows.add(this.lastCursor.y);
|
|
377
413
|
if (cursor?.visible) cursorRows.add(cursor.y);
|
|
414
|
+
// Wherever the pointer was and is has to be repainted: the text under it
|
|
415
|
+
// has not changed, so no row hash would say so.
|
|
416
|
+
for (const position of [this.lastPointer, pointer]) {
|
|
417
|
+
if (!position) continue;
|
|
418
|
+
const [first, last] = this.pointerRows(position);
|
|
419
|
+
for (let row = Math.max(0, first); row < Math.min(this.rows, last); row++) cursorRows.add(row);
|
|
420
|
+
}
|
|
378
421
|
for (const row of cursorRows) {
|
|
379
422
|
if (row >= 0 && row < this.rows && !dirty.includes(row)) dirty.push(row);
|
|
380
423
|
}
|
|
@@ -390,6 +433,8 @@ export class Renderer {
|
|
|
390
433
|
}
|
|
391
434
|
this.lastCursor = { x: cursor?.x ?? -1, y: cursor?.y ?? -1, visible: Boolean(cursor?.visible) };
|
|
392
435
|
if (images.length) this.drawImages(images, dirty);
|
|
436
|
+
if (pointer) this.drawPointer(pointer);
|
|
437
|
+
this.lastPointer = pointer ? { x: pointer.x, y: pointer.y } : null;
|
|
393
438
|
this.screen.flush();
|
|
394
439
|
return true;
|
|
395
440
|
}
|
|
@@ -50,6 +50,7 @@ export const createSession = ({
|
|
|
50
50
|
theme = defaultTheme,
|
|
51
51
|
scrollback = 1000,
|
|
52
52
|
cursorBlink = true,
|
|
53
|
+
mouse = true,
|
|
53
54
|
env = {},
|
|
54
55
|
onError = (error) => console.error(`bunterm: ${error?.stack ?? error}`),
|
|
55
56
|
}) => {
|
|
@@ -82,13 +83,24 @@ export const createSession = ({
|
|
|
82
83
|
let frameTimer = null;
|
|
83
84
|
let blinkOn = true;
|
|
84
85
|
const cursorHidden = () => Boolean(term._core?.coreService?.isCursorHidden);
|
|
86
|
+
// Only set when a pointer is attached; without one the renderer is called
|
|
87
|
+
// exactly as before.
|
|
88
|
+
let pointer = null;
|
|
85
89
|
const paint = () => {
|
|
86
90
|
if (frameTimer) clearTimeout(frameTimer);
|
|
87
91
|
frameTimer = null;
|
|
88
92
|
const buffer = term.buffer.active;
|
|
93
|
+
// cursorY counts from the buffer's base, so a scrolled-back viewport
|
|
94
|
+
// moves the cursor down the screen and eventually off it.
|
|
95
|
+
const row = buffer.baseY + buffer.cursorY - buffer.viewportY;
|
|
89
96
|
return renderer.render(term, {
|
|
90
|
-
cursor: {
|
|
97
|
+
cursor: {
|
|
98
|
+
x: buffer.cursorX,
|
|
99
|
+
y: row,
|
|
100
|
+
visible: blinkOn && !cursorHidden() && row >= 0 && row < renderer.rows,
|
|
101
|
+
},
|
|
91
102
|
images: images.visible(),
|
|
103
|
+
pointer,
|
|
92
104
|
});
|
|
93
105
|
};
|
|
94
106
|
const requestFrame = () => {
|
|
@@ -137,10 +149,102 @@ export const createSession = ({
|
|
|
137
149
|
const applicationCursorKeys = Boolean(term._core?.coreService?.decPrivateModes?.applicationCursorKeys);
|
|
138
150
|
const text = translateKeys(bytes.toString("latin1"), { applicationCursorKeys });
|
|
139
151
|
backend.write(Buffer.from(text, "latin1"));
|
|
152
|
+
// Typing returns to the live screen, as every terminal does.
|
|
153
|
+
if (term.buffer.active.viewportY !== term.buffer.active.baseY) {
|
|
154
|
+
term.scrollToBottom();
|
|
155
|
+
renderer.invalidate();
|
|
156
|
+
}
|
|
140
157
|
resetBlink();
|
|
141
158
|
requestFrame();
|
|
142
159
|
};
|
|
143
160
|
|
|
161
|
+
// Turns a pointer report from mouse.js into the mouse sequences the
|
|
162
|
+
// program asked for. xterm.js's CoreMouseService owns the protocol: it
|
|
163
|
+
// knows which events the program enabled (DECSET 1000/1002/1003), encodes
|
|
164
|
+
// them (SGR 1006 or the default form) and sends them through onData, so
|
|
165
|
+
// nothing here builds an escape sequence. Reports arrive only while a
|
|
166
|
+
// pointer is open, and this is the only place the terminal sees one.
|
|
167
|
+
const CoreMouseButton = { LEFT: 0, MIDDLE: 1, RIGHT: 2, NONE: 3, WHEEL: 4 };
|
|
168
|
+
const CoreMouseAction = { UP: 0, DOWN: 1, LEFT: 2, RIGHT: 3, MOVE: 32 };
|
|
169
|
+
const heldButton = (buttons) => (buttons.left ? CoreMouseButton.LEFT
|
|
170
|
+
: buttons.middle ? CoreMouseButton.MIDDLE
|
|
171
|
+
: buttons.right ? CoreMouseButton.RIGHT
|
|
172
|
+
: CoreMouseButton.NONE);
|
|
173
|
+
|
|
174
|
+
// Scrolls the viewport when the program is not reading the mouse itself:
|
|
175
|
+
// through the scrollback on the normal screen, and as arrow keys on the
|
|
176
|
+
// alternate screen, which has no scrollback and is what a pager expects.
|
|
177
|
+
const wheelScroll = (notches) => {
|
|
178
|
+
const lines = notches * 3;
|
|
179
|
+
if (term.buffer.active.type === "alternate") {
|
|
180
|
+
const application = Boolean(term._core?.coreService?.decPrivateModes?.applicationCursorKeys);
|
|
181
|
+
const key = `\u001b${application ? "O" : "["}${lines > 0 ? "A" : "B"}`;
|
|
182
|
+
backend.write(Buffer.from(key.repeat(Math.abs(lines)), "latin1"));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const before = term.buffer.active.viewportY;
|
|
186
|
+
term.scrollLines(-lines);
|
|
187
|
+
if (term.buffer.active.viewportY !== before) {
|
|
188
|
+
// Every row on screen is different now, and identical text at the same
|
|
189
|
+
// row would otherwise hash the same as before.
|
|
190
|
+
renderer.invalidate();
|
|
191
|
+
requestFrame();
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const reportMouse = (report) => {
|
|
196
|
+
pointer = { x: report.x, y: report.y };
|
|
197
|
+
requestFrame();
|
|
198
|
+
const service = term._core?.coreMouseService;
|
|
199
|
+
if (!service?.areMouseEventsActive) {
|
|
200
|
+
if (report.wheel) wheelScroll(report.wheel);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const column = Math.floor((report.x - renderer.offsetX) / renderer.cellWidth);
|
|
204
|
+
const row = Math.floor((report.y - renderer.offsetY) / renderer.cellHeight);
|
|
205
|
+
const send = (button, action) => {
|
|
206
|
+
try {
|
|
207
|
+
service.triggerMouseEvent({ col: column, row, button, action, ctrl: false, alt: false, shift: false });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
onError(error);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
for (const name of report.released) {
|
|
213
|
+
send(CoreMouseButton[name.toUpperCase()], CoreMouseAction.UP);
|
|
214
|
+
}
|
|
215
|
+
for (const name of report.pressed) {
|
|
216
|
+
send(CoreMouseButton[name.toUpperCase()], CoreMouseAction.DOWN);
|
|
217
|
+
}
|
|
218
|
+
if (report.moved) send(heldButton(report.buttons), CoreMouseAction.MOVE);
|
|
219
|
+
for (let notch = 0; notch < Math.abs(report.wheel); notch++) {
|
|
220
|
+
send(CoreMouseButton.WHEEL, report.wheel > 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN);
|
|
221
|
+
}
|
|
222
|
+
for (let notch = 0; notch < Math.abs(report.hwheel); notch++) {
|
|
223
|
+
send(CoreMouseButton.WHEEL, report.hwheel > 0 ? CoreMouseAction.RIGHT : CoreMouseAction.LEFT);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// The pointer is opened only when asked for, by a module that is imported
|
|
228
|
+
// only then, so a machine without one — or a broken input layer — cannot
|
|
229
|
+
// affect a plain session.
|
|
230
|
+
let pointerDevice = null;
|
|
231
|
+
const attachPointer = async () => {
|
|
232
|
+
if (!mouse) return null;
|
|
233
|
+
const { openPointer, findPointers } = await import("./mouse.js");
|
|
234
|
+
const devices = findPointers();
|
|
235
|
+
if (!devices.length) throw new Error("no pointing device in /dev/input (is the evdev module loaded?)");
|
|
236
|
+
pointerDevice = openPointer({
|
|
237
|
+
devices,
|
|
238
|
+
width: screen.width,
|
|
239
|
+
height: screen.height,
|
|
240
|
+
onSync: reportMouse,
|
|
241
|
+
onError,
|
|
242
|
+
});
|
|
243
|
+
pointer = { x: pointerDevice.state.x, y: pointerDevice.state.y };
|
|
244
|
+
paint();
|
|
245
|
+
return pointerDevice;
|
|
246
|
+
};
|
|
247
|
+
|
|
144
248
|
// Waits until every byte received so far has been parsed, then paints.
|
|
145
249
|
const settle = async () => {
|
|
146
250
|
await queue;
|
|
@@ -154,13 +258,18 @@ export const createSession = ({
|
|
|
154
258
|
if (frameTimer) clearTimeout(frameTimer);
|
|
155
259
|
frameTimer = null;
|
|
156
260
|
backend.close();
|
|
261
|
+
pointerDevice?.close();
|
|
262
|
+
pointerDevice = null;
|
|
157
263
|
images.delete();
|
|
158
264
|
renderer.delete();
|
|
159
265
|
term.dispose();
|
|
160
266
|
};
|
|
161
267
|
|
|
162
268
|
paint();
|
|
163
|
-
return {
|
|
269
|
+
return {
|
|
270
|
+
term, backend, renderer, images, kitty,
|
|
271
|
+
paint, requestFrame, settle, input, exited, close, attachPointer, reportMouse, wheelScroll,
|
|
272
|
+
};
|
|
164
273
|
};
|
|
165
274
|
|
|
166
275
|
// Runs a session on the framebuffer with the console in graphics mode and
|
|
@@ -193,6 +302,18 @@ export const runTerminal = async ({ console: consoleDevice = null, device = "/de
|
|
|
193
302
|
},
|
|
194
303
|
});
|
|
195
304
|
onRestore(() => keyboard.close());
|
|
305
|
+
if (options.mouse !== false) {
|
|
306
|
+
// A pointer that cannot be opened is reported and the session
|
|
307
|
+
// continues as a keyboard-only terminal.
|
|
308
|
+
try {
|
|
309
|
+
const attached = await session.attachPointer();
|
|
310
|
+
if (attached?.devices.length) {
|
|
311
|
+
console.error(`bunterm: mouse: ${attached.devices.map((device) => device.name).join(", ")}`);
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error(`bunterm: mouse: ${error.message}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
196
317
|
return await session.exited;
|
|
197
318
|
} finally {
|
|
198
319
|
keyboard?.close();
|
package/initramfs/lib/dlopen.js
CHANGED
|
@@ -85,6 +85,10 @@ export const libc = dlopen(libcPath, {
|
|
|
85
85
|
args: [FFIType.i32, FFIType.ptr, FFIType.i32],
|
|
86
86
|
returns: FFIType.i32,
|
|
87
87
|
},
|
|
88
|
+
chroot: {
|
|
89
|
+
args: [FFIType.ptr],
|
|
90
|
+
returns: FFIType.i32,
|
|
91
|
+
},
|
|
88
92
|
});
|
|
89
93
|
|
|
90
94
|
export const cString = (value) => new TextEncoder().encode(`${value}\0`);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Shared renderer for /usr/share/doc/buninu-linux/<name>.md command manuals.
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
2
4
|
export const showDocument = async (name) => {
|
|
3
|
-
const path = `${import.meta.dir}/../usr/share/doc/buninu-linux/${name}.md
|
|
5
|
+
const path = resolve(`${import.meta.dir}/../usr/share/doc/buninu-linux/${name}.md`);
|
|
4
6
|
const rendered = Bun.markdown.ansi(await Bun.file(path).text(), { hyperlinks: true });
|
|
5
7
|
process.stdout.write(rendered.endsWith("\n") ? rendered : `${rendered}\n`);
|
|
8
|
+
// Where the page lives, for anyone who wants to read or edit the source.
|
|
9
|
+
process.stdout.write(`\u001b[2m${path}\u001b[0m\n`);
|
|
6
10
|
};
|
|
@@ -10,6 +10,7 @@ bunterm # run /bin/sh on the console you are on
|
|
|
10
10
|
bunterm /dev/tty1 # use virtual console 1 for graphics and keys
|
|
11
11
|
bunterm --font-size 20
|
|
12
12
|
bunterm -e bun /buninu/apps/jsmdcui/src/index.js --demo
|
|
13
|
+
bunterm --no-mouse # keyboard only
|
|
13
14
|
bunterm -h
|
|
14
15
|
```
|
|
15
16
|
|
|
@@ -28,6 +29,7 @@ text mode and `bunterm` exits with its status.
|
|
|
28
29
|
| `--line-height F` | Multiplies the cell height (default 1). |
|
|
29
30
|
| `--fb /dev/fbN` | The framebuffer device (default `/dev/fb0`). |
|
|
30
31
|
| `--no-blink` | A steady cursor. |
|
|
32
|
+
| `--no-mouse` | Do not attach a pointing device; nothing then reads `/dev/input`. |
|
|
31
33
|
|
|
32
34
|
## What is drawn
|
|
33
35
|
|
|
@@ -59,13 +61,40 @@ screen. A few console-specific
|
|
|
59
61
|
sequences are rewritten to xterm's (`F1`–`F5`, `Home`, `End`), and arrow keys
|
|
60
62
|
follow the program's application cursor mode.
|
|
61
63
|
|
|
64
|
+
## Mouse
|
|
65
|
+
|
|
66
|
+
Every pointing device under `/dev/input/event*` is opened and an arrow is
|
|
67
|
+
drawn on the screen. Clicks, drags and wheel notches reach the program as
|
|
68
|
+
the xterm mouse sequences it asked for: nothing is sent unless the program
|
|
69
|
+
enabled reporting (`DECSET 1000`, `1002` or `1003`), and what is sent follows
|
|
70
|
+
the mode it chose, SGR (`1006`) included. `jsmdcui`, `micro`, `htop`, `vim`
|
|
71
|
+
and anything else that speaks the protocol work with no configuration.
|
|
72
|
+
|
|
73
|
+
When the program is not reading the mouse, the wheel scrolls instead: back
|
|
74
|
+
through the scrollback on the normal screen, three lines per notch, and as
|
|
75
|
+
arrow keys on the alternate screen, which has no scrollback and is what a
|
|
76
|
+
pager expects. Typing returns to the live screen, and the text cursor is
|
|
77
|
+
hidden while it is scrolled out of view.
|
|
78
|
+
|
|
79
|
+
`--no-mouse` turns all of this off: the terminal then never looks at
|
|
80
|
+
`/dev/input` and never loads the mouse module.
|
|
81
|
+
|
|
82
|
+
`init.js` loads `evdev` and `psmouse` at boot, so a PS/2 mouse — which is
|
|
83
|
+
what QEMU's q35 machine and most PCs present — has a device node ready. A USB
|
|
84
|
+
mouse also needs the `usbhid` stack, which a `--real` image carries and
|
|
85
|
+
`cfg.all` loads. The pointer follows relative devices (a mouse) and absolute
|
|
86
|
+
ones (a tablet or touchscreen) alike; a device that cannot be opened is
|
|
87
|
+
reported and the session continues without it.
|
|
88
|
+
|
|
62
89
|
## Limits
|
|
63
90
|
|
|
64
91
|
- Needs a framebuffer: `linux-lts` has one built in, while the default
|
|
65
92
|
`linux-virt` builds it as modules the image does not ship, so build with
|
|
66
93
|
`--linux-lts` or `--real` (efifb/simpledrm from UEFI).
|
|
67
|
-
- No
|
|
68
|
-
lines
|
|
94
|
+
- No selection, and no keyboard way to scroll back: the wheel is it. The
|
|
95
|
+
buffer keeps 1000 lines. A program that does not enable mouse reporting
|
|
96
|
+
sees no clicks — the arrow still moves, so the pointer is visibly alive.
|
|
97
|
+
`--no-mouse` removes the pointer entirely.
|
|
69
98
|
- A program that prints a terminal reply while the tty echoes (that is, one
|
|
70
99
|
not in raw mode) will see the reply echoed, as on any terminal.
|
|
71
100
|
- Kitty images are drawn above text; `z` ordering below text and animation
|