buninu 0.4.6 → 0.4.8

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.
@@ -20,9 +20,32 @@
20
20
  touchScroll: null,
21
21
  contextMenu: null,
22
22
  imageContextBound: false,
23
- socket: null
23
+ socket: null,
24
+ // False until the client has sent its first resize. Until then cols/rows
25
+ // above are placeholders, not the PTY's real geometry.
26
+ sizeFromClient: false
24
27
  };
25
28
 
29
+ // The PTY size only reaches us through the resize messages the client
30
+ // sends. A placement that arrives before the first of those would be laid
31
+ // out against 80x24 instead of the terminal the user is looking at, so
32
+ // borrow xterm's own geometry until the real size shows up.
33
+ function syncViewerSize() {
34
+ if (state.sizeFromClient) {
35
+ return;
36
+ }
37
+ const xterm = terminal && terminal.__gottyXterm;
38
+ if (!xterm) {
39
+ return;
40
+ }
41
+ if (Number.isFinite(xterm.cols) && xterm.cols > 0) {
42
+ state.cols = xterm.cols;
43
+ }
44
+ if (Number.isFinite(xterm.rows) && xterm.rows > 0) {
45
+ state.rows = xterm.rows;
46
+ }
47
+ }
48
+
26
49
  function browserTerminalState() {
27
50
  const xterm = terminal && terminal.__gottyXterm;
28
51
  const active = xterm && xterm.buffer && xterm.buffer.active;
@@ -152,6 +175,7 @@
152
175
  }
153
176
 
154
177
  function createPlacementMarker(cursor) {
178
+ syncViewerSize();
155
179
  const xterm = terminal && terminal.__gottyXterm;
156
180
  if (!xterm || typeof xterm.registerMarker !== "function") {
157
181
  return null;
@@ -372,6 +396,7 @@
372
396
  return;
373
397
  }
374
398
  const { control, cursor, intrinsicWidth, intrinsicHeight, sourceRect } = node.__kittyPlacement;
399
+ syncViewerSize();
375
400
  const metrics = cellMetrics();
376
401
  const anchor = anchorOffset();
377
402
  const controlCols = Math.max(0, Number.parseInt(control.c || "0", 10) || 0);
@@ -825,6 +850,17 @@
825
850
  const generation = (node.__kittyGeneration || 0) + 1;
826
851
  node.__kittyGeneration = generation;
827
852
 
853
+ // Bind before the decode, not after it. The server sends the placement
854
+ // ahead of the synthetic newlines that scroll room for the image, so the
855
+ // scroll this layout depends on can land while we are still decoding.
856
+ // Binding afterwards left the very first image of a session with no
857
+ // listener to correct it, which is why it could come out blank while
858
+ // every later image in the same session was fine.
859
+ bindViewportScroll();
860
+ bindTerminalRender();
861
+ bindTouchScrolling();
862
+ bindImageContextMenu();
863
+
828
864
  const asset = await resolveImageAsset(message.image, control);
829
865
  if (!asset || node.__kittyGeneration !== generation) {
830
866
  return;
@@ -839,11 +875,14 @@
839
875
  intrinsicHeight: asset.height,
840
876
  sourceRect: asset.sourceRect,
841
877
  };
842
- node.__kittyImageElement.src = objectUrl;
843
- bindViewportScroll();
844
- bindTerminalRender();
845
- bindTouchScrolling();
846
- bindImageContextMenu();
878
+ const imageElement = node.__kittyImageElement;
879
+ imageElement.onload = () => {
880
+ imageElement.onload = null;
881
+ if (node.__kittyGeneration === generation) {
882
+ layoutNode(node);
883
+ }
884
+ };
885
+ imageElement.src = objectUrl;
847
886
  layoutNode(node);
848
887
  }
849
888
 
@@ -995,7 +1034,14 @@
995
1034
  return nativeRemoveEventListener.call(this, type, listener, options);
996
1035
  };
997
1036
 
