qbrsh 0.2.0

A fast, keyboard-driven web browser
# 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):

```toml
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:

  ```sh
  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](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE) at
your option.