buninu-linux 0.2.1 → 0.3.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 +547 -190
- package/build-uki.sh +4 -2
- package/fetch-alpine.sh +78 -43
- package/index.js +4 -1
- package/initramfs/bin/ip +630 -0
- package/initramfs/bin/mount +401 -0
- package/initramfs/bin/poweroff +10 -0
- package/initramfs/bin/ps +256 -0
- package/initramfs/bin/reboot +10 -0
- package/initramfs/bin/tar +169 -0
- package/initramfs/bin/top +129 -0
- package/initramfs/bin/umount +71 -0
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +36 -0
- package/initramfs/buninu/apps/bunmsh/README.md +7 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/fancy-ls.js +29 -14
- package/initramfs/buninu/apps/bunmsh/src/line-edit.js +43 -0
- package/initramfs/buninu/apps/bunmsh/src/main.js +15 -1
- package/initramfs/buninu/apps/bunmsh/src/mouse.js +29 -4
- package/initramfs/buninu/apps/bunmsh/src/shell.js +9 -1
- package/initramfs/buninu/apps/bunmsh/test/shell1.test.js +70 -0
- package/initramfs/buninu/apps/jsmdcui/CHANGELOG.md +10 -0
- package/initramfs/buninu/apps/jsmdcui/README.md +2 -0
- package/initramfs/buninu/apps/jsmdcui/package.json +1 -1
- package/initramfs/buninu/apps/jsmdcui/runtime/help/commands.md +4 -1
- package/initramfs/buninu/apps/jsmdcui/runtime/help/help.md +2 -0
- package/initramfs/buninu/apps/jsmdcui/src/index.js +40 -13
- package/initramfs/buninu/package.json +1 -1
- package/initramfs/init.js +164 -11
- package/initramfs/lib/dlopen.js +121 -0
- package/initramfs/lib/document.js +6 -0
- package/initramfs/lib/modprobe.js +235 -0
- package/initramfs/lib/power.js +35 -0
- package/initramfs/usr/share/doc/buninu-linux/ip.md +155 -0
- package/initramfs/usr/share/doc/buninu-linux/mount.md +155 -0
- package/initramfs/usr/share/doc/buninu-linux/poweroff.md +15 -0
- package/initramfs/usr/share/doc/buninu-linux/ps.md +94 -0
- package/initramfs/usr/share/doc/buninu-linux/reboot.md +15 -0
- package/initramfs/usr/share/doc/buninu-linux/tar.md +73 -0
- package/initramfs/usr/share/doc/buninu-linux/top.md +23 -0
- package/initramfs/usr/share/doc/buninu-linux/umount.md +48 -0
- package/package.json +2 -2
|
@@ -21,7 +21,13 @@ function partialMarkerSuffixLength(text, marker) {
|
|
|
21
21
|
return 0;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function mouseInput(
|
|
24
|
+
export function mouseInput(
|
|
25
|
+
onMouse,
|
|
26
|
+
onCursorPosition,
|
|
27
|
+
onShortcut = () => {},
|
|
28
|
+
onPaste = () => {},
|
|
29
|
+
onEditShortcut = () => {},
|
|
30
|
+
) {
|
|
25
31
|
const decoder = new StringDecoder("utf8");
|
|
26
32
|
let pending = "";
|
|
27
33
|
// Bracketed paste (\x1b[200~ ... \x1b[201~): everything between the
|
|
@@ -48,6 +54,25 @@ export function mouseInput(onMouse, onCursorPosition, onShortcut = () => {}, onP
|
|
|
48
54
|
source = source.replace(/\x1bc/g, () => { onShortcut("tab-close"); return ""; });
|
|
49
55
|
}
|
|
50
56
|
let output = "";
|
|
57
|
+
const flushOutput = () => {
|
|
58
|
+
if (!output) return;
|
|
59
|
+
this.push(output);
|
|
60
|
+
output = "";
|
|
61
|
+
};
|
|
62
|
+
const appendPlainText = (text) => {
|
|
63
|
+
let start = 0;
|
|
64
|
+
for (let index = 0; index < text.length; index++) {
|
|
65
|
+
const side = text[index] === "\x15" ? "head"
|
|
66
|
+
: text[index] === "\x0b" ? "tail"
|
|
67
|
+
: null;
|
|
68
|
+
if (!side) continue;
|
|
69
|
+
output += text.slice(start, index);
|
|
70
|
+
flushOutput();
|
|
71
|
+
onEditShortcut(side);
|
|
72
|
+
start = index + 1;
|
|
73
|
+
}
|
|
74
|
+
output += text.slice(start);
|
|
75
|
+
};
|
|
51
76
|
while (source) {
|
|
52
77
|
if (pasting) {
|
|
53
78
|
const end = source.indexOf(PASTE_END);
|
|
@@ -66,8 +91,8 @@ export function mouseInput(onMouse, onCursorPosition, onShortcut = () => {}, onP
|
|
|
66
91
|
continue;
|
|
67
92
|
}
|
|
68
93
|
const escape = source.indexOf("\x1b[");
|
|
69
|
-
if (escape < 0) {
|
|
70
|
-
|
|
94
|
+
if (escape < 0) { appendPlainText(source); break; }
|
|
95
|
+
appendPlainText(source.slice(0, escape));
|
|
71
96
|
source = source.slice(escape);
|
|
72
97
|
if (source.startsWith(PASTE_START)) {
|
|
73
98
|
pasting = true;
|
|
@@ -93,7 +118,7 @@ export function mouseInput(onMouse, onCursorPosition, onShortcut = () => {}, onP
|
|
|
93
118
|
output += source.slice(0, 2);
|
|
94
119
|
source = source.slice(2);
|
|
95
120
|
}
|
|
96
|
-
|
|
121
|
+
flushOutput();
|
|
97
122
|
done();
|
|
98
123
|
},
|
|
99
124
|
});
|
|
@@ -787,7 +787,15 @@ async function pathnameFields(field, state) {
|
|
|
787
787
|
return ch;
|
|
788
788
|
}).join("");
|
|
789
789
|
const glob = new Bun.Glob(pattern);
|
|
790
|
-
|
|
790
|
+
// Shell pathname expansion follows a directory symlink used as an
|
|
791
|
+
// intermediate path component. Bun.Glob defaults this off, which makes
|
|
792
|
+
// patterns such as /sys/class/*/device silently miss sysfs class links.
|
|
793
|
+
const matches = [...glob.scanSync({
|
|
794
|
+
cwd: state.cwd,
|
|
795
|
+
dot: false,
|
|
796
|
+
onlyFiles: false,
|
|
797
|
+
followSymlinks: true,
|
|
798
|
+
})].sort();
|
|
791
799
|
return matches.length ? matches : [value];
|
|
792
800
|
}
|
|
793
801
|
|
|
@@ -53,6 +53,7 @@ import { fancyLs } from "../src/fancy-ls.js";
|
|
|
53
53
|
import { MOUSE_OFF, MOUSE_ON, mouseInput } from "../src/mouse.js";
|
|
54
54
|
import { canonicalEnvironment, environmentValue, homeRelativePath } from "../src/environment.js";
|
|
55
55
|
import { findIsRegularBuiltin } from "../src/find.js";
|
|
56
|
+
import { toggleSavedText } from "../src/line-edit.js";
|
|
56
57
|
|
|
57
58
|
async function run(source, options = {}) {
|
|
58
59
|
const state = createState({
|
|
@@ -181,6 +182,36 @@ describe("completion", () => {
|
|
|
181
182
|
]);
|
|
182
183
|
});
|
|
183
184
|
|
|
185
|
+
test("toggles independently saved Ctrl-U heads and Ctrl-K tails", () => {
|
|
186
|
+
const cutHead = toggleSavedText("headtail", 4, "", "head");
|
|
187
|
+
expect(cutHead).toEqual({ line: "tail", cursor: 0, saved: "head", changed: true });
|
|
188
|
+
expect(toggleSavedText(cutHead.line, cutHead.cursor, cutHead.saved, "head"))
|
|
189
|
+
.toEqual({ line: "headtail", cursor: 4, saved: "", changed: true });
|
|
190
|
+
|
|
191
|
+
const cutTail = toggleSavedText("headtail", 4, "", "tail");
|
|
192
|
+
expect(cutTail).toEqual({ line: "head", cursor: 4, saved: "tail", changed: true });
|
|
193
|
+
expect(toggleSavedText(cutTail.line, cutTail.cursor, cutTail.saved, "tail"))
|
|
194
|
+
.toEqual({ line: "headtail", cursor: 4, saved: "", changed: true });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("intercepts Ctrl-U and Ctrl-K outside bracketed paste", async () => {
|
|
198
|
+
const edits = [];
|
|
199
|
+
const pasted = [];
|
|
200
|
+
let forwarded = "";
|
|
201
|
+
const input = mouseInput(() => {}, () => {}, () => {},
|
|
202
|
+
(text) => pasted.push(text),
|
|
203
|
+
(side) => edits.push({ side, forwarded }));
|
|
204
|
+
input.on("data", (chunk) => { forwarded += chunk.toString(); });
|
|
205
|
+
input.end("abc\x15def\x0b\x1b[200~x\x15y\x0bz\x1b[201~");
|
|
206
|
+
await new Promise((resolve) => input.once("end", resolve));
|
|
207
|
+
expect(edits).toEqual([
|
|
208
|
+
{ side: "head", forwarded: "abc" },
|
|
209
|
+
{ side: "tail", forwarded: "abcdef" },
|
|
210
|
+
]);
|
|
211
|
+
expect(forwarded).toBe("abcdef");
|
|
212
|
+
expect(pasted).toEqual(["x\x15y\x0bz"]);
|
|
213
|
+
});
|
|
214
|
+
|
|
184
215
|
test("imports Bash and Fish history by default and can be disabled", async () => {
|
|
185
216
|
expect(parseBashHistory("#1720000000\necho bash\n\nshared\n"))
|
|
186
217
|
.toEqual(["echo bash", "shared"]);
|
|
@@ -575,6 +606,7 @@ describe("execution", () => {
|
|
|
575
606
|
mkdirSync(join(cwd, "targetdir"));
|
|
576
607
|
symlinkSync("target.txt", join(cwd, "link_ok"));
|
|
577
608
|
symlinkSync("targetdir", join(cwd, "link_dir"));
|
|
609
|
+
await Bun.write(join(cwd, "targetdir", "inside.js"), "export {};\n");
|
|
578
610
|
symlinkSync("/nonexistent/path", join(cwd, "link_broken"));
|
|
579
611
|
symlinkSync("loop_b", join(cwd, "loop_a"));
|
|
580
612
|
symlinkSync("loop_a", join(cwd, "loop_b"));
|
|
@@ -597,6 +629,15 @@ describe("execution", () => {
|
|
|
597
629
|
const plain = await run("builtin lsfancy", { cwd });
|
|
598
630
|
expect(plain.stdout).toContain("🔗 link_ok");
|
|
599
631
|
expect(plain.stdout).toContain("🚫 link_broken");
|
|
632
|
+
|
|
633
|
+
// A command-line link to a directory is followed for an ordinary
|
|
634
|
+
// listing, but -l, -d, and -F inspect/classify the link itself.
|
|
635
|
+
const followed = await run("builtin lsfancy link_dir", { cwd });
|
|
636
|
+
expect(followed.stdout).toContain("inside.js");
|
|
637
|
+
const linkLong = await run("builtin lsfancy -l link_dir", { cwd });
|
|
638
|
+
expect(linkLong.stdout).toContain("link_dir -> targetdir");
|
|
639
|
+
expect((await run("builtin lsfancy -d link_dir", { cwd })).stdout).toContain("🔗 link_dir");
|
|
640
|
+
expect((await run("builtin lsfancy -F link_dir", { cwd })).stdout).toContain("link_dir@");
|
|
600
641
|
} finally { rmSync(cwd, { recursive: true, force: true }); }
|
|
601
642
|
});
|
|
602
643
|
|
|
@@ -624,6 +665,35 @@ describe("execution", () => {
|
|
|
624
665
|
} finally { rmSync(cwd, { recursive: true, force: true }); }
|
|
625
666
|
});
|
|
626
667
|
|
|
668
|
+
test("builtin ls lists glob-expanded files as files, not directory sections", async () => {
|
|
669
|
+
const cwd = mkdtempSync(join(tmpdir(), "bunmsh-ls-glob-"));
|
|
670
|
+
try {
|
|
671
|
+
await Bun.write(join(cwd, "first.sh"), "#!/bin/sh\n");
|
|
672
|
+
await Bun.write(join(cwd, "final.sh"), "#!/bin/sh\n");
|
|
673
|
+
await Bun.write(join(cwd, "other.txt"), "other\n");
|
|
674
|
+
const output = await run("builtin ls f*.sh", { cwd });
|
|
675
|
+
expect(output).toMatchObject({ status: 0, stderr: "" });
|
|
676
|
+
expect(output.stdout).toContain("first.sh");
|
|
677
|
+
expect(output.stdout).toContain("final.sh");
|
|
678
|
+
expect(output.stdout).not.toContain("other.txt");
|
|
679
|
+
expect(output.stdout).not.toContain("first.sh:");
|
|
680
|
+
expect(output.stdout).not.toContain("final.sh:");
|
|
681
|
+
expect(output.stdout).not.toContain("\n\n");
|
|
682
|
+
|
|
683
|
+
await Bun.write(join(cwd, "-odd"), "odd\n");
|
|
684
|
+
const afterDoubleDash = await run("builtin ls -- -odd", { cwd });
|
|
685
|
+
expect(afterDoubleDash).toMatchObject({ status: 0, stderr: "" });
|
|
686
|
+
expect(afterDoubleDash.stdout).toContain("-odd");
|
|
687
|
+
|
|
688
|
+
mkdirSync(join(cwd, ".hidden-target"));
|
|
689
|
+
await Bun.write(join(cwd, ".hidden-target", "through-link.js"), "export {};\n");
|
|
690
|
+
symlinkSync(".hidden-target", join(cwd, "class-link"));
|
|
691
|
+
const throughLink = await run("builtin ls ./*/*.js", { cwd });
|
|
692
|
+
expect(throughLink).toMatchObject({ status: 0, stderr: "" });
|
|
693
|
+
expect(throughLink.stdout).toContain("./class-link/through-link.js");
|
|
694
|
+
} finally { rmSync(cwd, { recursive: true, force: true }); }
|
|
695
|
+
});
|
|
696
|
+
|
|
627
697
|
test("fallback find filters paths and supports both -exec modes", async () => {
|
|
628
698
|
const cwd = mkdtempSync(join(tmpdir(), "bunmsh-find-"));
|
|
629
699
|
try {
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-visible changes to jsmdcui are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.19.2] - 2026-09-21
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Quickly pressing `Ctrl-W` twice now switches to the next tab like `Alt-T`
|
|
10
|
+
from either an editor or terminal pane. A single press retains its existing
|
|
11
|
+
next-pane behavior. At the rightmost tab, when every tab contains only
|
|
12
|
+
terminal panes, the double press creates a new editor tab instead of cycling
|
|
13
|
+
back, ensuring keyboard-only access to editing and other non-terminal work.
|
|
14
|
+
|
|
5
15
|
## [0.19.0] - 2026-08-24
|
|
6
16
|
|
|
7
17
|
### Fixed
|
|
@@ -793,6 +793,8 @@ selection, search, and copy remain available.
|
|
|
793
793
|
| `Ctrl-F` | Search; use `Ctrl-N` and `Ctrl-P` for the next and previous match. |
|
|
794
794
|
| `Ctrl-G` | Open the editor help. |
|
|
795
795
|
| `Alt-G` | Show or hide the shortcut bar. |
|
|
796
|
+
| `Ctrl-W` | Switch to the next pane in the current tab. |
|
|
797
|
+
| `Ctrl-W` twice quickly | Switch to the next tab like `Alt-T`. At the rightmost tab, if every tab contains only terminal panes, create a new editor tab instead of cycling back. |
|
|
796
798
|
| `Ctrl-Q` or `Alt-Q` | Close the current UI. |
|
|
797
799
|
|
|
798
800
|
To preview another color theme, press `Ctrl-E` or click on `€`, type `theme `, then use `Tab`
|
|
@@ -151,7 +151,10 @@ quotes here but these are not necessary when entering the command in micro.
|
|
|
151
151
|
|
|
152
152
|
* `term ['exec']`: Open a terminal emulator running the given executable. If no
|
|
153
153
|
executable is given, this will open the default shell in the terminal
|
|
154
|
-
emulator.
|
|
154
|
+
emulator. `Ctrl-W` switches panes in both editor and terminal panes. Press
|
|
155
|
+
it twice quickly to cycle to the next tab like `Alt-T`; at the rightmost
|
|
156
|
+
tab, if every tab is a terminal, this creates a new editor tab instead of
|
|
157
|
+
cycling back.
|
|
155
158
|
|
|
156
159
|
---
|
|
157
160
|
|
|
@@ -793,6 +793,8 @@ selection, search, and copy remain available.
|
|
|
793
793
|
| `Ctrl-F` | Search; use `Ctrl-N` and `Ctrl-P` for the next and previous match. |
|
|
794
794
|
| `Ctrl-G` | Open the editor help. |
|
|
795
795
|
| `Alt-G` | Show or hide the shortcut bar. |
|
|
796
|
+
| `Ctrl-W` | Switch to the next pane in the current tab. |
|
|
797
|
+
| `Ctrl-W` twice quickly | Switch to the next tab like `Alt-T`. At the rightmost tab, if every tab contains only terminal panes, create a new editor tab instead of cycling back. |
|
|
796
798
|
| `Ctrl-Q` or `Alt-Q` | Close the current UI. |
|
|
797
799
|
|
|
798
800
|
To preview another color theme, press `Ctrl-E` or click on `€`, type `theme `, then use `Tab`
|
|
@@ -2796,6 +2796,8 @@ class App {
|
|
|
2796
2796
|
this.tabRects = [];
|
|
2797
2797
|
this._escBuf = null; // pending lone ESC bytes waiting for alt-key combo
|
|
2798
2798
|
this._escTimer = null;
|
|
2799
|
+
this._ctrlWTime = 0;
|
|
2800
|
+
this._ctrlWTimer = null;
|
|
2799
2801
|
this._suggestionsRow = null;
|
|
2800
2802
|
this._suggestionRects = [];
|
|
2801
2803
|
this._acHScroll = 0;
|
|
@@ -3790,7 +3792,7 @@ class App {
|
|
|
3790
3792
|
const titleStyle = { ...defaultStyle, reverse: true };
|
|
3791
3793
|
const scrollMsg = vt && vt.scrollOffset > 0
|
|
3792
3794
|
? ` -- SCROLLBACK (${vt.scrollOffset}/${vt.scrollback.length}) wheel↑↓ to browse, any key to return`
|
|
3793
|
-
: (isActive ? " [Esc
|
|
3795
|
+
: (isActive ? " [Esc=close,Ctrl-W/*2=Next Pane/Tab]" : "");
|
|
3794
3796
|
putText(this.screen, pane.x, pane.y, ` Terminal${scrollMsg}`.padEnd(pane.w), titleStyle, pane.w);
|
|
3795
3797
|
if (!vt) return;
|
|
3796
3798
|
const renderRows = pane.h - 1;
|
|
@@ -4228,14 +4230,12 @@ class App {
|
|
|
4228
4230
|
this.render();
|
|
4229
4231
|
return;
|
|
4230
4232
|
}
|
|
4231
|
-
// Ctrl-W: switch pane focus without closing
|
|
4233
|
+
// Ctrl-W: switch pane focus without closing. A quick second Ctrl-W
|
|
4234
|
+
// cycles tabs, with an editor-tab escape hatch when every tab is a
|
|
4235
|
+
// terminal and the current one is already the rightmost tab.
|
|
4232
4236
|
if (text === "\x17") {
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
const idx = panes.indexOf(this.tab.activePane);
|
|
4236
|
-
this.tab.activePane = panes[(idx + 1) % panes.length];
|
|
4237
|
-
this.render();
|
|
4238
|
-
}
|
|
4237
|
+
await this.handleCtrlW();
|
|
4238
|
+
this.render();
|
|
4239
4239
|
return;
|
|
4240
4240
|
}
|
|
4241
4241
|
|
|
@@ -4525,11 +4525,7 @@ class App {
|
|
|
4525
4525
|
await runAction("AddTab", this);
|
|
4526
4526
|
break;
|
|
4527
4527
|
case "ctrl-w": { //switchPane
|
|
4528
|
-
|
|
4529
|
-
if (panes.length > 1) {
|
|
4530
|
-
const idx = panes.indexOf(this.tab.activePane);
|
|
4531
|
-
this.tab.activePane = panes[(idx + 1) % panes.length];
|
|
4532
|
-
}
|
|
4528
|
+
await this.handleCtrlW();
|
|
4533
4529
|
break;
|
|
4534
4530
|
}
|
|
4535
4531
|
case "ctrl-y": //redo
|
|
@@ -5403,6 +5399,37 @@ class App {
|
|
|
5403
5399
|
return this.setActiveTab((this.activeTabIdx + 1) % this.tabs.length);
|
|
5404
5400
|
}
|
|
5405
5401
|
|
|
5402
|
+
_clearCtrlW() {
|
|
5403
|
+
if (this._ctrlWTimer) clearTimeout(this._ctrlWTimer);
|
|
5404
|
+
this._ctrlWTimer = null;
|
|
5405
|
+
this._ctrlWTime = 0;
|
|
5406
|
+
}
|
|
5407
|
+
|
|
5408
|
+
async handleCtrlW() {
|
|
5409
|
+
const now = Date.now();
|
|
5410
|
+
const isDouble = this._ctrlWTime > 0 && now - this._ctrlWTime < 400;
|
|
5411
|
+
this._clearCtrlW();
|
|
5412
|
+
|
|
5413
|
+
if (isDouble) {
|
|
5414
|
+
const atRightmostTab = this.activeTabIdx === this.tabs.length - 1;
|
|
5415
|
+
const allTabsAreTerminal = this.tabs.every((tab) =>
|
|
5416
|
+
tab.panes().length > 0 && tab.panes().every((pane) => pane.type === "term")
|
|
5417
|
+
);
|
|
5418
|
+
if (atRightmostTab && allTabsAreTerminal) await this.addTab();
|
|
5419
|
+
else this.nextTab();
|
|
5420
|
+
return;
|
|
5421
|
+
}
|
|
5422
|
+
|
|
5423
|
+
this._ctrlWTime = now;
|
|
5424
|
+
this._ctrlWTimer = setTimeout(() => this._clearCtrlW(), 400);
|
|
5425
|
+
|
|
5426
|
+
const panes = this.tab.panes();
|
|
5427
|
+
if (panes.length > 1) {
|
|
5428
|
+
const idx = panes.indexOf(this.tab.activePane);
|
|
5429
|
+
this.tab.activePane = panes[(idx + 1) % panes.length];
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
|
|
5406
5433
|
async handlePrompt(text) {
|
|
5407
5434
|
const key = parseKey(text);
|
|
5408
5435
|
|
package/initramfs/init.js
CHANGED
|
@@ -37,6 +37,10 @@ const libc = dlopen("/lib/libc.musl-x86_64.so.1", {
|
|
|
37
37
|
args: [FFIType.i32],
|
|
38
38
|
returns: FFIType.i32,
|
|
39
39
|
},
|
|
40
|
+
mkdir: {
|
|
41
|
+
args: [FFIType.ptr, FFIType.i32],
|
|
42
|
+
returns: FFIType.i32,
|
|
43
|
+
},
|
|
40
44
|
});
|
|
41
45
|
|
|
42
46
|
const cString = (value) => new TextEncoder().encode(`${value}\0`);
|
|
@@ -63,18 +67,23 @@ const errno = () => read.i32(errnoLocation);
|
|
|
63
67
|
//
|
|
64
68
|
// EBUSY is tolerated so this still works if something else mounted them first.
|
|
65
69
|
const mountFilesystems = () => {
|
|
66
|
-
for (const [source, target, type] of [
|
|
70
|
+
for (const [source, target, type, data] of [
|
|
67
71
|
["proc", "/proc", "proc"],
|
|
68
72
|
["sysfs", "/sys", "sysfs"],
|
|
69
73
|
["devtmpfs", "/dev", "devtmpfs"],
|
|
70
74
|
["tmpfs", "/tmp", "tmpfs"],
|
|
75
|
+
// Since Linux 4.7 open("/dev/ptmx") resolves through /dev/pts/ptmx, so
|
|
76
|
+
// without this mount every pty open fails with ENODEV. The directory is
|
|
77
|
+
// created on the devtmpfs mounted just above.
|
|
78
|
+
["devpts", "/dev/pts", "devpts", "gid=5,mode=620,ptmxmode=666"],
|
|
71
79
|
]) {
|
|
80
|
+
if (type === "devpts") libc.symbols.mkdir(ptr(cString(target)), 0o755);
|
|
72
81
|
const result = libc.symbols.mount(
|
|
73
82
|
ptr(cString(source)),
|
|
74
83
|
ptr(cString(target)),
|
|
75
84
|
ptr(cString(type)),
|
|
76
85
|
0n,
|
|
77
|
-
null,
|
|
86
|
+
data === undefined ? null : ptr(cString(data)),
|
|
78
87
|
);
|
|
79
88
|
if (result === 0) continue;
|
|
80
89
|
if (errno() === 16) continue; // EBUSY: already mounted
|
|
@@ -83,13 +92,23 @@ const mountFilesystems = () => {
|
|
|
83
92
|
};
|
|
84
93
|
|
|
85
94
|
mountFilesystems();
|
|
86
|
-
console.log("init.js: mounted /proc, /sys, /dev, /tmp");
|
|
95
|
+
console.log("init.js: mounted /proc, /sys, /dev, /tmp, /dev/pts");
|
|
87
96
|
|
|
88
97
|
const { default: repl } = await import("node:repl");
|
|
89
98
|
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
90
99
|
|
|
91
100
|
global.repl = repl;
|
|
92
101
|
|
|
102
|
+
// QEMU user networking exposes its DNS proxy at 10.0.2.3. A real-machine
|
|
103
|
+
// image instead starts with public resolvers until the user configures DNS
|
|
104
|
+
// for the local network.
|
|
105
|
+
writeFileSync(
|
|
106
|
+
"/etc/resolv.conf",
|
|
107
|
+
process.env.REAL_MACHINE === "1"
|
|
108
|
+
? "nameserver 1.1.1.1\nnameserver 8.8.8.8\n"
|
|
109
|
+
: "nameserver 10.0.2.3\n",
|
|
110
|
+
);
|
|
111
|
+
|
|
93
112
|
|
|
94
113
|
const loadModule = (path) => {
|
|
95
114
|
const name = cString(path);
|
|
@@ -203,6 +222,36 @@ const configureRealKeyboard = () => {
|
|
|
203
222
|
]) loadModule(`/lib/modules/${release}/kernel/${module}`);
|
|
204
223
|
};
|
|
205
224
|
|
|
225
|
+
// ACPI battery, AC adapter, power button and thermal zones are modules on
|
|
226
|
+
// Alpine's kernels; until they are loaded /sys/class/power_supply is empty.
|
|
227
|
+
// Loaded on demand from the REPL as cfg.power, through /lib/modprobe.js (a
|
|
228
|
+
// require() is fine here: /proc exists by the time anyone can type it). The
|
|
229
|
+
// modules are only in the image when it was built with --real.
|
|
230
|
+
const configureBattery = () => {
|
|
231
|
+
const { modprobe } = require("/lib/modprobe.js");
|
|
232
|
+
for (const module of ["battery", "ac", "button", "thermal"]) {
|
|
233
|
+
try {
|
|
234
|
+
const inserted = modprobe(module);
|
|
235
|
+
console.log(`acpi: ${module} ${inserted.length ? "loaded" : "already loaded"}`);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error(`acpi: ${module}: ${error.message}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const { readdirSync, readFileSync } = require("node:fs");
|
|
241
|
+
const read = (path) => { try { return readFileSync(path, "utf8").trim(); } catch { return "-"; } };
|
|
242
|
+
const supplies = (() => { try { return readdirSync("/sys/class/power_supply"); } catch { return []; } })();
|
|
243
|
+
if (supplies.length === 0) console.log("power_supply: nothing registered (no ACPI battery or AC adapter on this machine)");
|
|
244
|
+
for (const name of supplies) {
|
|
245
|
+
const base = `/sys/class/power_supply/${name}`;
|
|
246
|
+
const type = read(`${base}/type`);
|
|
247
|
+
const detail = type === "Battery"
|
|
248
|
+
? `${read(`${base}/status`)} ${read(`${base}/capacity`)}%`
|
|
249
|
+
: `online=${read(`${base}/online`)}`;
|
|
250
|
+
console.log(`power_supply: ${name} ${type} ${detail}`);
|
|
251
|
+
}
|
|
252
|
+
return supplies;
|
|
253
|
+
};
|
|
254
|
+
|
|
206
255
|
const reapChildren = () => {
|
|
207
256
|
const status = new Int32Array(1);
|
|
208
257
|
while (libc.symbols.waitpid(-1, ptr(status), 1) > 0) {}
|
|
@@ -213,6 +262,13 @@ process.on("SIGCHLD", reapChildren);
|
|
|
213
262
|
process.on("SIGTERM", () => console.log("PID 1 received SIGTERM"));
|
|
214
263
|
process.on("SIGINT", () => console.log("PID 1 received SIGINT"));
|
|
215
264
|
|
|
265
|
+
// The REPL catches what its eval throws, but not a rejection or a throw
|
|
266
|
+
// from a timer or callback: Bun then exits, and PID 1 exiting is a kernel
|
|
267
|
+
// panic ("Attempted to kill init!"). With a listener installed Bun only
|
|
268
|
+
// reports, so a stray `Promise.reject()` at the prompt stays a message.
|
|
269
|
+
process.on("uncaughtException", (error) => console.error("PID 1 uncaught exception (ignored):", error));
|
|
270
|
+
process.on("unhandledRejection", (reason) => console.error("PID 1 unhandled rejection (ignored):", reason));
|
|
271
|
+
|
|
216
272
|
|
|
217
273
|
try {
|
|
218
274
|
bringUpLoopback();
|
|
@@ -228,6 +284,7 @@ try {
|
|
|
228
284
|
console.error("USB keyboard setup failed:", error);
|
|
229
285
|
}
|
|
230
286
|
|
|
287
|
+
|
|
231
288
|
try {
|
|
232
289
|
configureQemuNetwork();
|
|
233
290
|
console.log("network: eth0 10.0.2.15/24 via 10.0.2.2");
|
|
@@ -289,7 +346,8 @@ const launchBuninu = () => {
|
|
|
289
346
|
const shell = Bun.spawnSync([
|
|
290
347
|
"/bin/bun", entry, "--local"
|
|
291
348
|
], {
|
|
292
|
-
env: {
|
|
349
|
+
env: {
|
|
350
|
+
...process.env,
|
|
293
351
|
PATH:"/bin:/usr/bin:/buninu/bin",
|
|
294
352
|
HOME:"/buninu"
|
|
295
353
|
},
|
|
@@ -309,19 +367,109 @@ const launchBuninu = () => {
|
|
|
309
367
|
// Configuration complete
|
|
310
368
|
|
|
311
369
|
|
|
370
|
+
globalThis.bunmsh = launchBunmsh;
|
|
371
|
+
//console.log("Type bunmsh() to install and enter the bunmsh shell.");
|
|
372
|
+
|
|
373
|
+
globalThis.start = launchBuninu;
|
|
374
|
+
|
|
375
|
+
// cfg.net loads every packaged network module, including PHY and bus support
|
|
376
|
+
// that a kernel userspace modprobe helper would normally load on demand. It
|
|
377
|
+
// then reports the device matches and lists the resulting interfaces. The
|
|
378
|
+
// address and route are still yours to set with `ip`.
|
|
379
|
+
const configureEthernet = () => {
|
|
380
|
+
const { autoload, modprobeAll } = require("/lib/modprobe.js");
|
|
381
|
+
const { readdirSync, readFileSync } = require("node:fs");
|
|
382
|
+
const read = (path) => { try { return readFileSync(path, "utf8").trim(); } catch { return "-"; } };
|
|
383
|
+
const all = modprobeAll({ accept: (_module, path) => path.startsWith("kernel/drivers/net/") });
|
|
384
|
+
console.log(`eth: loaded all packaged network modules (${all.inserted.length} newly loaded, ${all.errors.length} failed)`);
|
|
385
|
+
for (const { module, error } of all.errors) console.log(`eth: ${module}: ${error}`);
|
|
386
|
+
const report = autoload({
|
|
387
|
+
// PCI class 02 (network), virtio device id 1 (net), every USB device —
|
|
388
|
+
// but only network drivers, so a USB keyboard does not show up as usbhid.
|
|
389
|
+
accept: (modalias) => /^usb:/.test(modalias) || /^virtio:d00000001v/.test(modalias) || /bc02sc/.test(modalias),
|
|
390
|
+
acceptModule: (module, path) => path.startsWith("kernel/drivers/net/"),
|
|
391
|
+
});
|
|
392
|
+
if (report.length === 0) console.log("eth: no network device matched a module in the image (driver built in, or not built with --real)");
|
|
393
|
+
// Network devices nothing in the image claims, so the chip is at least named.
|
|
394
|
+
try {
|
|
395
|
+
for (const device of readdirSync("/sys/bus/pci/devices")) {
|
|
396
|
+
const modalias = read(`/sys/bus/pci/devices/${device}/modalias`);
|
|
397
|
+
if (!/bc02sc/.test(modalias) || report.some((entry) => entry.device === device)) continue;
|
|
398
|
+
const [, vendor, id] = /^pci:v0000([0-9A-F]{4})d0000([0-9A-F]{4})/.exec(modalias) ?? [];
|
|
399
|
+
const kind = /bc02sc80/.test(modalias) ? "wireless" : "network";
|
|
400
|
+
console.log(`eth: pci ${device} ${kind} controller ${vendor}:${id} has no driver in the image`);
|
|
401
|
+
}
|
|
402
|
+
} catch {}
|
|
403
|
+
for (const { bus, device, modules, loaded, error } of report) {
|
|
404
|
+
const status = error ?? (loaded.length ? `loaded ${loaded.join(" ")}` : "already loaded");
|
|
405
|
+
console.log(`eth: ${bus} ${device} -> ${modules.join(" ")}: ${status}`);
|
|
406
|
+
}
|
|
407
|
+
const interfaces = (() => { try { return readdirSync("/sys/class/net").filter((name) => name !== "lo"); } catch { return []; } })();
|
|
408
|
+
for (const name of interfaces) {
|
|
409
|
+
const carrier = read(`/sys/class/net/${name}/carrier`);
|
|
410
|
+
console.log(`eth: ${name} ${read(`/sys/class/net/${name}/address`)} ${read(`/sys/class/net/${name}/operstate`)}${carrier === "1" ? " carrier" : ""}`);
|
|
411
|
+
}
|
|
412
|
+
if (interfaces.length) console.log(`eth:
|
|
413
|
+
Next steps in shell( after start() ):
|
|
414
|
+
ip link set ${interfaces[0]} up
|
|
415
|
+
ip addr add A.B.C.D/24 dev ${interfaces[0]};
|
|
416
|
+
ip route add default via GATEWAY
|
|
417
|
+
`);
|
|
418
|
+
return interfaces;
|
|
419
|
+
};
|
|
312
420
|
|
|
421
|
+
// Explicit full coldplug for this deliberately tiny system: load every .ko
|
|
422
|
+
// represented in modules.dep, recursively including each module's declared
|
|
423
|
+
// dependencies. Failures are reported individually without stopping the rest.
|
|
424
|
+
const configureModules = () => {
|
|
425
|
+
const { modprobeAll } = require("/lib/modprobe.js");
|
|
426
|
+
const report = modprobeAll();
|
|
427
|
+
console.log(`modules: ${report.inserted.length} newly loaded, ${report.errors.length} failed`);
|
|
428
|
+
for (const { module, error } of report.errors) console.log(`modules: ${module}: ${error}`);
|
|
429
|
+
return report;
|
|
430
|
+
};
|
|
313
431
|
|
|
432
|
+
// Load the packaged storage stack so devtmpfs can expose disks before the
|
|
433
|
+
// user has a concrete /dev path to pass to mount. This covers virtio block,
|
|
434
|
+
// SATA/PATA/SCSI, NVMe behind VMD, and USB mass-storage/UAS.
|
|
435
|
+
const configureDisks = () => {
|
|
436
|
+
const { modprobeAll } = require("/lib/modprobe.js");
|
|
437
|
+
const { readdirSync } = require("node:fs");
|
|
438
|
+
const storagePath = /^kernel\/drivers\/(?:ata\/|block\/virtio_blk\.ko$|nvme\/|pci\/controller\/vmd\.ko$|scsi\/|usb\/storage\/)/;
|
|
439
|
+
const report = modprobeAll({ accept: (_module, path) => storagePath.test(path) });
|
|
440
|
+
console.log(`disk: ${report.inserted.length} newly loaded, ${report.errors.length} failed`);
|
|
441
|
+
for (const { module, error } of report.errors) console.log(`disk: ${module}: ${error}`);
|
|
442
|
+
const devices = (() => { try { return readdirSync("/sys/class/block"); } catch { return []; } })();
|
|
443
|
+
console.log(`disk: ${devices.length ? devices.map((name) => `/dev/${name}`).join(" ") : "no block devices registered"}`);
|
|
444
|
+
return devices;
|
|
445
|
+
};
|
|
314
446
|
|
|
315
|
-
|
|
447
|
+
globalThis.cfg = Object.freeze(Object.defineProperties({}, {
|
|
448
|
+
all: { enumerable: true, get: configureModules },
|
|
449
|
+
disk: { enumerable: true, get: configureDisks },
|
|
450
|
+
net: { enumerable: true, get: configureEthernet },
|
|
451
|
+
power: { enumerable: true, get: configureBattery },
|
|
452
|
+
}));
|
|
453
|
+
|
|
454
|
+
// Compatibility aliases for images and notes that used the original API.
|
|
455
|
+
globalThis.cfgEth = configureEthernet;
|
|
456
|
+
globalThis.cfgNet = configureEthernet;
|
|
457
|
+
globalThis.cfgDisk = configureDisks;
|
|
458
|
+
globalThis.cfgMod = configureModules;
|
|
459
|
+
globalThis.cfgBat = configureBattery;
|
|
460
|
+
|
|
461
|
+
const showWelcome = () => {
|
|
462
|
+
console.log(`
|
|
316
463
|
Welcome to Buninu Linux!
|
|
317
464
|
Bun ${Bun.version} is now PID ${process.pid}
|
|
318
|
-
`);
|
|
319
|
-
|
|
320
|
-
globalThis.
|
|
321
|
-
|
|
465
|
+
Type start() to run buninu --local`);
|
|
466
|
+
if (process.env.REAL_MACHINE === "1") {
|
|
467
|
+
console.log(`Configuration getters: ${Object.keys(globalThis.cfg).map((name) => `cfg.${name}`).join(", ")}`);
|
|
468
|
+
}
|
|
469
|
+
console.log();
|
|
470
|
+
};
|
|
322
471
|
|
|
323
|
-
|
|
324
|
-
console.log("Type start() to run buninu --local");
|
|
472
|
+
showWelcome();
|
|
325
473
|
|
|
326
474
|
const startRepl = () => {
|
|
327
475
|
const server = repl.start({
|
|
@@ -330,6 +478,11 @@ const startRepl = () => {
|
|
|
330
478
|
output: process.stdout,
|
|
331
479
|
terminal: true,
|
|
332
480
|
});
|
|
481
|
+
// Run before REPL's own line listener so its next prompt is printed after
|
|
482
|
+
// the repeated welcome. Empty and whitespace-only lines both trigger it.
|
|
483
|
+
server.prependListener("line", (line) => {
|
|
484
|
+
if (line.trim() === "") showWelcome();
|
|
485
|
+
});
|
|
333
486
|
server.on("exit", () => {
|
|
334
487
|
console.log("REPL exited; restarting it to keep PID 1 alive.");
|
|
335
488
|
setTimeout(startRepl, 0);
|