998
- const descriptor = Object.getOwnPropertyDescriptor(WebSocket.prototype, "onmessage");
1037
+ // Walk the prototype chain to find the native onmessage descriptor.
1038
+ // Safari/WebKit defines it on a parent prototype (e.g. EventTarget),
1039
+ // not directly on WebSocket.prototype, so a single-level lookup fails.
1040
+ let descriptor = null;
1041
+ for (let proto = WebSocket.prototype; proto; proto = Object.getPrototypeOf(proto)) {
1042
+ descriptor = Object.getOwnPropertyDescriptor(proto, "onmessage");
1043
+ if (descriptor) break;
1044
+ }
999
1045
  Object.defineProperty(WebSocket.prototype, "onmessage", {
1000
1046
  configurable: true,
1001
1047
  enumerable: descriptor ? descriptor.enumerable : true,
@@ -1004,14 +1050,13 @@
1004
1050
  },
1005
1051
  set(handler) {
1006
1052
  this.__kittyOnMessage = handler;
1007
- if (!descriptor || !descriptor.set) {
1008
- return;
1009
- }
1010
1053
  if (typeof handler !== "function") {
1011
- descriptor.set.call(this, handler);
1054
+ if (descriptor && descriptor.set) {
1055
+ descriptor.set.call(this, handler);
1056
+ }
1012
1057
  return;
1013
1058
  }
1014
- descriptor.set.call(this, function (event) {
1059
+ const wrapped = function (event) {
1015
1060
  state.socket = this;
1016
1061
  inspectTerminalOutput(event.data);
1017
1062
  if (typeof event.data === "string" && event.data[0] === MSG_KITTY) {
@@ -1020,7 +1065,13 @@
1020
1065
  }
1021
1066
  }
1022
1067
  return handler.call(this, event);
1023
- });
1068
+ };
1069
+ if (descriptor && descriptor.set) {
1070
+ descriptor.set.call(this, wrapped);
1071
+ } else {
1072
+ // Fallback: use addEventListener when no native setter is available.
1073
+ this.addEventListener("message", wrapped);
1074
+ }
1024
1075
  }
1025
1076
  });
1026
1077
 
@@ -1034,9 +1085,11 @@
1034
1085
  const resize = JSON.parse(data.slice(1));
1035
1086
  if (resize.columns) {
1036
1087
  state.cols = resize.columns;
1088
+ state.sizeFromClient = true;
1037
1089
  }
1038
1090
  if (resize.rows) {
1039
1091
  state.rows = resize.rows;
1092
+ state.sizeFromClient = true;
1040
1093
  }
