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.
- package/LICENSES/Apache-2.0.txt +202 -0
- package/LICENSES/BSD-3-Clause-Skia.txt +29 -0
- package/LICENSES/Bitstream-Vera.txt +44 -0
- package/LICENSES/MIT-xterm.js.txt +21 -0
- package/LICENSES/OFL-1.1.txt +91 -0
- package/NOTICE.md +18 -1
- package/README.md +226 -22
- package/fetch-alpine.sh +9 -4
- package/graphics.md +213 -0
- package/initramfs/bin/bunterm +66 -0
- package/initramfs/bin/ip +2 -1
- package/initramfs/bin/mount +2 -1
- package/initramfs/bin/poweroff +6 -27
- 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 +2 -1
- package/initramfs/buninu/CHANGELOG.md +40 -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/jsgotty/gotty.js +4 -3
- package/initramfs/buninu/apps/jsgotty/package.json +1 -1
- 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 +6 -0
- package/initramfs/lib/bunterm/glyphs.js +446 -0
- package/initramfs/lib/bunterm/images.js +225 -0
- package/initramfs/lib/bunterm/input.js +140 -0
- package/initramfs/lib/bunterm/renderer.js +424 -0
- package/initramfs/lib/bunterm/terminal.js +203 -0
- package/initramfs/lib/canvas.js +181 -0
- package/initramfs/lib/canvaskit/LICENSE +29 -0
- package/initramfs/lib/canvaskit/canvaskit.js +264 -0
- package/initramfs/lib/canvaskit/canvaskit.wasm +0 -0
- package/initramfs/lib/dlopen.js +14 -11
- package/initramfs/lib/document.js +6 -0
- package/initramfs/lib/fbdev.js +581 -0
- package/initramfs/lib/power.js +35 -0
- package/initramfs/lib/xterm/LICENSE +21 -0
- package/initramfs/lib/xterm/addon-unicode-graphemes.mjs +44 -0
- package/initramfs/lib/xterm/xterm-headless.mjs +46 -0
- package/initramfs/usr/share/doc/buninu-linux/bunterm.md +83 -0
- package/initramfs/usr/share/doc/buninu-linux/canvas.md +91 -0
- package/initramfs/usr/share/doc/buninu-linux/fbdev.md +113 -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/fonts/DejaVuSansMono-Bold.ttf +0 -0
- package/initramfs/usr/share/fonts/DejaVuSansMono.ttf +0 -0
- package/initramfs/usr/share/fonts/LICENSE-Apache-2.0.txt +202 -0
- package/initramfs/usr/share/fonts/LICENSE-DejaVu.txt +44 -0
- package/initramfs/usr/share/fonts/LICENSE-OFL-1.1.txt +91 -0
- package/initramfs/usr/share/fonts/NotoColorEmoji.ttf +0 -0
- package/initramfs/usr/share/fonts/NotoColorEmojiFlags.ttf +0 -0
- package/initramfs/usr/share/fonts/NotoSansCJK-Regular.ttc +0 -0
- package/initramfs/usr/share/fonts/NotoSansSymbols-Regular-Subsetted.ttf +0 -0
- package/initramfs/usr/share/fonts/NotoSansSymbols-Regular-Subsetted2.ttf +0 -0
- package/initramfs/usr/share/fonts/Roboto-Regular.ttf +0 -0
- package/initramfs/usr/share/fonts/fonts.json +28 -0
- package/package.json +2 -2
- package/run-qemu.sh +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// top — periodically render snapshots and process lines supplied by ps.
|
|
3
|
+
|
|
4
|
+
import { showDocument } from "../lib/document.js";
|
|
5
|
+
import {
|
|
6
|
+
CLOCK_TICKS,
|
|
7
|
+
formatProcessHeader,
|
|
8
|
+
formatProcessLine,
|
|
9
|
+
prepareProcessFormat,
|
|
10
|
+
readProcessSnapshot,
|
|
11
|
+
} from "./ps";
|
|
12
|
+
|
|
13
|
+
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
14
|
+
const fields = ["pid", "user", "pri", "ni", "vsz", "rss", "state", "%cpu", "%mem", "time", "comm"];
|
|
15
|
+
|
|
16
|
+
const uptimeText = (seconds) => {
|
|
17
|
+
const days = Math.floor(seconds / 86400);
|
|
18
|
+
const hours = Math.floor(seconds % 86400 / 3600);
|
|
19
|
+
const minutes = Math.floor(seconds % 3600 / 60);
|
|
20
|
+
return `${days ? `${days} day${days === 1 ? "" : "s"}, ` : ""}${hours}:${String(minutes).padStart(2, "0")}`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const memoryText = (memory) => {
|
|
24
|
+
const total = memory.MemTotal ?? 0;
|
|
25
|
+
const free = memory.MemFree ?? 0;
|
|
26
|
+
const cache = (memory.Buffers ?? 0) + (memory.Cached ?? 0) + (memory.SReclaimable ?? 0);
|
|
27
|
+
return `MiB Mem : ${(total / 1024).toFixed(1)} total, ${(free / 1024).toFixed(1)} free, ${((total - free - cache) / 1024).toFixed(1)} used, ${(cache / 1024).toFixed(1)} buff/cache`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const intervalRows = (snapshot, previous) => {
|
|
31
|
+
if (!previous) return snapshot.processes;
|
|
32
|
+
const elapsed = Math.max(0.001, (snapshot.at - previous.at) / 1000);
|
|
33
|
+
const oldTicks = new Map(previous.processes.map((row) => [row.pid, row.ticks]));
|
|
34
|
+
return snapshot.processes.map((row) => ({
|
|
35
|
+
...row,
|
|
36
|
+
cpu: Math.max(0, row.ticks - (oldTicks.get(row.pid) ?? row.ticks)) / CLOCK_TICKS / elapsed * 100,
|
|
37
|
+
}));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const render = (snapshot, previous, sort, batch) => {
|
|
41
|
+
const rows = intervalRows(snapshot, previous);
|
|
42
|
+
rows.sort(sort === "memory"
|
|
43
|
+
? (a, b) => b.memory - a.memory || a.pid - b.pid
|
|
44
|
+
: sort === "pid" ? (a, b) => a.pid - b.pid : (a, b) => b.cpu - a.cpu || a.pid - b.pid);
|
|
45
|
+
const states = { running: 0, sleeping: 0, stopped: 0, zombie: 0 };
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
if (row.state[0] === "R") states.running++;
|
|
48
|
+
else if (row.state[0] === "T") states.stopped++;
|
|
49
|
+
else if (row.state[0] === "Z") states.zombie++;
|
|
50
|
+
else states.sleeping++;
|
|
51
|
+
}
|
|
52
|
+
const total = previous ? snapshot.cpu.total - previous.cpu.total : snapshot.cpu.total;
|
|
53
|
+
const idleTicks = previous ? snapshot.cpu.idle - previous.cpu.idle : snapshot.cpu.idle;
|
|
54
|
+
const idle = total > 0 ? idleTicks / total * 100 : 0;
|
|
55
|
+
const clock = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
56
|
+
const lines = [
|
|
57
|
+
`top - ${clock} up ${uptimeText(snapshot.uptime)}, load average: ${snapshot.loadAverage.map((v) => v.toFixed(2)).join(", ")}`,
|
|
58
|
+
`Tasks: ${rows.length} total, ${states.running} running, ${states.sleeping} sleeping, ${states.stopped} stopped, ${states.zombie} zombie`,
|
|
59
|
+
`%Cpu(s): ${(100 - idle).toFixed(1)} used, ${idle.toFixed(1)} id`,
|
|
60
|
+
memoryText(snapshot.memory),
|
|
61
|
+
"",
|
|
62
|
+
];
|
|
63
|
+
const format = prepareProcessFormat(rows, fields);
|
|
64
|
+
lines.push(formatProcessHeader(format));
|
|
65
|
+
// Some early boot consoles report a TTY size of 0x0. Treat that as
|
|
66
|
+
// unknown; otherwise Math.max(1, 0 - headers) displays only one process
|
|
67
|
+
// and makes it look as if the snapshot contained only top itself.
|
|
68
|
+
const terminalRows = Number(process.stdout.rows);
|
|
69
|
+
const screenRows = Number.isFinite(terminalRows) && terminalRows > lines.length
|
|
70
|
+
? terminalRows
|
|
71
|
+
: 24;
|
|
72
|
+
const limit = batch ? rows.length : Math.max(1, screenRows - lines.length);
|
|
73
|
+
for (const row of rows.slice(0, limit)) lines.push(formatProcessLine(row, format));
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
async function main(argv) {
|
|
78
|
+
let delay = 3;
|
|
79
|
+
let iterations = Infinity;
|
|
80
|
+
let batch = false;
|
|
81
|
+
for (let index = 0; index < argv.length; index++) {
|
|
82
|
+
const argument = argv[index];
|
|
83
|
+
if (argument === "-h" || argument === "--help") return showDocument("top");
|
|
84
|
+
if (argument === "-b") { batch = true; continue; }
|
|
85
|
+
if (argument === "-d") { delay = Number(argv[++index]); continue; }
|
|
86
|
+
if (argument === "-n") { iterations = Number(argv[++index]); continue; }
|
|
87
|
+
throw new Error(`top: unsupported option '${argument}'`);
|
|
88
|
+
}
|
|
89
|
+
if (!(delay > 0) || !(iterations > 0)) throw new Error("top: -d and -n require positive numbers");
|
|
90
|
+
let quit = false;
|
|
91
|
+
let sort = "cpu";
|
|
92
|
+
const interactive = !batch && process.stdin.isTTY && process.stdout.isTTY;
|
|
93
|
+
const cleanup = () => {
|
|
94
|
+
if (interactive) {
|
|
95
|
+
process.stdin.setRawMode(false);
|
|
96
|
+
process.stdin.pause();
|
|
97
|
+
process.stdout.write("\x1b[?25h\n");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
if (interactive) {
|
|
101
|
+
process.stdin.setRawMode(true);
|
|
102
|
+
process.stdin.resume();
|
|
103
|
+
process.stdin.setEncoding("utf8");
|
|
104
|
+
process.stdin.on("data", (key) => {
|
|
105
|
+
if (key === "q" || key === "\u0003") quit = true;
|
|
106
|
+
else if (key === "P") sort = "cpu";
|
|
107
|
+
else if (key === "M") sort = "memory";
|
|
108
|
+
else if (key === "N") sort = "pid";
|
|
109
|
+
});
|
|
110
|
+
process.stdout.write("\x1b[?25l");
|
|
111
|
+
process.on("exit", cleanup);
|
|
112
|
+
} else if (!batch && iterations === Infinity) iterations = 1;
|
|
113
|
+
|
|
114
|
+
let previous = null;
|
|
115
|
+
try {
|
|
116
|
+
for (let count = 0; count < iterations && !quit; count++) {
|
|
117
|
+
const snapshot = readProcessSnapshot();
|
|
118
|
+
const output = render(snapshot, previous, sort, batch);
|
|
119
|
+
process.stdout.write(`${interactive ? "\x1b[H\x1b[2J" : ""}${output}\n`);
|
|
120
|
+
previous = snapshot;
|
|
121
|
+
if (count + 1 < iterations && !quit) await sleep(delay * 1000);
|
|
122
|
+
}
|
|
123
|
+
} finally { cleanup(); }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (import.meta.main) {
|
|
127
|
+
try { await main(process.argv.slice(2)); }
|
|
128
|
+
catch (error) { console.error(error.message); process.exit(1); }
|
|
129
|
+
}
|
package/initramfs/bin/umount
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// umount — unmount a filesystem. `umount --help` renders
|
|
3
3
|
// /usr/share/doc/buninu-linux/umount.md.
|
|
4
4
|
|
|
5
|
-
import { libc, cString, ptr, errno, strerror, Errno
|
|
5
|
+
import { libc, cString, ptr, errno, strerror, Errno } from "../lib/dlopen.js";
|
|
6
|
+
import { showDocument } from "../lib/document.js";
|
|
6
7
|
|
|
7
8
|
const MNT = { FORCE: 1, DETACH: 2, EXPIRE: 4, NOFOLLOW: 8 };
|
|
8
9
|
|
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.15 - 2026-09-22
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Sync jsgotty to 1.1.12
|
|
8
|
+
* Export `BunPtyBackend` from `gotty.js`, the PTY layer built on
|
|
9
|
+
`Bun.spawn({ terminal })`, so other programs can run a command on a
|
|
10
|
+
pseudo-terminal without their own FFI. Buninu Linux's `bunterm`, a
|
|
11
|
+
graphical terminal on the framebuffer, is its first user, together with
|
|
12
|
+
the already exported `KittyGraphicsParser`
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- jsgotty: a chunked kitty graphics transfer whose first chunk names an
|
|
17
|
+
image id (`i=`) is now continued by later chunks that carry only `m=`, as
|
|
18
|
+
the protocol allows. Previously those chunks were treated as new anonymous
|
|
19
|
+
transfers, so the image never completed. jsgotty's own `showimg` repeats
|
|
20
|
+
the id in every chunk and was unaffected; `kitten icat`, timg, chafa and
|
|
21
|
+
similar clients were
|
|
22
|
+
|
|
23
|
+
## 0.4.13 - 2026-09-22
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Sync bunmsh to 0.3.9
|
|
28
|
+
* `Ctrl-U` removes and saves the text before the cursor; pressing it again
|
|
29
|
+
at the beginning of the line restores it. `Ctrl-K` independently toggles
|
|
30
|
+
the text after the cursor when at the end of the line
|
|
31
|
+
* Unquoted pathname patterns follow directory symbolic links used as
|
|
32
|
+
intermediate path components, as traditional shells do, so patterns such
|
|
33
|
+
as `/sys/class/net/*/device` pass through sysfs class links
|
|
34
|
+
* The fallback `ls`/`lsfancy` lists a glob's files as one listing instead
|
|
35
|
+
of one section per file, honors `--`, and follows a command-line symlink
|
|
36
|
+
to a directory
|
|
37
|
+
- Sync jsmdcui to 0.19.2
|
|
38
|
+
* Pressing `Ctrl-W` twice quickly switches to the next tab like `Alt-T`
|
|
39
|
+
from either an editor or a terminal pane; at the rightmost tab, when every
|
|
40
|
+
tab holds only terminal panes, it opens a new editor tab instead of
|
|
41
|
+
cycling back
|
|
42
|
+
|
|
3
43
|
## 0.4.12 - 2026-09-20
|
|
4
44
|
|
|
5
45
|
### Fixed
|
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-visible changes to bunmsh are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.3.9] - 2026-09-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Make pathname expansion follow directory symbolic links used as intermediate
|
|
10
|
+
path components, matching traditional shell behaviour. `Bun.Glob` disables
|
|
11
|
+
that traversal by default, so patterns such as `./*/*.js` silently skipped
|
|
12
|
+
a matching directory symlink, and sysfs patterns such as
|
|
13
|
+
`/sys/class/net/*/device` failed to pass through class-device links. bunmsh
|
|
14
|
+
now enables `followSymlinks` while scanning; a symlink matched as the final
|
|
15
|
+
component is still passed to the command as a symlink.
|
|
16
|
+
|
|
17
|
+
- Stop the fallback `ls`/`lsfancy` from printing every glob-expanded file as
|
|
18
|
+
a directory section. `builtin ls L*` previously expanded the glob
|
|
19
|
+
correctly, but multiple operands unconditionally received `filename:`
|
|
20
|
+
headings and blank separators. Only directories whose contents are being
|
|
21
|
+
listed now receive section headings; multiple ordinary files form one
|
|
22
|
+
continuous listing and retain their full operand paths rather than being
|
|
23
|
+
reduced to basenames.
|
|
24
|
+
|
|
25
|
+
- Honor `--` in `ls`/`lsfancy`, so names such as `-odd` after it are operands
|
|
26
|
+
rather than option clusters. Also follow a command-line symlink to a
|
|
27
|
+
directory for ordinary and recursive listings, as traditional `ls` does;
|
|
28
|
+
`-d`, `-l`, and `-F` continue to inspect the symlink itself.
|
|
29
|
+
|
|
30
|
+
## [0.3.8] - 2026-09-21
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Make `Ctrl-U` and `Ctrl-K` toggle their edits in the interactive line
|
|
35
|
+
editor. `Ctrl-U` saves the text removed before the cursor and restores it
|
|
36
|
+
when pressed again at the beginning of the line; `Ctrl-K` does the same for
|
|
37
|
+
text after the cursor when pressed again at the end. Each shortcut keeps an
|
|
38
|
+
independent saved value, and control bytes inside bracketed paste remain
|
|
39
|
+
literal pasted content rather than triggering either action.
|
|
40
|
+
|
|
5
41
|
## [0.3.7] - 2026-09-20
|
|
6
42
|
|
|
7
43
|
### Fixed
|
|
@@ -476,6 +476,13 @@ Bun.e, Bun.file("/tm # -> a path, completed inside the string
|
|
|
476
476
|
|
|
477
477
|
- `Ctrl-D`: On an empty input line, sends EOF and exits the interactive shell.
|
|
478
478
|
|
|
479
|
+
- `Ctrl-U`: Removes and saves everything before the cursor. Press it again
|
|
480
|
+
while the cursor is at the beginning to restore that saved prefix.
|
|
481
|
+
|
|
482
|
+
- `Ctrl-K`: Removes and saves everything after the cursor. Press it again
|
|
483
|
+
while the cursor is at the end to restore that saved suffix. Its saved text
|
|
484
|
+
is independent from `Ctrl-U`.
|
|
485
|
+
|
|
479
486
|
- `Ctrl-T`: Calls `builtin tab` without adding a command to history. It creates
|
|
480
487
|
a tab when only one exists, or switches to the next tab otherwise. Any
|
|
481
488
|
command text currently being edited is preserved.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, lstatSync, readdirSync, readlinkSync } from "node:fs";
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readlinkSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
const IMAGE = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", "avif", "heic"]);
|
|
5
5
|
const MUSIC = new Set(["mp3", "wav", "flac", "ogg", "m4a", "aac", "opus", "mid", "midi"]);
|
|
@@ -120,9 +120,11 @@ export function fancyLs(argv, state, terminal = Boolean(process.stdout.isTTY)) {
|
|
|
120
120
|
recursive: false, time: false, reverse: false, size: false, one: false, classify: false,
|
|
121
121
|
};
|
|
122
122
|
const operands = [];
|
|
123
|
+
let parseOptions = true;
|
|
123
124
|
for (const argument of argv.slice(1)) {
|
|
124
|
-
if (
|
|
125
|
-
if (
|
|
125
|
+
if (parseOptions && argument === "--") { parseOptions = false; continue; }
|
|
126
|
+
if (parseOptions && (argument === "--color=auto" || argument === "--color")) continue;
|
|
127
|
+
if (parseOptions && /^-[^-]/.test(argument)) {
|
|
126
128
|
for (const flag of argument.slice(1)) {
|
|
127
129
|
if (flag === "a") options.all = true;
|
|
128
130
|
else if (flag === "A") options.almostAll = true;
|
|
@@ -142,15 +144,30 @@ export function fancyLs(argv, state, terminal = Boolean(process.stdout.isTTY)) {
|
|
|
142
144
|
if (!operands.length) operands.push(".");
|
|
143
145
|
let status = 0, stdout = "", stderr = "";
|
|
144
146
|
const visited = new Set();
|
|
145
|
-
|
|
147
|
+
let previousWasDirectory = false;
|
|
148
|
+
const show = (operand, heading = operands.length > 1, separate = true) => {
|
|
146
149
|
const path = isAbsolute(operand) ? operand : resolve(state.cwd, operand);
|
|
147
150
|
try {
|
|
148
151
|
const stats = lstatSync(path);
|
|
152
|
+
let listingDirectory = stats.isDirectory() && !options.directory;
|
|
153
|
+
// GNU/POSIX-style ls follows a command-line symlink to a directory for
|
|
154
|
+
// an ordinary listing and for -R. -d, -l, and -F request information
|
|
155
|
+
// about the link itself instead. A broken link simply cannot qualify.
|
|
156
|
+
if (stats.isSymbolicLink() && !options.directory && !options.long && !options.classify) {
|
|
157
|
+
try { listingDirectory = statSync(path).isDirectory(); } catch {}
|
|
158
|
+
}
|
|
159
|
+
// Multiple file operands are one listing, not a series of directory
|
|
160
|
+
// sections. Only actual directory contents receive `name:` headings
|
|
161
|
+
// and blank section separators.
|
|
162
|
+
if (separate && stdout && (listingDirectory || previousWasDirectory)) stdout += "\n";
|
|
149
163
|
let entries;
|
|
150
|
-
if (!
|
|
151
|
-
|
|
164
|
+
if (!listingDirectory)
|
|
165
|
+
// Preserve the operand spelling just like traditional ls. A glob such
|
|
166
|
+
// as ./*/*.js has already expanded to paths, and reducing each one to
|
|
167
|
+
// basename would lose the information that distinguishes its parent.
|
|
168
|
+
entries = [{ name: operand, stats, path }];
|
|
152
169
|
else entries = listDirectory(path, options);
|
|
153
|
-
if (heading) stdout += `${operand}:\n`;
|
|
170
|
+
if (heading && listingDirectory) stdout += `${operand}:\n`;
|
|
154
171
|
const rendered = entries.map((entry) => displayEntry(entry, options));
|
|
155
172
|
if (options.long) {
|
|
156
173
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -164,23 +181,21 @@ export function fancyLs(argv, state, terminal = Boolean(process.stdout.isTTY)) {
|
|
|
164
181
|
} else stdout += terminal && !options.one
|
|
165
182
|
? columns(rendered, process.stdout.columns ?? 80)
|
|
166
183
|
: `${rendered.join("\n")}${rendered.length ? "\n" : ""}`;
|
|
167
|
-
if (options.recursive &&
|
|
184
|
+
if (options.recursive && listingDirectory) {
|
|
168
185
|
for (const entry of entries) if (entry.stats.isDirectory() && entry.name !== "." && entry.name !== "..") {
|
|
169
186
|
const child = resolve(path, entry.name);
|
|
170
187
|
if (visited.has(child)) continue;
|
|
171
188
|
visited.add(child);
|
|
172
189
|
stdout += "\n";
|
|
173
|
-
show(`${operand.replace(/\/$/, "")}/${entry.name}`, true);
|
|
190
|
+
show(`${operand.replace(/\/$/, "")}/${entry.name}`, true, false);
|
|
174
191
|
}
|
|
175
192
|
}
|
|
193
|
+
previousWasDirectory = listingDirectory;
|
|
176
194
|
} catch (error) {
|
|
177
195
|
status = 1;
|
|
178
196
|
stderr += `bunmsh: ${argv[0]}: ${operand}: ${error.message}\n`;
|
|
179
197
|
}
|
|
180
198
|
};
|
|
181
|
-
for (
|
|
182
|
-
if (i) stdout += "\n";
|
|
183
|
-
show(operands[i]);
|
|
184
|
-
}
|
|
199
|
+
for (const operand of operands) show(operand);
|
|
185
200
|
return { status, stdout, stderr };
|
|
186
201
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function toggleSavedText(line, cursor, saved, side) {
|
|
2
|
+
line = String(line ?? "");
|
|
3
|
+
cursor = Math.max(0, Math.min(line.length, Number.isFinite(cursor) ? cursor : line.length));
|
|
4
|
+
saved = String(saved ?? "");
|
|
5
|
+
|
|
6
|
+
if (side === "head") {
|
|
7
|
+
if (cursor > 0) {
|
|
8
|
+
return {
|
|
9
|
+
line: line.slice(cursor),
|
|
10
|
+
cursor: 0,
|
|
11
|
+
saved: line.slice(0, cursor),
|
|
12
|
+
changed: true,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (saved) {
|
|
16
|
+
return {
|
|
17
|
+
line: saved + line,
|
|
18
|
+
cursor: saved.length,
|
|
19
|
+
saved: "",
|
|
20
|
+
changed: true,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
} else if (side === "tail") {
|
|
24
|
+
if (cursor < line.length) {
|
|
25
|
+
return {
|
|
26
|
+
line: line.slice(0, cursor),
|
|
27
|
+
cursor,
|
|
28
|
+
saved: line.slice(cursor),
|
|
29
|
+
changed: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (saved) {
|
|
33
|
+
return {
|
|
34
|
+
line: line + saved,
|
|
35
|
+
cursor,
|
|
36
|
+
saved: "",
|
|
37
|
+
changed: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { line, cursor, saved, changed: false };
|
|
43
|
+
}
|
|
@@ -29,6 +29,7 @@ import { importedHistory, readlineHistory, saveBunmshHistory } from "./history.j
|
|
|
29
29
|
import pkg from "../package.json" with { type:"json" }
|
|
30
30
|
import { MOUSE_OFF, MOUSE_ON, PASTE_OFF, PASTE_ON, mouseInput } from "./mouse.js";
|
|
31
31
|
import { homeRelativePath } from "./environment.js";
|
|
32
|
+
import { toggleSavedText } from "./line-edit.js";
|
|
32
33
|
|
|
33
34
|
const VERSION = `
|
|
34
35
|
${pkg.name}: ${pkg.description}
|
|
@@ -223,6 +224,8 @@ async function interactive(state) {
|
|
|
223
224
|
let lastTabClick = null;
|
|
224
225
|
let mouseCommandRunning = false;
|
|
225
226
|
let foregroundCommand = false;
|
|
227
|
+
let savedHead = "";
|
|
228
|
+
let savedTail = "";
|
|
226
229
|
let promptAbort = null;
|
|
227
230
|
// Set by onPaste (below) when a bracketed paste contains a newline, and
|
|
228
231
|
// consumed by the main loop's readline.question() catch block right after
|
|
@@ -332,6 +335,17 @@ async function interactive(state) {
|
|
|
332
335
|
const key = { name: delta > 0 ? "right" : "left" };
|
|
333
336
|
for (let n = 0; n < Math.abs(delta); n++) readline.write(null, key);
|
|
334
337
|
};
|
|
338
|
+
const toggleLineText = (side) => {
|
|
339
|
+
if (!readline || foregroundCommand) return;
|
|
340
|
+
const saved = side === "head" ? savedHead : savedTail;
|
|
341
|
+
const result = toggleSavedText(readline.line, readline.cursor, saved, side);
|
|
342
|
+
if (side === "head") savedHead = result.saved;
|
|
343
|
+
else savedTail = result.saved;
|
|
344
|
+
if (!result.changed) return;
|
|
345
|
+
readline.line = result.line;
|
|
346
|
+
readline.cursor = result.cursor;
|
|
347
|
+
readline.prompt(true);
|
|
348
|
+
};
|
|
335
349
|
const filteredInput = terminal ? mouseInput((mouse) => {
|
|
336
350
|
if (!mouse.press || (mouse.button & 3) !== 0 || (mouse.button & 32)) return;
|
|
337
351
|
pendingClick = { ...mouse, at: Date.now() };
|
|
@@ -384,7 +398,7 @@ async function interactive(state) {
|
|
|
384
398
|
else if (shortcut === "lsfancy") runFancyShortcut();
|
|
385
399
|
else if (shortcut === "lsfancy-parent") runFancyShortcut([".."]);
|
|
386
400
|
else if (shortcut === "tab-close") runTabShortcut(["x"]);
|
|
387
|
-
}, onPaste) : process.stdin;
|
|
401
|
+
}, onPaste, toggleLineText) : process.stdin;
|
|
388
402
|
const completer = (line) => {
|
|
389
403
|
commandIndex.refreshIfChanged(state);
|
|
390
404
|
if (javascriptLine(line)) {
|
|
@@ -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 {
|
|
@@ -1173,9 +1173,9 @@ class KittyGraphicsParser {
|
|
|
1173
1173
|
|
|
1174
1174
|
if (control.m === "1") {
|
|
1175
1175
|
this.transfers.set(key, existing);
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1176
|
+
// Later chunks of a transfer only need m=; they continue the transfer
|
|
1177
|
+
// in progress whether or not its first chunk named an image id.
|
|
1178
|
+
this.implicitTransferKey = key;
|
|
1179
1179
|
return null;
|
|
1180
1180
|
}
|
|
1181
1181
|
|
|
@@ -3314,6 +3314,7 @@ async function bootstrap() {
|
|
|
3314
3314
|
|
|
3315
3315
|
module.exports.CursorStateTracker = CursorStateTracker;
|
|
3316
3316
|
module.exports.KittyGraphicsParser = KittyGraphicsParser;
|
|
3317
|
+
module.exports.BunPtyBackend = BunPtyBackend;
|
|
3317
3318
|
module.exports.webSocketOriginAllowed = webSocketOriginAllowed;
|
|
3318
3319
|
|
|
3319
3320
|
if (require.main === module) {
|
|
@@ -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
|