Files
att_browser/CLAUDE.md
T

5.6 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

att is a small, keyboard-driven QtWebEngine browser modelled on qutebrowser, in C++20 / Qt 6 (pinned nixpkgs ships Qt 6.11). The README is the user-facing reference for key bindings, modes and commands. When you add a binding, mode or command, update its tables there.

Build and run

Everything goes through the Nix flake:

nix develop -c sh -c 'cmake -B build -G Ninja && cmake --build build'   # dev build
nix develop -c ./build/att [url]                                        # run the dev build
nix build                                                               # package build (→ ./result)
nix run . -- example.com

Run ./build/att from inside nix develop. The shell hook exports QT_PLUGIN_PATH, QML2_IMPORT_PATH and QTWEBENGINEPROCESS_PATH, because wrapQtAppsHook only wraps installed binaries. Run outside the dev shell, the binary can't find Qt's plugins.

package.nix uses a lib.fileset source: CMakeLists.txt, src/ and the listed adblock/ files. Every new source file must also be added to qt_add_executable in CMakeLists.txt. The crate's cargo dependencies are vendored from adblock/Cargo.lock (importCargoLock + cargoSetupHook) for the offline sandbox build, so commit Cargo.lock changes along with Cargo.toml ones.

The only tests are the Rust crate's (nix develop -c sh -c 'cd adblock && cargo test'). There is no linter. The dev shell includes clang-tools (clang-format, clangd) and gdb.

Architecture

BrowserWindow (src/browserwindow.cpp) is the core of the browser. It owns a single QWebEngineView, the StatusBar and the CommandLine, and it runs the mode state machine (Normal, Insert, Command, Hint).

  • Key handling uses an application-wide event filter (qApp->installEventFilter, BrowserWindow::eventFilter), not keyPressEvent. QtWebEngine delivers keys straight to its internal render widget, so the window would never see them otherwise. Dispatch order:
    1. Command mode passes keys through to the CommandLine.
    2. Escape / Ctrl-[ always return to normal mode.
    3. Insert mode passes keys through to the page.
    4. Hint mode swallows every key.
    5. Normal mode goes to handleNormalModeKey. Bindings are if (text == "x") chains, and two-key sequences (gg) use m_pendingKeys.
  • setMode is where entering and leaving a mode takes effect. It updates the status line and focus. Leaving Hint also runs the hint-clear script.
  • Page interaction goes through page()->runJavaScript. Scrolling, for example, is done with JS calls rather than Qt APIs.
  • Hint mode (src/hints.cpp):
    • The state lives in JS (window.__attHints), injected into QWebEngineScript::ApplicationWorld. That world shares the DOM with the page but keeps its JS globals isolated from the page's scripts.
    • C++ sends one call per key (keyScript / backspaceScript / clearScript). JS returns a {state: ...} map, and handleHintResult acts on it. That map is the protocol, and it is documented in hints.h.
    • Results are asynchronous, so handleHintResult ignores them once the mode is no longer Hint.
    • Following a hint sends a real QMouseEvent press/release to m_view->focusProxy() (the render widget). The position is in CSS px multiplied by the zoom factor.
  • CommandLine keeps the leading : in its text; deleting it cancels. It emits commandEntered(cmd) without the :, and BrowserWindow::runCommand parses it as name plus the rest of the line.
  • urlutil::fromUserInput turns what the user typed into a URL. In order, it tries an existing local path, a full URL, then something that looks like a host (gets https://). Anything else becomes a DuckDuckGo search.
  • gpuworkaround::apply() must run before QApplication is constructed, because Chromium reads QTWEBENGINE_CHROMIUM_FLAGS from the environment. It adds --disable-accelerated-video-decode only on Wayland with QtWebEngine < 6.11.2 (QTBUG-145344). Set ATT_NO_GPU_WORKAROUND=1 to skip it.
  • The QWebEngineProfile is created in main.cpp and passed into BrowserWindow. It is a named, on-disk profile with ForcePersistentCookies, because Qt 6's default profile is off-the-record. It is parented to the QApplication so it outlives the view's page. Qt warns if a profile is destroyed while a page still uses it.
  • Adblocking is split across two parts:
    • adblock/ is a Rust staticlib exposing a hand-written C API (att_adblock.h, kept in sync with src/lib.rs by hand) over Brave's adblock crate. Corrosion builds it as part of cmake --build, always in release. Its single-thread default feature is off, which makes Engine Send + Sync.
    • AdBlocker (src/adblock.cpp) is the profile's QWebEngineUrlRequestInterceptor. interceptRequest may run off the GUI thread, so it reads the engine through a mutex-guarded shared_ptr that update() swaps.
    • Lists are compiled on a QtConcurrent worker and cached as adblock-cache.dat in AppDataLocation. A cache that fails to deserialize (e.g. after an adblock-rust upgrade) triggers a re-download.
    • main.cpp creates AdBlocker after the profile, so it is destroyed after it.
  • The UI stylesheet is the kStyleSheet string in src/main.cpp. Widgets are styled by objectName and by dynamic properties: StatusBar toggles error and re-polishes the label to pick up the change.

There are no tabs yet. createWindow isn't implemented, so target=_blank and window.open go nowhere. To work around this, hint mode rewrites link targets to _self.