1041
1094
  } catch (error) {
1042
1095
  // ignore
@@ -27,6 +27,34 @@ function usage(exitCode = 0) {
27
27
  process.exitCode = exitCode;
28
28
  }
29
29
 
30
+ // A browser terminal only reports its size to the PTY once the page has laid
31
+ // out, so a window that was just opened, restored, resized or rotated answers
32
+ // with a couple of columns for a few hundred milliseconds. An image sized
33
+ // against that is emitted as a one-cell speck — indistinguishable from the
34
+ // command having printed nothing — while the next run in the same session,
35
+ // by which time the real size has arrived, comes out correct. Give the size a
36
+ // short chance to settle when what we read back is too small to be a terminal
37
+ // anyone could be looking at.
38
+ const MIN_SANE_COLS = 20;
39
+ const MIN_SANE_ROWS = 5;
40
+ const SIZE_SETTLE_TIMEOUT_MS = 400;
41
+ const SIZE_POLL_INTERVAL_MS = 25;
42
+
43
+ function terminalSizeLooksUnset() {
44
+ return (process.stdout.columns || 0) < MIN_SANE_COLS
45
+ || (process.stdout.rows || 0) < MIN_SANE_ROWS;
46
+ }
47
+
48
+ async function waitForStableTerminalSize() {
49
+ if (!process.stdout.isTTY) {
50
+ return;
51
+ }
52
+ const deadline = Date.now() + SIZE_SETTLE_TIMEOUT_MS;
53
+ while (terminalSizeLooksUnset() && Date.now() < deadline) {
54
+ await new Promise((resolve) => setTimeout(resolve, SIZE_POLL_INTERVAL_MS));
55
+ }
56
+ }
57
+
30
58
  function fitImageCells(width, height) {
31
59
  const terminalCols = Math.max(1, process.stdout.columns || 80);
32
60
  const maxCols = Math.max(1, terminalCols - 1);
@@ -384,6 +412,7 @@ async function main() {
384
412
  const fullPath = path.resolve(inputPath);
385
413
  const buffer = fs.readFileSync(fullPath);
386
414
  const image = sniffImage(buffer, fullPath, { compat });
415
+ await waitForStableTerminalSize();
387
416
  writeKittyImage(buffer, image.mime, image.width, image.height, compat);
388
417
  }
389
418
 
@@ -1,5 +1,26 @@
1
1
  This product includes third-party components:
2
2
 
3
+ - ld-musl-aarch64.so.1
4
+ License: MIT
5
+ See: ./LICENSE_musl.txt
6
+
7
+ The musl dynamic loader, version 1.2.5, as packaged by Alpine Linux
8
+ (package musl, version 1.2.5-r10, aarch64).
9
+
10
+ Upstream musl source:
11
+ https://musl.libc.org/releases/musl-1.2.5.tar.gz
12
+
13
+ Alpine's CDN serves only the current pkgrel of each branch, and 1.2.5-r10
14
+ has since been superseded, so it is no longer downloadable there. An
15
+ archived copy of that exact build ships in the Alpine aarch64 rootfs
16
+ published by Termux proot-distro, which reports Alpine 3.22.2 and
17
+ musl 1.2.5-r10 in its apk database:
18
+ https://github.com/termux/proot-distro/releases/download/v4.30.1/alpine-aarch64-pd-v4.30.1.tar.xz
19
+
20
+ SHA-256 (verified byte-for-byte identical to lib/ld-musl-aarch64.so.1 in
21
+ that rootfs):
22
+ ld-musl-aarch64.so.1: da9c2d18642cf67ade0947b88b6859385c085fdbc6a022f5a8b518d7677e55df
23
+
3
24
  - libgcc_s.so.1 / libstdc++.so.6
4
25
  License: GPL-3.0 + GCC Runtime Library Exception
5
26
  See: ./LICENSES/GPL-3.0.txt and ./LICENSES/gcc_runtime_exception.txt
package/bin/init.js CHANGED
@@ -23,26 +23,73 @@ Usage:
23
23
  ${pkg.name} [init options] [jsgotty options]
24
24
 
25
25
  Init options:
26
- -h, --help Show this help and exit
27
- -V, --version Show version and runtime information, then exit
28
- --readme Render README.md in the terminal and exit
29
- --changelog Render CHANGELOG.md in the terminal and exit
30
- --export [output.tgz] Export this Buninu installation (default: ./buninu.tgz)
31
- --export-config [output.json] Export this package.json (default: ./buninu.json)
32
- -i, --install [dir] Install this package into <dir>/${pkg.name} (default: .)
33
- -si, --strip-install [dir] Install into <dir> directly, without a
34
- top-level directory of its own
35
- (--install --help shows the full install options)
36
- --shell <path|name> Override buninu.shell for this run
37
- --command <command> Override buninu.command for this run
38
-
39
- Direct app launch (must be the first argument; skips the shell/command
40
- startup flow entirely):
41
- --jsgotty [args...] Spawn jsgotty directly, forwarding remaining
42
- arguments, and exit with its exit code
43
- (--jsgotty --help shows jsgotty's own options)
44
- --jsmdcui [args...] Spawn jsmdcui directly, same forwarding behavior
45
- --musl-la [args...] Spawn musl-la directly, same forwarding behavior
26
+ --local
27
+ Start Buninu in this terminal
28
+ instead of a Remote Shell
29
+ reached from a browser
30
+ (via jsgotty, the default)
31
+
32
+ -h, --help
33
+ Show this help and exit
34
+ -V, --version
35
+ Show version & runtime info, then exit
36
+
37
+ --readme
38
+ Render README.md in the terminal and exit
39
+ --changelog
40
+ Render CHANGELOG.md in the terminal and exit
41
+
42
+ --readme-tui
43
+ Open README.md as a Terminal UI
44
+ --readme-wui
45
+ Serve README.md as a Web UI
46
+
47
+ --export [output.tgz]
48
+ Export this Buninu installation
49
+ (default: ./buninu.tgz)
50
+
51
+ --export-config [output.json]
52
+ Export Buninu's package.json
53
+ (default: ./buninu.json)
54
+
55
+ Install options:
56
+ -i, --install [dir]
57
+ Install this package into <dir>/${pkg.name}
58
+ (default: .)
59
+
60
+ -si, --strip-install [dir]
61
+ Install into <dir> directly
62
+ Without a top-level directory of its own
63
+
64
+ (--install --help for full options)
65
+
66
+ Remote Shell options:
67
+ --shell <path|name>
68
+ Override buninu.shell for this run
69
+ --command <command>
70
+ Override buninu.command for this run
71
+
72
+ Direct app launch
73
+ (must be the first argument)
74
+ (skips the shell/command startup flow entirely):
75
+
76
+ --jsgotty [args...]
77
+ Spawn jsgotty directly,
78
+ forwarding remaining arguments,
79
+ and exit with its exit code
80
+ (--jsgotty --help = jsgotty options)
81
+
82
+ --jsmdcui [args...]
83
+ Spawn jsmdcui directly,
84
+ same forwarding behavior
85
+
86
+ --bunmsh [args...]
87
+ Spawn bunmsh directly,
88
+ same forwarding behavior
89
+
90
+ --musl-la [args...]
91
+ Spawn musl-la directly,
92
+ same forwarding behavior
46
93
 
47
94
  Package configuration:
48
95
  package.json contains a buninu section for default and platform settings.
@@ -93,6 +140,16 @@ async function handleInformationArguments(arguments_) {
93
140
  return true;
94
141
  }
95
142
 
143
+ const markdownUi = arguments_.includes("--readme-tui")
144
+ ? "--tui"
145
+ : arguments_.includes("--readme-wui")
146
+ ? "--wui"
147
+ : null;
148
+ if (markdownUi) {
149
+ await openReadmeMarkdownUi(markdownUi);
150
+ return true;
151
+ }
152
+
96
153
  if (arguments_.includes("--changelog")) {
97
154
  const changelogPath = resolve(REPO_ROOT, "CHANGELOG.md");
98
155
  if (!await pathExists(changelogPath)) fail(`CHANGELOG not found: ${changelogPath}`);
@@ -105,6 +162,47 @@ async function handleInformationArguments(arguments_) {
105
162
  return false;
106
163
  }
107
164
 
165
+ // jsmdcui writes five generated companion files beside the Markdown it opens
166
+ // (`*.md.front.js`, `*.md.back.js`, `*.md.html`, `*.md-rpc.js`,
167
+ // `*.md-server.js`), and needs that directory to be writable. Run it on a copy
168
+ // under TMPDIR rather than on README.md in place: an installation is the
169
+ // user's to keep, `--export` would carry the generated files to another
170
+ // machine, an update never deletes them again, and under `npx` the package
171
+ // directory is a cache that should not be written to at all. Nothing is lost
172
+ // by the move -- the README's only image is a remote URL, and what makes it
173
+ // navigable is same-document `#heading-id` links, which resolve wherever the
174
+ // file sits.
175
+ async function openReadmeMarkdownUi(mode) {
176
+ const readmePath = resolve(REPO_ROOT, "README.md");
177
+ if (!await pathExists(readmePath)) fail(`README not found: ${readmePath}`);
178
+
179
+ const entry = resolve(rootDir, "apps", "jsmdcui", "src", "index.js");
180
+ if (!await pathExists(entry)) fail(`jsmdcui not found: ${entry}`);
181
+
182
+ const environment = await detectEnvironment();
183
+ const workDir = resolve(resolveTmpDir(environment), "buninu-readme");
184
+ // Start from a clean directory so a stale generated file from an older
185
+ // README can never be served in place of a freshly generated one.
186
+ rmSync(workDir, { recursive: true, force: true });
187
+ mkdirSync(workDir, { recursive: true });
188
+
189
+ const target = resolve(workDir, "README.md");
190
+ await Bun.write(target, Bun.file(readmePath));
191
+
192
+ // jsmdcui's entry point is a bundled CLI with no exports: it reads
193
+ // process.argv at the top level and runs. Set the argv it expects and import
194
+ // it, so this stays one process instead of spawning a second Bun.
195
+ process.argv = [process.argv[0], entry, mode, target];
196
+ await import(entry);
197
+
198
+ // jsmdcui owns the process from here on: it holds the terminal UI, or the
199
+ // Web UI server, open. Never resolve -- returning would hand control back to
200
+ // the caller, whose process.exit(0) would cut the UI off before it draws.
201
+ // This keeps no handle of its own, so once jsmdcui's are gone the process
202
+ // still ends on its own.
203
+ await new Promise(() => {});
204
+ }
205
+
108
206
  function readExportArgument(arguments_) {
109
207
  for (let index = 0; index < arguments_.length; index += 1) {
110
208
  const argument = arguments_[index];
@@ -418,16 +516,19 @@ function configuredXdgDataHome() {
418
516
  return isAbsolute(configured) ? configured : resolve(rootDir, configured);
419
517
  }
420
518
 
421
- async function createChildEnvironment(environment) {
422
- const packageDir = dirname(rootDir);
423
- const androidCacheDir = resolve(packageDir, "cache");
424
- const tmpDir = process.env.TMPDIR || (
519
+ function resolveTmpDir(environment) {
520
+ const androidCacheDir = resolve(dirname(rootDir), "cache");
521
+ return process.env.TMPDIR || (
425
522
  environment.name === "android"
426
523
  ? existsSync(androidCacheDir)
427
524
  ? androidCacheDir
428
525
  : resolve(rootDir, "tmp")
429
526
  : systemTmpDir()
430
527
  );
528
+ }
529
+
530
+ async function createChildEnvironment(environment) {
531
+ const tmpDir = resolveTmpDir(environment);
431
532
  const homeDir = process.env.HOME || homedir() || rootDir;
432
533
  const envFile = await pathExists(resolve(rootDir, ".bashrc"))
433
534
  ? resolve(rootDir, ".bashrc")
@@ -474,6 +575,7 @@ async function createChildEnvironment(environment) {
474
575
  const directAppTargets = {
475
576
  "--jsgotty": "jsgotty",
476
577
  "--jsmdcui": "jsmdcui",
578
+ "--bunmsh": "bunmsh",
477
579
  "--musl-la": "musl-la",
478
580
  };
479
581
 
package/package.json CHANGED
@@ -1,10 +1,33 @@
1
1
  {
2
2
  "name": "buninu",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Buninu is a portable, self-bootstrapping Unix-like userspace built on Bun that runs across operating systems",
5
5
  "type": "module",
6
6
  "main": "./bin/init.js",
7
7
  "license": "MIT",
8
+ "author": "Dr. John (醫者小智)",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/jjtseng93/buninu.git"
12
+ },
13
+ "homepage": "https://github.com/jjtseng93/buninu#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/jjtseng93/buninu/issues"
16
+ },
17
+ "keywords": [
18
+ "bun",
19
+ "terminal",
20
+ "web-terminal",
21
+ "browser-terminal",
22
+ "shell",
23
+ "userspace",
24
+ "portable",
25
+ "cross-platform",
26
+ "android",
27
+ "termux",
28
+ "pty",
29
+ "self-bootstrapping"
30
+ ],
8
31
  "bin": {
9
32
  "buninu": "bin/init.js"
10
33
  },