A webview CLI. It opens a native window, renders the HTML you give it, gives the page one channel to send a result back, prints that result, and exits. That's the whole tool.
Any caller — a shell, an agent, Python, Node — integrates the same way: spawn the process, write HTML, read the result off stdout.
The page does JSON.stringify; the binary prints that string verbatim. The
Rust side never parses or validates the result — its shape is entirely the
caller's concern.
Install script (macOS / Linux) — grabs the right prebuilt binary for your platform from the latest GitHub release:
curl -fsSL https://raw.githubusercontent.com/just-be-dev/webview-cli/main/install.sh | shPrebuilt binaries — download from the
Releases page. Each
release ships webview-<platform> for macos-arm64, linux-x64,
linux-arm64, and windows-x64, alongside SHA256SUMS.
Cargo — if you have a Rust toolchain:
cargo install webview-cli # installs the `webview` binaryFrom source:
mise run build # -> target/release/webview
# or
cargo build --releaseOn Linux you need the WebKitGTK headers to build (or to run the binary):
libwebkit2gtk-4.1-dev libgtk-3-dev.
echo '<html>…</html>' | webview # HTML piped on stdin
webview ./page.html # or a file path
webview https://example.com # or an http(s) URL
webview ./page.html --title T --width 900 --height 700 --devtools --icon ./icon.png --timeout-ms 60000Input precedence: non-empty piped stdin wins; otherwise the positional argument
(an http:///https:// URL is loaded remotely, anything else is treated as a
file); otherwise it's a usage error. File pages are served over a custom origin
so relative CSS/JS/images and fetch resolve (and so they load at all under
WKWebView).
| Flag | Default | Meaning |
|---|---|---|
--title |
webview |
Window title. |
--width |
800 |
Window width (logical px). |
--height |
600 |
Window height (logical px). |
--devtools |
off | Open dev tools on launch. |
--icon |
none | Image to show as the Dock icon (macOS only). |
--timeout-ms |
none | Exit 3 if the page hasn't settled in time. |
Everything else lives in the HTML — there are no other flags by design.
The page talks back through one injected object:
window.webview.version; // the webview-cli version string, e.g. "0.2.0"
window.webview.resolve(value); // any JSON-serializable value
window.webview.reject(error); // string or ErrorThe first resolve/reject wins; the process exits immediately after.
A page — especially one loaded from a URL — can tell it's running inside
webview two ways:
- In JavaScript: check for the injected object, e.g.
if (window.webview) { … }.window.webview.versiondisambiguates it from any same-named global and tells you which build. - Server-side / before any JS: the User-Agent is set to
webview-cli/<version> (+https://github.com/just-be-dev/webview-cli), so a server can detect the context and tailor the page on first byte.
| Outcome | stdout | stderr | exit |
|---|---|---|---|
page called resolve(v) |
JSON.stringify(v) |
— | 0 |
page called reject(e) |
— | message | 1 |
| user closed window first | (empty) | — | 2 |
--timeout-ms elapsed |
(empty) | — | 3 |
| bad usage (no input, etc.) | (empty) | usage | 64 |
A tiny confirmation prompt that returns true/false:
cat <<'HTML' | webview --title "Confirm" --width 360 --height 160
<!doctype html>
<body style="font:14px system-ui;display:grid;place-content:center;gap:12px">
<p>Delete everything?</p>
<div>
<button onclick="webview.resolve(true)">Yes</button>
<button onclick="webview.resolve(false)">No</button>
</div>
</body>
HTML
# prints `true` or `false`; exit 0No SDK: spawn the binary, write HTML to stdin, read stdout.
Shell
result=$(echo "$html" | webview --timeout-ms 60000)Python
import subprocess
out = subprocess.run(["webview", "--timeout-ms", "60000"],
input=html, capture_output=True, text=True)
result = out.stdout # JSON string; parse if you wantNode
import { spawnSync } from "node:child_process";
const out = spawnSync("webview", ["--timeout-ms", "60000"], { input: html });
const result = out.stdout.toString(); // JSON stringwebview does "show something, get one answer back" — it isn't a live,
two-way session. When you need multiple steps, put them all in one page (a
wizard, several screens in one document) and only call resolve at the very
end. Keep the interaction in the HTML.
mise run test # cargo test (window-launch tests skip without a display)
mise run lint # cargo clippy -D warnings
mise run format # cargo fmt
mise run typecheck # cargo check --all-targets
mise run check # all of the aboveSource layout:
| File | Concern |
|---|---|
main.rs |
orchestration: parse args, resolve input, run |
cli.rs |
clap arg struct + usage |
input.rs |
stdin / URL / path resolution → a Load enum |
bridge.rs |
the BRIDGE JS + AppEvent + message parsing |
assets.rs |
custom-protocol file server (MIME, path-traversal safety) |
icon.rs |
runtime Dock-icon swap for --icon (macOS; no-op else) |
run.rs |
window + webview build, event loop, exit codes |
Pushing a v* tag (e.g. v0.1.0) triggers the release workflow, which builds
all four platform binaries, attaches them plus SHA256SUMS to a GitHub
Release, and publishes the crate to crates.io. See
.github/workflows/release.yml.
MIT © Justin Bennett