buninu 0.2.4 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 - 2026-08-21
4
+
5
+ ### Added
6
+
7
+ - Add `openWebView`, `evalWebView`, `showWebView` and `currWebView` to
8
+ `apps/native-bridge` (short spellings `openwv`, `evalwv`, `showwv`,
9
+ `currwv`, both accepted over the wire and listed in the host's `_discover`,
10
+ the same arrangement `getcb`/`setcb` already had), plus the
11
+ `WEBVIEW_CURRENT`/`WEBVIEW_CONSOLE`/`WEBVIEW_APP` constants. The host app
12
+ has exactly two WebViews, alive from startup and never created or closed at
13
+ runtime: `0` is the console showing the jsgotty terminal, `1` is the app
14
+ WebView, which starts blank and behind. An id of `-1` means whichever is in
15
+ front. `openWebView` loads without bringing its target forward, so loading
16
+ the app WebView while the user keeps looking at the terminal is one call;
17
+ `showWebView` is the only thing that changes what is on screen, and
18
+ `showWebView(-1)` switches to the *next* WebView rather than being a no-op.
19
+ `evalWebView` resolves to the value the expression produced rather than a
20
+ string containing it, with `undefined`, functions and thrown exceptions all
21
+ arriving as `null` because WebView itself does not distinguish them.
22
+ - `xdg-open` gained `MINAPK_WEBVIEW`, which names the WebView to open a URL
23
+ in (and brings it to the front) instead of handing the URL to the
24
+ platform's default handler. Read here rather than by the host app on
25
+ purpose: it is an environment variable of this process tree, so the host
26
+ could not see a value exported in the shell a moment ago, which is the
27
+ whole point of having it. Only URLs are redirected -- a filesystem path
28
+ still goes to the host, whose `content://` provider can actually serve it,
29
+ unlike a `file://` URL in a WebView. A value that is not a plain integer is
30
+ reported and ignored rather than guessed at, and a WebView the host does
31
+ not have falls back to the default handler after saying so.
32
+ - Add `demo-server.js`: serves one fixed page on an OS-assigned port and
33
+ opens it in the app WebView through `MINAPK_WEBVIEW=1 xdg-open`, as the
34
+ smallest end-to-end exercise of the above.
35
+ - Add `buninu.backToConsole` (default `true`), read by the minapk host app,
36
+ not by Buninu itself: it decides whether the Android back key, pressed on
37
+ the app WebView with no page of its own to go back to, switches to the
38
+ console WebView or takes the leave-the-app path. The host treats a missing,
39
+ unreadable or non-boolean value as `true`, so it cannot fail in a way that
40
+ leaves the back key broken. minapk's `--no-back-to-console` sets it for a
41
+ single build.
42
+
43
+ ## 0.2.5 - 2026-08-19
44
+
45
+ - Fixed fish shell startup breaking buninu
46
+ - Start fish after entering the shell please
47
+
3
48
  ## 0.2.4 - 2026-08-19
4
49
 
5
50
  ### Added
package/README.md CHANGED
@@ -125,7 +125,7 @@ jsmdcui Edit and run interactive Markdown applications
125
125
  musl-la Launch AArch64 ELF programs with the bundled musl loader
126
126
  buninu-help Render README.md with glow, then show icon.png with jsgotty --viu
127
127
  bunx Globally install a package with bun, then exec its matching binary
128
- native-bridge Call the Android host app (toast, clipboard, speak) over PKG_BRIDGE_SOCK
128
+ native-bridge Call the Android host app (toast, clipboard, speak, WebViews) over PKG_BRIDGE_SOCK
129
129
  xclip X11-style clipboard tool; -selection clipboard/-clip bridges to the system clipboard
130
130
  tts Speak text and wait for it to finish (-a to not wait)
131
131
  showimg Shorthand for jsgotty --viu
@@ -150,10 +150,45 @@ APK with, over an abstract-namespace unix socket. A bare `native-bridge`
150
150
  lists what the host implements. Every call
