buffr — configuration
User config is a single TOML file. Every key has a default; the loader emits an
error with a line/column span when a key is misspelt, unknown, or has the wrong
type. A copy-pasteable defaults-equivalent lives at
config.example.toml at the repo root.
File location
buffr is XDG-everywhere: the same path on all three platforms.
| Platform | Path |
|---|---|
| Linux | $XDG_CONFIG_HOME/buffr/config.toml (~/.config/buffr/…) |
| macOS | $XDG_CONFIG_HOME/buffr/config.toml (~/.config/buffr/…) |
| Windows | %XDG_CONFIG_HOME%\buffr\config.toml (~\.config\buffr\…) |
Path resolution goes through hjkl_config::config_path::<Config>() (see
crates/buffr-config/src/loader.rs). $XDG_CONFIG_HOME is honored on every
platform; there is no ~/Library/Application Support or %APPDATA% fallback,
and buffr does not depend on the directories crate. Debug builds use
buffr-debug instead of buffr as the directory name so a dev tree never
shares state with an installed release.
Everything else the browser persists lives under XDG_DATA_HOME —
~/.local/share/buffr/ by default, ~/.local/share/buffr-debug/ in debug
builds. That includes the six SQLite stores (history.sqlite,
bookmarks.sqlite, downloads.sqlite, zoom.sqlite, permissions.sqlite,
favicons.sqlite), session.json, update-cache.json, usage-counters.json,
crashes/, and CEF's own profile tree — cookies, Local Storage,
IndexedDB, and the HTTP Cache directory all sit in there together, because
apps/buffr-app/src/main.rs passes the data dir as CEF's root_cache_path.
That is deliberate: the XDG spec says ~/.cache contents may be deleted at any
time without warning, and losing cookies and local storage to a tmpfiles sweep
is not acceptable. ~/.cache/buffr/ (XDG_CACHE_HOME) is still created at
startup and is used to derive the single-instance profile id, but CEF does not
store anything there.
Per-engine subtrees are namespaced as ~/.local/share/buffr/engines/<id>/. See
[engines] for what that currently does — and does not — isolate.
Override the config file per-run with --config <PATH>.
Config-related CLI flags
These are the flags that touch config specifically. They are not the full
CLI surface — buffr --help lists ~30 flags (bookmark/history/download/zoom/
permission dumps, --private, --audit-keymap, update flags, and so on).
| Flag | Effect |
|---|---|
--print-config | Print the resolved (defaults + user overrides) config; exit 0. |
--check-config | Validate the config file; exit non-zero on parse / schema error. |
--config <PATH> | Override the XDG-discovered config path. |
--homepage <URL> | Override general.homepage for this run only. |
--engine <NAME> | Ignore [engines] and route every tab through <NAME> (cef). |
--private | In-memory stores + throwaway CEF cache; forces telemetry off. |
--audit-keymap | Print every default-bound PageAction and its keys; exit 0. |
Both --print-config and --check-config short-circuit before CEF initializes,
so they're safe to run on a headless host.
The flags above are parsed by the browser binary (
buffr-app,apps/buffr-app/src/main.rs). Thebuffrsupervisor takes only--heartbeat-timeout,--heartbeat-disable, and--help/--version; everything else it forwards verbatim to the child, sobuffr --check-configworks from the user's point of view.
Schema
The 13 sections below are the complete Config surface
(crates/buffr-config/src/lib.rs).
[general]
| Key | Type | Default | Notes |
|---|---|---|---|
homepage | string | buffr://new | Initial URL on first window. |
leader | string | " " (space) | Exactly one character. Validated. |
show_favicons | bool | true | false skips favicon render and the CEF icon fetch. |
[startup]
restore_session = true reopens the previous session's tabs on launch (opt-in —
default false). A fresh tab (o/O/:tabnew) opens new_tab_url (default
about:blank); the cold-start tab 0 still opens general.homepage.
| Key | Type | Default | Notes |
|---|---|---|---|
restore_session | bool | false | true restores the previous session's tabs on launch (opt-in). |
new_tab_url | string | about:blank | URL fresh tabs (o/O/:tabnew) open. |
[search]
| Key | Type | Default | Notes |
|---|---|---|---|
default_engine | string | duckduckgo | Must reference a [search.engines.<name>] block. |
[search.engines.<name>] blocks define each engine:
[search.engines.duckduckgo]
url = "https://duckduckgo.com/?q={query}"
prefix = "ddg" # optional
[search.engines.github]
url = "https://github.com/search?q={query}"
prefix = "gh"
{query} is replaced with the URL-encoded omnibar input.
prefix is an optional shortcut keyword. When set, an omnibar input of
<prefix> <query> routes to that engine instead of default_engine — e.g.
gh tokio searches GitHub, g rust closures searches Google, plain cats
falls through to the default. Bare prefix words with no query (e.g. g) fall
through to the default so they still produce a useful result. Prefix collisions
across engines are rejected at config validation time.
[theme]
Every colour is a 7-character #RRGGBB string. An unparseable value is a
hard config error (--check-config exits non-zero) — it is no longer
silently replaced by the built-in default.
| Key | Type | Default | Notes |
|---|---|---|---|
accent | string | #7aa2f7 | Statusline mode block, omnibar caret, hint labels, active tab. |
cert_secure | string | #66e08a | Secure cert indicator (lock dot, find counts). |
cert_insecure | string | #e05a5a | Insecure cert indicator. |
private | string | #ffc8c8 | PRIVATE marker on the statusline. |
progress | string | #66c2ff | Page-load progress bar. |
update | string | #e0c85a | Update-available indicator (* upd). |
high_contrast | bool | false | Overrides every colour above; see accessibility.md. |
[privacy]
| Key | Type | Default | Notes |
|---|---|---|---|
enable_telemetry | bool | false | Opt-in local-only counters. No network endpoint exists. |
clear_on_exit | string[] | [] | Any of cookies, cache, history, bookmarks, downloads, local_storage. |
skip_schemes | string[] | ["about", "cef", "chrome", "data", "file"] | URL schemes never recorded in history (case-insensitive). |
Telemetry is opt-in, local-only, and has no network endpoint — there is no collector to send counters to. The only network request buffr makes by default is the update check; see privacy.md and updates.md.
[downloads]
| Key | Type | Default | Notes |
|---|---|---|---|
default_dir | path? | unset | Unset resolves at runtime: dirs::download_dir(), then $HOME/Downloads, then the cwd. |
open_on_finish | bool | false | Launch the file via xdg-open / open / start on completion. |
ask_each_time | bool | false | true shows the OS Save-As dialog and suppresses the notification strip. |
show_notifications | bool | true | Chrome strip on download start/finish (2 s started, 4 s finished). |
[hint]
| Key | Type | Default | Notes |
|---|---|---|---|
alphabet | string | asdfghjkl;weruio | Label alphabet. Validated: non-empty, ASCII-only, no duplicates. |
See hint-mode.md.
[crash_reporter]
| Key | Type | Default | Notes |
|---|---|---|---|
enabled | bool | false | Opt-in Rust panic hook writing JSON under <data>/crashes/. |
purge_after_days | u32 | 30 | Cutoff for --purge-crashes. Must be > 0; 0 is rejected at load. |
[updates]
| Key | Type | Default | Notes |
|---|---|---|---|
enabled | bool | true | The only network request buffr makes by default. |
check_interval_hours | u32 | 24 | Must be > 0. |
github_repo | string | kryptic-sh/buffr | owner/repo slug; shape-validated. |
See updates.md.
[accessibility]
| Key | Type | Default | Notes |
|---|---|---|---|
force_renderer_accessibility | bool | false | Passes --force-renderer-accessibility to the CEF renderers. |
[engines]
| Key | Type | Default | Notes |
|---|---|---|---|
default | string | cef | Engine id used when no rule matches. Must name an instance. |
instances | table[] | [] | [[engines.instances]] — empty synthesises one cef instance. |
rules | table[] | [] | [[engines.rules]] — ordered; first host-glob match wins. |
[engines]
default = "cef"
[[engines.instances]]
id = "cef"
backend = "cef"
# data_dir = "/tmp/cef-b-cache" # accepted, but has no effect today
[[engines.rules]]
match = "*.figma.com"
engine = "cef"
backend accepts only "cef". It is the sole backend the browser can
construct: the WPE WebKit backend (crates/buffr-webkit) is excluded from the
workspace, Linux-only, and not built by CI. --engine <NAME> overrides the
whole section for one run and likewise only accepts cef.
data_dir and per-engine isolation
data_dir on an instance is parsed and plumbed all the way through —
buffr-app resolves it (explicit value, else <data>/engines/<id>/) and hands
it to the backend as BackendOpenOptions::data_dir. The CEF backend then
discards it: BrowserHost::new_with_options
(crates/buffr-cef/src/host.rs) does let _ = data_dir; and creates no
per-engine RequestContext, because CEF's Alloy runtime collapses a child
context's cache_path back onto the global Default/ profile anyway
(kryptic-sh/buffr#158).
Net effect today: every engine instance shares one on-disk profile — the
root_cache_path, which is the data dir described under
File location. Setting data_dir changes nothing you can
observe. The key is kept so configs do not break when per-engine isolation lands
(it needs the Chrome runtime, not Alloy).
[keymap.<mode>]
Mode is one of normal, visual, command, hint. Each entry maps a
vim-notation key sequence to a PageAction:
[keymap.normal]
"j" = "scroll_down"
"5j" = "scroll_down(5)"
"/" = "find(forward = true)"
"<Esc>" = "enter_mode(\"normal\")"
The full default keymap lives in keymap.md.
There is deliberately no [keymap.insert]. Insert mode forwards every key
straight to the page so the focused field handles typing natively — a binding
there would shadow whatever the user is typing, and the engine never consults
the keymap while in Insert anyway. The section is a hard validation error rather
than a silent no-op. Press <Esc> to leave Insert mode, then use a
[keymap.normal] binding.
Action notation
- Unit variants — bare snake_case name.
"scroll_down","reload","tab_close", etc. - Count-bearing scrolls —
name(N)whereN >= 0. Applies toscroll_up,scroll_down,scroll_left,scroll_right. - Find —
find(forward = true)orfind(forward = false). - Mode transition —
enter_mode("<mode>")with a quoted mode name.
Anything else surfaces a validation error pointing at the offending key.
[idle_inhibit]
Keeps the screen awake while video (or optionally audio) is playing in the focused window. Backed by four platform implementations:
- Linux Wayland —
zwp_idle_inhibit_manager_v1protocol. - Linux X11 —
org.freedesktop.ScreenSaver.Inhibitover D-Bus. - macOS —
IOPMAssertionCreateWithName(NoDisplaySleepAssertion). - Windows —
SetThreadExecutionState(ES_DISPLAY_REQUIRED)on a worker thread.
The inhibitor is acquired and released at runtime; no restart needed. The
video/audio signal comes from the JS media probe (__buffr_media__ console
sentinel) plus CEF's audio callbacks, re-evaluated every frame in
about_to_wait:
enabled && (video || (inhibit_audio_only && audio)) && (!require_focus || window_focused).
| Key | Type | Default | Notes |
|---|---|---|---|
enabled | bool | true | Master switch. false disables the feature entirely — no inhibitor is ever acquired. |
inhibit_audio_only | bool | false | When true, audio-only activity (no video) also triggers the inhibitor. |
require_focus | bool | true | When true, the inhibitor is held only while the buffr window has OS-level focus. Set to false to inhibit even when the window is in the background (useful for PiP setups). |
[idle_inhibit]
enabled = true
inhibit_audio_only = false
require_focus = true
Hot reload
The watcher uses notify with a 250ms debounce. On a successful reload, the
keymap only is swapped on the running engine — homepage, theme, startup, and
search settings still require a restart for now (full hot-apply is Phase 5+
work). A failed reload (parse or validate error) is logged and the previous
config stays live.
Validation rules
general.leadermust be exactly one character.search.default_enginemust reference an existing[search.engines.<name>]block.search.engines.<name>.prefix(when set) must be non-empty and unique across all engines.hint.alphabetmust be non-empty, ASCII-only, duplicate-free, and at least two characters.- All six
[theme]colours must parse as#RRGGBB. A typo is a hard error — it is no longer silently swapped for the built-in colour. crash_reporter.purge_after_daysmust be> 0.updates.check_interval_hoursmust be> 0;updates.github_repomust be anowner/reposlug.engines.defaultmust be non-empty and name a declared (or synthesised) instance; instance ids must be unique; every rule'senginemust resolve.- Every keymap binding's key sequence must parse via the engine's
parse_keys, and its action notation must match the table above. [keymap.insert]is rejected outright — Insert mode has no bindable keymap.- Unknown top-level keys, unknown nested keys, and unknown enum variants all
error out (
#[serde(deny_unknown_fields)]).