6.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, and git added: the flake only sees tracked files, so nix build fails on untracked sources (nix build path:. works around that). 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), notkeyPressEvent. QtWebEngine delivers keys straight to its internal render widget, so the window would never see them otherwise. Dispatch order:- Command mode passes keys through to the
CommandLine. - Escape / Ctrl-[ always return to normal mode.
- Insert mode passes keys through to the page.
- Hint mode swallows every key.
- Normal mode goes to
handleNormalModeKey. Bindings areif (text == "x")chains, and two-key sequences (gg) usem_pendingKeys.
- Command mode passes keys through to the
setModeis where entering and leaving a mode takes effect. It updates the status line and focus. LeavingHintalso 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 intoQWebEngineScript::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, andhandleHintResultacts on it. That map is the protocol, and it is documented inhints.h. - Results are asynchronous, so
handleHintResultignores them once the mode is no longerHint. - Following a hint sends a real
QMouseEventpress/release tom_view->focusProxy()(the render widget). The position is in CSS px multiplied by the zoom factor.
- The state lives in JS (
CommandLinekeeps the leading:in its text; deleting it cancels. It emitscommandEntered(cmd)without the:, andBrowserWindow::runCommandparses it as name plus the rest of the line.urlutil::fromUserInputturns 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 (getshttps://). Anything else becomes a DuckDuckGo search.gpuworkaround::apply()must run beforeQApplicationis constructed, because Chromium readsQTWEBENGINE_CHROMIUM_FLAGSfrom the environment. It adds--disable-accelerated-video-decodeonly on Wayland with QtWebEngine < 6.11.2 (QTBUG-145344). SetATT_NO_GPU_WORKAROUND=1to skip it.- The
QWebEngineProfileis created inmain.cppand passed intoBrowserWindow. It is a named, on-disk profile withForcePersistentCookies, because Qt 6's default profile is off-the-record. It is parented to theQApplicationso 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 withsrc/lib.rsby hand) over Brave'sadblockcrate. Corrosion builds it as part ofcmake --build, always in release. Itssingle-threaddefault feature is off, which makesEngineSend + Sync.AdBlocker(src/adblock.cpp) is the profile'sQWebEngineUrlRequestInterceptor.interceptRequestmay run off the GUI thread, so it reads the engine through a mutex-guardedshared_ptrthatupdate()swaps.- Lists are compiled on a
QtConcurrentworker and cached asadblock-cache.datin AppDataLocation. A cache that fails to deserialize (e.g. after an adblock-rust upgrade) triggers a re-download. main.cppcreatesAdBlockerafter the profile, so it is destroyed after it.
- History (
History,src/history.cpp) is SQLite through QtSql, run synchronously on the GUI thread with WAL.visitsis an append-only log;urls(one row per URL, upserted) is what completion searches. Visits are recorded onloadFinished(true), noturlChanged. URLs are stored astoDisplayString()with the fragment removed, so atitleChangedafter an in-page jump still updates the right row. :opencompletion:BrowserWindow::updateCompletionruns on the command line'stextEditedand on entering command mode. It parses:open/:oand showsHistory::searchresults inCompletionView, which never takes focus.CommandLine::eventintercepts Tab/Backtab, becauseQWidget::eventwould otherwise turn them into focus changes. Picking an entry usessetCommandText, which doesn't emittextEdited, so it doesn't re-filter.- The UI stylesheet is the
kStyleSheetstring insrc/main.cpp. Widgets are styled byobjectNameand by dynamic properties:StatusBartoggleserrorand 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.