151
151
  times out after 5 seconds instead of hanging; outside such an APK, every call
152
152
  fails with a clear error instead of doing nothing silently. `import { toast,
153
- clipboardRead, clipboardWrite, speak, ttsStatus, call, available } from
153
+ clipboardRead, clipboardWrite, speak, ttsStatus, openWebView, evalWebView,
154
+ showWebView, currWebView, call, available } from
154
155
  "apps/native-bridge/native-bridge.js"` gives the same functions as a library,
155
156
  for use from a `js back` block.
156
157
 
158
+ `openWebView(id, url)`, `evalWebView(id, js)`, `showWebView(id)` and
159
+ `currWebView()` (short: `openwv`, `evalwv`, `showwv`, `currwv`) drive the host
160
+ app's WebViews. There are exactly two, both
161
+ alive from startup and neither ever created nor closed at runtime: id `0` is
162
+ the console -- the jsgotty terminal this shell is rendered in -- and id `1` is
163
+ the app WebView, which starts out blank and behind. Anywhere an id is taken,
164
+ `-1` means whichever WebView is in front right now.
165
+
166
+ `openWebView` loads a URL *without* bringing that WebView to the front, so
167
+ loading the app WebView while the user keeps looking at the terminal is one
168
+ call. `showWebView` is the only thing that changes what is on screen; the
169
+ host's on-screen key bar (Ctrl/Alt/Shift and friends), its volume-key menu and
170
+ its back key all act on whichever WebView is in front, so they follow the
171
+ switch. Back on the app WebView with no page of its own to go back to
172
+ switches to the console rather than leaving the app, unless
173
+ `buninu.backToConsole` says otherwise (see "Back key" below).
174
+ `showWebView(-1)` switches to the next WebView instead of being a no-op --
175
+ with two of them, a toggle. `evalWebView` resolves to the value the
176
+ expression produced, not a string containing it; `undefined`, a function, and
177
+ a thrown exception all arrive as `null`, since WebView itself does not
178
+ distinguish them.
179
+
180
+ All four also answer to a short `openwv`/`evalwv`/`showwv`/`currwv` spelling,
181
+ the same way `clipboardRead`/`clipboardWrite` answer to `getcb`/`setcb`: the
182
+ host accepts either and lists both in its `_discover` response, so either name
183
+ works from the CLI, from `rpcraw`, and as an `import`.
184
+
185
+ ```sh
186
+ native-bridge openwv 1 https://example.com # load it, screen unchanged
187
+ native-bridge showwv 1 # now bring it to the front
188
+ native-bridge evalwv 1 document.title
189
+ native-bridge currwv
190
+ ```
191
+
157
192
  `xclip [-o] [-selection primary|clipboard] [-clip]` is a small X11-`xclip`-
158
193
  compatible clipboard tool. `-selection primary` (the default) never touches
159
194
  the system clipboard, matching real X11 semantics. `-selection
@@ -178,6 +213,25 @@ as its default handler: the host app through native-bridge on Android (or
178
213
  `termux-open` under plain Termux), `open` on macOS, `start` on Windows, and
179
214
  the real system `xdg-open` on Linux.
180
215
 
216
+ Inside minapk's APK, `MINAPK_WEBVIEW` redirects that: set it to a WebView id
217
+ and a URL is loaded into that WebView and brought to the front (`openWebView`
218
+ then `showWebView`) instead of being handed to the system's default handler.
219
+
220
+ ```sh
221
+ MINAPK_WEBVIEW=1 xdg-open https://example.com # in the app WebView, on screen
222
+ export MINAPK_WEBVIEW=1 # ...or for the whole session
223
+ ```
224
+
225
+ `0` is the console, so it navigates the terminal page away -- the back key
226
+ returns to it and jsgotty reconnects, but it is not usually what you want.
227
+ `-1` is whichever WebView is in front. Only URLs are redirected: a file path
228
+ always goes to the host's own handler, since WebView cannot read a `file://`
229
+ URL under Buninu's home (`setAllowFileAccess` is false from API 30 on) while
230
+ the host serves that same file through its content:// provider. A value that
231
+ is not a plain integer is reported on stderr and ignored rather than guessed
232
+ at, an unset or empty value keeps the default behavior, and a WebView the host
233
+ does not have falls back to the default handler after saying so.
234
+
181
235
  ## Export
