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.

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

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

FlagEffect
--print-configPrint the resolved (defaults + user overrides) config; exit 0.
--check-configValidate 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).
--privateIn-memory stores + throwaway CEF cache; forces telemetry off.
--audit-keymapPrint 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). The buffr supervisor takes only --heartbeat-timeout, --heartbeat-disable, and --help/--version; everything else it forwards verbatim to the child, so buffr --check-config works from the user's point of view.

Schema

The 13 sections below are the complete Config surface (crates/buffr-config/src/lib.rs).

[general]

KeyTypeDefaultNotes
homepagestringbuffr://newInitial URL on first window.
leaderstring" " (space)Exactly one character. Validated.
show_faviconsbooltruefalse 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.

KeyTypeDefaultNotes
restore_sessionboolfalsetrue restores the previous session's tabs on launch (opt-in).
new_tab_urlstringabout:blankURL fresh tabs (o/O/:tabnew) open.
KeyTypeDefaultNotes
default_enginestringduckduckgoMust 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.

KeyTypeDefaultNotes
accentstring#7aa2f7Statusline mode block, omnibar caret, hint labels, active tab.
cert_securestring#66e08aSecure cert indicator (lock dot, find counts).
cert_insecurestring#e05a5aInsecure cert indicator.
privatestring#ffc8c8PRIVATE marker on the statusline.
progressstring#66c2ffPage-load progress bar.
updatestring#e0c85aUpdate-available indicator (* upd).
high_contrastboolfalseOverrides every colour above; see accessibility.md.

[privacy]

KeyTypeDefaultNotes
enable_telemetryboolfalseOpt-in local-only counters. No network endpoint exists.
clear_on_exitstring[][]Any of cookies, cache, history, bookmarks, downloads, local_storage.
skip_schemesstring[]["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]

KeyTypeDefaultNotes
default_dirpath?unsetUnset resolves at runtime: dirs::download_dir(), then $HOME/Downloads, then the cwd.
open_on_finishboolfalseLaunch the file via xdg-open / open / start on completion.
ask_each_timeboolfalsetrue shows the OS Save-As dialog and suppresses the notification strip.
show_notificationsbooltrueChrome strip on download start/finish (2 s started, 4 s finished).

[hint]

KeyTypeDefaultNotes
alphabetstringasdfghjkl;weruioLabel alphabet. Validated: non-empty, ASCII-only, no duplicates.

See hint-mode.md.

[crash_reporter]

KeyTypeDefaultNotes
enabledboolfalseOpt-in Rust panic hook writing JSON under <data>/crashes/.
purge_after_daysu3230Cutoff for --purge-crashes. Must be > 0; 0 is rejected at load.

[updates]

KeyTypeDefaultNotes
enabledbooltrueThe only network request buffr makes by default.
check_interval_hoursu3224Must be > 0.
github_repostringkryptic-sh/buffrowner/repo slug; shape-validated.

See updates.md.

[accessibility]

KeyTypeDefaultNotes
force_renderer_accessibilityboolfalsePasses --force-renderer-accessibility to the CEF renderers.

[engines]

KeyTypeDefaultNotes
defaultstringcefEngine id used when no rule matches. Must name an instance.
instancestable[][][[engines.instances]] — empty synthesises one cef instance.
rulestable[][][[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 scrollsname(N) where N >= 0. Applies to scroll_up, scroll_down, scroll_left, scroll_right.
  • Findfind(forward = true) or find(forward = false).
  • Mode transitionenter_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 Waylandzwp_idle_inhibit_manager_v1 protocol.
  • Linux X11org.freedesktop.ScreenSaver.Inhibit over D-Bus.
  • macOSIOPMAssertionCreateWithName(NoDisplaySleepAssertion).
  • WindowsSetThreadExecutionState(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).

KeyTypeDefaultNotes
enabledbooltrueMaster switch. false disables the feature entirely — no inhibitor is ever acquired.
inhibit_audio_onlyboolfalseWhen true, audio-only activity (no video) also triggers the inhibitor.
require_focusbooltrueWhen 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.leader must be exactly one character.
  • search.default_engine must reference an existing [search.engines.<name>] block.
  • search.engines.<name>.prefix (when set) must be non-empty and unique across all engines.
  • hint.alphabet must 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_days must be > 0.
  • updates.check_interval_hours must be > 0; updates.github_repo must be an owner/repo slug.
  • engines.default must be non-empty and name a declared (or synthesised) instance; instance ids must be unique; every rule's engine must 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)]).