buninu 0.2.3 → 0.2.4

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,109 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.4 - 2026-08-19
4
+
5
+ ### Added
6
+
7
+ - Add the `xdg-open` command (`apps/xdg-open`), which opens a file or URL
8
+ with whatever the platform considers its default handler: native-bridge's
9
+ `xdgOpen` on Android (the host resolves a path under Buninu's home and
10
+ hands it to another app through a read-only `content://` provider, since
11
+ that directory is otherwise private to the APK), `termux-open` when
12
+ running under plain Termux with no native bridge, `open` on macOS, `start`
13
+ on Windows, and the real system `xdg-open` on Linux. That last one skips
14
+ Buninu's own `bin` while searching `PATH`, since this script is itself
15
+ registered as `xdg-open` and would otherwise find and run itself.
16
+ - Add `showimg`, a `bin/`-only shorthand for `jsgotty --viu` (no `apps/`
17
+ subfolder, since it has no logic of its own beyond forwarding args).
18
+ - Add `rz`/`sz`, `bin/`-only companion scripts wrapping the existing
19
+ `apps/jsgotty/rz.js`/`sz.js` -- `rz` uploads a file over ZMODEM, `sz`
20
+ downloads one -- over the same terminal connection jsgotty renders in a
21
+ browser or WebView.
22
+
23
+ - Add `apps/native-bridge`, a Bun module (`toast`, `clipboardRead`/`getcb`,
24
+ `clipboardWrite`/`setcb`, and the raw `call(func, args, envp)` it and the
25
+ CLI both sit on top of) that reaches minapk's Android native bridge over
26
+ `PKG_BRIDGE_SOCK` -- a Bun unix socket, filesystem-path or Linux
27
+ abstract-namespace depending on how that env var is encoded -- using
28
+ jsmdcui's `rpc.mjs` `switchBackend`. Only meaningful inside an APK built by
29
+ minapk with the native bridge wired in; elsewhere `available()` returns
30
+ `false` and calls throw a clear error instead of doing nothing silently.
31
+ Every call times out after 5s by default, so a stuck or unresponsive host
32
+ can never hang the caller. `bun apps/native-bridge/native-bridge.js [func]
33
+ [args...]` runs it directly; a bare invocation defaults `func` to
34
+ `_discover` and lists what the host implements.
35
+ - Add the `xclip` command (`apps/xclip`), ported from the DroidScript-era
36
+ `../tmpk/bin/xclip.sh` to use native-bridge instead of `dsapi.pipe`.
37
+ `-selection primary` (the default) stays local-file-only, matching real X11
38
+ semantics where the primary selection is never the same thing as the
39
+ clipboard; `-selection clipboard`/`-clip` bridges to the real Android
40
+ clipboard through native-bridge. jsmdcui's own clipboard backend detection
41
+ already shells out to `xclip` on Linux-like platforms (`isLinuxLike()`
42
+ counts `process.platform === "android"`), so jsmdcui's middle-click paste,
43
+ selection auto-sync, and `PastePrimary` command all work inside an APK with
44
+ no further wiring once `xclip` is on `PATH`.
45
+ - Add `speak`/`ttsStatus`/`tts` to native-bridge and the `tts` command
46
+ (`apps/tts`) built on them. Android's TextToSpeech completion signal is an
47
+ asynchronous callback with no way to push it to the far side over this
48
+ protocol, so `speak(text, speed, pitch, flush)` never blocks -- it returns
49
+ a handle immediately, and `ttsStatus(handle)`/`tts(handle)` (`"speaking"`,
50
+ `"done"`, `"error"`, or `"unknown"` once a terminal state has already been
51
+ consumed) is polled to find out when it finishes. `pitch`/`speed` both
52
+ default to `1.0` (normal), the same convention as `termux-tts-speak`'s
53
+ `-p`/`-r`; native-bridge passes them straight to `TextToSpeech.setPitch()`/
54
+ `setSpeechRate()` with no unit conversion. `tts <text>` wraps the polling
55
+ loop for the common case, blocking the calling process until speech
56
+ finishes -- the same as any other CLI tool that waits for the thing it
57
+ started, and, like real TTS tooling (espeak-ng has no timeout concept at
58
+ all; Windows SAPI's `WaitUntilDone` documents `-1`/infinite as the default),
59
+ waits as long as it takes with no wall-clock cap by default. Pass
60
+ `--timeout <ms>` to opt into a bounded wait instead, or `-a`/`--async` to
61
+ not wait at all. `--pitch`/`--speed` override the environment; without
62
+ them, `tts` reads `$TTS_PITCH`/`$TTS_SPEED` itself (falling back to `1` for
63
+ either that is unset), so jsmdcui's own `TTS_PITCH`/`TTS_SPEED` convention
64
+ (which it sets on `Bun.env` before spawning a TTS command, inherited here
65
+ like any other child process env var) is honored automatically -- jsmdcui's
66
+ own `detectTtsCmd()` already falls back to a bare `Bun.which("tts")` after
67
+ termux-tts-speak/espeak-ng/espeak, which this `tts` now satisfies, but that
68
+ particular fallback branch passes no `-p`/`-r`-equivalent flags at all, so
69
+ reading the env vars directly here is the only way `tts`'s pitch/speed
70
+ actually reaches Android's TTS engine through that path.
71
+ - Give `xclip` and `tts` a real fallback on non-Android platforms too, since
72
+ Buninu is cross-OS, not Android-only. `xclip`'s `-selection clipboard`
73
+ register now also tries jsmdcui's own `ClipboardManager`
74
+ (`src/platform/clipboard.js`, imported directly rather than reimplemented)
75
+ on win32/darwin, then `wl-copy`/`wl-paste` on Linux; `tts` gains a
76
+ `detectTtsCmd()` copied verbatim from jsmdcui's `src/index.js` (same
77
+ pitch/speed/lang math and command construction, including the win32
78
+ PowerShell/SAPI SSML branch, copied rather than hand-retyped since that
79
+ quoting is easy to silently break and this had no Windows machine to
80
+ verify it on) covering termux-tts-speak/say/PowerShell/espeak-ng/espeak.
81
+ Both deliberately skip the one path that would reintroduce the exact
82
+ self-reference risk they otherwise avoid: `ClipboardManager`'s Linux-like
83
+ branch searches PATH for a binary named `xclip`, and jsmdcui's own
84
+ `detectTtsCmd()` ends with a `Bun.which("tts")` fallback -- since these two
85
+ commands are themselves registered as Buninu's `xclip`/`tts`, ahead of
86
+ anything else of the same name on PATH, using either the way jsmdcui does
87
+ from inside xclip.js/tts.js would just be each script invoking itself.
88
+ - Add `bin/*.bat` launchers for the new commands (`native-bridge`, `xclip`,
89
+ `tts`, `showimg`, `rz`, `sz`, `xdg-open`). POSIX platforms get their
90
+ `bin/<name>` entries as symlinks rebuilt by `bin/init.js` on startup,
91
+ which Windows cannot use, so each command needs its own `.bat` there.
92
+
93
+ ### Changed
94
+
95
+ - Update the bundled jsmdcui to 0.18.1, which adds `switchBackend()` to
96
+ `src/cui/rpc.mjs` so an RPC caller can point at something other than the
97
+ default `rpc` endpoint -- specifically a unix socket, which is what
98
+ `native-bridge` dials. Also picks up 0.18.1's fix for decoding a
99
+ percent-encoded socket path, needed because `new URL("unix:" + sock)`
100
+ turns a leading NUL (a Linux abstract-namespace socket) into `%00`.
101
+ - `native-bridge`'s CLI now prints `_discover` one function per line instead
102
+ of as a single line of JSON, which makes the list greppable. The value
103
+ itself is untouched -- rpc.mjs dispatches every call against that object,
104
+ so it stays exactly what the host returned; only the CLI's own display of
105
+ it changed.
106
+
3
107
  ## 0.2.3 - 2026-08-18