182
236
 
183
237
  Export the current Buninu installation as a gzip-compressed tar archive:
@@ -349,6 +403,29 @@ from the directory containing `package.json`.
349
403
 
350
404
  Use `--shell <path-or-name>` for a one-time override.
351
405
 
406
+ ## Back key (Android, optional)
407
+
408
+ Set `buninu.backToConsole` in `package.json` to decide what the Android host
409
+ app's back key does while the app WebView (id `1`, see `native-bridge` above)
410
+ is in front and has no page of its own left to go back to. It defaults to
411
+ `true`: back switches to the console WebView, leaving the app WebView loaded
412
+ and running behind it. Set it to `false` and back leaves the app instead.
413
+
414
+ ```json
415
+ {
416
+ "buninu": {
417
+ "backToConsole": false
418
+ }
419
+ }
420
+ ```
421
+
422
+ This one is read by the host app, not by Buninu itself, so it does nothing
423
+ outside an APK built with
424
+ [minapk](https://www.npmjs.com/package/@drxiaozhi/minapk) -- where it can also
425
+ be set for a single build with `--no-back-to-console`. The host treats a
426
+ missing, unreadable, or non-boolean value as `true`, so nothing here can fail
427
+ in a way that leaves the back key broken.
428
+
352
429
  ## Process-list helpers
353
430
 
354
431
  The bundled `.bashrc` provides `pspa` (`ps -eo pid,args`) and `pspac`, which
@@ -101,6 +101,51 @@ export const tts = ttsStatus
101
101
  // the receiving app on modern Android.
102
102
  export const xdgOpen = (target) => call("xdgOpen", [target])
103
103
 
104
+ // The host has exactly two WebViews, both alive from startup and neither ever
105
+ // created nor closed at runtime: WEBVIEW_CONSOLE (0) is the jsgotty terminal
106
+ // this shell is being rendered in, WEBVIEW_APP (1) is the app WebView, which
107
+ // simply starts out blank and behind. WEBVIEW_CURRENT (-1) means whichever
108
+ // one is in front right now, and is the default wherever an id is optional.
109
+ // All four functions also go by openwv/evalwv/showwv/currwv, both here and
110
+ // over the wire (the Java dispatcher answers to either spelling and lists
111
+ // both in _discover), on the same grounds as getcb/setcb above.
112
+ export const WEBVIEW_CURRENT = -1
113
+ export const WEBVIEW_CONSOLE = 0
114
+ export const WEBVIEW_APP = 1
115
+
116
+ // Loads url into a WebView *without* bringing it to the front, so
117
+ // openWebView(WEBVIEW_APP, url) while the user is looking at the terminal is
118
+ // a plain background load -- showWebView is the only thing that changes what
119
+ // is on screen. Omitting url loads nothing and just reports the id back,
120
+ // which is how -1 gets resolved to a real id. Returns the id loaded into.
121
+ export const openWebView = (id = WEBVIEW_CURRENT, url = "") =>
122
+ call("openWebView", [id, url])
123
+ export const openwv = openWebView
124
+
125
+ // Resolves to the value the expression produced -- a real number/string/
126
+ // object, not a string containing one -- because the host sends
127
+ // evaluateJavascript's own JSON text straight back as the response body.
128
+ // undefined, a function, and a thrown exception all arrive as null: WebView
129
+ // itself does not distinguish them, so return something JSON can carry when
130
+ // you need to tell them apart. A page that never answers is given up on by
131
+ // the host (inside this module's own call timeout) and comes back as an
132
+ // "error: ..." string rather than hanging.
133
+ export const evalWebView = (id, js) => call("evalWebView", [id, js])
134
+ export const evalwv = evalWebView
135
+
136
+ // Brings a WebView to the front. The bottom key bar (Ctrl/Alt/Shift and
137
+ // friends), the volume-key menu and the back key all act on whatever is in
138
+ // front, so this is also what re-points them. An id of -1 -- the default --
139
+ // switches to the next WebView rather than being a no-op, which with two of
140
+ // them is simply a toggle. Returns the id now in front.
141
+ export const showWebView = (id = WEBVIEW_CURRENT) => call("showWebView", [id])
142
+ export const showwv = showWebView
143
+
144
+ // The id of the WebView in front, for code that wants to put something back
145
+ // the way it found it.
146
+ export const currWebView = () => call("currWebView")
147
+ export const currwv = currWebView
148
+
104
149
  // `bun native-bridge.js <func> [args...]`, dispatched straight to call(func,
105
150
  // args) -- not restricted to toast/clipboardRead/clipboardWrite, exactly like
106
151
  // calling rpcraw(func, args) would be. This is a raw RPC, equivalent to a
@@ -7,9 +7,74 @@
7
7
  // the default handler" mechanism -- macOS/Windows are real OS
8
8
  // commands/APIs, but Linux's is conventionally *also* a command named
9
9
  // xdg-open, so this script has to be careful not to find itself on PATH.
10
- import { xdgOpen as bridgeOpen, available } from "../native-bridge/native-bridge.js"
10
+ import {
11
+ xdgOpen as bridgeOpen, available, openWebView, showWebView,
12
+ } from "../native-bridge/native-bridge.js"
11
13
  const fs = require("fs")
12
14
 
15
+ // MINAPK_WEBVIEW names *which* of the host app's WebViews to open into (0 is
16
+ // the console, 1 the app WebView, -1 whichever is in front); unset means the
17
+ // unchanged behavior of handing the target to the system's default handler.
18
+ // It is read here rather than being detected by the host, because it is an
19
+ // environment variable of this Bun process tree -- the host app's own process
20
+ // cannot see a value exported in this shell a moment ago, and the whole point
21
+ // of the variable is that it can be.
22
+ //
23
+ // Deliberately never Number()/parseInt(): Number("") is 0, and 0 is a
24
+ // perfectly valid WebView id, so an exported-but-empty MINAPK_WEBVIEW= (the
25
+ // usual way to switch something off) would come out meaning "open in the
26
+ // console" -- i.e. navigate the terminal away. parseInt would likewise turn
27
+ // "1x" into 1. Only a bare optionally-signed integer counts; anything else is
28
+ // reported and treated as unset.
29
+ function webViewIdFromEnv() {
30
+ const raw = (process.env.MINAPK_WEBVIEW ?? "").trim()
31
+ if (raw === "") return null
32
+ if (!/^-?\d+$/.test(raw)) {
33
+ process.stderr.write(
34
+ `xdg-open: ignoring MINAPK_WEBVIEW=${raw} (not an integer WebView id)\n`)
35
+ return null
36
+ }
37
+ return Number(raw)
38
+ }
39
+
40
+ // Only URLs. A filesystem path stays on the bridgeOpen path even when
41
+ // MINAPK_WEBVIEW is set: WebView's setAllowFileAccess defaults to false from
42
+ // API 30 on, so a file:// URL under Buninu's home would load as a blank page,
43
+ // whereas the host's xdgOpen already serves that file properly through its
44
+ // read-only content:// provider.
45
+ const isUrl = (target) => /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(target)
46
+
47
+ // The host answers with the id it actually used -- a JSON number -- and with
48
+ // a string when something went wrong: "error: no such WebView: 9" from the
49
+ // dispatcher, or rpc.mjs's own "Unknown func ..." when the APK is older than
50
+ // these functions and does not list them in _discover. rpc.mjs also *resolves*
51
+ // to an Error object instead of throwing when the fetch itself fails. So
52
+ // success is checked as "is genuinely a number", never by parsing the result:
53
+ // see webViewIdFromEnv above for why a coercing parse is dangerous here.
54
+ //
55
+ // showWebView(-1) means "switch to the next one", not "the one just loaded",
56
+ // so the switch uses the id openWebView resolved and handed back, not the id
57
+ // that was asked for.
58
+ async function openInWebView(id, url) {
59
+ const opened = await openWebView(id, url)
60
+ if (typeof opened !== "number" || !Number.isInteger(opened)) {
61
+ process.stderr.write(
62
+ `xdg-open: MINAPK_WEBVIEW=${id}: ${opened?.message ?? opened}\n`)
63
+ return false
64
+ }
65
+
66
+ const shown = await showWebView(opened)
67
+ if (typeof shown !== "number" || !Number.isInteger(shown)) {
68
+ // Loaded but not brought to the front: say so rather than reporting
69
+ // success for something the user cannot see, but do not fall back to the
70
+ // system handler either -- the page really is loaded in that WebView.
71
+ process.stderr.write(
72
+ `xdg-open: loaded in WebView ${opened} but could not switch to it: ` +
73
+ `${shown?.message ?? shown}\n`)
74
+ }
75
+ return true
76
+ }
77
+
13
78
  // Buninu's own bin/ is prepended to PATH ahead of any real system binary of
14
79
  // the same name (same concern as xclip.js/tts.js), and this script is
15
80
  // itself registered as Buninu's "xdg-open" command -- a naive PATH search
@@ -46,7 +111,14 @@ let ok = false
46
111
  try {
47
112
  if (process.platform === "android") {
48
113
  if (available()) {
49
- ok = await bridgeOpen(target)
114
+ const webViewId = webViewIdFromEnv()
115
+ // A failed WebView open falls back to the system handler rather than
116
+ // failing outright: the request was "open this", and MINAPK_WEBVIEW
117
+ // only says where it would be preferred. openInWebView has already
118
+ // explained on stderr why it could not.
119
+ ok = webViewId !== null && isUrl(target)
120
+ ? (await openInWebView(webViewId, target)) || (await bridgeOpen(target))
121
+ : await bridgeOpen(target)
50
122
  } else if (Bun.which("termux-open")) {
51
123
  // Plain Termux (no minapk) still reports platform "android" but has
52
124
  // no PKG_BRIDGE_SOCK -- termux-open (Termux:API) does the same
package/bin/init.js CHANGED
@@ -258,7 +258,7 @@ async function detectEnvironment() {
258
258
  return {
259
259
  name: "linux",
260
260
  shell:
261
- await findCommand(process.env.SHELL) ||
261
+ // await findCommand(process.env.SHELL) ||
262
262
  Bun.which("bash") ||
263
263
  Bun.which("sh") ||
264
264
  "/bin/sh",
@@ -269,10 +269,11 @@ async function detectEnvironment() {
269
269
  return {
270
270
  name: "android",
271
271
  shell:
272
- await findCommand(process.env.SHELL) ||
272
+ // await findCommand(process.env.SHELL) ||
273
273
  (await pathExists("/system/bin/sh") ? "/system/bin/sh" : null) ||
274
+ Bun.which("bash") ||
274
275
  Bun.which("sh") ||
275
- "sh",
276
+ "/bin/sh",
276
277
  };
277
278
  }
278
279
 
@@ -280,7 +281,7 @@ async function detectEnvironment() {
280
281
  return {
281
282
  name: "macos",
282
283
  shell:
283
- await findCommand(process.env.SHELL) ||
284
+ // await findCommand(process.env.SHELL) ||
284
285
  Bun.which("zsh") ||
285
286
  Bun.which("bash") ||
286
287
  Bun.which("sh") ||
@@ -292,7 +293,7 @@ async function detectEnvironment() {
292
293
  return {
293
294
  name: "linux",
294
295
  shell:
295
- await findCommand(process.env.SHELL) ||
296
+ // await findCommand(process.env.SHELL) ||
296
297
  Bun.which("bash") ||
297
298
  Bun.which("sh") ||
298
299
  "/bin/sh",
@@ -302,7 +303,7 @@ async function detectEnvironment() {
302
303
  return {
303
304
  name: `unix:${process.platform}`,
304
305
  shell:
305
- await findCommand(process.env.SHELL) ||
306
+ // await findCommand(process.env.SHELL) ||
306
307
  Bun.which("bash") ||
307
308
  Bun.which("sh") ||
308
309
  "/bin/sh",
package/demo-server.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // Smallest end-to-end demo of the WebView bridge: serve one fixed page on a
4
+ // port nobody picked in advance, then have xdg-open put it in the host app's
5
+ // app WebView and switch to it. Run it from a Buninu shell inside minapk's
6
+ // APK:
7
+ //
8
+ // bun demo-server.js
9
+ //
10
+ // Outside such an APK it still runs and still serves; xdg-open just falls
11
+ // back to whatever that platform's default handler is (see apps/xdg-open).
12
+
13
+ const BODY = "Hello 你好 こんにちは"
14
+
15
+ // port: 0 asks the OS for a free port instead of guessing one and racing
16
+ // whatever already has it -- Buninu's own launcher picks jsgotty's port the
17
+ // same way. The real number is only knowable after the listener exists, so
18
+ // the URL is built from server.port, never from a constant.
19
+ // hostname stays on loopback: this is a demo page for the WebView on this
20
+ // device, not something other machines on the network should reach.
21
+ const server = Bun.serve({
22
+ hostname: "127.0.0.1",
23
+ port: 0,
24
+ fetch() {
25
+ // charset is not optional here: without it the WebView guesses an
26
+ // encoding for the response, and the CJK half of BODY is what shows up
27
+ // as mojibake when it guesses wrong.
28
+ return new Response(BODY, {
29
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
30
+ })
31
+ },
32
+ })
33
+
34
+ // 127.0.0.1 rather than localhost: the listener is bound to the v4 loopback
35
+ // address specifically, while "localhost" can resolve to ::1 first and give
36
+ // the WebView a connection refused before it ever tries v4.
37
+ const url = `http://127.0.0.1:${server.port}`
38
+ console.log(`demo-server: serving ${JSON.stringify(BODY)} at ${url}`)
39
+
40
+ // Buninu puts its own bin/ on PATH, so "xdg-open" normally resolves to
41
+ // apps/xdg-open through the shloader. Fall back to running the script
42
+ // directly for the case where this file is run outside a Buninu shell (a bare
43
+ // `bun demo-server.js` in a checkout), where that PATH entry is not there.
44
+ const xdgOpen = Bun.which("xdg-open")
45
+ const command = xdgOpen
46
+ ? [xdgOpen, url]
47
+ : [process.execPath, `${import.meta.dir}/apps/xdg-open/xdg-open.js`, url]
48
+
49
+ // MINAPK_WEBVIEW is read by xdg-open itself, not by the host app -- it names
50
+ // which WebView to load into (1 is the app WebView) and brings it to the
51
+ // front. Passed to this one child rather than exported, so it changes nothing
52
+ // for the rest of the session.
53
+ const child = Bun.spawn(command, {
54
+ env: { ...process.env, MINAPK_WEBVIEW: "1" },
55
+ stdout: "inherit",
56
+ stderr: "inherit",
57
+ })
58
+
59
+ if (await child.exited !== 0)
60
+ console.error(`demo-server: ${command[0]} exited with ${child.exitCode}; page is still served at ${url}`)
61
+
62
+ // The server has to outlive the open: the WebView fetches the page after
63
+ // xdg-open has already returned. Ctrl-C to stop.
64
+ console.log("demo-server: Ctrl-C to stop")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
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",
@@ -23,7 +23,8 @@
23
23
  "macos": null,
24
24
  "windows": null
25
25
  },
26
- "exitAfterCmd": false
26
+ "exitAfterCmd": false,
27
+ "backToConsole": true
27
28
  },
28
29
  "scripts": {
29
30
  "start": "bun ./apps/jsgotty/gotty.js --reconnect -r -w --webgl -a 127.0.0.1",