Skip to content

veilm/rethread

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

106 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rethread

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

screenshot

basic hyprland+tmux screenshot

(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)

running

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.

tabs

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

key bindings

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-click bindings

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_PAYLOAD reproduces the raw newline-delimited payload
  • RETHREAD_CONTEXT_TYPE_FLAGS, RETHREAD_CONTEXT_X, RETHREAD_CONTEXT_Y, RETHREAD_CONTEXT_EDITABLE
  • RETHREAD_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.

command menu

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.

per-site rules

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.

userscripts

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.

cosmetic filtering

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

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.

devtools

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.

network log

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.

tab strip overlay

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.

evaluating JavaScript

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.

support

open a GitHub issue or ping me on twitter. I'd be happy to answer any possible question

license

MIT

About

a simple, extensible CLI-driven browser

Resources

Stars

Watchers

Forks

Contributors

Languages