4
108
 
5
109
  ### Added
package/README.md CHANGED
@@ -118,13 +118,20 @@ validated source list is `apps/cmdlist`; see [Add a command](#add-a-command)
118
118
  for how it works):
119
119
 
120
120
  ```text
121
- glow Render files with jsmdcui syntax highlighting
122
- jmi Open files in the js micro editor
123
- jsgotty Run a browser-accessible terminal
124
- jsmdcui Edit and run interactive Markdown applications
125
- musl-la Launch AArch64 ELF programs with the bundled musl loader
126
- buninu-help Render README.md with glow, then show icon.png with jsgotty --viu
127
- bunx Globally install a package with bun, then exec its matching binary
121
+ glow Render files with jsmdcui syntax highlighting
122
+ jmi Open files in the js micro editor
123
+ jsgotty Run a browser-accessible terminal
124
+ jsmdcui Edit and run interactive Markdown applications
125
+ musl-la Launch AArch64 ELF programs with the bundled musl loader
126
+ buninu-help Render README.md with glow, then show icon.png with jsgotty --viu
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
129
+ xclip X11-style clipboard tool; -selection clipboard/-clip bridges to the system clipboard
130
+ tts Speak text and wait for it to finish (-a to not wait)
131
+ showimg Shorthand for jsgotty --viu
132
+ rz Upload a file over ZMODEM
133
+ sz Download a file over ZMODEM
134
+ xdg-open Open a file or URL with the platform's default handler
128
135
  ```
129
136
 
130
137
  `buninu-help` renders README.md with jsmdcui's `--cat` mode and then shows
@@ -137,6 +144,40 @@ matching binary. On Android, the underlying `bun i -g` currently needs
137
144
  upstream — without it, install is killed by SIGSYS (Android's seccomp policy
138
145
  rejects a syscall bin-linking uses), so `bunx` can't install anything there yet.
139
146
 
