webview-cli↗

active
P25B4 |

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

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 | sh

Prebuilt 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` binary

From source:

mise run build                 # -> target/release/webview
# or
cargo build --release

On Linux you need the WebKitGTK headers to build (or to run the binary): libwebkit2gtk-4.1-dev libgtk-3-dev.

Usage

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 60000

Input 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).

Flags

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 bridge

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 Error

The first resolve/reject wins; the process exits immediately after.

Detecting the webview

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.version disambiguates 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.

Exit codes — this table is the public API

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

End-to-end example

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 0

From an agent — the same call, three ways

No 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 want

Node

import { spawnSync } from "node:child_process";
const out = spawnSync("webview", ["--timeout-ms", "60000"], { input: html });
const result = out.stdout.toString(); // JSON string

The one boundary

webview 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.

Development

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 above

Source 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

Releasing

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.

License

MIT © Justin Bennett