55 lines
5.6 KiB
Markdown
55 lines
5.6 KiB
Markdown
# 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:
|
|
|
|
```sh
|
|
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`.
|