# att_browser A small, keyboard-driven QtWebEngine browser in the spirit of qutebrowser. One page per window, a qutebrowser-style status line and `:` command line, `f` link hints and adblocking. There is deliberately no tab bar: where qutebrowser opens or closes a tab, att_browser opens or closes a window, and your window manager does the tabbing. No config file yet. ## Running ```sh nix run . # open the default start page nix run . -- news.ycombinator.com ``` ## Developing ```sh nix develop cmake -B build -G Ninja cmake --build build ./build/att_browser ``` The dev shell exports `QT_PLUGIN_PATH`, `QML2_IMPORT_PATH` and `QTWEBENGINEPROCESS_PATH` by hand, because `wrapQtAppsHook` only wraps binaries that have been *installed* — a binary run straight out of `./build` would otherwise fail to find Qt's platform plugin. Pinned to `nixos-26.05` (current NixOS stable), which ships Qt 6.11. ## Modes Like qutebrowser, keys mean different things depending on the mode: | Mode | How you get there | Behaviour | | --------- | ---------------------------- | ---------------------------------- | | `normal` | default, `Escape` | keys are browser bindings | | `insert` | `i` | keys go to the page (type in forms)| | `command` | `o`, `O`, `:` | keys go to the command line | | `hint` | `f` | keys pick a link label | `Escape` (or `Ctrl-[`) always returns to normal mode. In normal mode, it also leaves fullscreen when a page (e.g. a YouTube video) is fullscreen. The status bar is hidden while a page is fullscreen. ### Status bar Laid out and coloured like qutebrowser's defaults: black, turning dark green in insert mode. On the left is the mode (`-- INSERT MODE --`, `-- HINT MODE --`), on the right the keys typed so far (e.g. `g`), the URL, the scroll position (`[top]`, `[42%]`, `[bot]`) and, while a page loads, a progress bar. The URL is white while loading and for plain `http://`, green once an HTTPS page has loaded, orange if the load failed, and aqua while it shows a link under the mouse. Messages stack above the bar (info on black, errors on red) and vanish after three seconds. All of it uses the `fonts` setting (14pt monospace). > Note: unlike qutebrowser, att_browser does not yet auto-switch to insert mode when you > click into a text field on a page — press `i` first. Doing it automatically > needs a JS focus-tracking script over QWebChannel; see *Next steps*. ## Key bindings (normal mode) | Key | Action | | -------------- | ---------------------------------------- | | `o` | open the command line prefilled `:open ` | | `O` | same, but opens in a new window (`-w`) | | `go` / `gO` | edit the current URL, here / new window | | `d` | close this window | | `:` | open a bare command line | | `i` | enter insert mode | | `gi` | focus the first text field, insert mode | | `f` | hint mode: follow a link by its label | | `Escape` | back to normal mode | | `j` `k` | scroll down / up 25 CSS px (`scroll-px 0 ±25`) | | `h` `l` | scroll left / right | | `Ctrl-d/u` | scroll half a page | | `Ctrl-f/b` | scroll a full page | | `gg` / `G` | jump to top / bottom | | `H` / `L` | back / forward in history | | `r` / `R` | reload / reload bypassing cache | | `y` | yank the current URL to the clipboard | | `+` `-` `=` | zoom in / out a level, back to default | The command line keeps a history (`Up`/`Down`) and aborts on `Escape`, `Ctrl-c`, or if you delete the leading `:`. While typing `:open` (or `:o`), a completion list of previously visited pages appears above it, most recent first. Every word you type must appear in the URL or the title. `Tab` / `Shift-Tab` cycle through the list and put the selected URL on the command line; `Enter` opens it. `:useragent` completes preset names the same way. ## Hint mode `f` labels every clickable element in the viewport with home-row letters (`asdfghjkl`). Type a label to click it; `Backspace` undoes a letter, `Escape` cancels. The click is a real mouse event sent to the render widget, so it works on sites that ignore `element.click()`. Following a text field drops you into insert mode. Links that would open a new window are opened in place for now. The hint script runs in `QWebEngineScript::ApplicationWorld`, so the page's own JavaScript can't see or interfere with it. It doesn't descend into iframes or shadow DOM yet. ## Browsing data Browsing history is kept in `~/.local/share/att_browser/history.sqlite` (tables `visits`, one row per page load, and `urls`, one row per URL for completion). A page is recorded once it finishes loading, so redirect hops and in-page `#anchor` jumps don't create entries. `:history-clear` empties it. Cookies, local storage and Chromium's own state are kept in a persistent QtWebEngine profile under `~/.local/share/att_browser/QtWebEngine/default` (the cache is in `~/.cache/att_browser/`). Session cookies are saved too, like qutebrowser's `content.cookies.store`, so you stay logged in across restarts. Delete that directory to start fresh. Before the rename to att_browser, this data lived in `~/.local/share/att/att` and `~/.cache/att/att`. The first start moves it to the new locations. ## Adblocking Network requests are checked against EasyList and EasyPrivacy with Brave's [adblock-rust](https://github.com/brave/adblock-rust), the same engine and default lists as qutebrowser. Blocked requests never leave the browser. There is no cosmetic filtering (element hiding), so ads served from the site's own URLs still get through. The lists are downloaded on first start and compiled into `~/.local/share/att_browser/adblock-cache.dat`, which later starts just load. `:adblock-update` re-downloads them. `:adblock-toggle` switches blocking off and on, for when a site breaks. ## Greasemonkey Userscripts go in `~/.local/share/att_browser/greasemonkey/` as `*.js` files. They are loaded at startup; after changing them, run `:greasemonkey-reload` and reload the page. Like qutebrowser, att_browser: - runs scripts in the page's own JavaScript context (so `unsafeWindow` is just `window`), in iframes too unless `@noframes` is set; - honours `@include`, `@exclude` and `@match` (checked by QtWebEngine itself; a script with none of them runs everywhere), `@run-at document-start` / `document-end` (the default) / `document-idle`, and `@require`, which is downloaded once into `greasemonkey/requires/`; - provides `GM_info`, `GM_setValue`/`GM_getValue`/`GM_deleteValue`/`GM_listValues`, `GM_addStyle`, `GM_addElement`, `GM_xmlhttpRequest`, `GM_openInTab`, `GM_setClipboard`, `GM_log`, `GM_notification` and the promise-based `GM.*` versions. `@require` must be `https://`; see *Security* for pinning with `#sha256=`. Limits: stored values live in the site's `localStorage`, so they are per site rather than shared across sites. `GM_xmlhttpRequest` is a plain XHR, so it can't make cross-origin requests the server doesn't allow (CORS). `@resource` isn't supported. `GM_openInTab` does nothing until new-window requests are handled. ## Security On top of Chromium's own protections, att_browser: - blocks third-party cookies, which stops most cross-site tracking. A few embedded logins (e.g. some comment widgets) may stop working. - doesn't let local HTML files (`file://`) read other local files. QtWebEngine allows that by default, so opening a malicious downloaded page could leak e.g. `~/.ssh`. - keeps WebRTC from revealing your LAN address, and doesn't let scripts open pop-up windows. - pins the safe defaults for mixed content, clipboard access and plugins. - shows the URL in green in the status bar only once an HTTPS page has loaded; plain `http://` stays white, as in qutebrowser. - only accepts Greasemonkey `@require` files over HTTPS. A `@require` URL may pin its content as in Tampermonkey (`…#sha256=` or `…#sha256-`), and the script is skipped if the file doesn't match. - warns at startup if Chromium's sandbox has been disabled (`QTWEBENGINE_DISABLE_SANDBOX`, `--no-sandbox`). The engine's security fixes come from QtWebEngine, so keep it current with `nix flake update`. `:version` shows the Chromium version and the security patch level it carries. ## User agent `:useragent ` (alias `:ua`) changes the browser identity that every window sends; with no name it shows the current one. Requests use the new one at once, and `navigator.userAgent` from each page's next load. The client hints (`Sec-CH-UA*`, `navigator.userAgentData`) are set to match the preset's brand and platform. | Preset | Claims to be | | ----------------- | ------------------------------------------------- | | `chrome-linux` | Chrome on Linux (the default) | | `chrome-windows` | Chrome on Windows | | `chrome-mac` | Chrome on macOS | | `chrome-android` | Chrome on Android (sites may serve mobile pages) | | `edge-windows` | Edge on Windows | | `firefox-linux`, `firefox-windows`, `firefox-mac` | Firefox | | `safari-mac`, `safari-iphone` | Safari | | `qtwebengine` | QtWebEngine's own string, which names it | Chrome and Edge claim the engine's real Chromium major version (as qutebrowser does), so sites don't use features the engine lacks. The Firefox and Safari versions are pinned in `src/useragent.cpp`. Those presets only change what att_browser says, not how it behaves: Chromium's low-entropy client hints still name Chromium, and feature detection still sees Chromium. ## Settings att_browser has no config file yet. The settings live in `src/config.h`, named after their qutebrowser equivalents; change them there and rebuild: | Setting | Default | Meaning | | -------------- | ------------------------------- | ---------------------------------------- | | `zoom.default` | 125% | zoom of new windows; `=` returns to it | | `zoom.levels` | 25%, 33%, 50%, … 125%, 150%, … 500% | the steps `+` / `-` move through | | `content.cookies.accept` | no third-party | `kBlockThirdPartyCookies` | | `content.pdfjs` | on | `kPdfViewer`: Chromium's PDF viewer | | `j` / `k` step (`scroll-px`) | 25 CSS px | `kScrollStepPx`; grows with the zoom | | `fonts.*` | 14pt monospace | `kFont`: bar, completion, messages, hints, context menu | | `messages.timeout` | 3000 ms | `kMessageTimeoutMs` | | `content.headers.user_agent` | `chrome-linux` | `kUserAgent`: the preset att_browser starts with | ## Commands `:open [-w] ` (alias `:o`; `-w`, or qutebrowser's `-t`, opens a new window), `:close` (alias `:tab-close`, closes this window), `:quit` (alias `:q`, closes all windows), `:reload`, `:back`, `:forward`, `:stop`, `:history-clear`, `:greasemonkey-reload`, `:adblock-update`, `:adblock-toggle`, `:zoom [level]` (`150` or `150%`; no level means the default), `:zoom-in`, `:zoom-out`, `:useragent [preset]` (alias `:ua`), `:version`. `:open` accepts a full URL, a bare host (`example.com` → `https://example.com`), an existing local path, or free text, which goes to DuckDuckGo. ## Layout | File | Role | | ------------------- | ------------------------------------------------------- | | `flake.nix` | pinned inputs, package, dev shell | | `package.nix` | the derivation itself, usable via `callPackage` | | `src/main.cpp` | `QApplication`, CLI parsing, stylesheet | | `src/browserwindow` | the window, mode state machine, key dispatch, commands | | `src/commandline` | the `:` prompt | | `src/greasemonkey` | userscript loading and the GM_* API wrapper | | `src/hardening` | profile security settings, sandbox check | | `src/history` | SQLite browsing history | | `src/completionview`| the `:open` / `:useragent` completion list | | `src/statusbar` | mode / keys / URL / scroll / progress line | | `src/messageview` | the message stack above the status bar | | `src/adblock` | request interceptor around the adblock engine | | `adblock/` | Rust crate: C API over adblock-rust, built by Corrosion | | `src/hints` | the injected hint-mode script | | `src/urlutil` | "what the user typed" → `QUrl` | | `src/useragent` | user-agent presets and matching client hints | Key events are intercepted with an **application-wide event filter** (`BrowserWindow::eventFilter`) rather than `QWidget::keyPressEvent`, because QtWebEngine delivers key presses directly to its internal render widget; the parent window never sees them otherwise. ## Next steps - Open `target=_blank` links, `window.open` and middle-clicks in new windows (`QWebEnginePage::newWindowRequested`). - Auto insert mode: inject a `QWebEngineScript` that reports `focusin` / `focusout` on editable elements back over `QWebChannel`. - More hint modes: `F` (follow in a new window), `;y` to yank a link. - `/` and `?` search with `n`/`N`, using `QWebEnginePage::findText`. - A config file for bindings, start page and search engine. - Counts before commands (`3j`) and a proper command parser with completion.