att
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 opens or closes a window, and your window manager
does the tabbing. No config file yet.
Running
nix run . # open the default start page
nix run . -- news.ycombinator.com
Developing
nix develop
cmake -B build -G Ninja
cmake --build build
./build/att
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.
Note: unlike qutebrowser, att does not yet auto-switch to insert mode when you click into a text field on a page — press
ifirst. 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 |
h j k l |
scroll |
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 / reset |
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.
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/att/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/att/QtWebEngine/default (the cache is in
~/.cache/att/att/). Session cookies are saved too, like qutebrowser's
content.cookies.store, so you stay logged in across restarts. Delete that
directory to start fresh.
Adblocking
Network requests are checked against EasyList and EasyPrivacy with Brave's 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/att/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/att/greasemonkey/ as *.js files. They
are loaded at startup; after changing them, run :greasemonkey-reload and reload
the page. Like qutebrowser, att:
- runs scripts in the page's own JavaScript context (so
unsafeWindowis justwindow), in iframes too unless@noframesis set; - honours
@include,@excludeand@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 intogreasemonkey/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_notificationand the promise-basedGM.*versions.
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.
Commands
:open [-w] <url|query> (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.
: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/history |
SQLite browsing history |
src/completionview |
the :open completion list |
src/statusbar |
mode / URL / progress line |
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 |
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=_blanklinks,window.openand middle-clicks in new windows (QWebEnginePage::newWindowRequested). - Auto insert mode: inject a
QWebEngineScriptthat reportsfocusin/focusouton editable elements back overQWebChannel. - More hint modes:
F(follow in a new window),;yto yank a link. /and?search withn/N, usingQWebEnginePage::findText.- A config file for bindings, start page and search engine.
- Counts before commands (
3j) and a proper command parser with completion.