qbrsh 0.2.0

A fast, keyboard-driven web browser
qbrsh-0.2.0 is not a library.

qbrsh

A fast, keyboard-driven web browser in Rust, built on WebKitGTK 6 / GTK 4 with a hand-rolled Elm-style (TEA) core.

Architecture

All application state lives in one owned State. Every source of change (key presses, WebKit signals, IPC, worker results) produces a Msg drained by a single consumer on glib's main context. The pure update(&mut State, Msg) -> Vec<Effect> is the only place state mutates. It returns Effect values that an effect runner carries out, and results return as new messages. There is no shared interior mutability, no re-entrancy, and no polling timers.

sources ─▶ Msg queue ─▶ update(&mut State) ─▶ [Effect] ─▶ runner ─▶ results back as Msg
  • src/core/: the engine-agnostic TEA core (state, msg, effect, update, command, key/trie/bindings, completion), fully unit-tested without GTK.
  • src/engine/: the EngineView trait and the WebKitGTK 6 backend (the only place webkit6 types appear).
  • src/input/, src/ui/, src/app.rs: GDK key translation, the window, and the effect runner that drives GTK/WebKit.
  • src/history.rs: browsing history on a dedicated SQLite writer thread.
  • src/plugin.rs: the Rune plugin runtime.

Build & run

Requires gtk4 and webkitgtk-6.0 (and gst-plugins-good + gst-libav for media). Then:

cargo run

Keybindings (Normal mode)

Key Action Key Action
h j k l scroll f / F hint: follow / open in tab
gg / G top / bottom H / L back / forward
<C-f>/<C-b> page down/up r / R reload / hard reload
<C-d>/<C-u> half page J / K next / prev tab
o / O open / open current URL d / u close / reopen tab
t open in new tab gC gJ gK clone / move tab
: command line <A-1>..<A-9> focus tab N
i / Esc insert / leave mode yy / yt yank url / title
m / b save / load quickmark M / gb bookmark / load
td toggle dark mode co close other tabs
/ find in page n / N next / prev match
zi / zo zoom in / out zz reset zoom

Counts work (e.g. 5j). Type /text on the command line to search the page, then n to step forward through matches (wrapping at the end). N steps backward, but is best-effort: WebKitGTK's backward search cannot reach hidden/collapsed content (so it can't step back into a closed accordion) and corrupts the find controller's heap, which surfaces as a harmless free(): corrupted unsorted chunks message when the browser exits. Prefer n (it wraps to every match) if you want to avoid that. In command mode, Tab/Shift-Tab move the highlight through the completion list (your typed text stays in the command line), Space applies the highlighted item so you can continue with an argument, and Enter runs the highlighted item (or the typed text if none is selected).

Commands

:open, :tabopen, :back, :forward, :reload, :tab-close/next/prev/focus, :tab-clone/move/only, :undo, :hint, :yank, :quickmark-save/load/del, :bookmark-add/load/del, :find-next/prev, :zoom-in/out/reset, :zoom <pct>, :set, :config-source, :darkmode, :session-save/load, :plugin-reload, :permissions, :quit.

Downloads are saved to your XDG downloads directory with a safe, non-colliding filename; start, completion, and failure are reported in the status bar.

Configuration

~/.config/qbrsh/config.toml (all fields optional):

homepage = "https://duckduckgo.com"

[colors]
background = "#1a1a2e"
foreground = "#e0e0e0"
accent = "#ffd76e"

[font]
family = "monospace"
size = 11

[zoom]
# Default page zoom for new tabs (1.0 = 100%).
default = 1.0

[permissions]
# Default policy for a capability with no rule: ask, allow, or deny.
# "ask" shows an interactive prompt (allow once / deny once / always / deny-always).
default = "ask"

[permissions.sites]
# Per-site overrides, matched by exact host or subdomain suffix. A bare policy
# applies to every capability; a table sets capabilities (geolocation,
# notifications, camera, microphone) independently.
"example.com" = "allow"
"maps.example.org" = { geolocation = "allow", camera = "deny" }

Permissions are decided per capability. When a capability's policy is ask, a prompt appears: y allow once, n deny once, a always allow, d always deny (Esc denies). "Always" choices and edits made in the management view (:permissions, where a/d/s set and x revokes the selected rule) persist to a data-dir store, separate from your hand-written config.toml.

Change settings live with :set colors.accent "#ff0000", :set permissions.example.com allow (all capabilities), or :set permissions.example.com.camera deny (one capability), and reload the file with :config-source.

Extensibility

qbrsh does not run Firefox/Chrome extensions. Extensibility is native:

  • Ad blocking: built-in domain blocking, applied both at navigation (frames, popups) and as a WebKit content filter for subresources (images, scripts, XHR). Extend via ~/.local/share/qbrsh/adblock, one domain per line.

  • Plugins: Rune scripts in ~/.local/share/qbrsh/plugins/*.rn. See examples/plugins/example.rn. The qbrsh API: command, open, message, eval_js(s).await (suspends the plugin and returns the page result), and on(event, handler) for cold-event hooks (page_load, tab_open, command). The Rune sandbox blocks ambient host access (no filesystem, network, or process) and plugins run under an instruction budget. Note, though, that plugins are trusted code: eval_js reads the active page's DOM and open/command can navigate, which together suffice to exfiltrate data. Run untrusted automation over the IPC interface, not as a plugin. Reload with :plugin-reload.

  • Automation: drive the browser from an external process over the IPC control interface, a JSON-RPC socket at $XDG_RUNTIME_DIR/qbrsh/ipc.sock. Send newline-delimited requests, for example:

    printf '{"method":"run_command","params":{"command":"tabopen https://x"}}\n' \
      | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/qbrsh/ipc.sock
    

    Launching qbrsh <url> while an instance is running forwards the URL to it.

Resource use

To balance memory against isolation, tabs of the same site share a WebKit web process, while different sites run in separate processes. A renderer crash therefore affects only the same-site tabs sharing that process; each shows a recoverable error page and can be reloaded with r. This uses more memory than forcing every tab into one process, which is the intended trade: one site cannot read another site's data. The in-memory back/forward page cache is disabled, so navigating back or forward reloads the page (from the resource cache where possible) rather than restoring it instantly. Run :memory to see current resident memory and the number of live views.

License

Licensed under either of MIT or Apache-2.0 at your option.