147
+ `native-bridge [func] [args...]` calls into the Android host app that
148
+ [minapk](https://www.npmjs.com/package/@drxiaozhi/minapk) built the running
149
+ APK with, over an abstract-namespace unix socket. A bare `native-bridge`
150
+ lists what the host implements. Every call
151
+ times out after 5 seconds instead of hanging; outside such an APK, every call
152
+ fails with a clear error instead of doing nothing silently. `import { toast,
153
+ clipboardRead, clipboardWrite, speak, ttsStatus, call, available } from
154
+ "apps/native-bridge/native-bridge.js"` gives the same functions as a library,
155
+ for use from a `js back` block.
156
+
157
+ `xclip [-o] [-selection primary|clipboard] [-clip]` is a small X11-`xclip`-
158
+ compatible clipboard tool. `-selection primary` (the default) never touches
159
+ the system clipboard, matching real X11 semantics. `-selection
160
+ clipboard`/`-clip` reaches the real system clipboard on Android, macOS,
161
+ Windows, and Linux/Wayland. jsmdcui picks this up automatically once it's on
162
+ `PATH`, so its middle-click paste and copy/paste commands just work.
163
+
164
+ `tts <text> [-f|--flush] [-a|--async] [--timeout <ms>] [--pitch <n>]
165
+ [--speed <n>]` speaks text and, by default, blocks until it finishes -- no
166
+ timeout unless you pass `--timeout`. Works on Android, macOS, Windows, and
167
+ Linux (via espeak-ng/espeak). `--pitch`/`--speed` fall back to
168
+ `$TTS_PITCH`/`$TTS_SPEED` when not given explicitly, so jsmdcui's own
169
+ pitch/speed setting is honored automatically.
170
+
171
+ `rz [target-dir]` (upload into `target-dir`, default: cwd) and `sz <file>
172
+ [more files...]` (download one or more files) are thin `bin/`-only wrappers
173
+ around `apps/jsgotty/rz.js`/`sz.js`, transferring files over ZMODEM through
174
+ the same terminal connection jsgotty already renders in a browser or WebView.
175
+
176
+ `xdg-open <file-or-url>` hands a file or URL to whatever the platform treats
177
+ as its default handler: the host app through native-bridge on Android (or
178
+ `termux-open` under plain Termux), `open` on macOS, `start` on Windows, and
179
+ the real system `xdg-open` on Linux.
180
+
140
181
  ## Export
141
182
 
142
183
  Export the current Buninu installation as a gzip-compressed tar archive:
@@ -184,6 +225,8 @@ paths obtained from Android APIs:
184
225
 
185
226
  - `PKG_DDIR`: `/storage/emulated/<user-id>/Android/data/<package>`.
186
227
  - `PKG_MDIR`: `/storage/emulated/<user-id>/Android/media/<package>`.
228
+ - `PKG_BRIDGE_SOCK`: the `native-bridge` unix socket, when the host app
229
+ wires one in; see `native-bridge` above.
187
230
 
188
231
  ## Add a command
189
232
 
package/apps/cmdlist CHANGED
@@ -20,3 +20,24 @@ buninu-help
20
20
 
21
21
  ### bunx — Globally install a package with bun, then exec its matching binary
22
22
  bunx
23
+
24
+ ### native-bridge — Call the Android host app (toast, clipboard) over PKG_BRIDGE_SOCK; bare invocation lists what the host implements
25
+ native-bridge
26
+
27
+ ### xclip — X11-style clipboard tool; -selection clipboard/-clip bridges to the real Android clipboard via native-bridge
28
+ xclip
29
+
30
+ ### tts — Speak text through the Android host and wait for it to finish (-a to not wait)
31
+ tts
32
+
33
+ ### showimg — Shorthand for jsgotty --viu
34
+ showimg
35
+
36
+ ### rz — Upload a file over ZMODEM (bun apps/jsgotty/rz.js)
37
+ rz
38
+
39
+ ### sz — Download a file over ZMODEM (bun apps/jsgotty/sz.js)
40
+ sz
41
+
42
+ ### xdg-open — Open a file or URL with the platform's default handler (native bridge on Android, open/start/xdg-open elsewhere)
43
+ xdg-open
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable user-visible changes to jsmdcui are documented here.
4
4
 
5
+ ## [0.18.0] - 2026-08-19
6
+
7
+ ### Added
8
+
9
+ - Add `switchBackend(endpoint)` to `rpc.mjs`, repointing every `rpc.*` call
10
+ (including `_discover`) at a different endpoint instead of the default
11
+ `"rpc"`. Returns the previous endpoint so a caller can restore it.
12
+ - Accept a Bun unix socket as that endpoint: pass `"unix:/path/to/sock"` (or
13
+ `"UNIX:/path/to/sock"`) to `switchBackend`, and every `rpc.*` fetch is
14
+ issued over that socket via Bun's `unix` fetch option instead of as an
15
+ http path. Dispatch is unaffected, since routing is by function name in
16
+ the request body rather than by HTTP path. Only meaningful when `rpc.mjs`
17
+ runs under Bun (the `unix` fetch option has no effect in a browser).
18
+
5
19
  ## [0.17.3] - 2026-08-12
6
20
 
7
21
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsmdcui",
3
- "version": "0.17.3",
3
+ "version": "0.18.0",
4
4
  "description": "Markdown as a Common UI for Terminals and Web Browsers",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -7,6 +7,35 @@ const crlf="\r\n"
7
7
 
8
8
  let apilist = new Map()
9
9
 
10
+ // Endpoint every frontend rpc fetch posts to. Returns the previous one.
11
+ let rpcBackend = "rpc"
12
+
13
+ export function switchBackend( endpoint )
14
+ {
15
+ const previous = rpcBackend ;
16
+
17
+ rpcBackend = ( endpoint || "rpc" ) + "" ;
18
+
19
+ return previous ;
20
+ }
21
+
22
+ // rpcBackend starting with "unix"/"UNIX" (e.g. "unix:/path/to/sock") names a
23
+ // Bun unix socket instead of an http path. fetch() rejects the unix: scheme
24
+ // outright, so the request goes to a fixed dummy URL and the real path comes
25
+ // from fetch's `unix` option instead; routing here is by func name in the
26
+ // JSON body, not by HTTP path, so the dummy URL costs nothing.
27
+ const rpcFetchUrl = () =>
28
+ ( rpcBackend.slice(0,4) === "unix" || rpcBackend.slice(0,4) === "UNIX" )
29
+ ? "http://unix/rpc" : rpcBackend ;
30
+
31
+ // decodeURIComponent undoes new URL()'s percent-encoding of the path, which
32
+ // otherwise turns a leading NUL byte (a Linux abstract-namespace socket name,
33
+ // e.g. "unix:\0name") into the three literal characters "%00" instead of the
34
+ // single byte 0x00 Bun's `unix` option needs to actually reach it.
35
+ const rpcFetchOpt = () =>
36
+ ( rpcBackend.slice(0,4) === "unix" || rpcBackend.slice(0,4) === "UNIX" )
37
+ ? { unix: decodeURIComponent(new URL(rpcBackend).pathname) } : {} ;
38
+
10
39
  export const jss = JSON.stringify
11
40
 
12
41
  const webMdcuiIdSource = String.raw`[_\p{L}\p{N}][_\p{L}\p{M}\p{N}:-]*`;
@@ -309,7 +338,7 @@ function webComponentMarkdownHtml(markdown)
309
338
  const source = String(markdown ?? "");
310
339
  if (typeof Bun !== "undefined" && Bun?.markdown?.html)
311
340
  return renderWebComponentMarkdownOnServer(source);
312
- return fetch("rpc", {
341
+ return fetch(rpcFetchUrl(), { ...rpcFetchOpt(),
313
342
  method: "POST",
314
343
  headers: { "content-type": "application/json" },
315
344
  body: JSON.stringify(["_mdcui_render_markdown", [source]]),
@@ -1389,7 +1418,7 @@ export const rpcraw = async (func,argv,envp)=>{
1389
1418
  const apilistMod = await FrontendDiscoverApi()
1390
1419
 
1391
1420
  if(apilistMod[func])
1392
- return await fetch("rpc", {
1421
+ return await fetch(rpcFetchUrl(), { ...rpcFetchOpt(),
1393
1422
  method: "POST",
1394
1423
  body: JSON.stringify([
1395
1424
  func,argv,envp
@@ -1463,7 +1492,7 @@ export function getfuncparams( func )
1463
1492
  export async function FrontendDiscoverApi()
1464
1493
  {
1465
1494
  if(!apilist.get(0))
1466
- apilist.set(0, await fetch("rpc", {
1495
+ apilist.set(0, await fetch(rpcFetchUrl(), { ...rpcFetchOpt(),
1467
1496
  method: "POST",
1468
1497
  body: JSON.stringify(["_discover"])
1469
1498
  }).then(r=>r.json()).catch(e=>e) ) ;
@@ -0,0 +1,154 @@
1
+ // Called from a jsmdcui `js back` block (a Bun process), never from `js front`:
2
+ // the `unix` fetch option native-bridge relies on only works under Bun, not
3
+ // inside the Android WebView that renders front code.
4
+ // rpc.mjs's rpc.* Proxy exists so a caller can dial an RPC by a name it only
5
+ // knows at runtime; every call here uses a fixed, known-in-advance name, so
6
+ // there is nothing that sugar buys us. Going straight to rpcraw also gets us
7
+ // a real envp slot -- rpc.foo(...) always sends envp as {} (it is fed by the
8
+ // Proxy's own internal `this`, not anything a caller supplies).
9
+ import { rpcraw, switchBackend } from "../jsmdcui/src/cui/rpc.mjs"
10
+
11
+ // PKG_BRIDGE_SOCK carries whatever rpc.mjs's rpcFetchOpt() needs after
12
+ // decodeURIComponent(new URL("unix:"+sock).pathname): percent-encoded
13
+ // (e.g. "%00native-bridge") for a Linux abstract-namespace socket -- an env
14
+ // var cannot carry a literal NUL byte, but it can carry the three ASCII
15
+ // characters "%00" -- or a plain path for a real filesystem socket, which
16
+ // has nothing that needs encoding and passes through unchanged. Either way,
17
+ // native-bridge.js does not decide which; it forwards the value as-is.
18
+ const sock = process.env.PKG_BRIDGE_SOCK
19
+
20
+ if (sock) switchBackend(`unix:${sock}`)
21
+
22
+ // rpc.mjs never throws for a call the far side does not implement; it
23
+ // resolves to a string starting with "Unknown func" instead (evalBack/rpcraw
24
+ // in rpc.mjs both use this exact prefix). Default here is the same silent
25
+ // behavior; call nothrow(false) to make native-bridge throw in that case
26
+ // instead, so a missing PKG_BRIDGE_SOCK or an unimplemented host method fails
27
+ // loudly rather than being mistaken for a successful result. Any value other
28
+ // than an explicit `false` (including calling nothrow() with no argument)
29
+ // restores the default silent behavior.
30
+ let noThrowUnknownFunc = true
31
+
32
+ export function nothrow(bool)
33
+ {
34
+ noThrowUnknownFunc = bool !== false
35
+ }
36
+
37
+ // fetch() never times out on its own: if the host never answers (main thread
38
+ // wedged, bridge crashed mid-request, anything), the await below would hang
39
+ // forever without this race. The abandoned rpcraw() call keeps running in
40
+ // the background after the race settles -- Bun's event loop treats a
41
+ // pending fetch as a reason to stay alive, so it is not enough for this
42
+ // function to merely return; whatever ultimately calls call() must still
43
+ // process.exit() once it is done, or the process itself will not exit even
44
+ // though every await has resolved.
45
+ const DEFAULT_TIMEOUT_MS = 5000
46
+
47
+ export async function call(name, args = [], envp, timeoutMs = DEFAULT_TIMEOUT_MS)
48
+ {
49
+ if (!sock)
50
+ throw new Error(`native-bridge: PKG_BRIDGE_SOCK is not set (not running inside minapk's APK)`)
51
+
52
+ const result = await Promise.race([
53
+ rpcraw(name, args, envp),
54
+ new Promise((_, reject) =>
55
+ setTimeout(() => reject(new Error(
56
+ `native-bridge: "${name}" timed out after ${timeoutMs}ms`)), timeoutMs)),
57
+ ])
58
+
59
+ if (!noThrowUnknownFunc && typeof result === "string" && result.startsWith("Unknown func"))
60
+ throw new Error(`native-bridge: host does not implement "${name}"`)
61
+
62
+ return result
63
+ }
64
+
65
+ export const available = () => !!sock
66
+
67
+ // clipboardRead/clipboardWrite are long enough to want a short alias; toast
68
+ // already is one. The Java dispatcher accepts either spelling over the wire
69
+ // and lists both in its _discover response, so calling the alias directly
70
+ // (e.g. from the CLI below, or another rpcraw caller) works too, not just
71
+ // through these.
72
+ export const toast = (text, long = false) => call("toast", [text, long])
73
+
74
+ export const clipboardRead = () => call("clipboardRead")
75
+ export const getcb = clipboardRead
76
+
77
+ export const clipboardWrite = (text) => call("clipboardWrite", [text])
78
+ export const setcb = clipboardWrite
79
+
80
+ // TTS has no push/callback path over this protocol -- speak() only waits for
81
+ // the host to accept the utterance, not for it to finish speaking, and
82
+ // returns a handle immediately. Poll ttsStatus(handle) yourself ("speaking",
83
+ // "done", "error", or "unknown" once a terminal state has already been
84
+ // consumed) until it leaves "speaking"; apps/tts wraps that loop for the
85
+ // common case instead of every caller writing its own.
86
+ // Only text is required. 1.0 is normal for both speed and pitch, same
87
+ // convention as termux-tts-speak's -p/-r; native-bridge does not assume any
88
+ // particular caller's own default (jsmdcui's TTS_PITCH/TTS_SPEED, for
89
+ // instance, default to 1/1.5 -- that is jsmdcui's own choice, read
90
+ // TTS_PITCH/TTS_SPEED yourself if you want to honor it).
91
+ export const speak = (text, speed = 1, pitch = 1, flush = false) =>
92
+ call("speak", [text, speed, pitch, flush])
93
+
94
+ export const ttsStatus = (handle) => call("ttsStatus", [handle])
95
+ export const tts = ttsStatus
96
+
97
+ // target is either a URL (any scheme:// prefix, opened via ACTION_VIEW same
98
+ // as a WebView link tap) or a path under Buninu's home -- served back out
99
+ // through a read-only content:// provider, since the home directory is
100
+ // otherwise private to this app and a raw file:// Uri would be rejected by
101
+ // the receiving app on modern Android.
102
+ export const xdgOpen = (target) => call("xdgOpen", [target])
103
+
104
+ // `bun native-bridge.js <func> [args...]`, dispatched straight to call(func,
105
+ // args) -- not restricted to toast/clipboardRead/clipboardWrite, exactly like
106
+ // calling rpcraw(func, args) would be. This is a raw RPC, equivalent to a
107
+ // native function call on the host side: the result can be any JSON value
108
+ // (string, number, boolean, null, array, object), not just a plain object,
109
+ // so it is printed as-is rather than assumed to have any particular shape.
110
+ // Each arg is JSON.parse'd when it parses cleanly (so `42`, `true`, `[1,2]`,
111
+ // `{"a":1}` arrive as their real types) and kept as a plain string otherwise
112
+ // (so `hi` stays "hi" rather than failing to parse as JSON). No func given at
113
+ // all defaults to `_discover`, so a bare `bun native-bridge.js` lists what
114
+ // the far side implements instead of erroring.
115
+ export async function main()
116
+ {
117
+ const [name = "_discover", ...rawArgs] = process.argv.slice(2)
118
+
119
+ const args = rawArgs.map(arg => {
120
+ try { return JSON.parse(arg) } catch { return arg }
121
+ })
122
+
123
+ try
124
+ {
125
+ const result = await call(name, args)
126
+
127
+ // Display-only: _discover's real value (an object, relied on as-is by
128
+ // rpc.mjs's own dispatch gate -- see FrontendDiscoverApi/rpcraw) is
129
+ // never touched. This only changes what gets printed here, one
130
+ // function per line instead of JSON.stringify's single-line output, so
131
+ // the list is easy to grep.
132
+ if (name === "_discover" && result && typeof result === "object")
133
+ {
134
+ const lines = Object.entries(result).map(([k, v]) => `"${k}":${JSON.stringify(v)}`)
135
+ console.log("{\n" + lines.join(",\n") + "\n}")
136
+ }
137
+ else
138
+ {
139
+ console.log(JSON.stringify(result))
140
+ }
141
+ }
142
+ catch (e)
143
+ {
144
+ console.error(e.message)
145
+ process.exit(1)
146
+ }
147
+
148
+ // Explicit exit even on success: see the note above call()'s timeout race
149
+ // -- an abandoned rpcraw() from a prior timeout can keep the process alive
150
+ // on its own otherwise.
151
+ process.exit(0)
152
+ }
153
+
154
+ if (import.meta.main) main()
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "native-bridge",
3
+ "version": "0.1.0",
4
+ "description": "Call the Android host app (toast, clipboard, ...) from a Buninu Bun back script, over the unix socket minapk exposes as PKG_BRIDGE_SOCK",
5
+ "type": "module",
6
+ "main": "./native-bridge.js",
7
+ "license": "MIT"
8
+ }
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // Wraps native-bridge's raw speak()/ttsStatus() -- which never block, since
4
+ // TextToSpeech's completion signal is a callback the far side has no way to
5
+ // push to us over this protocol -- in the polling loop most callers actually
6
+ // want. Invoked as a subprocess this necessarily blocks the caller until
7
+ // speech finishes (or --async is given): that is expected, the same as any
8
+ // other CLI tool that waits for the thing it started.
9
+ import { speak, ttsStatus, available } from "../native-bridge/native-bridge.js"
10
+
11
+ // Buninu is cross-OS, not just Android: when native-bridge isn't available,
12
+ // fall back to a locally-spawned platform TTS command instead of just
13
+ // failing. This block is copied verbatim from jsmdcui's own
14
+ // src/index.js detectTtsCmd() (same pitch/speed/lang math, same command
15
+ // construction) rather than reimplemented, since the PowerShell quoting in
16
+ // the win32 branch is exactly the kind of thing that is easy to silently
17
+ // break by hand-retyping and hard to verify without a real Windows machine.
18
+ //
19
+ // One deliberate omission from the original: it ends with an
20
+ // `if (Bun.which("tts")) return {cmd:['tts'],via:'arg'}` fallback. Calling
21
+ // that from inside this exact script (also registered as Buninu's "tts"
22
+ // command, ahead of anything else named "tts" on PATH) would just be this
23
+ // script invoking itself -- the same self-reference risk xclip.js avoids
24
+ // around ClipboardManager's own "xclip" search, so it is left out here.
25
+ function detectTtsCmd(speed, pitch) {
26
+ const platform = process.platform;
27
+
28
+ const lang = process.env.TTS_LANG || 'zh-TW'
29
+ process.env.TTS_LANG = lang ;
30
+
31
+ if (platform === "android") {
32
+ if (Bun.which("termux-tts-speak"))
33
+ return { cmd: ["termux-tts-speak", "-p", String(pitch), "-r", String(speed)], via: "arg" };
34
+ }
35
+
36
+ if (platform === "darwin") {
37
+ // say -r <wpm>; pitch via [[pbas n]] embedded TTS command (0-127, 48 = normal)
38
+ const rate = Math.round(175 * speed);
39
+ const pitchN = Math.max(0, Math.min(127, Math.round(48 * pitch)));
40
+ return {
41
+ cmd: ["say", "-r", String(rate)],
42
+ via: "arg",
43
+ textTransform: pitchN !== 48 ? (t) => `[[pbas ${pitchN}]] ${t}` : null,
44
+ };
45
+ }
46
+
47
+ if (platform === "win32") {
48
+ // Rate property: -10 to 10 (0 = normal); pitch via SSML <prosody>
49
+ const rate = Math.max(-10, Math.min(10, Math.round((speed - 1) * 10)));
50
+ const pitchPct = Math.round((pitch - 1) * 100);
51
+ const pitchAttr = (pitchPct >= 0 ? "+" : "") + pitchPct + "%";
52
+ for (const shell of ["pwsh.exe", "powershell.exe"]) {
53
+ if (Bun.which(shell)) {
54
+ const psCmd =
55
+ "Add-Type -AssemblyName System.Speech; " +
56
+ `$s = New-Object System.Speech.Synthesis.SpeechSynthesizer; $s.Rate = ${rate}; ` +
57
+ `$t = [Console]::In.ReadToEnd(); ` +
58
+ `$x = [System.Security.SecurityElement]::Escape($t); ` +
59
+ `$s.SpeakSsml('<speak xml:lang="${lang}" version="1.0" xmlns="http://www.w3.org/2001/10/synthesis"><prosody pitch="${pitchAttr}">' + $x + '</prosody></speak>')`;
60
+ return { cmd: [shell, "-NoProfile", "-Command", psCmd], via: "stdin" };
61
+ }
62
+ }
63
+ }
64
+
65
+ // Linux / Android fallback: espeak-ng / espeak
66
+ // Speed: -s <wpm> (175 = normal), Pitch: -p <n> (0-99, 50 = normal)
67
+ for (const bin of ["espeak-ng", "espeak"]) {
68
+ if (Bun.which(bin)) {
69
+ const spd = Math.round(175 * speed);
70
+ const pit = Math.max(0, Math.min(99, Math.round(50 * pitch)));
71
+ return { cmd: [bin, '-s', spd, '-p', pit], via: "arg" };
72
+ }
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ // Also copied verbatim (as the spawn side of the same jsmdcui code path,
79
+ // runTts) other than operating on the whole text at once instead of
80
+ // per-sentence, since a CLI invocation has no buffer/cursor to highlight
81
+ // against sentence by sentence the way the jsmdcui TUI does.
82
+ async function speakViaPlatformCommand(text, cmd) {
83
+ const spawnOpts = { stdout: "ignore", stderr: "ignore", env: process.env };
84
+ spawnOpts.stdin = cmd.via === "stdin" ? new Blob([text]) : "ignore";
85
+
86
+ if (cmd.via === "arg") {
87
+ text = (text + "")
88
+ .replace(/^-+/, "")
89
+ .replaceAll("`", "")
90
+ .replaceAll("$", "");
91
+ if (cmd.textTransform) text = cmd.textTransform(text);
92
+ }
93
+ const args = cmd.via === "arg" ? [...cmd.cmd, text] : cmd.cmd;
94
+ const proc = Bun.spawn(args, spawnOpts);
95
+ return await proc.exited;
96
+ }
97
+
98
+ // Not too short: each tick is a full unix-socket round trip, and TTS
99
+ // utterances run several seconds at minimum, so polling faster just burns
100
+ // cycles without getting the answer any sooner.
101
+ const POLL_MS = 500
102
+
103
+ // No wall-clock cap by default. Real TTS tooling doesn't invent one: espeak-ng
104
+ // has no timeout concept at all (its man page documents speed/pitch/gaps but
105
+ // nothing about how long it may block), and Windows SAPI's WaitUntilDone
106
+ // treats -1 ("wait forever") as the documented default -- a fixed cap here
107
+ // would just mean this tool gives up early on long text, which has nothing
108
+ // to do with the host actually being broken. The loop below still can't hang
109
+ // on a genuinely dead host: each poll has call()'s own 5s timeout (throws,
110
+ // caught below), and the loop condition itself already exits the moment
111
+ // status leaves "speaking" -- "unknown"/"error" are the real failure signal,
112
+ // not a timer. Pass --timeout to opt into a bounded wait instead.
113
+ const argv = process.argv.slice(2)
114
+
115
+ if (argv.includes("--help") || argv.includes("-h")) {
116
+ console.log(`
117
+ Usage: tts <text>
118
+ tts "hello world"
119
+ tts -f "interrupt and say this instead"
120
+ tts -a "say this, don't wait for it to finish"
121
+ tts --timeout 30000 "give up after 30s if still speaking"
122
+ tts --pitch 1.2 --speed 0.8 "slower and higher-pitched"
123
+
124
+ -f, --flush interrupt whatever is currently speaking
125
+ -a, --async print the handle and exit immediately instead of polling
126
+ --timeout <ms> give up and exit non-zero after this long (default: none,
127
+ wait as long as it takes -- matches espeak-ng/SAPI)
128
+ --pitch <n> 1.0 is normal (default: $TTS_PITCH, or 1 if unset)
129
+ --speed <n> 1.0 is normal (default: $TTS_SPEED, or 1 if unset)
130
+ `)
131
+ process.exit(0)
132
+ }
133
+
134
+ const flush = argv.includes("-f") || argv.includes("--flush")
135
+ const isAsync = argv.includes("-a") || argv.includes("--async")
136
+
137
+ const optValue = (flag, envFallback) => {
138
+ const i = argv.indexOf(flag)
139
+ const raw = i !== -1 ? argv[i + 1] : process.env[envFallback]
140
+ const n = Number(raw)
141
+ return Number.isFinite(n) ? n : 1
142
+ }
143
+ const timeoutIdx = argv.indexOf("--timeout")
144
+ const maxWaitMs = timeoutIdx !== -1 ? Number(argv[timeoutIdx + 1]) : null
145
+ const pitch = optValue("--pitch", "TTS_PITCH")
146
+ const speed = optValue("--speed", "TTS_SPEED")
147
+
148
+ const valueFlags = new Set(["--timeout", "--pitch", "--speed"])
149
+ const text = argv.find((a, i) => !a.startsWith("-") && !valueFlags.has(argv[i - 1])) ?? ""
150
+
151
+ if (!available()) {
152
+ // No Android native bridge -- fall back to a local platform TTS command
153
+ // (see detectTtsCmd() above). flush/polling/handles are all native-bridge
154
+ // concepts specific to Android's async TextToSpeech callback; there is no
155
+ // equivalent "interrupt what's currently speaking" here, so -f/--flush is
156
+ // silently a no-op on this path.
157
+ const cmd = detectTtsCmd(speed, pitch)
158
+ if (!cmd) {
159
+ console.error("no TTS backend found: no native bridge, and no termux-tts-speak/say/PowerShell/espeak-ng/espeak on PATH")
160
+ process.exit(1)
161
+ }
162
+
163
+ if (isAsync) {
164
+ speakViaPlatformCommand(text, cmd)
165
+ process.exit(0)
166
+ }
167
+
168
+ const code = await speakViaPlatformCommand(text, cmd)
169
+ process.exit(code === 0 ? 0 : 1)
170
+ }
171
+
172
+ let handle
173
+ try {
174
+ handle = await speak(text, speed, pitch, flush)
175
+ } catch (e) {
176
+ console.error(`speak failed: ${e.message}`)
177
+ process.exit(1)
178
+ }
179
+
180
+ if (isAsync) {
181
+ console.log(handle)
182
+ process.exit(0)
183
+ }
184
+
185
+ const start = Date.now()
186
+ let status = "speaking"
187
+
188
+ while (status === "speaking") {
189
+ if (maxWaitMs != null && Date.now() - start > maxWaitMs) {
190
+ console.error(`tts: gave up waiting for "${handle}" after ${maxWaitMs}ms`)
191
+ process.exit(1)
192
+ }
193
+
194
+ await new Promise(resolve => setTimeout(resolve, POLL_MS))
195
+
196
+ try {
197
+ status = await ttsStatus(handle)
198
+ } catch (e) {
199
+ console.error(`ttsStatus failed: ${e.message}`)
200
+ process.exit(1)
201
+ }
202
+ }
203
+
204
+ if (status === "error") {
205
+ console.error(`tts: "${handle}" reported an error`)
206
+ process.exit(1)
207
+ }
208
+
209
+ if (status === "unknown") {
210
+ console.error(`tts: "${handle}" went unknown before finishing`)
211
+ process.exit(1)
212
+ }
213
+
214
+ process.exit(0)
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // Ported from ../tmpk/bin/xclip.sh (the DroidScript-era xclip). That version
4
+ // bridged to native only through DroidScript's dsapi.pipe; this one uses
5
+ // native-bridge instead. The core semantics are unchanged: "primary" is
6
+ // local-file-only (matches X11's mouse-selection clipboard, which xclip also
7
+ // keeps separate from the real clipboard), and only "-selection clipboard"
8
+ // (or its "-clip"/"-clipboard" shorthand) touches the real system clipboard.
9
+ import { getcb, setcb, available } from "../native-bridge/native-bridge.js"
10
+ import { ClipboardManager } from "../jsmdcui/src/platform/clipboard.js"
11
+
12
+ // Buninu is cross-OS, not just Android, so the "clipboard" register falls
13
+ // back through: native-bridge (Android) -> jsmdcui's own ClipboardManager
14
+ // (win32/darwin only, see below) -> wl-copy/wl-paste (Linux, Wayland).
15
+ //
16
+ // ClipboardManager is deliberately NOT used on Linux-like platforms
17
+ // (including Android): its own backend detection searches PATH for a binary
18
+ // literally named "xclip" there, and this script is itself registered as
19
+ // Buninu's "xclip" command, ahead of any real system xclip on PATH -- using
20
+ // it here would make this script shell out to itself. Its win32/darwin
21
+ // branches never search for "xclip" at all, so those two are safe.
22
+ let desktopClipboard = null
23
+ function getDesktopClipboard() {
24
+ if (process.platform !== "win32" && process.platform !== "darwin") return null
25
+ if (!desktopClipboard) desktopClipboard = new ClipboardManager()
26
+ return desktopClipboard
27
+ }
28
+
29
+ const wlAvailable = () => !!(Bun.which("wl-copy") && Bun.which("wl-paste"))
30
+ const wlRead = () => {
31
+ const proc = Bun.spawnSync(["wl-paste", "--no-newline"])
32
+ return proc.success ? new TextDecoder().decode(proc.stdout) : null
33
+ }
34
+ const wlWrite = (text) => Bun.spawnSync(["wl-copy"], { stdin: Buffer.from(text) }).success
35
+
36
+ const argv = process.argv.slice(2)
37
+
38
+ if (argv.includes("--help") || argv.includes("-h")) {
39
+ console.log(`
40
+ Usage: Paste/Copy
41
+ xclip -o [-selection clipboard]
42
+ xclip -o [-clip]
43
+ echo hello | xclip [-selection clipboard]
44
+ echo hello | xclip [-clip]
45
+
46
+ Uses primary by default(mouse selection)
47
+ `)
48
+ process.exit(0)
49
+ }
50
+
51
+ const selMatch = argv.join(" ").match(/-selection\s+(\S+)/)
52
+ const cb = selMatch ? selMatch[1]
53
+ : argv.some(a => a.includes("-clip")) ? "clipboard"
54
+ : "primary"
55
+
56
+ process.stderr.write(`\x1b[35mClipboard 剪貼板📋 : ${cb} \n\x1b[0m`)
57
+
58
+ const cbf = `${process.env.HOME}/.xclip.${cb}`
59
+ const useNative = cb !== "primary"
60
+ const desktop = useNative ? getDesktopClipboard() : null
61
+
62
+ if (useNative && !available() && !desktop && !wlAvailable())
63
+ process.stderr.write(`no system clipboard found (no native bridge, no ClipboardManager backend, no wl-copy/wl-paste)\nNo native clipboard now!\n`)
64
+
65
+ const isPaste = argv.some(a => a.toLowerCase() === "-o")
66
+
67
+ if (isPaste) {
68
+ process.stderr.write(`\x1b[33mPaste 貼上 📋 \n\x1b[0m`)
69
+
70
+ if (useNative) {
71
+ if (available()) {
72
+ try {
73
+ await Bun.write(cbf, (await getcb()) ?? "")
74
+ } catch (e) {
75
+ process.stderr.write(`getcb failed: ${e.message}\n`)
76
+ }
77
+ } else if (desktop) {
78
+ try {
79
+ await Bun.write(cbf, desktop.read("clipboard") ?? "")
80
+ } catch (e) {
81
+ process.stderr.write(`clipboard read failed: ${e.message}\n`)
82
+ }
83
+ } else if (wlAvailable()) {
84
+ const text = wlRead()
85
+ if (text != null) await Bun.write(cbf, text)
86
+ }
87
+ }
88
+
89
+ const file = Bun.file(cbf)
90
+ if (await file.exists()) process.stdout.write(new Uint8Array(await file.arrayBuffer()))
91
+ } else {
92
+ process.stderr.write(`\x1b[33mCopy 複製 ✂️ \n\x1b[0m`)
93
+
94
+ const input = await Bun.stdin.arrayBuffer()
95
+ await Bun.write(cbf, input)
96
+
97
+ if (useNative) {
98
+ if (available()) {
99
+ try {
100
+ await setcb(new TextDecoder().decode(input))
101
+ } catch (e) {
102
+ process.stderr.write(`setcb failed: ${e.message}\n`)
103
+ }
104
+ } else if (desktop) {
105
+ try {
106
+ desktop.write(new TextDecoder().decode(input), "clipboard")
107
+ } catch (e) {
108
+ process.stderr.write(`clipboard write failed: ${e.message}\n`)
109
+ }
110
+ } else if (wlAvailable()) {
111
+ wlWrite(new TextDecoder().decode(input))
112
+ }
113
+ }
114
+ }
115
+
116
+ // Explicit exit: call()'s own timeout race means every await above is
117
+ // guaranteed to settle, but a getcb()/setcb() that hit that timeout leaves
118
+ // an abandoned fetch running, which keeps Bun's event loop alive on its own.
119
+ // Without this, xclip could still hang after printing everything it needs
120
+ // to -- the shell would just never get its prompt back.
121
+ process.exit(0)
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // Android goes through native-bridge's xdgOpen (MainActivity.java validates
4
+ // the path stays under Buninu's home and serves it back out through a
5
+ // read-only content:// provider; a bare URL just goes to ACTION_VIEW). The
6
+ // other platforms each have their own "open with whatever the OS thinks is
7
+ // the default handler" mechanism -- macOS/Windows are real OS
8
+ // commands/APIs, but Linux's is conventionally *also* a command named
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"
11
+ const fs = require("fs")
12
+
13
+ // Buninu's own bin/ is prepended to PATH ahead of any real system binary of
14
+ // the same name (same concern as xclip.js/tts.js), and this script is
15
+ // itself registered as Buninu's "xdg-open" command -- a naive PATH search
16
+ // would just find this file. Skip Buninu's own bin dir and keep looking
17
+ // further down PATH for wherever a real xdg-open actually lives.
18
+ function findSystemXdgOpen() {
19
+ const ownBinDir = process.env.BUNINU_HOME ? `${process.env.BUNINU_HOME}/bin` : null
20
+ for (const dir of (process.env.PATH || "").split(":").filter(Boolean)) {
21
+ if (dir === ownBinDir) continue
22
+ const candidate = `${dir}/xdg-open`
23
+ try {
24
+ fs.accessSync(candidate, fs.constants.X_OK)
25
+ return candidate
26
+ } catch {}
27
+ }
28
+ return null
29
+ }
30
+
31
+ async function runCommand(args) {
32
+ const proc = Bun.spawn(args, { stdout: "ignore", stderr: "ignore" })
33
+ return (await proc.exited) === 0
34
+ }
35
+
36
+ const argv = process.argv.slice(2)
37
+
38
+ if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
39
+ console.log("usage: xdg-open <file-or-url>")
40
+ process.exit(argv.length === 0 ? 1 : 0)
41
+ }
42
+
43
+ const target = argv[0]
44
+ let ok = false
45
+
46
+ try {
47
+ if (process.platform === "android") {
48
+ if (available()) {
49
+ ok = await bridgeOpen(target)
50
+ } else if (Bun.which("termux-open")) {
51
+ // Plain Termux (no minapk) still reports platform "android" but has
52
+ // no PKG_BRIDGE_SOCK -- termux-open (Termux:API) does the same
53
+ // ACTION_VIEW-based open, same fallback relationship as tts.js's
54
+ // termux-tts-speak branch when native-bridge isn't available.
55
+ ok = await runCommand(["termux-open", target])
56
+ } else {
57
+ throw new Error("no native bridge and no termux-open (install termux-api, or run inside minapk's APK)")
58
+ }
59
+ } else if (process.platform === "darwin") {
60
+ ok = await runCommand(["open", target])
61
+ } else if (process.platform === "win32") {
62
+ // start is a cmd builtin, not a real executable, and needs an empty ""
63
+ // title argument first or it treats a quoted target as the title.
64
+ ok = await runCommand(["cmd", "/c", "start", "", target])
65
+ } else {
66
+ const real = findSystemXdgOpen()
67
+ if (!real) throw new Error("no system xdg-open found on PATH")
68
+ ok = await runCommand([real, target])
69
+ }
70
+ } catch (e) {
71
+ process.stderr.write(`xdg-open: ${e.message}\n`)
72
+ process.exit(1)
73
+ }
74
+
75
+ // Explicit exit: bridgeOpen() goes through native-bridge's call(), whose own
76
+ // timeout race leaves an abandoned fetch running the event loop on a
77
+ // timeout -- without this, xdg-open could hang after printing everything it
78
+ // needs to.
79
+ process.exit(ok ? 0 : 1)
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ call "%~dp0bun.bat" "%~dp0..\apps\native-bridge\native-bridge.js" %*
3
+ exit /b %ERRORLEVEL%
package/bin/rz.bat ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ call "%~dp0bun.bat" "%~dp0..\apps\jsgotty\rz.js" %*
3
+ exit /b %ERRORLEVEL%
package/bin/rz.sh ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ sd=$(dirname "$(realpath "$0")")
4
+
5
+ exec bun "$sd"/../apps/jsgotty/rz.js "$@"
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ call "%~dp0bun.bat" "%~dp0..\apps\jsgotty\gotty.js" --viu %*
3
+ exit /b %ERRORLEVEL%
package/bin/showimg.sh ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ sd=$(dirname "$(realpath "$0")")
4
+
5
+ exec bun "$sd"/../apps/jsgotty/gotty.js --viu "$@"
package/bin/sz.bat ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ call "%~dp0bun.bat" "%~dp0..\apps\jsgotty\sz.js" %*
3
+ exit /b %ERRORLEVEL%
package/bin/sz.sh ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ sd=$(dirname "$(realpath "$0")")
4
+
5
+ exec bun "$sd"/../apps/jsgotty/sz.js "$@"
package/bin/tts.bat ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ call "%~dp0bun.bat" "%~dp0..\apps\tts\tts.js" %*
3
+ exit /b %ERRORLEVEL%
package/bin/xclip.bat ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ call "%~dp0bun.bat" "%~dp0..\apps\xclip\xclip.js" %*
3
+ exit /b %ERRORLEVEL%
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ call "%~dp0bun.bat" "%~dp0..\apps\xdg-open\xdg-open.js" %*
3
+ exit /b %ERRORLEVEL%
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
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",