a new browser, designed to be very simple conceptually, easily extensible by humans and LLMs
similar to qutebrowser but more minimal, Unix-y, and language-agnostic
(there's nothing of note. because the UI is minimal and usually non-existent, as it should be. this is just a website open, not any special new tab page)
requirements: Linux, CMake, a C++20 compiler, and Qt 6 with Web Engine and
WebChannel (on Arch: sudo pacman -S cmake qt6-base qt6-webengine qt6-webchannel)
git clone --depth 1 https://github.com/veilm/rethread
cd rethread
# configure + build (Makefile wraps CMake)
make
# install the default config and helper scripts without overwriting existing files
./install.sh
# launch the browser UI
./build/rethread browser --help
./build/rethread browser
The rethread binary is a light CLI wrapper. Use rethread browser ... to
launch the UI and rethread tabs ... to talk to a running instance without
reloading the Qt stack each time. After startup, the browser automatically runs
$XDG_CONFIG_HOME/rethread/init (override with --startup-script=PATH)
so you can register keybindings, rules, and userscripts declaratively. The
installed default config is usable as an example, but some bindings expect
wl-copy, wl-paste, jq, and a menu program such as rofi or dmenu.
Browser data is stored in $XDG_DATA_HOME/rethread/default by default. Use
--profile=NAME to run an independent profile, and pass the same flag to CLI
commands that should target it. --user-data-dir=PATH and
RETHREAD_USER_DATA_DIR can target an explicit directory instead.
rethread tabs open inserts the new tab immediately after the active tab; add
--at-end to append to the end of the strip when you need the old behavior.
Run rethread --help, rethread browser --help, or a command without its
required arguments for the complete command-line usage.
The browser exposes tab management over a Unix socket, so bindings and other programs can control an already-running instance without launching another Qt process:
rethread tabs list # JSON including ids, positions, URLs, and active state
rethread tabs open https://example.com
rethread tabs switch 3 # tab id, not strip position
rethread tabs cycle -1
rethread tabs close # active tab; pass a 1-based index to close another
rethread tabs history-back
rethread tabs history-forward
Bindings now live inside the browser—no external handler required. Register one with the CLI and it stays in memory until you quit:
# Alt+J / Alt+K cycle tabs without touching Python
rethread bind --alt --key=j -- rethread tabs cycle 1
rethread bind --alt --key=k -- rethread tabs cycle -1
rethread bind --ctrl --key=t --no-consume -- "notify-send 'new tab'"
Each binding accepts modifier flags (--alt, --ctrl, --shift,
--command/--meta), a --key=<value>, optional --no-consume, and the shell
command to run. -- is optional, but useful when the command itself starts
with a flag. Commands execute via /bin/sh -c ..., so any shell snippet works.
Drop the same lines into
$XDG_CONFIG_HOME/rethread/init to have them applied automatically on launch.
Use rethread unbind [mods] --key=... to clear a binding and fall back to the
browser's default behavior for that key combo.
Move tabs without leaving the keyboard. Relative offsets wrap around the strip:
# move the active tab left / right
rethread tabs swap -1
rethread tabs swap +1
# swap two explicit positions
rethread tabs swap 1 4
Right clicks follow the same in-memory model. Bind the handler once and rethread will reuse it until exit:
# invoke the sample handler installed by install.sh
rethread bind --context-menu "$XDG_CONFIG_HOME/rethread/right-click-handler.py"
Any shell snippet works; it runs via /bin/sh -c ... with your profile exported
just like the keyboard bindings. When the handler executes, the browser sets a
few environment variables so the script knows what was under the cursor:
RETHREAD_CONTEXT_PAYLOADreproduces the raw newline-delimited payloadRETHREAD_CONTEXT_TYPE_FLAGS,RETHREAD_CONTEXT_X,RETHREAD_CONTEXT_Y,RETHREAD_CONTEXT_EDITABLERETHREAD_CONTEXT_SELECTION,RETHREAD_CONTEXT_LINK_URL,RETHREAD_CONTEXT_SOURCE_URL,RETHREAD_CONTEXT_FRAME_URL,RETHREAD_CONTEXT_PAGE_URL,RETHREAD_CONTEXT_MEDIA_TYPE
The helper installed at $XDG_CONFIG_HOME/rethread/right-click-handler.py
copies the link or media URL to the clipboard via wl-copy, so you can
right-click either kind of target to grab its URL instantly. It can instead
append URLs to a file with --save=PATH. util/init wires it up automatically
when present.
The installed default config binds Alt+; to rethread-command-menu.py. It
uses rofi, dmenu, bemenu, tofi, or menu to expose occasional actions
without adding permanent browser UI. The current actions reload the page, add
the current host to the iframe whitelist, and switch right-click handling
between copying URLs and appending them to /tmp/rethread-rclick.
Use rethread rules to load newline-delimited hostname lists. Each rule accepts
either a --whitelist (deny everything else) or --blacklist (allow
everything else) flag.
Disable JavaScript on specific hosts:
cat hosts_to_block_js.txt | rethread rules js --blacklist
Block or allow third-party iframes to kill most popup hijacks:
cat iframe_whitelist.txt | rethread rules iframes --whitelist
Drop the same commands into your startup script (with input redirection) to populate the in-memory lists at launch. Tabs consult the rules whenever they navigate, so changes apply immediately without restarting the browser.
Need to tack on more hosts later without flushing the old list? Pass
--append and the rules manager will merge them when the mode matches:
cat ad_networks.txt | rethread rules js --blacklist --append
Switching from blacklist to whitelist (or the other way around) while using
--append automatically replaces the previous entries, so you never end up
with mixed modes in memory.
Use rethread scripts to manage Greasemonkey-style userscripts per profile.
Scripts live under $XDG_DATA_HOME/rethread/PROFILE/scripts/<id>.user.js.
Registration is in memory, so add them from the startup script if they should
be active after every restart. The source files remain on disk, and
rethread scripts list shows the scripts registered in the current session.
# add or replace a script (reads stdin)
cat foo.js | rethread scripts add --id=my-script --match='*://example.com/*'
# inject CSS via the helper wrapper
cat tweaks.css | rethread scripts add --id=my-style --stylesheet --match='*://*/*'
# inspect everything currently active
rethread scripts list
# remove a script by id
rethread scripts rm --id=my-style
When stdin already starts with // ==UserScript==, the CLI preserves it and
ignores --match, --run-at, and --stylesheet. Otherwise it generates the
header automatically, fills in the provided --match, and defaults to
@run-at document-end (use --run-at=document-start|document-end|document-idle
to override it). Passing --stylesheet treats stdin as CSS and wraps it in a
<style> injector that defaults to document-start.
Rethread includes a small persistent cosmetic-filtering system built from its
userscript and JavaScript-evaluation APIs. With the default util/init, press
Alt+X and click an element to hide matching elements on that host. The picker
can generate ordinary CSS selectors as well as text-matching rules, saves them
in $XDG_CONFIG_HOME/rethread/cosmetic-filtering/cosmetic-filters.json, and
reloads the page after adding a rule.
The helper can also be used directly:
$XDG_CONFIG_HOME/rethread/cosmetic-filtering/cosmetic-filters.py list
$XDG_CONFIG_HOME/rethread/cosmetic-filtering/cosmetic-filters.py rm example.com 1
$XDG_CONFIG_HOME/rethread/cosmetic-filtering/cosmetic-filters.py sync
sync registers userscripts for all saved hosts and is run automatically by
the installed startup script.
Downloads are accepted automatically. Before accepting one, rethread passes
its URL, page URL, MIME type, suggested name, sizes, and Qt download fields as
JSON on stdin to
$XDG_CONFIG_HOME/rethread/rethread-download-handler. The handler returns a
JSON decision such as:
{"accept": true, "path": "/home/me/Downloads/file.zip"}
It can also return {"accept": false} or provide separate directory and
filename fields. The included handler saves to $XDG_DOWNLOAD_DIR or
~/Downloads, sanitizes the suggested name, and avoids overwriting an existing
file. If the handler is missing or fails, rethread falls back to Qt's download
location and accepts the download.
Open the inspector for the active tab at any time:
rethread devtools open
It spawns a standard Qt WebEngine DevTools window (one per tab) so you can keep working from the CLI and still reach the familiar debugging tools.
Capture network traffic (including response bodies) for a tab:
rethread network-log --id=3
Use the tab id from rethread tabs list. By default it writes captures into
./rethread-tab-<id>-network-log/, mirroring
metadata.json, headers, and response bodies for each request. You can filter
requests with --url, --method, --status, or --mime, or choose a
different destination with --dir.
Rethread enables the CDP debug port by default on 127.0.0.1:9222. Use
rethread browser --cdp-port=PORT to change it or rethread browser --cdp-disable
to turn it off.
The tab strip overlay starts hidden. Use the CLI to control it at runtime:
# show / hide / toggle visibility
rethread tabstrip show
rethread tabstrip hide
rethread tabstrip toggle
# briefly show it for 400ms, then auto-hide
rethread tabstrip peek 400
# switch to next/previous tab, then peek for 750ms
rethread tabs cycle 1 && rethread tabstrip peek 750
rethread tabs cycle -1 && rethread tabstrip peek 750
# flash arbitrary text (read from stdin here) for half a second
printf 'Copied URL' | rethread tabstrip message --duration=500 --stdin
rethread tabstrip message --duration=1200 "Pinned tab saved"
peek always shows the overlay immediately and schedules a hide after the given
duration (milliseconds). Any manual show/hide/toggle commands cancel pending
peek hides. tabstrip message piggybacks on the same overlay to display one or
more lines of text for the duration you specify, so bindings can provide inline
status toasts without building extra UI.
Use rethread eval to run JavaScript inside a tab without leaving the terminal.
By default the active tab receives the snippet:
rethread eval "window.location.href"
Read scripts from stdin with --stdin (handy for multi-line snippets) and
target specific tabs with either an id or 1-based index:
cat snippet.js | rethread eval --stdin --tab-index=2
rethread eval --tab-id=7 "({title: document.title, url: location.href})"
The command prints the JSON-encoded return value (strings stay quoted, objects
and arrays render as expected). If your snippet returns a Promise, rethread
waits for it to settle before printing the resolved value (or propagating the
rejection). Errors bubble up as ERR ... lines. Snippets execute inside a
function body, so return the value you need (e.g.
rethread eval "console.log(1); return 5;") and use await freely—the helper
will treat async/sync code the same way.
open a GitHub issue or ping me on twitter. I'd be happy to answer any possible question
MIT
