rename: att -> att_browser
This commit is contained in:
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## What this is
|
## 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.
|
`att_browser` 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
|
## Build and run
|
||||||
|
|
||||||
@@ -12,12 +12,12 @@ Everything goes through the Nix flake:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
nix develop -c sh -c 'cmake -B build -G Ninja && cmake --build build' # dev build
|
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 develop -c ./build/att_browser [url] # run the dev build
|
||||||
nix build # package build (→ ./result)
|
nix build # package build (→ ./result)
|
||||||
nix run . -- example.com
|
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.
|
Run `./build/att_browser` 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 add`ed: 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.
|
`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 add`ed: 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.
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ The only tests are the Rust crate's (`nix develop -c sh -c 'cd adblock && cargo
|
|||||||
- Open the command line only through `openCommandLine`, which hides the status bar *before* showing the command line. Showing a widget re-runs the layout at once, so the other order shrinks and regrows the page.
|
- Open the command line only through `openCommandLine`, which hides the status bar *before* showing the command line. Showing a widget re-runs the layout at once, so the other order shrinks and regrows the page.
|
||||||
- **`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.
|
- **`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 mostly goes through `page()->runJavaScript`**, except for input that should feel native:
|
- **Page interaction mostly goes through `page()->runJavaScript`**, except for input that should feel native:
|
||||||
- Scrolling (`scrollBy`, used by `j/k/h/l`, `Ctrl-d/u/f/b` and `gg/G`) sends a pixel-delta `QWheelEvent` to `m_view->focusProxy()` at the pointer, which is what a touchpad sends. Chromium then scrolls whatever is under the pointer. `window.scrollBy` only moves the document, so it did nothing on sites that scroll an inner element.
|
- Scrolling (`scrollBy`, used by `j/k/h/l`, `Ctrl-d/u/f/b` and `gg/G`) sends a pixel-delta `QWheelEvent` to `m_view->focusProxy()` at the pointer, which is what a touchpad sends. Chromium then scrolls whatever is under the pointer. Wheel deltas are screen pixels, so the page zoom shrinks them; `j`/`k` go through `scrollByCssPx`, which multiplies by the zoom so `config::kScrollStepPx` means CSS px like qutebrowser's `scroll-px`. `window.scrollBy` only moves the document, so it did nothing on sites that scroll an inner element.
|
||||||
- Hint clicks are real mouse events (see below).
|
- Hint clicks are real mouse events (see below).
|
||||||
- **Hint mode** (`src/hints.cpp`):
|
- **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.
|
- 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.
|
||||||
@@ -48,6 +48,7 @@ The only tests are the Rust crate's (`nix develop -c sh -c 'cd adblock && cargo
|
|||||||
- 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.
|
- 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.
|
- **`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.
|
- **`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.
|
||||||
|
- **Data paths**: the application name is `att_browser` and there is deliberately no organization name, so `AppDataLocation` is `~/.local/share/att_browser` (and `CacheLocation` `~/.cache/att_browser`). `migrateOldDataDirs` in `main.cpp` moves the pre-rename `att/att` directories there once, before anything opens a file.
|
||||||
- **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.
|
- **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:
|
- **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`.
|
- `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`.
|
||||||
@@ -58,14 +59,14 @@ The only tests are the Rust crate's (`nix develop -c sh -c 'cd adblock && cargo
|
|||||||
- **`:open` completion:** `BrowserWindow::updateCompletion` runs on the command line's `textEdited` and on entering command mode. It parses `:open`/`:o` and shows `History::search` results in `CompletionView`, which never takes focus. `CommandLine::event` intercepts Tab/Backtab, because `QWidget::event` would otherwise turn them into focus changes. Picking an entry uses `setCommandText`, which doesn't emit `textEdited`, so it doesn't re-filter.
|
- **`:open` completion:** `BrowserWindow::updateCompletion` runs on the command line's `textEdited` and on entering command mode. It parses `:open`/`:o` and shows `History::search` results in `CompletionView`, which never takes focus. `CommandLine::event` intercepts Tab/Backtab, because `QWidget::event` would otherwise turn them into focus changes. Picking an entry uses `setCommandText`, which doesn't emit `textEdited`, so it doesn't re-filter.
|
||||||
- **Greasemonkey** (`Greasemonkey`, `src/greasemonkey.cpp`, part of the `Session`) inserts each `greasemonkey/*.js` into `profile->scripts()` as a `QWebEngineScript` named `att-greasemonkey:<file>`, in `MainWorld`.
|
- **Greasemonkey** (`Greasemonkey`, `src/greasemonkey.cpp`, part of the `Session`) inserts each `greasemonkey/*.js` into `profile->scripts()` as a `QWebEngineScript` named `att-greasemonkey:<file>`, in `MainWorld`.
|
||||||
- URL matching is left to QtWebEngine, which parses the `==UserScript==` block during `setSourceCode` and checks `@include/@exclude/@match` in Chromium. So the block must stay in the source. `setSourceCode` also overwrites the script's name (with `@name`) and injection point, so `setName` and `setInjectionPoint` must come *after* it. `install()` finds the old scripts by the `att-greasemonkey:` name prefix to remove them; a lost prefix makes scripts run twice after a reload.
|
- URL matching is left to QtWebEngine, which parses the `==UserScript==` block during `setSourceCode` and checks `@include/@exclude/@match` in Chromium. So the block must stay in the source. `setSourceCode` also overwrites the script's name (with `@name`) and injection point, so `setName` and `setInjectionPoint` must come *after* it. `install()` finds the old scripts by the `att-greasemonkey:` name prefix to remove them; a lost prefix makes scripts run twice after a reload.
|
||||||
- att's own `parseMetadata` is for run-at, noframes, `@require` and `GM_info`. A `@require` is cached by the SHA-1 of its URL. It must be `https`, and an optional `#sha256=` pin is checked against the cached file on every install (`requireProblem`).
|
- att_browser's own `parseMetadata` is for run-at, noframes, `@require` and `GM_info`. A `@require` is cached by the SHA-1 of its URL. It must be `https`, and an optional `#sha256=` pin is checked against the cached file on every install (`requireProblem`).
|
||||||
- Scripts are wrapped in an IIFE (`kWrapperHead`/`kWrapperTail`) that defines the GM API as locals.
|
- Scripts are wrapped in an IIFE (`kWrapperHead`/`kWrapperTail`) that defines the GM API as locals.
|
||||||
- At `document-start`, `document.documentElement` is still null, which the GM helpers must tolerate.
|
- At `document-start`, `document.documentElement` is still null, which the GM helpers must tolerate.
|
||||||
- **Hardening** (`src/hardening.cpp`): `applyToProfile` sets the restrictive `QWebEngineSettings` and the third-party cookie filter. The filter runs on the network thread and must stay a pure function. `main.cpp` applies it right after creating the profile, and warns in the first window if `sandboxDisabledReason` finds the sandbox off. `StatusBar::setUrl` takes a `QUrl` so it can mark plain-HTTP pages (`insecure` property).
|
- **Hardening** (`src/hardening.cpp`): `applyToProfile` sets the restrictive `QWebEngineSettings` and the third-party cookie filter. The filter runs on the network thread and must stay a pure function. `main.cpp` applies it right after creating the profile, and warns in the first window if `sandboxDisabledReason` finds the sandbox off. `StatusBar::setUrl` takes a `QUrl` so it can mark plain-HTTP pages (`insecure` property).
|
||||||
- **User agent** (`src/useragent.cpp`): `useragent::apply` sets the profile's UA string *and* its `QWebEngineClientHints`, so the two don't contradict each other. Chromium sends the low-entropy hints (brands, platform, mobile) even with `setAllClientHintsEnabled(false)`, so every preset sets a platform; the brand ("Google Chrome", "Microsoft Edge") goes in through `setFullVersionList`. Chrome-based presets use the engine's own major version. `:useragent` completion reuses `CompletionView` with name / string columns.
|
- **User agent** (`src/useragent.cpp`): `useragent::apply` sets the profile's UA string *and* its `QWebEngineClientHints`, so the two don't contradict each other. Chromium sends the low-entropy hints (brands, platform, mobile) even with `setAllClientHintsEnabled(false)`, so every preset sets a platform; the brand ("Google Chrome", "Microsoft Edge") goes in through `setFullVersionList`. Chrome-based presets use the engine's own major version. `:useragent` completion reuses `CompletionView` with name / string columns.
|
||||||
- **Settings** live as constants in `src/config.h` (`config::kZoomDefault`, `config::kZoomLevels`, `kBlockThirdPartyCookies`, `kPdfViewer`, `kUserAgent`), named after qutebrowser's settings, until there's a config file. Zoom is per window: `setZoom` / `zoomStep` in `BrowserWindow`, applied before the first load. QtWebEngine keeps a view's zoom across navigations, so it doesn't need re-applying.
|
- **Settings** live as constants in `src/config.h` (`config::kZoomDefault`, `config::kZoomLevels`, `kBlockThirdPartyCookies`, `kPdfViewer`, `kUserAgent`, `kScrollStepPx`), named after qutebrowser's settings, until there's a config file. Zoom is per window: `setZoom` / `zoomStep` in `BrowserWindow`, applied before the first load. QtWebEngine keeps a view's zoom across navigations, so it doesn't need re-applying.
|
||||||
- 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.
|
- 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.
|
||||||
|
|
||||||
**Windows, not tabs.** att deliberately has no tab bar, because the user's window manager does the tabbing. Map qutebrowser's tab features onto top-level windows: `d` closes the window, `O` / `:open -w` (or `-t`) opens a new one. Each `BrowserWindow` is one page, with `WA_DeleteOnClose`. All windows share a `Session` (profile, `AdBlocker`, `History`) that `main()` owns. After `app.exec()`, `main()` deletes any window still alive before the history and profile go away. `:quit` closes all windows; `:close` closes one.
|
**Windows, not tabs.** att_browser deliberately has no tab bar, because the user's window manager does the tabbing. Map qutebrowser's tab features onto top-level windows: `d` closes the window, `O` / `:open -w` (or `-t`) opens a new one. Each `BrowserWindow` is one page, with `WA_DeleteOnClose`. All windows share a `Session` (profile, `AdBlocker`, `History`) that `main()` owns. After `app.exec()`, `main()` deletes any window still alive before the history and profile go away. `:quit` closes all windows; `:close` closes one.
|
||||||
|
|
||||||
`newWindowRequested` isn't handled yet, so `target=_blank` and `window.open` go nowhere. Hint mode rewrites link targets to `_self` to work around this.
|
`newWindowRequested` isn't handled yet, so `target=_blank` and `window.open` go nowhere. Hint mode rewrites link targets to `_self` to work around this.
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.21)
|
cmake_minimum_required(VERSION 3.21)
|
||||||
|
|
||||||
project(att
|
project(att_browser
|
||||||
VERSION 0.1.0
|
VERSION 0.1.0
|
||||||
DESCRIPTION "Minimal keyboard-driven QtWebEngine browser"
|
DESCRIPTION "Minimal keyboard-driven QtWebEngine browser"
|
||||||
LANGUAGES CXX
|
LANGUAGES CXX
|
||||||
@@ -26,7 +26,7 @@ corrosion_import_crate(
|
|||||||
PROFILE release
|
PROFILE release
|
||||||
)
|
)
|
||||||
|
|
||||||
qt_add_executable(att
|
qt_add_executable(att_browser
|
||||||
src/adblock.cpp
|
src/adblock.cpp
|
||||||
src/adblock.h
|
src/adblock.h
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
@@ -53,9 +53,9 @@ qt_add_executable(att
|
|||||||
src/useragent.h
|
src/useragent.h
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(att PRIVATE src adblock)
|
target_include_directories(att_browser PRIVATE src adblock)
|
||||||
|
|
||||||
target_link_libraries(att PRIVATE
|
target_link_libraries(att_browser PRIVATE
|
||||||
att_adblock
|
att_adblock
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
Qt6::Gui
|
Qt6::Gui
|
||||||
@@ -67,6 +67,6 @@ target_link_libraries(att PRIVATE
|
|||||||
Qt6::WebEngineWidgets
|
Qt6::WebEngineWidgets
|
||||||
)
|
)
|
||||||
|
|
||||||
install(TARGETS att
|
install(TARGETS att_browser
|
||||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# att
|
# att_browser
|
||||||
|
|
||||||
A small, keyboard-driven QtWebEngine browser in the spirit of qutebrowser.
|
A small, keyboard-driven QtWebEngine browser in the spirit of qutebrowser.
|
||||||
|
|
||||||
One page per window, a qutebrowser-style status line and `:` command line, `f`
|
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
|
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
|
opens or closes a tab, att_browser opens or closes a window, and your window manager
|
||||||
does the tabbing. No config file yet.
|
does the tabbing. No config file yet.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
@@ -20,7 +20,7 @@ nix run . -- news.ycombinator.com
|
|||||||
nix develop
|
nix develop
|
||||||
cmake -B build -G Ninja
|
cmake -B build -G Ninja
|
||||||
cmake --build build
|
cmake --build build
|
||||||
./build/att
|
./build/att_browser
|
||||||
```
|
```
|
||||||
|
|
||||||
The dev shell exports `QT_PLUGIN_PATH`, `QML2_IMPORT_PATH` and
|
The dev shell exports `QT_PLUGIN_PATH`, `QML2_IMPORT_PATH` and
|
||||||
@@ -45,7 +45,7 @@ Like qutebrowser, keys mean different things depending on the mode:
|
|||||||
leaves fullscreen when a page (e.g. a YouTube video) is fullscreen. The status bar
|
leaves fullscreen when a page (e.g. a YouTube video) is fullscreen. The status bar
|
||||||
is hidden while a page is fullscreen.
|
is hidden while a page is fullscreen.
|
||||||
|
|
||||||
> Note: unlike qutebrowser, att does not yet auto-switch to insert mode when you
|
> 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
|
> 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*.
|
> needs a JS focus-tracking script over QWebChannel; see *Next steps*.
|
||||||
|
|
||||||
@@ -62,7 +62,8 @@ is hidden while a page is fullscreen.
|
|||||||
| `gi` | focus the first text field, insert mode |
|
| `gi` | focus the first text field, insert mode |
|
||||||
| `f` | hint mode: follow a link by its label |
|
| `f` | hint mode: follow a link by its label |
|
||||||
| `Escape` | back to normal mode |
|
| `Escape` | back to normal mode |
|
||||||
| `h` `j` `k` `l`| scroll |
|
| `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-d/u` | scroll half a page |
|
||||||
| `Ctrl-f/b` | scroll a full page |
|
| `Ctrl-f/b` | scroll a full page |
|
||||||
| `gg` / `G` | jump to top / bottom |
|
| `gg` / `G` | jump to top / bottom |
|
||||||
@@ -94,17 +95,20 @@ shadow DOM yet.
|
|||||||
|
|
||||||
## Browsing data
|
## Browsing data
|
||||||
|
|
||||||
Browsing history is kept in `~/.local/share/att/att/history.sqlite` (tables
|
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).
|
`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
|
A page is recorded once it finishes loading, so redirect hops and in-page
|
||||||
`#anchor` jumps don't create entries. `:history-clear` empties it.
|
`#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
|
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
|
under `~/.local/share/att_browser/QtWebEngine/default` (the cache is in
|
||||||
`~/.cache/att/att/`). Session cookies are saved too, like qutebrowser's
|
`~/.cache/att_browser/`). Session cookies are saved too, like qutebrowser's
|
||||||
`content.cookies.store`, so you stay logged in across restarts. Delete that
|
`content.cookies.store`, so you stay logged in across restarts. Delete that
|
||||||
directory to start fresh.
|
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
|
## Adblocking
|
||||||
|
|
||||||
Network requests are checked against EasyList and EasyPrivacy with Brave's
|
Network requests are checked against EasyList and EasyPrivacy with Brave's
|
||||||
@@ -114,15 +118,15 @@ no cosmetic filtering (element hiding), so ads served from the site's own URLs
|
|||||||
still get through.
|
still get through.
|
||||||
|
|
||||||
The lists are downloaded on first start and compiled into
|
The lists are downloaded on first start and compiled into
|
||||||
`~/.local/share/att/att/adblock-cache.dat`, which later starts just load.
|
`~/.local/share/att_browser/adblock-cache.dat`, which later starts just load.
|
||||||
`:adblock-update` re-downloads them. `:adblock-toggle` switches blocking off and
|
`:adblock-update` re-downloads them. `:adblock-toggle` switches blocking off and
|
||||||
on, for when a site breaks.
|
on, for when a site breaks.
|
||||||
|
|
||||||
## Greasemonkey
|
## Greasemonkey
|
||||||
|
|
||||||
Userscripts go in `~/.local/share/att/att/greasemonkey/` as `*.js` files. They
|
Userscripts go in `~/.local/share/att_browser/greasemonkey/` as `*.js` files. They
|
||||||
are loaded at startup; after changing them, run `:greasemonkey-reload` and reload
|
are loaded at startup; after changing them, run `:greasemonkey-reload` and reload
|
||||||
the page. Like qutebrowser, att:
|
the page. Like qutebrowser, att_browser:
|
||||||
|
|
||||||
- runs scripts in the page's own JavaScript context (so `unsafeWindow` is just
|
- runs scripts in the page's own JavaScript context (so `unsafeWindow` is just
|
||||||
`window`), in iframes too unless `@noframes` is set;
|
`window`), in iframes too unless `@noframes` is set;
|
||||||
@@ -144,7 +148,7 @@ supported. `GM_openInTab` does nothing until new-window requests are handled.
|
|||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
On top of Chromium's own protections, att:
|
On top of Chromium's own protections, att_browser:
|
||||||
|
|
||||||
- blocks third-party cookies, which stops most cross-site tracking. A few embedded
|
- blocks third-party cookies, which stops most cross-site tracking. A few embedded
|
||||||
logins (e.g. some comment widgets) may stop working.
|
logins (e.g. some comment widgets) may stop working.
|
||||||
@@ -186,13 +190,13 @@ and platform.
|
|||||||
|
|
||||||
Chrome and Edge claim the engine's real Chromium major version (as qutebrowser
|
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
|
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
|
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
|
says, not how it behaves: Chromium's low-entropy client hints still name
|
||||||
Chromium, and feature detection still sees Chromium.
|
Chromium, and feature detection still sees Chromium.
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
att has no config file yet. The settings live in `src/config.h`, named after
|
att_browser has no config file yet. The settings live in `src/config.h`, named after
|
||||||
their qutebrowser equivalents; change them there and rebuild:
|
their qutebrowser equivalents; change them there and rebuild:
|
||||||
|
|
||||||
| Setting | Default | Meaning |
|
| Setting | Default | Meaning |
|
||||||
@@ -201,7 +205,8 @@ their qutebrowser equivalents; change them there and rebuild:
|
|||||||
| `zoom.levels` | 25%, 33%, 50%, … 125%, 150%, … 500% | the steps `+` / `-` move through |
|
| `zoom.levels` | 25%, 33%, 50%, … 125%, 150%, … 500% | the steps `+` / `-` move through |
|
||||||
| `content.cookies.accept` | no third-party | `kBlockThirdPartyCookies` |
|
| `content.cookies.accept` | no third-party | `kBlockThirdPartyCookies` |
|
||||||
| `content.pdfjs` | on | `kPdfViewer`: Chromium's PDF viewer |
|
| `content.pdfjs` | on | `kPdfViewer`: Chromium's PDF viewer |
|
||||||
| `content.headers.user_agent` | `chrome-linux` | `kUserAgent`: the preset att starts with |
|
| `j` / `k` step (`scroll-px`) | 25 CSS px | `kScrollStepPx`; grows with the zoom |
|
||||||
|
| `content.headers.user_agent` | `chrome-linux` | `kUserAgent`: the preset att_browser starts with |
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
description = "att - a minimal, qutebrowser-like QtWebEngine browser";
|
description = "att_browser - a minimal, qutebrowser-like QtWebEngine browser";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
# Pinned to the current NixOS stable release.
|
# Pinned to the current NixOS stable release.
|
||||||
@@ -14,21 +14,21 @@
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages = forAllSystems (pkgs: rec {
|
packages = forAllSystems (pkgs: rec {
|
||||||
att = pkgs.callPackage ./package.nix { };
|
att_browser = pkgs.callPackage ./package.nix { };
|
||||||
default = att;
|
default = att_browser;
|
||||||
});
|
});
|
||||||
|
|
||||||
apps = forAllSystems (pkgs: rec {
|
apps = forAllSystems (pkgs: rec {
|
||||||
att = {
|
att_browser = {
|
||||||
type = "app";
|
type = "app";
|
||||||
program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.att}/bin/att";
|
program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.att_browser}/bin/att_browser";
|
||||||
};
|
};
|
||||||
default = att;
|
default = att_browser;
|
||||||
});
|
});
|
||||||
|
|
||||||
devShells = forAllSystems (pkgs: {
|
devShells = forAllSystems (pkgs: {
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
inputsFrom = [ self.packages.${pkgs.stdenv.hostPlatform.system}.att ];
|
inputsFrom = [ self.packages.${pkgs.stdenv.hostPlatform.system}.att_browser ];
|
||||||
|
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
clang-tools
|
clang-tools
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
export QT_PLUGIN_PATH="${qtbase}/${qtbase.qtPluginPrefix}:${qtwebengine}/${qtbase.qtPluginPrefix}"
|
export QT_PLUGIN_PATH="${qtbase}/${qtbase.qtPluginPrefix}:${qtwebengine}/${qtbase.qtPluginPrefix}"
|
||||||
export QML2_IMPORT_PATH="${qtdeclarative}/${qtbase.qtQmlPrefix}:${qtwebengine}/${qtbase.qtQmlPrefix}"
|
export QML2_IMPORT_PATH="${qtdeclarative}/${qtbase.qtQmlPrefix}:${qtwebengine}/${qtbase.qtQmlPrefix}"
|
||||||
export QTWEBENGINEPROCESS_PATH="${qtwebengine}/libexec/QtWebEngineProcess"
|
export QTWEBENGINEPROCESS_PATH="${qtwebengine}/libexec/QtWebEngineProcess"
|
||||||
echo "att dev shell - cmake -B build -G Ninja && cmake --build build && ./build/att"
|
echo "att_browser dev shell - cmake -B build -G Ninja && cmake --build build && ./build/att_browser"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@
|
|||||||
}:
|
}:
|
||||||
|
|
||||||
stdenv.mkDerivation (finalAttrs: {
|
stdenv.mkDerivation (finalAttrs: {
|
||||||
pname = "att";
|
pname = "att_browser";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
|
|
||||||
src = lib.fileset.toSource {
|
src = lib.fileset.toSource {
|
||||||
@@ -49,7 +49,7 @@ stdenv.mkDerivation (finalAttrs: {
|
|||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = "Minimal keyboard-driven QtWebEngine browser";
|
description = "Minimal keyboard-driven QtWebEngine browser";
|
||||||
mainProgram = "att";
|
mainProgram = "att_browser";
|
||||||
platforms = lib.platforms.linux;
|
platforms = lib.platforms.linux;
|
||||||
license = lib.licenses.mit;
|
license = lib.licenses.mit;
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-6
@@ -88,7 +88,7 @@ BrowserWindow::BrowserWindow(const Session &session, const QUrl &startUrl, QWidg
|
|||||||
|
|
||||||
void BrowserWindow::setupUi()
|
void BrowserWindow::setupUi()
|
||||||
{
|
{
|
||||||
setWindowTitle(QStringLiteral("att"));
|
setWindowTitle(QStringLiteral("att_browser"));
|
||||||
resize(1280, 800);
|
resize(1280, 800);
|
||||||
|
|
||||||
auto *central = new QWidget(this);
|
auto *central = new QWidget(this);
|
||||||
@@ -134,8 +134,8 @@ void BrowserWindow::connectView()
|
|||||||
setPageFullScreen(request.toggleOn());
|
setPageFullScreen(request.toggleOn());
|
||||||
});
|
});
|
||||||
connect(m_view, &QWebEngineView::titleChanged, this, [this](const QString &title) {
|
connect(m_view, &QWebEngineView::titleChanged, this, [this](const QString &title) {
|
||||||
setWindowTitle(title.isEmpty() ? QStringLiteral("att")
|
setWindowTitle(title.isEmpty() ? QStringLiteral("att_browser")
|
||||||
: QStringLiteral("%1 - att").arg(title));
|
: QStringLiteral("%1 - att_browser").arg(title));
|
||||||
m_session.history->updateTitle(m_view->url(), title);
|
m_session.history->updateTitle(m_view->url(), title);
|
||||||
});
|
});
|
||||||
connect(m_view, &QWebEngineView::loadFinished, this, [this](bool ok) {
|
connect(m_view, &QWebEngineView::loadFinished, this, [this](bool ok) {
|
||||||
@@ -344,11 +344,11 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (text == QLatin1String("j")) {
|
if (text == QLatin1String("j")) {
|
||||||
scrollBy(0, kScrollStep);
|
scrollByCssPx(0, config::kScrollStepPx);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (text == QLatin1String("k")) {
|
if (text == QLatin1String("k")) {
|
||||||
scrollBy(0, -kScrollStep);
|
scrollByCssPx(0, -config::kScrollStepPx);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (text == QLatin1String("h")) {
|
if (text == QLatin1String("h")) {
|
||||||
@@ -500,7 +500,7 @@ void BrowserWindow::runCommand(const QString &command)
|
|||||||
} else if (name == QLatin1String("version")) {
|
} else if (name == QLatin1String("version")) {
|
||||||
// The Chromium security patch level is what says how far behind
|
// The Chromium security patch level is what says how far behind
|
||||||
// Chrome's fixes this build is; "nix flake update" moves it forward.
|
// Chrome's fixes this build is; "nix flake update" moves it forward.
|
||||||
m_status->showMessage(QStringLiteral("att %1, Qt %2, Chromium %3, security patches %4")
|
m_status->showMessage(QStringLiteral("att_browser %1, Qt %2, Chromium %3, security patches %4")
|
||||||
.arg(QCoreApplication::applicationVersion(),
|
.arg(QCoreApplication::applicationVersion(),
|
||||||
QString::fromLatin1(qVersion()),
|
QString::fromLatin1(qVersion()),
|
||||||
QString::fromLatin1(qWebEngineChromiumVersion()),
|
QString::fromLatin1(qWebEngineChromiumVersion()),
|
||||||
@@ -655,6 +655,14 @@ void BrowserWindow::scrollBy(int dx, int dy)
|
|||||||
QCoreApplication::sendEvent(target, &event);
|
QCoreApplication::sendEvent(target, &event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BrowserWindow::scrollByCssPx(int dx, int dy)
|
||||||
|
{
|
||||||
|
// Wheel deltas are screen pixels, which the page zoom shrinks (at 125% a
|
||||||
|
// 25px wheel moves 20 CSS px). qutebrowser's scroll-px counts CSS pixels.
|
||||||
|
const double zoom = m_view->zoomFactor();
|
||||||
|
scrollBy(qRound(dx * zoom), qRound(dy * zoom));
|
||||||
|
}
|
||||||
|
|
||||||
void BrowserWindow::scrollPages(double factor)
|
void BrowserWindow::scrollPages(double factor)
|
||||||
{
|
{
|
||||||
scrollBy(0, qRound(m_view->height() * factor));
|
scrollBy(0, qRound(m_view->height() * factor));
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ private:
|
|||||||
void placeCompletion();
|
void placeCompletion();
|
||||||
void openUrl(const QString &input, bool newWindow);
|
void openUrl(const QString &input, bool newWindow);
|
||||||
void scrollBy(int dx, int dy);
|
void scrollBy(int dx, int dy);
|
||||||
|
void scrollByCssPx(int dx, int dy);
|
||||||
void scrollPages(double factor);
|
void scrollPages(double factor);
|
||||||
void scrollToEnd(bool bottom);
|
void scrollToEnd(bool bottom);
|
||||||
void focusView();
|
void focusView();
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ inline constexpr std::array kZoomLevels = {
|
|||||||
1.25, 1.50, 1.75, 2.00, 2.50, 3.00, 4.00, 5.00,
|
1.25, 1.50, 1.75, 2.00, 2.50, 3.00, 4.00, 5.00,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// What "j" / "k" do: qutebrowser's "scroll-px 0 25" / "scroll-px 0 -25". In
|
||||||
|
// CSS pixels, so the step grows with the zoom like qutebrowser's does.
|
||||||
|
inline constexpr int kScrollStepPx = 25;
|
||||||
|
|
||||||
// content.cookies.accept = no-3rdparty: refuse cookies a page's third-party
|
// content.cookies.accept = no-3rdparty: refuse cookies a page's third-party
|
||||||
// requests try to set or send. Blocks most cross-site tracking; a few embedded
|
// requests try to set or send. Blocks most cross-site tracking; a few embedded
|
||||||
// logins (e.g. some comment widgets) stop working.
|
// logins (e.g. some comment widgets) stop working.
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ QString wrap(const Greasemonkey::Metadata &meta, const QString &requiredCode, co
|
|||||||
QJsonObject info{
|
QJsonObject info{
|
||||||
{QStringLiteral("script"), script},
|
{QStringLiteral("script"), script},
|
||||||
{QStringLiteral("scriptMetaStr"), meta.block},
|
{QStringLiteral("scriptMetaStr"), meta.block},
|
||||||
{QStringLiteral("scriptHandler"), QStringLiteral("att")},
|
{QStringLiteral("scriptHandler"), QStringLiteral("att_browser")},
|
||||||
{QStringLiteral("version"), QCoreApplication::applicationVersion()},
|
{QStringLiteral("version"), QCoreApplication::applicationVersion()},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ QString sandboxDisabledReason(int argc, char **argv)
|
|||||||
return QStringLiteral("QTWEBENGINE_CHROMIUM_FLAGS contains --no-sandbox");
|
return QStringLiteral("QTWEBENGINE_CHROMIUM_FLAGS contains --no-sandbox");
|
||||||
for (int i = 1; i < argc; ++i) {
|
for (int i = 1; i < argc; ++i) {
|
||||||
if (QLatin1String(argv[i]) == QLatin1String("--no-sandbox"))
|
if (QLatin1String(argv[i]) == QLatin1String("--no-sandbox"))
|
||||||
return QStringLiteral("att was started with --no-sandbox");
|
return QStringLiteral("att_browser was started with --no-sandbox");
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-4
@@ -9,6 +9,9 @@
|
|||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QStandardPaths>
|
||||||
#include <QWebEngineProfile>
|
#include <QWebEngineProfile>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -48,18 +51,44 @@ constexpr auto kStyleSheet = R"(
|
|||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
// Before the rename, the browser was "att" with organization "att", so its
|
||||||
|
// data was in ~/.local/share/att/att and ~/.cache/att/att. Move those to the
|
||||||
|
// new locations once, before anything opens a file there, so cookies, history
|
||||||
|
// and userscripts survive. A new directory that already exists is left alone.
|
||||||
|
void migrateOldDataDirs()
|
||||||
|
{
|
||||||
|
const auto migrate = [](QStandardPaths::StandardLocation generic,
|
||||||
|
QStandardPaths::StandardLocation app) {
|
||||||
|
const QString base = QStandardPaths::writableLocation(generic);
|
||||||
|
const QString oldPath = base + QStringLiteral("/att/att");
|
||||||
|
const QString newPath = QStandardPaths::writableLocation(app);
|
||||||
|
if (!QFileInfo::exists(oldPath) || QFileInfo::exists(newPath))
|
||||||
|
return;
|
||||||
|
QDir().mkpath(QFileInfo(newPath).path());
|
||||||
|
if (QDir().rename(oldPath, newPath))
|
||||||
|
QDir(base).rmdir(QStringLiteral("att")); // only if now empty
|
||||||
|
else
|
||||||
|
qWarning("att_browser: could not move %s to %s", qPrintable(oldPath),
|
||||||
|
qPrintable(newPath));
|
||||||
|
};
|
||||||
|
migrate(QStandardPaths::GenericDataLocation, QStandardPaths::AppDataLocation);
|
||||||
|
migrate(QStandardPaths::GenericCacheLocation, QStandardPaths::CacheLocation);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
QCoreApplication::setApplicationName(QStringLiteral("att"));
|
// No organization name, so the data lives in ~/.local/share/att_browser
|
||||||
|
// rather than a doubled att_browser/att_browser.
|
||||||
|
QCoreApplication::setApplicationName(QStringLiteral("att_browser"));
|
||||||
QCoreApplication::setApplicationVersion(QStringLiteral("0.1.0"));
|
QCoreApplication::setApplicationVersion(QStringLiteral("0.1.0"));
|
||||||
QCoreApplication::setOrganizationName(QStringLiteral("att"));
|
migrateOldDataDirs();
|
||||||
|
|
||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(
|
parser.setApplicationDescription(
|
||||||
QStringLiteral("att - a minimal, keyboard-driven browser"));
|
QStringLiteral("att_browser - a minimal, keyboard-driven browser"));
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
parser.addVersionOption();
|
parser.addVersionOption();
|
||||||
parser.addPositionalArgument(QStringLiteral("url"), QStringLiteral("URL to open."));
|
parser.addPositionalArgument(QStringLiteral("url"), QStringLiteral("URL to open."));
|
||||||
@@ -97,7 +126,7 @@ int main(int argc, char *argv[])
|
|||||||
firstWindow->show();
|
firstWindow->show();
|
||||||
const QString sandboxOff = hardening::sandboxDisabledReason(argc, argv);
|
const QString sandboxOff = hardening::sandboxDisabledReason(argc, argv);
|
||||||
if (!sandboxOff.isEmpty()) {
|
if (!sandboxOff.isEmpty()) {
|
||||||
qWarning("att: Chromium's sandbox is disabled: %s", qPrintable(sandboxOff));
|
qWarning("att_browser: Chromium's sandbox is disabled: %s", qPrintable(sandboxOff));
|
||||||
firstWindow->showMessage(QStringLiteral("Sandbox disabled: %1").arg(sandboxOff), true);
|
firstWindow->showMessage(QStringLiteral("Sandbox disabled: %1").arg(sandboxOff), true);
|
||||||
}
|
}
|
||||||
// After the window exists, so its status bar shows their messages. The
|
// After the window exists, so its status bar shows their messages. The
|
||||||
|
|||||||
Reference in New Issue
Block a user