Riptide
Surfing the web really fast. Riptide is a keyboard-driven browser in the spirit of qutebrowser, built on CEF (Chromium 154) and controlled entirely from Rust through the cef crate.
Status: early prototype, Linux/X11 only, not ready for daily browsing. The project plan tracks what's done and what's missing.
What works today
- Keyboard first: normal, insert, command, hint, caret and passthrough modes, with qutebrowser's bindings. Also counts, marks, macros,
/search,:navigate, and a command line with history and completion. - Tabs and windows: pinned tabs, a tab bar that works with the mouse, favicons,
:tab-select, moving tabs between windows, and private windows. - Hints: for links, inputs, images, yanking and downloads, including number hints, iframes from any site and shadow DOM.
- Privacy: ad and tracker blocking with EasyList, EasyPrivacy and uBlock Origin's own lists, including its scriptlets (which strip YouTube's video ads), stand-ins for blocked files, tracking parameters taken off links, and element hiding with
:has-text()and the like, in frames too, plus your own rules, which an element picker writes for you. Also: Google background calls turned off, the Chromium sandbox where Linux allows it, and per-site permissions and certificate decisions. - Configuration:
config.toml, orconfig.luawith full scripting (functions on keys, custom commands, event hooks). Live:set, per-site settings, and a generated:helppage. - Extensions and plugins: Chrome (Manifest V3) extensions from the Web Store, such as uBlock Origin Lite, with their popups floating over the page; and sandboxed Lua plugins, like the ones that fill logins from pass, Bitwarden or KeePassXC.
- Scripts and data: userscripts, Greasemonkey scripts,
:open-editor, and qutebrowser's quickmark, bookmark and history formats (:history-import). - Page tools: zoom (
+-=), DevTools (wi), print or save as PDF, fullscreen, view source (gf),:jseval, tab muting,:messages, and.to repeat the last command. - Everything else: sessions with crash recovery, history and downloads pages, spell checking with keyboard-driven fixes, dark mode, opt-in Widevine, and handing commands to a running browser from the terminal (
riptide ':open -t x').
Where to start
- New here: Installing, then Keys and modes.
- Looking something up: the Reference lists every command, binding and setting. In the browser,
F1or:helpshows the same, with your own config applied. - Working on riptide itself: the Developer guide.
Riptide is licensed under GPL-3.0-or-later.
Installing
Release downloads
Each GitHub release has three Linux x86_64 downloads, with SHA256SUMS. Each contains the binary and the CEF runtime it needs.
| Download | Install |
|---|---|
riptide_X.Y.Z_amd64.deb | Ubuntu 24.04+ and Debian 13+: sudo apt install ./riptide_X.Y.Z_amd64.deb. It installs to /opt/riptide with riptide on your PATH, a desktop entry, and apt-managed dependencies. Its setuid sandbox helper works even where user namespaces are blocked. |
riptide-X.Y.Z-linux-x86_64.AppImage | Any distribution: chmod +x it and run it. |
riptide-X.Y.Z-linux-x86_64.tar.gz | Unpack it anywhere and run riptide. |
To check a download, run sha256sum -c SHA256SUMS --ignore-missing, or verify where it was built with gh attestation verify <file> --repo joshzcold/riptide.
The nightly pre-release is rebuilt from main every night it changes. It has the newest features, and may be broken.
macOS and Windows builds compile and pass their unit tests, but can't run the browser yet; they need the app bundle and installer work that's still planned.
Nix and Arch Linux
Both install the latest release's Linux tarball.
- Nix (flakes):
nix run github:joshzcold/riptide, or addgithub:joshzcold/riptideas a flake input and use itspackages.x86_64-linux.default. The package (packaging/nix/package.nix) patches the binaries for NixOS. The Nix store can't hold a setuid sandbox helper, so it relies on user namespaces for the sandbox, which NixOS allows. - Arch Linux:
packaging/aur/PKGBUILDbuildsriptide-bin. It installs to/opt/riptidewithriptideon yourPATH, plus a desktop entry and icon. Runmakepkg -siin that directory. It isn't published to the AUR yet.
To package a tarball you built yourself with Nix:
./task package
nix-build -E 'with import <nixpkgs> {}; callPackage ./packaging/nix/package.nix {
tarball = ./dist/riptide-0.1.0-linux-x86_64.tar.gz; }'
Building from source
Requirements: Rust 1.88+ (edition 2024).
Tasks run through Task. The ./task wrapper uses your installed task if there is one. Otherwise it downloads a pinned, checksum-verified release into .bin/.
./task setup # once: download CEF (~1.5 GB) into $CEF_PATH, default ~/.local/share/cef
./task run # build and launch; or: ./task run -- example.com
The developer guide covers the build in more detail, including building without Task.
The sandbox
On Linux, Chromium's sandbox needs unprivileged user namespaces or a setuid-root chrome-sandbox next to the binary. Riptide checks at startup. If neither is available, it runs without the sandbox and logs a warning. The help page (:help) shows the result under "Sandbox". --no-sandbox turns it off on purpose.
Ubuntu 23.10 and later block user namespaces through AppArmor unless a program has a profile that allows them. Pick one of these fixes:
- An AppArmor profile (recommended). It only affects this binary. Save it as
/etc/apparmor.d/riptide, then load it withsudo apparmor_parser -r /etc/apparmor.d/riptide:abi <abi/4.0>, include <tunables/global> profile riptide /path/to/riptide/target/*/riptide flags=(unconfined) { userns, include if exists <local/riptide> } - Setuid helper:
sudo chown root:root target/debug/chrome-sandbox && sudo chmod 4755 target/debug/chrome-sandbox. A rebuild that copies the file again undoes this. - System-wide:
sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0. It lowers the hardening for every program.
Inside an AppImage, chrome-sandbox can't be setuid, so the sandbox needs user namespaces.
macOS and Windows builds run without the sandbox for now; it needs the app bundle and installer work that's still planned.
Window managers
riptide's windows have the class riptide (WM_CLASS riptide, Riptide) for window manager rules and docks, and riptide's logo as their icon. They can be resized, so tiling window managers tile them, call windows included. Before 2026-10-07 they asked for a fixed 1280×800, which made dwm and similar window managers float them.
If another browser's screen-share picker doesn't list riptide's window, check its state with xprop WM_STATE and click the window. Programs that list windows skip any that aren't in the Normal state. dwm's swallow patch leaves a program started from a terminal in the Withdrawn state, because it marks the program's window instead of the terminal's. Either fix the patch (in swallow(), mark p instead of c as withdrawn), or exclude riptide from swallowing with a rule for the class Riptide.
Where things live
Run riptide --paths to see the config and data directories.
| Platform | Config directory | Data directory (profile, cookies, cache) |
|---|---|---|
| Linux | $XDG_CONFIG_HOME/riptide, default ~/.config/riptide | $XDG_DATA_HOME/riptide, default ~/.local/share/riptide |
| macOS | ~/.config/riptide (like Neovim, WezTerm, Zed) | ~/Library/Application Support/riptide |
| Windows | %APPDATA%\riptide\config | %LOCALAPPDATA%\riptide\data |
XDG_CONFIG_HOME and XDG_DATA_HOME are honoured on every platform. --basedir DIR puts everything under DIR/config and DIR/data, which is handy for a separate profile.
Keys and modes
Riptide is modal, like vim and qutebrowser. Keys do different things depending on the mode, which the status bar shows:
| Mode | What keys do | Enter with | Leave with |
|---|---|---|---|
| normal | Run commands: scroll, follow links, switch tabs | Escape from any other mode | — |
| insert | Go to the page, for typing into text fields | i, or automatically when a text field gets focus | Escape |
| command | Edit a :command line | : (or o, O, go…) | Return runs it, Escape cancels |
| hint | Pick an element by its label | f, F, ;y… | choosing a label, or Escape |
| caret | Move a text cursor and select text | v / V | Escape |
| passthrough | Every key goes to the page | Ctrl-v | Shift-Escape |
Most keys take a count first: 5j scrolls five times, 3J moves three tabs right, 50G jumps to 50%.
Common keys
This is a tour, not the whole list. The default key bindings page has every binding in every mode, and :help bindings shows yours.
| Keys | Command |
|---|---|
j k h l | scroll down/up/left/right (accept a count, e.g. 5j) |
gg / G | scroll-to-perc 0 / scroll-to-perc (50G = 50%) |
0 / $ | Scroll to the far left / right |
Ctrl-d Ctrl-u | Half page down / up |
Ctrl-f Ctrl-b | Full page down / up |
H / L | back / forward |
r / R | reload / reload -f |
o / O | :open / :open -t (new tab) |
go / gO | Edit the current URL, in this tab / a new tab |
Ctrl-t | open -t (start page in a new tab) |
J K, gT | tab-next / tab-prev |
Alt-1…Alt-9, g0 g$ | tab-focus N / first / last (a count also works, e.g. 3J) |
Ctrl-Tab, Ctrl-^ | tab-focus last (previously focused tab) |
gt, T | :tab-select: a list of the open tabs, this window's first (by number; other windows' as window/tab). Type a number to pick that tab, or words to filter by title or URL, then Return |
gD | tab-give: move the tab to a new window |
:tab-clone [-b] [-w], :tab-give [N], :tab-take W/T | Duplicate the tab (in the background / a new window), move it to window N or a new window, or bring a tab here from another window. Pages are reopened, so their back/forward history stays behind. |
d, Ctrl-w | tab-close |
u, Ctrl-Shift-t | undo (reopen the last closed tab where it was) |
gJ gK, gm | tab-move + / - / to the start (or to the count) |
co | tab-only (keeps pinned tabs; :tab-only --force closes them too) |
Ctrl-p | tab-pin: pin or unpin the tab (a count picks one, e.g. 3 Ctrl-p) |
f / F / ;b | Hint elements; click / open in a new tab / open in a background tab. With hints.mode = "number", labels are numbers and typing letters narrows the elements by their text (a single match is followed). Elements inside iframes (from any site, such as Gmail's Chat) and inside web components' shadow DOM are hinted too. |
;y / ;h / ;t | Hint a link to yank / an element to hover / an input to focus |
;i / ;I | Hint an image; open it here / in a new tab |
;o / ;O | Hint a link and put :open (or :open -t) with its URL on the command line |
;r | Rapid hinting: open several links in background tabs (leave with Escape) |
yy / yt / yd | Yank the URL / title / domain (yY yT yD: to the primary selection). Tracking parameters in url.yank_ignored_parameters (utm_*, fbclid…) are left out of the URL. |
pp / Pp | Open the clipboard contents here / in a new tab (pP / PP: the primary selection) |
m | Quickmark this page (type a name, then Return) |
b / B | Open a quickmark here / in a new tab |
M | Bookmark this page |
gb / gB | Open a bookmark here / in a new tab |
`a / 'a | Set / jump to mark a: a–z remember this page's scroll position, A–Z also the page itself; '' returns to where the last jump started |
Ctrl-e (insert mode) | open-editor: edit the text field in editor.command |
gu / gU | navigate up: one level up the URL, here / in a new tab (a count goes further) |
[[ ]] / {{ }} | navigate prev / next: follow the page's previous/next link (rel links, or link text matching hints.prev_regexes / hints.next_regexes, such as "Next »"), here / in a new tab |
Ctrl-a / Ctrl-x | navigate increment / decrement: change the last number in the URL (page/9 → page/10) |
Return / Ctrl-Return | selection-follow: follow the link a search found (or the focused link) here / in a new tab; otherwise the page gets the key |
/ ? then n N | Find text in the page forward / backward, then go to the next / previous match. Matches highlight as you type (search.incremental); case is ignored unless the text has a capital (search.ignore_case). :search with no text clears it. |
v / V | Caret mode: move with h j k l w b e 0 $ gg G, and between blocks with [ ] (start of the previous/next) and { } (end of the previous/next). Select with v (or V for lines), drop the selection with Ctrl-Space, swap the ends with o, yank with y (Y: to the primary selection), leave with Escape |
qa … q / @a | Record a macro into register a / replay it (@@ repeats the last one, 3@a runs it three times). Keys typed into pages are replayed too. |
F1, :help [topic] | Help: every command, setting (with its current value and where it was set) and key binding, generated from the running browser. :help :open, :help hints.chars, :help rt.ui.float, :help <plugin> and :help bindings jump to an entry, and :help completes them; / searches. |
:version | Version, git commit, CEF/Chromium versions, paths and loaded config files |
:history [-t] | Browsing history by day, with a search box |
:history-import [path] | Import qutebrowser's history.sqlite (default: qutebrowser's data directory); importing twice adds nothing new |
:q, :qa | Close this window (closing the last one quits) / quit, closing every window. As in qutebrowser, these are aliases for :close and :quit (aliases). |
ZZ, :wq | Save the tabs as the default session and quit (ZQ quits without saving) |
: | Command line |
i | Insert mode (also entered automatically when a text field gets focus) |
Ctrl-v | Passthrough mode (leave with Shift-Escape) |
Escape | Leave insert mode, or clear a pending key sequence |
ZQ ZZ Ctrl-q | quit |
Opening pages
o (:open) takes an address or words to search for. Text that looks like an address (example.org, localhost:8080) opens; anything else goes to the DEFAULT search engine in url.searchengines. Start with an engine's name to use it instead:
[url.searchengines]
DEFAULT = "https://duckduckgo.com/?q={}"
w = "https://en.wikipedia.org/w/index.php?search={}"
:open w riptide searches Wikipedia. url.auto_search changes when text is searched: schemeless searches everything without https:// or another scheme, and never opens it as an address unless it starts with an engine's name. With url.open_base_url = true, :open w alone opens Wikipedia's home page.
Ctrl-a and Ctrl-x add or take one from the last number in the URL's path or query (page/9 → page/10). url.incdec_segments picks which parts they look at: host, port, path, query, anchor.
Searching, scrolling and zoom
/ and ? search the page, and n/N move between matches. Searches go on from the top after the last match, saying so, unless you set search.wrap = false (they stop at the last match) or search.wrap_messages = false (they wrap quietly). search.ignore_case (smart, always, never) and search.incremental set how matching works.
scrolling.smooth = true animates scrolling by keys, and scrolling.bar shows page scrollbars always, never or as thin overlay ones (after a restart). + and - step through zoom.levels, and = goes back to zoom.default:
"zoom.levels" = ["50%", "75%", "100%", "125%", "150%", "200%"]
Insert mode and key mappings
Clicking into a text field enters insert mode (input.insert_mode.auto_enter), and loading a new page leaves it (input.insert_mode.leave_on_load). A field the page focuses by itself, like a search box with autofocus, doesn't take insert mode unless you set input.insert_mode.auto_load = true. Your keys keep working in normal mode until you click or press i.
input.mode_override picks the mode a site's pages start in, and when you switch to their tab. Set it per site, e.g. :set -u ssh.example.com input.mode_override passthrough for a web terminal.
Digits before a binding are a count (3j); input.match_counts = false lets you bind digits themselves. A half-typed chain like g waits for the next key, or for input.partial_timeout milliseconds if you set one. :cmd-repeat 3 tab-next runs a command several times and :cmd-run-with-count 3 tab-focus gives it a count, which helps in bindings. :debug-keytester shows the name and binding of each key you press, until Escape, which helps when writing bindings.
bindings.key_mappings treats one key as another in every mode, before bindings are looked up. By default Ctrl-[ is Escape, Ctrl-m and Ctrl-j are Return, Ctrl-i is Tab and Ctrl-6 is Ctrl-^. To add your own, include the defaults you want to keep:
[bindings.key_mappings]
"<Ctrl-[>" = "<Escape>"
"<Ctrl-m>" = "<Return>"
"<Ctrl-g>" = "<Escape>"
The mouse's back and forward buttons go back and forward. With input.mouse.rocker_gestures = true, holding the right button and clicking the left goes back, and the other way round goes forward; pages lose their context menu.
input.spatial_navigation = true moves focus between links and fields with the arrow keys in passthrough and insert mode. input.media_keys = false stops the keyboard's play and pause keys from controlling pages. Both apply after a restart.
Hints
:hint [group] [target] labels elements and acts on the one you pick. f is :hint, and ;y is :hint links yank. The groups come from hints.selectors:
- Built in:
all,links,images,media,inputs, andblocks, the page's parts, which;x(:hint blocks hide) uses to hide one for good. - Your own: add a group with a CSS selector list. Your groups are added to the built-in ones, which stay.
c.hints.selectors = { code = "pre, code" }
rt.bind(";c", "hint code yank")
When a hint is followed is set by hints.auto_follow:
| Value | Follows |
|---|---|
unique-match (default) | As soon as one hint is left |
full-match | Only when you type a whole label, not when number-mode text narrows to one |
always | Either way |
never | Only when you press Return (:hint-follow) |
Labels use hints.chars. hints.min_chars makes them at least that long, and hints.scatter = false hands them out in order instead of spreading them over the alphabet. A page that starts loading ends hint mode unless hints.leave_on_load = false.
hints.mode = "word" labels each link with a word from its own text or URL, so you type news for a News link. Words come from hints.dictionary (default /usr/share/dict/words); links without a usable word get the shortest unused one. In rapid mode (:hint --rapid), hints.hide_unmatched_rapid_hints = false keeps the labels you're not typing on screen.
hints.auto_follow_timeout ignores keys for a moment after a hint is followed, so a fast second keystroke doesn't land in the page. :hint --rapid (;r) keeps the labels up after each pick.
Key hints
Type the start of a key chain, like g or ;, and pause: after keyhint.delay (500 ms), a popup lists every binding the keys can still become, with its command. Finish the chain to run it, or press Escape.
To leave chains out of the popup, add globs to keyhint.blacklist. They match the whole chain:
keyhint.blacklist = ["<Ctrl-x>*", "g$"]
The command line
In the command line, Tab / Shift-Tab cycle through completions. :open completes from search engines, quickmarks, bookmarks, history and, for paths starting with / or ~/, files. Every typed word must match, in any order.
In :open, history, bookmarks, quickmarks and tabs show each site's icon (remembered from your visits, but not from private windows). Command names, settings, setting values and themes match anywhere in the name: :set hints offers hints.chars and also colors.hints.bg. Names that start with what you typed come first, then names with a part that starts with it (after a ., - or _), then the rest. When no name contains what you typed, its letters in order still match, so :set clrhnt offers colors.hints.*; names with fewer gaps come first. The matched text is highlighted in the list (colors.completion.match.fg).
| Setting | What it does |
|---|---|
completion.open_categories | Which of those :open offers, in order |
completion.web_history.exclude | URL globs never suggested from history, e.g. ["*://*.bank.example/*"] |
completion.show | always, only after pressing Tab (auto), or never |
completion.height | Rows (12) or a share of the window (50%) |
completion.min_chars | Characters to type after the command before its arguments complete |
completion.cmd_history_max_items | How many command lines Up and Down remember |
completion.shrink | false keeps the list completion.height tall however few items it has |
completion.timestamp_format | When each history entry was last visited, e.g. %d %b %H:%M; empty hides it |
completion.delay | Milliseconds to wait after a key before updating the list, for slow history searches |
completion.quick | With one command or setting name left, Tab takes it and goes on to its arguments |
ui.overlay.position | floating shows the command and its list in a box near the top of the page (Themes) |
completion.use_best_match | Return on an unknown command runs the first one it starts, so :rel runs :reload |
:set, :quickmark-load, :bookmark-load and :session-load complete their own names. :session-save [name], :session-load name and :session-delete name manage sessions. With auto_save.session = true, the tabs are saved on quit and restored at the next start. |
The command line and prompts support readline keys (Ctrl-a/e/u/k/w/h, Alt-b/Alt-f by word, Alt-d/Alt-Backspace to delete a word, Ctrl-y to paste what was last deleted, Ctrl-v to paste the clipboard and Shift-Insert the primary selection, arrows), history (Up/Down), command chaining with ;;, and completion of command names.
To clean up what completion offers, select an entry with Tab and press Ctrl-d (:completion-item-del). It deletes history entries, quickmarks, bookmarks and sessions, and closes tabs listed by T. Ctrl-c (:completion-item-yank) copies the selected entry, and Ctrl-Shift-c copies it to the primary selection.
Every command is listed in the commands reference. . repeats the last command, and :messages shows earlier status bar messages.
The status bar
The status bar shows the mode, messages and the command line on the left. On the right are widgets, in the order statusbar.widgets lists them:
| Widget | Shows |
|---|---|
keypress | Keys typed so far, and the count |
url | The page's address, green for HTTPS |
scroll / scroll_raw | How far down the page you are: [Top], [42%], [Bot], [All] / just the number |
history | [<] and [>] when you can go back or forward |
tabs | [current/total] |
progress | Loading progress |
search_match | Match [2/14] after a / search |
downloads, muted, zoom | Running downloads, a muted tab, and a zoom other than 100% |
media | What the tab captures: [V], [A], [A/V], [Share] |
sharing | [sharing your screen] (or a window or tab) while any tab shares; :share-stop stops it |
clock, clock:%a %H:%M | The time, in an optional strftime format |
text:… | Fixed text |
lua:<name> | Text from config.lua or a plugin; see Status bar widgets |
statusbar.widgets = ["keypress", "url", "scroll", "tabs", "clock:%H:%M"]
statusbar.position puts the bar at the top or bottom. statusbar.show hides it:
neverkeeps it hidden except while you type a command or answer a prompt, since those happen in the bar.in-modealso shows it outside normal mode, and while a message is up.
Tabs and windows
The tab keys are in Keys and modes: J/K switch tabs, d closes one, u reopens it, T picks one by name.
Pinned tabs
Pinned tabs stay where you put them (drag or :tab-move them anywhere, between unpinned tabs too), shrink to their icon and number (tabs.pinned.shrink), and survive co unless you add --force. d on a pinned tab asks first (tabs.pinned.close: ask, refuse or close); :tab-close --force doesn't, and u reopens it pinned. With tabs.pinned.frozen (the default), :open in a pinned tab opens a new tab instead. Sessions remember which tabs are pinned.
The tab bar
The tab bar shows site icons (tabs.favicons.show: always, never or pinned) and works with the mouse: click to select, middle-click to close (tabs.close_mouse_button: middle, right or none; on the empty part of the bar it opens a new tab, see tabs.close_mouse_button_on_bar), scroll to switch (tabs.mousewheel_switching), and drag to reorder: the tab shrinks and follows the pointer, and the others move aside to show where it lands.
The current tab is the darkest in the bar. To also underline it (or, in a vertical bar, mark its right edge), set colors.tabs.selected.accent to a CSS color, e.g. :set colors.tabs.selected.accent #2ec4b6; :config-unset colors.tabs.selected.accent removes the line again.
Tab titles follow tabs.title.format (default {audio}{media}{index}: {current_title}), and shrunk pinned tabs follow tabs.title.format_pinned (default {index}). The fields are {index}, {aligned_index}, {current_title}, {current_url}, {host}, {perc} (loading progress), {audio} ([M] on a muted tab), {media} and {private}.
{media}, and the status bar's media widget, show what a page is capturing: [V] while it uses a camera, [A] for a microphone, [A/V] for both, and [Share] while it shares your screen, a window or a tab (see Sharing your screen). tabs.tooltips = false turns off the title-and-URL tooltip.
In a top or bottom bar, tabs share the width evenly. tabs.max_width caps each tab and tabs.min_width keeps them from getting narrower; once they don't fit, the bar scrolls to keep the current tab in view. tabs.title.alignment (left, center, right) places the title, and tabs.indicator.width sets the loading indicator's width (0 hides it):
tabs.max_width = 250
tabs.min_width = 120
tabs.title.alignment = "center"
tabs.position puts the bar at the top, bottom, left or right (a vertical list, tabs.width pixels wide), and tabs.show hides it: always, never, multiple (only with more than one tab) or switching (briefly after switching tabs).
New tabs and popups
Links that open new windows (target=_blank, window.open) open as tabs next to the current one, keeping window.opener. Closing the last tab is ignored, like qutebrowser.
After you close the current tab, tabs.select_on_remove picks the next one to show: next (default), prev, or last-used. J/K wrap around from the last tab to the first unless tabs.wrap = false. u can reopen the last tabs.undo_stack_size closed tabs (100 by default).
Where new tabs go is set by tabs.new_position.related (tabs opened from a page) and tabs.new_position.unrelated (everything else).
Modes per tab
With tabs.mode_on_change = "restore", each tab keeps its own mode: leave a tab while typing in insert mode, and you're back in insert mode when you return. The default normal leaves insert mode on every switch, and persist keeps the current mode.
Windows and private windows
:open -w url opens a new window and :open -p url a private one. Private windows use an in-memory profile shared by all private windows: no cookies or cache on disk, no history, and they're left out of sessions. Their status bar is gray. :close closes the current window and :quit closes all of them. Sessions save and restore every normal window.
Call windows
:open --call url opens a video call in a call window. When the call's page shares your screen, Chrome's own picker opens, so you can share a single window or the whole screen, or a tab with its sound. In an ordinary tab, screen sharing asks in the status bar and always shares the whole screen.
:open --call https://meet.google.com/abc-defg-hij
Well-known call services open in a call window by themselves: Google Meet, Microsoft Teams, Zoom's web client and join links, Webex, Jitsi Meet and Whereby. Their URL patterns are the default of content.call_sites; :open and links in ordinary tabs send matching pages to a call window, and once the page has loaded the status bar says so. :tab-call reopens any other tab in one.
To add a service, extend the list. To stop opening calls in their own window, clear it; screen sharing on those sites then shares the whole screen, unless you open the call with :open --call or :tab-call.
-- Setting the list replaces it, so list every site you want, e.g. your own Jitsi:
c.content.call_sites = { "meet.google.com", "teams.microsoft.com", "meet.example.org" }
-- Or opt out:
c.content.call_sites = {}
The settings page (:settings) and :help content.call_sites show the default list.
- Only the first tab: only the tab a call window opened with shares this way. Further tabs you open in that window are ordinary ones.
- Popups: popups from the call tab open in call windows of their own.
- The picker's Tab list only offers call tabs, not riptide's ordinary tabs.
- Sessions: call windows are saved as ordinary windows, so a restored call shares the whole screen until you reopen it with
--call. - Turning it off:
content.desktop_capture = falsestill refuses screen sharing everywhere, call windows included. - In the background: a call keeps running at full speed in a tab you've switched away from, as in Chrome, since it plays sound. A silent page in a background tab has its timers slowed to once a second.
- Stopping a share: see Sharing your screen.
Sharing your screen
While a page shares your screen, a window or a tab, its tab shows [Share] in place of [V] ([Share/A] with a microphone). The status bar's sharing widget says what's being shared, such as [sharing your screen], whichever tab you're on. When a share starts, the status bar names the site.
:share-stop stops every share, in any tab or window. The page is told as if you'd pressed Chrome's "Stop sharing", so the call site updates its own buttons. You can still stop from the site or from Chrome's bar in a call window.
Picture-in-picture
gp (:pip) floats the page's main video in a small window that stays on top while you work in other tabs, such as a call or a talk; gp again brings it back. Sites' own picture-in-picture buttons work too, including Meet's floating call window.
Some tiling window managers don't float it by themselves; it's titled "Picture in picture". In dwm, a rule floats it:
{ NULL, NULL, "Picture in picture", 0, 1, 0, 0, -1 },
Muting a call from another tab
cm (:call-mute) mutes or unmutes your microphone in the call while you're in another tab. It presses the call site's own mute key in the tab that's using the microphone, so the site's mute button stays right. content.call_mute_keys has each site's key (Meet <Ctrl-d>, Teams <Ctrl-Shift-m>, Zoom <Alt-a>, Webex <Ctrl-m>, Jitsi m); add others the same way.
- A flash of the call: Chromium only takes keys in the tab that's showing, so the call tab shows for a moment and then the tab you were on comes back. The first time, it shows for under a second, while the page gets ready for keys.
- Another window: a call in another window isn't reached; riptide names the key to press there.
tabs.tabs_are_windows = true opens every tab, and every popup, in its own window and hides the tab bar, which suits tiling window managers that arrange windows themselves.
window.hide_decoration = true asks the window manager for windows without a title bar or borders, which suits tiling window managers. It applies to windows opened after the change.
:tab-give and :tab-take move tabs between windows, and :tab-clone duplicates one.
Sessions, history and bookmarks
Where browsing data lives
| What | Where | Format |
|---|---|---|
| History | <data>/history.sqlite | SQLite; :history-clear --force empties it |
| Quickmarks | <config>/quickmarks | qutebrowser's: one name url per line |
| Bookmarks | <config>/bookmarks/urls | qutebrowser's: one url title per line |
| Sessions | <data>/sessions/<name>.toml | TOML |
Quickmarks and bookmarks sit next to the config, so you can keep them in dotfiles. Sessions keep each tab's page, its back/forward history (up to 50 pages each way) and how far it was scrolled.
In a restored tab, H and L load the saved pages again, so they come from the network rather than the cache, and anything typed into forms on them is gone. The scroll position is the one at the last autosave (auto_save.interval).
Sessions
:session-save [name], :session-load name and :session-delete name manage sessions, and :session-load completes their names. ZZ or :wq saves the tabs and quits; ZQ quits without saving. With auto_save.session = true, the tabs are saved on quit and restored at the next start.
Unnamed saves and the restore at startup use session.default_name. Left empty (the default), that's the session you last loaded with :session-load, or default if you haven't loaded one, so after :session-load work a :wq saves work.
:save writes everything to disk now: config, cookies, quickmarks, bookmarks and the session. Name some of them to save only those, e.g. :save cookies session.
With session.lazy_restore = true, a restored session loads only the tab you're on. The others keep their titles in the tab bar and load when you first switch to them, which makes restoring many tabs fast.
confirm_quit asks before :quit, or before closing the last window, quits the browser. List the reasons to ask: multiple-tabs (more than one tab is open), downloads (downloads are still running), always, or the default never:
confirm_quit = ["multiple-tabs", "downloads"]
Crash recovery
Every auto_save.interval milliseconds (15 s by default, 0 turns it off), the open tabs are saved for crash recovery. Quitting riptide deletes that save. Being stopped from outside keeps it, like a crash: Ctrl-C in its terminal, the terminal closing, kill, or logging out.
After a crash, the next start keeps the saved tabs as a session named after the time of the crash, such as _crashed-2026-10-06-115803 (UTC). The last five crashes are kept, and :session-load _crashed- completes their names.
| Start after a crash | What happens |
|---|---|
Plain riptide | The crashed tabs reopen. |
| With URLs on the command line | Only those URLs open. A message names the session holding the crashed tabs, and the new run's autosaves don't touch it. |
| The browser crashed again within a minute of reopening a crash's tabs | They aren't reopened a second time, in case they caused the crash. The start page opens, and a message points to :recover. |
:recover lists the tabs of each kept crash, by window. Tick the ones you want and press Reopen selected, or Reopen all; they open in the current window with their back/forward history. Forget deletes that crash's tabs.
Crash reports
When riptide itself crashes because of a bug (a Rust panic), it writes a report to crashes/ in its data directory (riptide --paths shows where). The report has the version, the error and a backtrace, and no URLs or page content. The next start shows a message with the report's path. The newest ten reports are kept, and nothing is sent anywhere unless you send it.
:crash-report shows the newest report in a tab, where you can edit it before sending:
- Open a GitHub issue: fills in a new issue with the report. A report too long for a link is shortened, and the page asks you to attach the file.
- Email it: opens your mail program with the report. It only appears when
crash_report.emailis set. - Copy: copies the report.
Backtraces name the source files of the build, which for a build of your own include its directory.
When Chromium itself crashes (the browser, or a page's process), its crash reporter leaves a crash dump in pending/ or completed/ in the data directory. The next start mentions it, and :crash-report lists the dumps with their paths. A dump is a binary snapshot of the crashed process's memory, so it can hold parts of the pages that were open: only attach one to an issue if you're comfortable with that. Dumps are never uploaded, and the newest ten are kept. The crash reporter is turned on by crash_reporter.cfg next to the riptide executable, which riptide writes and the packages ship.
A crashed tab
Pages run in processes separate from the browser. If a page's process crashes, runs out of memory or is killed, the browser keeps running and the tab shows a notice saying why. r reloads the page, and the tab keeps its back and forward history.
History
:history (-t for a new tab) shows your browsing history by day, with a search box. :open completes from history as you type, with every typed word matching somewhere in the title or URL. completion.web_history.max_items sets how many entries completion shows. :history-clear --force empties it, and :history-import brings in qutebrowser's.
Quickmarks and bookmarks
| Keys | Does |
|---|---|
m | Quickmark this page (type a name, then Return) |
b / B | Open a quickmark here / in a new tab |
M | Bookmark this page |
gb / gB | Open a bookmark here / in a new tab |
:quickmark-add, :quickmark-del, :bookmark-add and :bookmark-del do the same from the command line.
:bookmark-list shows your quickmarks and bookmarks on a page (-t in a new tab). If you edit the quickmarks or bookmarks/urls files by hand, :quickmarks-reload (or :bookmarks-reload) reads them again. :quickmark-save writes the quickmarks file, although every change is saved anyway.
Restarting
:restart saves your tabs, quits, and starts riptide again with the same config and data directories, then restores them. Use it after changing a setting that only applies at startup, like colors.webpage.darkmode.enabled.
Prompts, downloads and permissions
Prompts
Everything that needs an answer appears in a box floating near the bottom of the page, one at a time. prompt.position = "center" puts the box in the middle of the page, "docked" puts it in a strip above the status bar, and prompt.width sets the box's width. The floating box lists each answer on its own line with its key, and you can click an answer instead of pressing the key:
- JavaScript
alert,confirm,promptand leave-page warnings - HTTP logins (username, then a hidden password)
- where to save a download
- site permission requests (camera, microphone, location, notifications…)
| Mode | Keys |
|---|---|
| prompt (text) | type, readline keys (Ctrl-w deletes one path component), Return accepts, Escape cancels |
| yesno | y / n, Return (the default), Escape cancels |
Permissions
A permission request shows the asking site in large type, with its host in bold so a look-alike name stands out, and an icon for each thing it wants (camera, microphone, location, notifications, screen…). A question belongs to the tab that asked: if a tab in the background asks, you stay where you are, the tab gets a ? badge in the tab bar, and the question shows when you go to that tab.
For permission prompts:
yallows once andn(orEscape) means "not now".Aalways allows andNalways blocks. These are saved as per-site settings inautoconfig.toml, as in qutebrowser, so they survive restarts. That includes camera and microphone. Chromium also remembersyfor its own permission prompts.
The content.geolocation, content.notifications.enabled, content.media.audio_capture, content.media.video_capture, content.desktop_capture, content.mouse_lock (pointer lock, as games use) and content.register_protocol_handler (a site offering to handle mailto: links) settings (ask, true or false) answer without asking.
Notifications
content.notifications.presenter picks where notifications from sites go:
| Presenter | Where |
|---|---|
auto (the default) | Your desktop, sent by Chromium |
libnotify | Your desktop, sent by riptide with notify-send (from libnotify), following the settings below. Clicking one shows its tab. |
messages | riptide's status bar, for messages.timeout milliseconds |
With libnotify:
| Setting | What it does |
|---|---|
content.notifications.app_name | The app name on the notification (riptide), which notification services can match to style riptide's own |
content.notifications.urgency | low, normal or critical |
content.notifications.timeout | Milliseconds it stays; -1 lets your desktop decide and 0 keeps it until you dismiss it |
content.notifications.site_icon | Show the site's icon |
content.notifications.show_origin | Put the site it came from on the first line (this also starts status bar messages with it) |
How notifications look (colors, fonts, position, sound) is up to your notification service: dunst, mako, GNOME or KDE. Most can style one app differently, for example in dunst:
[riptide]
appname = riptide
background = "#181616"
foreground = "#c5c9c5"
If notify-send isn't installed, riptide shows the notification in the status bar and says why.
Links to other programs
A link riptide can't show, like mailto:, magnet: or zoommtg:, asks before going to your desktop's handler (xdg-open). content.unknown_url_scheme_policy = "allow-all" hands them over without asking, and "disallow" never does.
Untrusted TLS certificates (self-signed, expired, wrong host…) ask before the page loads: y loads it once, A always loads that site, N always blocks it. content.tls.certificate_errors (ask, block or load-insecurely) sets the default and can be set per site. :debug-clear-ssl-errors forgets the y answers given this session.
Per-site settings
The permission settings above, content.tls.certificate_errors, content.blocking.enabled, content.headers.user_agent and the content.* settings listed in Privacy can differ per site. The last matching pattern wins. Patterns are hosts (example.com, *.example.com for subdomains too), origins (https://meet.example.com) or match patterns (*://*.example.com/app/*):
:set -u https://meet.example.com content.media.video_capture true
:set -u *.example.org content.javascript.enabled false
:config-unset -u https://meet.example.com content.media.video_capture forgets one site's value, and the Sites tab of :settings lists and forgets them with a click.
[per_domain."*.example.com"] # config.toml or autoconfig.toml
"content.blocking.enabled" = false
rt.set("content.geolocation", "false", "*.tracker.example") -- config.lua
Downloads
Downloads go to downloads.location.directory, or the system Downloads folder if that's empty (on Linux, XDG_DOWNLOAD_DIR or ~/.config/user-dirs.dirs). Server-suggested names are reduced to a plain file name, existing files get (1) appended, and typing an existing path asks before overwriting. Set downloads.location.prompt = false to skip the question. The status bar shows ↓2 41% while downloads run.
| Command | |
|---|---|
:download [url] | Download a URL, or the current page |
;d | Hint a link to download |
:download-cancel, :download-open | The newest running / finished download, or the one given as a count (2:download-open) |
:download-retry | Start the newest failed or cancelled download again |
:download-remove [--all] | Take a download off the list, cancelling it if it's running; --all takes every finished one, like :download-clear |
:download-delete | Delete the newest finished download's file |
:download-clear | Forget finished downloads |
:downloads | A page listing this session's downloads with their numbers and progress |
In the "Save file to" prompt, Tab completes file and directory names, as in a shell, and Alt-e picks the folder with fileselect.folder.command (see below). Ctrl-x opens the file instead of keeping it: it downloads to a temporary folder and opens with downloads.open_dispatcher or your desktop's default; :prompt-open-download zathura names a program. Alt-y copies the download's URL, in any prompt that has one.
:download-open uses downloads.open_dispatcher if you set one ({} is the file, or it goes at the end), and the system's opener (xdg-open, open or start) otherwise.
| Setting | What it does |
|---|---|
downloads.location.suggestion | What the save prompt starts with: folder and name (both), the folder (path), or the name (filename; a bare name saves into the download folder) |
downloads.location.remember | Start the prompt in the folder the last download went to (on by default) |
downloads.remove_finished | Take finished downloads off the list after this many milliseconds (-1, the default, keeps them) |
Choosing files to upload
Upload fields open Chromium's file dialog. To use a terminal file manager instead, set fileselect.handler = "external". Riptide runs the command for the field (fileselect.single_file.command, fileselect.multiple_files.command or fileselect.folder.command) with {} replaced by a file to write the chosen paths to, one per line. The defaults run ranger in xterm, as qutebrowser does. For yazi in foot:
fileselect.handler = "external"
fileselect.single_file.command = ["foot", "yazi", "--chooser-file={}"]
fileselect.multiple_files.command = ["foot", "yazi", "--chooser-file={}"]
Privacy and content blocking
Content blocking
Ads and trackers are blocked at the network level with Adblock Plus filter lists, using Brave's adblock-rust. Run :adblock-update once to download the lists in content.blocking.adblock.lists (by default EasyList, EasyPrivacy, and uBlock Origin's own lists: uBlock filters, Privacy, Quick fixes and Unbreak, as uBlock Origin enables them; file:// lists work too). The compiled engine is cached in the data directory and loads in the background at startup. Hosts files (lines like 0.0.0.0 ads.example.com, as in StevenBlack's lists) work in the same setting; riptide recognizes them and blocks each listed host:
"content.blocking.adblock.lists" = [
"https://easylist.to/easylist/easylist.txt",
"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
]
The status bar shows how many requests were blocked on the current page, e.g. ⊘12 (the blocked widget in statusbar.widgets).
content.blocking.enabledturns blocking on or off.content.blocking.whitelistlists hosts where nothing is blocked (subdomains included).- Top-level pages are never blocked, so a bad rule can't make a site unreachable. Their tracking parameters are taken out, though (
$removeparam):?utm_source=mail&id=7loads as?id=7. - Element-hiding rules (
##.ad,example.com##.sponsored) are applied once a page loads: the site-specific ones, and the generic ones for the classes and ids the page uses, checked again 2 and 6 seconds later for ads that arrive late. - Scriptlet rules (
example.com##+js(set-constant, adsEnabled, false)) run in the page before its own scripts, which is how lists defeat anti-adblock walls and in-player video ads.$redirectrules answer a blocked request with a harmless stand-in (an empty script, a 1×1 image), so the page carries on as if it had loaded. Both use uBlock Origin's scriptlets and stand-ins, built into riptide. As in uBlock Origin, the "trusted" scriptlets, which can click page elements or set arbitrary values, only run for rules from uBlock Origin's own lists.
If you set content.blocking.adblock.lists yourself before uBlock Origin's lists became defaults, add them to get most of the scriptlet rules:
"content.blocking.adblock.lists" = [
"https://easylist.to/easylist/easylist.txt",
"https://easylist.to/easylist/easyprivacy.txt",
"https://ublockorigin.github.io/uAssets/filters/filters.min.txt",
"https://ublockorigin.github.io/uAssets/filters/privacy.min.txt",
"https://ublockorigin.github.io/uAssets/filters/quick-fixes.min.txt",
"https://ublockorigin.github.io/uAssets/filters/unbreak.min.txt",
]
Run :adblock-update after changing the lists.
Procedural element hiding works too, for elements CSS alone can't pick out: :has-text(), :upward(), :matches-css() (and -before, -after), :matches-attr(), :matches-path(), :min-text-length() and :xpath(), with the actions :remove(), :style(), :remove-attr() and :remove-class(). They're applied as the page loads and again whenever it changes, so content added later is caught too.
Frames get element hiding too, by the rules for their own site: an ad in a frame from another site is hidden like one in the page. The page's site still decides whether anything is blocked, so allowing a site (content.blocking.whitelist) covers its frames.
Not supported yet: scriptlets in frames from another site than the page (frames from the page's own site get them).
Your own rules
content.blocking.adblock.rules holds rules of your own, in the same syntax as the lists. They apply as soon as you change them, without :adblock-update, and work even with no lists at all:
"content.blocking.adblock.rules" = [
"news.example.com##.newsletter-signup",
"||tracker.example.net^",
]
To hide something on a page without writing the rule yourself, press ;x (:hint blocks hide) and pick it. riptide proposes a rule for it, such as example.com##.promo-box, which you can edit (to ##.promo-box for every site, say) before pressing Return. The element and others like it disappear at once, and the rule is saved to content.blocking.adblock.rules. To take a rule back, remove it from the setting, on the settings page or with :set.
Your rules are never trusted: their scriptlets can't use uBlock Origin's trusted ones.
Network traffic
Chromium calls Google in the background. riptide turns off the calls that only serve Google and keeps the security updates (crates/rt-cef/src/privacy.rs). Measured on a fresh profile left on about:blank for 90 seconds, with --log-net-log:
| Request | Purpose | Status |
|---|---|---|
update.googleapis.com, edgedl.me.gvt1.com | Component updates (all of them for one run if you turn on content.widevine) | Only the components Chromium marks as security data still update: certificate revocation lists (CRLSets) and the subresource filter rules. The ~20 others no longer download, saving ~115 MB per profile. These include Widevine, optimization hints, the on-device suggest model, TTS and the password-strength data. |
clients2.google.com/time | Secure network time, used to explain certificate date errors | kept |
redirector.gvt1.com/…/dict | Spell-check dictionary | only once per language in spellcheck.languages (empty by default) |
www.google.com/async/folae | AI Mode eligibility | off (--disable-features=AimEnabled) |
www.google.com preconnects | Default search engine warm-up | off (Chrome's default search engine is disabled; riptide has its own url.searchengines) |
accounts.google.com/ListAccounts | Google accounts in the cookie jar | still sent once at startup. Google sign-in is off, but something still asks for the cookie jar; it carries your google.com cookies if you have any. |
Beyond the pages you visit, riptide goes online for what you set up or ask for: :adblock-update downloads the filter lists (from easylist.to and ublockorigin.github.io by default), :extension-install and :extension-update ask the Chrome Web Store (clients2.google.com), and plugins added with rt.pack.add are fetched with git the first time riptide starts with them, and when you check for updates.
The preferences are written into the profile (Local State, Default/Preferences) before Chromium starts, since most of these services start within 100 ms. To check for yourself: riptide --basedir /tmp/t --log-net-log=/tmp/net.json about:blank, then grep -o '"url":"[^"]*' /tmp/net.json | sort -u.
Proxy and network
| Setting | What it does |
|---|---|
content.proxy | system (default), none, a proxy URL (socks5://127.0.0.1:9050, http://proxy:3128), or pac+ and a PAC script's URL. Names are looked up through a SOCKS5 proxy, not locally |
content.webrtc_ip_handling_policy | Which addresses video calls may reveal: all-interfaces (default), default-public-and-private-interfaces, default-public-interface-only, or disable-non-proxied-udp to keep WebRTC behind the proxy |
content.dns_prefetch | false stops looking up the hosts of links before you follow them |
content.canvas_reading | false stops pages reading back what they drew, a common fingerprinting trick; some sites break (after a restart) |
content.cache.size | Disk cache size in bytes; 0 lets Chromium choose (after a restart) |
content.local_content_can_access_file_urls | true lets file:// pages read other local files (after a restart) |
content.webgl | false turns off WebGL, which 3D graphics need and fingerprinting scripts use (after a restart) |
content.proxy = "socks5://127.0.0.1:9050"
content.webrtc_ip_handling_policy = "disable-non-proxied-udp"
Cookies, JavaScript, images and the user agent
| Setting | What it does |
|---|---|
content.cookies.accept | all (default), no-3rdparty to refuse cookies from other sites embedded in a page, or never |
content.cookies.store | false makes every cookie last only until the browser closes |
content.javascript.enabled | false turns JavaScript off; set it per site to block or allow it on chosen sites only |
content.headers.user_agent | The user agent sites see, in requests and in navigator.userAgent; empty for Chromium's own. Set it per site for sites that check it |
content.headers.do_not_track | Sends DNT: 1 (the default); false stops it |
content.headers.referer | same-domain (default) sends the Referer only within a site and its subdomains; always or never |
content.headers.accept_language | The languages sites are asked for, e.g. de-DE,de;q=0.9. Requests follow a change at once; navigator.languages after a restart |
content.headers.custom | Extra headers for every request, e.g. { "X-Requested-By" = "me" } |
content.images | false stops loading images |
content.autoplay | false keeps videos from playing until you interact with the page (after a restart) |
content.pdf_viewer | false downloads PDFs instead of showing them |
content.prefers_reduced_motion | true asks pages for fewer animations (after a restart) |
content.javascript.can_close_tabs | false stops pages closing their own tab with window.close() (login popups do this) |
content.javascript.log_message.levels | Page console messages to show in the status bar and :messages, e.g. ["error", "warning"]; per site too |
content.mute | true mutes pages; :tab-mute mutes one tab instead |
content.javascript.can_open_tabs_automatically | true lets pages open tabs without a click (popups) |
content.javascript.clipboard | none, access (copy after a click, the default) or access-paste (read it too) |
content.images, content.mute, popups, the clipboard, JavaScript, console messages and the user agent can also be set per site:
content.cookies.accept = "no-3rdparty"
[per_domain."*.example.org"]
"content.javascript.enabled" = false
"content.images" = false
Private windows
:open -p url opens a private window. Private windows share an in-memory profile: no cookies or cache on disk, no history, and they're left out of sessions. Their status bar is gray.
The sandbox
Riptide runs Chromium's sandbox wherever Linux allows it. See Installing for how to enable it on Ubuntu.
Pages: dark mode, spell checking, DRM
Dark mode
colors.webpage.preferred_color_scheme(auto,lightordark) is what pages see inprefers-color-scheme. It applies immediately.colors.webpage.darkmode.enabled = truerenders light pages dark with Chromium's automatic dark mode. It applies at once to open tabs, and can be set per site, e.g.:set -u docs.example.com colors.webpage.darkmode.enabled falsefor a site that's already dark.colors.webpage.bgis the color a new tab shows before its page paints; set it to a dark color such as#1e1e2eso loading pages don't flash white.
Saving and printing
| Command | What it does |
|---|---|
:print | Opens the system print dialog |
:print --pdf ~/page.pdf | Saves the page as a PDF, backgrounds included |
:screenshot ~/shot.png | Saves what the tab shows as an image; .jpg and .webp work too. It won't replace an existing file without --force. |
gf, :view-source | Shows the page's source in a new tab |
:debug-dump-page ~/page.html | Saves the page's current HTML, as scripts have changed it |
wi, :devtools | Opens or closes the developer tools; :devtools-focus brings them to the front |
Spell checking
Off by default. Turn it on with a list of languages, e.g. c.spellcheck.languages = { "en-US", "de-DE" } in config.lua or :set spellcheck.languages '["en-US"]'. Chromium then downloads each dictionary once from Google (redirector.gvt1.com) and underlines mistakes as you type.
To keep that request away from Google, install dictionaries yourself first: :spell-install en-US de-DE, or :spell-install alone to pick from the 47 languages (type a code or a name such as german) downloads them from Chromium's own dictionary repository at a fixed version, checks each file against a checksum built into riptide, saves it where Chromium looks, and turns the language on. Chromium then uses that file and fetches nothing.
From the keyboard, in a text field:
:spell-suggestlists fixes for the word at the text cursor as completions.Tabpicks one,Returnreplaces the word, and you're back in insert mode.:spell-addadds that word to your dictionary.
Nothing is bound by default. For example, rt.bind("<Ctrl-s>", "spell-suggest", "insert"). Right-click suggestions work too.
Widevine (DRM)
Off by default. With c.content.widevine = true and a restart, Chromium downloads Google's Widevine CDM (about 21 MB) into the data directory, and it loads from the next start. Chromium has no switch for a single component, so component updates are on during that one run. Once the CDM is installed they go back off, and the CDM itself isn't updated. To update or remove it, delete <data>/WidevineCdm.
Limits: only VP9/AV1 streams work (prebuilt CEF has no H.264/AAC), Linux Widevine is the software-only level (L3) that services often cap at lower resolutions, and this hasn't been checked against Google's Widevine terms for third-party browsers.
Programs, userscripts and Greasemonkey
Programs and userscripts
:spawn [-v] [-m] [-o] [-d] <cmd> [args]runs a program, with arguments split like a shell would (no shell runs).{url}is the current page.-vreports success too,-mshows the program's output as messages,-oshows it in a new tab (riptide://process/), and-ddetaches. A non-zero exit is shown as an error. For example,rt.bind(",m", "spawn -d mpv {url}").:spawn -u <name>runs a userscript. It is looked up in<config>/userscripts/, then<data>/userscripts/, thenPATH. It getsRIPTIDE_URL,RIPTIDE_CURRENT_URL,RIPTIDE_TITLE,RIPTIDE_SELECTED_TEXT,RIPTIDE_SELECTED_HTML,RIPTIDE_HTML/RIPTIDE_TEXT(files with the page's HTML and text),RIPTIDE_TAB_INDEX,RIPTIDE_COUNT,RIPTIDE_MODE,RIPTIDE_USER_AGENT,RIPTIDE_CONFIG_DIR,RIPTIDE_DATA_DIR,RIPTIDE_DOWNLOAD_DIRandRIPTIDE_VERSION. Commands it writes toRIPTIDE_FIFO, one per line, run when it exits. A one-word argument is unquoted as qutebrowser does it, somessage-info 'two words'andfake-key \awork.- Hints can run them on a link:
:hint links spawn mpv {hint-url}(the URL is appended if there's no{hint-url}), or:hint links userscript name, which gets the link asRIPTIDE_URLandRIPTIDE_MODE=hints. For example,rt.bind(";m", "hint links spawn mpv"). :open-editor, orCtrl-ein insert mode, edits the focused text field ineditor.command(defaultgvim -f {file} -c "normal {line}G{column0}l", as in qutebrowser). The text is written back when the editor exits successfully. For a terminal editor:c.editor.command = { "foot", "nvim", "+call cursor({line}, {column})", "{file}" }.:edit-textis the same command under qutebrowser's name.:edit-urledits the page's address in the editor and opens what you save. It takes:open's flags, so:edit-url -topens the result in a new tab.:cmd-editedits the command line you're typing in the editor and puts it back, or runs it with--run. It works from command mode, so bind it there:rt.bind("<Ctrl-x>", "cmd-edit", "command").
Greasemonkey scripts
*.js files in <data>/greasemonkey/ (as in qutebrowser) or <config>/greasemonkey/ run in matching pages. They follow the usual // ==UserScript== block: @match, @include, @exclude, @run-at (document-start, document-end (the default) or document-idle) and @noframes. Scripts get GM_info, GM_addStyle, unsafeWindow, and GM_getValue, GM_setValue, GM_deleteValue and GM_listValues, plus the promise versions under GM.*. Values are kept per script and survive restarts; a page sees the values as they were when it loaded. @require URLs are downloaded once, into the data directory, and run before the script. A new @require is fetched in the background; reload the page once it says the download is done. :greasemonkey-reload reads the files again; reload a page to run the new versions.
Two more APIs need the script to ask for them with @grant:
| API | What it does |
|---|---|
GM_xmlhttpRequest (GM.xmlHttpRequest) | Fetches a URL from another site, with the page's cookies for that site. It only reaches the page's own host and the hosts listed with @connect (and their subdomains); @connect * allows any. Responses are text, or parsed JSON with responseType: "json". |
GM_openInTab (GM.openInTab) | Opens an http(s) URL in a new tab, in the background with GM_openInTab(url, true) or { active: false }. |
// ==UserScript==
// @match https://news.example/*
// @grant GM_xmlhttpRequest
// @connect api.example.org
// ==/UserScript==
GM_xmlhttpRequest({ url: "https://api.example.org/scores", responseType: "json",
onload: (r) => console.log(r.response) });
A request riptide refuses calls onerror with error: "not allowed" and says why in the status bar.
Passwords
The password plugins fill logins from your password manager's command line tool. Password manager extensions can't fill logins in riptide, so this is the way to use Bitwarden, KeePassXC or pass here. They live in riptide-plugins: add the one for your password manager in config.lua.
rt.pack.add({ "https://github.com/joshzcold/riptide-plugins", subdir = "pass" })
Or open :plugins, press Browse and Add the one you want. riptide installs it the first time it starts with this (it needs git), along with the passwords plugin it fills through, and asks you to allow each: the password manager plugin to run programs (your password manager), and passwords to fill in pages. Like other plugins from git, they stay on the version installed until you update them from :plugins. Add more than one, and the picker lists the logins of all of them.
| Key | Command | Fills |
|---|---|---|
<Space>pp | :password-fill | the username and the password |
<Space>pu | :password-fill-username | only the username |
<Space>pw | :password-fill-password | only the password |
It finds the logins saved for the site you're on. If there's more than one, it asks which, each with a key: 1, 2, …. It fills the form around the focused field, or the first form with a password field, so you don't need to click into it first.
A login saved for example.com fills on example.com and its subdomains, and never on a name that only contains it, such as example.com.evil.net. If the tab moves to another site while your password manager answers, nothing is filled. Passwords aren't kept in the command history, :messages or riptide's log.
Password managers
Plugin (subdir) | Tool | Unlocking |
|---|---|---|
pass | pass, or gopass with gopass = true | GPG's own pinentry |
rbw | rbw, for Bitwarden | rbw's agent and pinentry |
bitwarden | the Bitwarden CLI (bw) | riptide asks for the master password once and keeps the session until riptide quits or :password-lock |
keepassxc | keepassxc-cli, with database = "~/Passwords.kdbx" | riptide asks for the database password on every fill, or keeps it for remember = 300 seconds |
The master and database passwords go to the tool on its input or in its environment, never on its command line, where other programs could see them. For pass and gopass, use a graphical pinentry (such as pinentry-gnome3 or pinentry-qt): riptide has no terminal for a text one.
How entries are matched
- pass and gopass: an entry belongs to a site when part of its path is the site, such as
websites/example.com/alice. The first line is the password. The username is alogin:,user:,username:oremail:line, or else the part of the path after the site (alice). - Bitwarden and KeePassXC: an entry belongs to a site when one of its saved addresses (or its name) is the site.
Options
Each password manager plugin takes its own options, and passwords takes the ones about filling; add passwords yourself only to pass those:
rt.pack.add({
{ "https://github.com/joshzcold/riptide-plugins", subdir = "keepassxc", opts = {
database = "~/Passwords.kdbx",
keyfile = "~/Passwords.key", -- optional
remember = 300, -- seconds to keep the database password
command = "/opt/bin/keepassxc-cli", -- the tool, if it isn't on your PATH
} },
{ "https://github.com/joshzcold/riptide-plugins", subdir = "passwords", opts = {
submit = true, -- press the form's submit button after :password-fill
keys = { login = "<Ctrl-Shift-l>" }, -- your own keys; false for none
} },
})
pass also takes store = "~/.password-store" (if not $PASSWORD_STORE_DIR) and gopass = true; rbw and bitwarden take command.
Extensions
riptide runs Chrome extensions that use Manifest V3, such as uBlock Origin Lite, Bitwarden or KeePassXC-Browser. Firefox add-ons don't run (riptide is Chromium), and neither do Manifest V2 extensions such as the original uBlock Origin, which Chromium no longer supports.
Installing
- Find the extension on the Chrome Web Store. Ignore its "Switch to Chrome" banner; riptide shows a message saying how to install instead.
- On the extension's page, run
:extension-install. If the store offers a download (Add to Chrome), that works too: riptide installs the.crxfile instead of asking where to save it. - riptide shows what the extension asks for, such as "read and change everything on every site you visit", and asks before installing. Extensions load when riptide starts, so it then offers to restart.
:extension-install also takes a store page's address or an extension's id from anywhere, or a .crx file (/path/to/file.crx). The Extensions tab (:extensions) has the same steps, and a box to paste a store page into.
:extension-install https://chromewebstore.google.com/detail/ublock-origin-lite/ddkjiahejlhfcafbddmgiahcphecmpfh
Installed extensions keep their Web Store id, so their settings carry over between versions, and desktop apps that talk to them recognise them. They're in extensions/ in the data folder (riptide --paths).
Managing them
:extensions opens the Extensions tab of :settings. It lists every extension with its version, where it's from and what it can do, and these buttons:
- Popup opens the extension's popup over the top right of the page, as Chrome does. Drag its bar to move it; the next popup opens where you left the last one. Escape or its ✕ closes it, and so does switching tabs. Options opens its options page in a tab.
- Remove deletes an installed one.
- Check for updates asks the Web Store for newer versions. Update to … then shows what the new version asks for that the old one didn't, before installing it.
- Chrome's extensions page turns extensions on or off, and shows their errors.
When something changed since riptide started, the tab says so, with a Restart now button.
| Command | |
|---|---|
:extension-install [page, id or file] | install or update (the store page you're on, without an argument), after showing what it asks for |
:extension-open <name> [popup|options] | open its popup over the page (or, without one, its options in a tab) |
:extension-update [name] | check every installed extension for updates, or update one |
:extension-remove <name or id> | delete an installed extension |
:extensions | the Extensions tab |
Names complete with Tab. See Limits for what works differently from Chrome.
To load an extension you're writing, or one you unpacked yourself, list its folder:
# config.toml
extensions.load = ["~/code/my-extension"]
What works
Blocking, and changes an extension makes to every page (such as dark mode or hiding page elements), work in every tab. So do extensions' options pages and settings.
What doesn't work is anything that needs the extension to know which site you're on. riptide's tabs aren't Chrome's, so to an extension you're never "on" a site. See the first row of Limits, and Password managers.
Limits
| Limit | What to do instead |
|---|---|
| Extensions can't tell which site you're on. Anything that depends on it doesn't work: uBlock Origin Lite's per-site switch, filtering level and element picker (its popup says "not a website"), and password managers' login suggestions. | Settings that don't depend on the site, such as filter lists and the default blocking level, work from the popup or options. For logins, see Password managers. |
| Only Manifest V3. Chromium no longer runs Manifest V2 extensions, such as the original uBlock Origin, and riptide refuses to install them. Firefox add-ons don't run either. | uBlock Origin Lite, or riptide's own ad blocker (content.blocking). |
| Changes need a restart. Extensions load when riptide starts. | Accept the "Restart now?" question, or use Restart now on the Extensions tab. |
| No automatic updates. | Press Check for updates on the Extensions tab, or run :extension-update. |
| Not in private windows. | Use a normal window. |
| Chrome's own Remove only hides an extension until the next start, because riptide loads it from its folder each time. | Remove on the Extensions tab, or :extension-remove. |
| Desktop apps are linked at startup. riptide only finds a password manager's desktop app if the app set itself up for another Chromium browser first. | Turn on the app's browser integration for Chromium or Chrome, then restart riptide. |
| Not tested yet: extensions' keyboard shortcuts and passkeys. | Please report what you find. |
What an install checks
riptide downloads extensions over HTTPS from Google, and checks that the file's key gives the extension id you asked for. It doesn't check the Web Store's signature on the file. A .crx file you install from disk has no id to check, so only install files you trust. Either way, you see what the extension asks for before anything is installed.
Password managers
Password manager extensions install, unlock and sync, but they can't fill logins. To suggest a login, a password manager asks which site you're on, and in riptide the answer is always "none". So its menu in a login field says "No items to show", and its "fill this page" button does nothing. Your vault is fine; the extension just never learns which site's logins to offer.
To fill logins, use the passwords plugin instead. It reads the same vault through your password manager's command line tool, finds the logins for the site you're on, and fills them with <Space>pp:
| Password manager | Use the passwords plugin with |
|---|---|
| Bitwarden | rbw or the Bitwarden CLI (bw) |
| KeePassXC | keepassxc-cli |
| pass, gopass | pass or gopass |
1Password and Proton Pass have no backend in the passwords plugin yet.
Desktop apps
KeePassXC-Browser and 1Password talk to their desktop app through a "native messaging host" that the app installs for other browsers. When riptide starts, it links the hosts it finds for Chromium, Chrome, Brave, Edge and Vivaldi into its data folder. It only links a host that allows one of your installed extensions. In KeePassXC, turn on browser integration for Chromium.
From the terminal
While the browser is running, riptide hands its arguments to that instance (per profile, so --basedir instances stay separate) and exits:
riptide https://example.com # opens per new_instance_open_target (default: new tab)
riptide --target tab-bg notes.html # relative files become file:// URLs
riptide ':tab-focus 1' ':reload' # arguments starting with ':' run as commands
With several windows open, URLs go to the one you used last; new_instance_open_target_window can pick the first-opened or last-opened window instead.
The browser listens on a Unix socket in $XDG_RUNTIME_DIR/riptide/ (or the data directory), inside a 0700 directory and with 0600 permissions, so only your user can send commands. On Windows each start is a new instance for now.
Internal pages
The tab bar, status bar and overlay are HTML pages served from the browser itself at riptide://ui/…. Web pages can't link to, frame or redirect to riptide:// addresses, and only riptide://ui/ pages get the rt.send() channel to Rust. The browser accepts only the messages each page is allowed to send.
Pages you can open: riptide://help/ (:help), riptide://history/ (:history), riptide://downloads/ (:downloads), riptide://settings/ (:settings) and riptide://changelog/ (:changelog). The first start after an update opens the changelog in a background tab when the version's minor or major number changed; changelog_after_upgrade (major, minor, patch or never) sets how big a step that takes.
Config files
Run riptide --paths to see where config and data live. All config files are optional and load in this order (later wins):
| File | Purpose |
|---|---|
autoconfig.toml | Written by :set, :bind and :unbind. Don't edit it by hand. |
config.toml | Declarative settings and bindings. See the example below. |
config.lua | The same, as a Lua 5.4 program. See Lua. |
The directories for each platform are listed in Installing.
:settings opens a page with every setting, grouped by section and searchable. Each one has a control for its kind (a checkbox, a list of choices, a number, a color picker, or rows for lists and maps), and a change applies at once and is saved in autoconfig.toml, like :set. The page shows where a changed value came from, marks settings your config.toml or config.lua also sets (those win at the next start), and has a reset button for anything not at its default. Settings that can differ per site list their per-site values under the setting, each editable, with ✕ to forget it and "+ For a site" to add one for a pattern such as *.example.com. In normal mode, j and k move a highlight between settings and Return edits the highlighted one: a switch toggles, a choice steps to the next, and a text field gets the cursor (Escape leaves it). f hints reach every control too.
The page's Keys tab lists each mode's bindings, marking the ones you added or changed. Edit a binding's command in place, remove it with ✕, or put a default back with ↺ (removed defaults are listed at the bottom with a Restore button). To add one, type the keys in riptide's notation (gx, <Ctrl-y>) or press Record and then the keys, enter a command and press Bind. The page warns first when the new keys are the start of another binding, or another binding is the start of them, since riptide then waits to see which you mean. Like :bind, it's saved in autoconfig.toml. Keys riptide itself uses in insert mode, such as Escape, can't be recorded; type them instead.
The Sites tab lists every site with saved values: the answers you gave with A or N when it asked for the camera, microphone, location or notifications, certificate exceptions, and anything set with :set -u. ✕ forgets one value (permissions go back to asking), "Forget all" forgets the site, and "Clear data" deletes its cookies and stored data (local storage, IndexedDB, cache). The field at the top clears any site, e.g. example.com. One-time y answers are kept by Chromium itself and aren't listed.
Every setting is listed in the settings reference, and the completion popup lists them as you type :set , with each one's current value. After a name it offers the values: true/false, a setting's choices, or its current and default values. In the browser:
:set hints.chars asdfchanges a setting;:set hints.uppercase!toggles one;:set hints.charsshows the value.:bind <Ctrl-x> tab-closeadds a binding (--mode insertfor other modes);:bind <Ctrl-x>shows one;:unbind dremoves one.:config-list-addand:config-list-removechange one item of a list setting, and:config-dict-add [--replace]and:config-dict-removeone key of a map setting. For example,:config-dict-add url.searchengines ddg https://duckduckgo.com/?q={}. Like:set, they're saved inautoconfig.toml.:config-difflists the settings that differ from their defaults,:config-clearputs them all back, and:config-write-toml [--force]writes your current settings toconfig.toml.:config-editopensconfig.lua(orconfig.toml) ineditor.commandand reloads it when you close the editor.:config-sourcereloads every file. Errors show in the status bar withfile:line, and the rest of the file still applies.
A few settings, such as content.webgl and input.spatial_navigation, only apply when riptide starts; the reference marks them "after a restart". When you change one, the status bar says so, and :restart restarts riptide with your tabs.
Per-site values use [per_domain."<pattern>"] tables; see Per-site settings.
Example config.toml
# Example config.toml for riptide.
# Copy to the config directory (run `riptide --paths` to find it).
# Every key is a setting name, as listed in `:set` completion and the README.
hints.chars = "asdfghjkl"
hints.uppercase = false
messages.timeout = 3000 # milliseconds; 0 keeps messages up
tabs.last_close = "ignore" # ignore, blank, startpage, default-page, close
tabs.new_position.related = "next" # prev, next, first, last
tabs.new_position.unrelated = "last"
url.default_page = "https://start.duckduckgo.com/"
url.start_pages = ["https://start.duckduckgo.com/"]
# ":open w rust" searches Wikipedia; anything else uses DEFAULT.
[url.searchengines]
DEFAULT = "https://duckduckgo.com/?q={}"
w = "https://en.wikipedia.org/w/index.php?search={}"
gh = "https://github.com/search?q={}"
[aliases]
q = "quit"
qa = "quit"
wq = "quit"
[bindings.normal]
"<Ctrl-x>" = "tab-close"
"gw" = "cmd-set-text -s :open w"
# An empty command removes a default binding:
# "d" = ""
[bindings.insert]
"<Ctrl-[>" = "mode-leave"
Lua
config.lua gets c (qutebrowser-style c.hints.chars = "asdf"), rt.set/get/bind/unbind, rt.platform (linux, macos, windows), rt.config_dir, and require() from the config directory (name.lua or lua/name.lua). It is a normal Lua with the standard library, trusted like a shell rc file.
The Lua VM stays alive after the file runs, so config can also script the browser:
-- A key bound to a function, with access to the page and the count.
rt.bind("<Ctrl-g>", function() rt.message(rt.title() .. " — " .. rt.url()) end)
-- A command, :wiki rust, with completion next to the built-in ones.
rt.command("wiki", function(args)
rt.open("https://en.wikipedia.org/wiki/" .. args, "tab")
end, "Search Wikipedia")
-- A hook: runs whenever a page on news.example.com finishes loading.
rt.on("load_finished", { pattern = "news.example.com" }, function(e)
rt.run("scroll-to-perc 0")
end)
Keys and commands
rt.keymap.set(mode, keys, rhs, opts) binds keys like vim.keymap.set: mode is one mode or a list, rhs a command line or a function, and opts.desc describes it in the key hints popup. rt.keymap.del(mode, keys) removes a binding. rt.bind and rt.unbind still work, and both also work inside callbacks.
rt.command(name, fn, opts) takes a description, or a table with desc and complete. complete gets what's typed after the command and returns its completions, as strings or { name = "…", desc = "…" }:
rt.keymap.set({ "normal", "insert" }, "<Ctrl-y>", "reload", { desc = "Reload the page" })
rt.command("project", function(name) rt.open("https://github.com/me/" .. name, "tab") end, {
desc = "Open one of my projects",
complete = function(arglead)
local out = {}
for _, p in ipairs({ "riptide", "dotfiles", "notes" }) do
if p:find(arglead, 1, true) then table.insert(out, p) end
end
return out
end,
})
Keeping data
rt.store(name) keeps data between runs, saved as <data>/plugin-data/<name>.json on every change. It has get(key), set(key, value), all() and clear(); values are strings, numbers, booleans and tables of them:
local list = rt.store("reading-list")
rt.keymap.set("normal", "<Space>r", function()
local urls = list.get("urls") or {}
table.insert(urls, rt.url())
list.set("urls", urls)
rt.notify("Saved for later (" .. #urls .. ")")
end, { desc = "Read later" })
Status bar widgets
rt.statusbar.widget(name, fn) adds a widget that statusbar.widgets shows as lua:<name>. The function returns its text each time the bar is drawn, so keep it quick: it gets 50 ms, and one that fails or runs longer is removed with an error.
c.statusbar.widgets = { "keypress", "url", "lua:list", "tabs" }
local list = rt.store("reading-list")
rt.statusbar.widget("list", function()
local urls = list.get("urls") or {}
return #urls > 0 and ("📚" .. #urls) or ""
end)
An empty string hides the widget. What a widget function asks for besides its text, such as rt.notify, is ignored; to change a widget on a timer, use rt.every, which redraws the bar when it runs.
Events
rt.on(event, [opts], fn) runs fn with a table describing the event:
| Event | When | The table has |
|---|---|---|
startup | riptide has started and loaded config.lua | |
quit | riptide is about to quit | |
load_started, load_finished | a tab starts or finishes loading a page | url |
url_changed | a tab's address changes | url |
title_changed | a tab's title changes | url, title |
tab_opened, tab_closed | a tab opens or closes | url |
tab_selected | another tab becomes the current one | url, index (from 1) |
window_opened, window_closed | a window opens or closes | private ("true", "false") on opening |
mode_changed | the mode changes | from, to |
setting_changed | a setting changes (:set, the settings page) | name, value (as text) |
download_started, download_finished | a download starts or ends | url, path; state (done, failed, cancelled) when it ends |
opts can have:
pattern: only pages matching it, written as for:set -u(example.com,*.example.com,https://example.com,*://*.example.com/docs/*).once = true: run the first time only.group: a name, sort.off(group)removes them all.
rt.on returns an id for rt.off(id). rt.group(name, { clear = true }) removes the group's hooks and returns its name, so a script that runs again (:config-source) doesn't add its hooks twice:
local g = rt.group("reading", { clear = true })
rt.on("tab_selected", { group = g, pattern = "*.wikipedia.org" }, function(e)
rt.message("Reading tab " .. e.index)
end)
rt.on("download_finished", { group = g }, function(e)
if e.state == "done" then rt.spawn({ "notify-send", "Downloaded", e.path }) end
end)
rt.defer(ms, fn) calls fn once after ms milliseconds, and rt.every(ms, fn) keeps calling it; both return a handle whose :stop() cancels it. rt.notify(text, level) (or rt.message) shows a message, with level info (the default), warning or error:
-- Remind me to stretch every 45 minutes.
rt.every(45 * 60 * 1000, function() rt.notify("Time to stretch", "warning") end)
A callback that runs for more than 2 seconds is stopped with an error, so a mistake like an endless loop can't freeze the browser; long work belongs in rt.spawn or a timer.
In callbacks, rt.url(), rt.title(), rt.mode(), rt.count() and rt.tabs() (the window's tabs, with title, url, current and pinned) describe the current state. rt.run(line), rt.open(url, target), rt.message(text, level) and rt.set(...) act on it. Errors show as config.lua:line: message. :config-source reloads everything.
rt.spawn(argv, [opts], [callback]) runs a program in the background, without a shell, and calls callback with {code, stdout, stderr, error} when it exits. argv is a list, or a command line split as :spawn splits it. opts can set stdin, cwd and env:
rt.command("translate", function(text)
rt.spawn({ "trans", "-brief", ":en" }, { stdin = text }, function(r)
if r.code == 0 then rt.message(r.stdout) else rt.message(r.stderr, "error") end
end)
end)
rt.json.decode(text) and rt.json.encode(value) read and write JSON, such as a program's output; null becomes nil.
Questions and the page
rt.ui.select(items, opts, on_choice) and rt.ui.input(opts, on_confirm) ask in the prompt area, like Neovim's vim.ui. A picker gives each item a key (1–9, then a–z); secret = true masks what's typed:
rt.ui.select({ "work", "home" }, { prompt = "Profile" }, function(choice)
if choice then rt.notify("Using " .. choice) end
end)
rt.ui.input({ prompt = "Passphrase", secret = true }, function(text) --[[ nil if cancelled ]] end)
rt.page acts on the current tab's page:
rt.page.type(text) | types into the focused field, like :insert-text |
rt.page.key(keys) | presses keys, like :fake-key |
rt.page.fill_login({ host, username, password, submit }) | fills the page's login form, but only while the tab is still on host |
rt.page.eval(code, fn) | evaluates a JavaScript expression and calls fn(value) with its value, or fn(nil, why) |
rt.page.css(css) | adds a stylesheet to the page until it next loads |
rt.page.selection(fn) | calls fn(text) with the selected text |
rt.page.hint({ selector, action }) | hints the elements a CSS selector matches and calls action({ url, text }) with the one you pick, instead of clicking it |
eval runs in the page's own world: the page can see the code and change what it returns, so treat the value as the page's word.
They never go through a command line, so what's typed isn't kept in the command history or :messages. They don't act on riptide's own pages, and a plugin needs the pages permission for the site.
Floats
rt.ui.float(opts) draws a box of text over the page, beside whatever else is on screen, and returns a handle with update(changes), close() and is_open(). Lines are text, or lists of { text, highlight } chunks coloured by the theme (title, muted, accent, match, url, key, info, warning, error); they're never HTML.
local list = rt.store("reading-list")
rt.keymap.set("normal", "<Space>l", function()
local lines = {}
for i, url in ipairs(list.get("urls") or {}) do
lines[#lines + 1] = { { tostring(i), "key" }, { " " .. url, "url" } }
end
rt.ui.float({
title = "Reading list",
lines = lines,
keys = { c = function(f) list.clear(); f:close() end },
})
end, { desc = "Show the reading list" })
| Option | |
|---|---|
position | center (the default), top, bottom, top-right or bottom-right of the page |
width | the widest it gets, in characters (default 60) |
timeout | close by itself after this many milliseconds, for a notice |
keys | functions for keys pressed in normal mode while it's the newest float with keys; Escape closes it |
on_close | runs when riptide closes it: Escape, its timeout or its window closing |
A plugin's floats show its name in the corner, so a float can't pass for riptide's own question.
Panels
rt.ui.panel(opts) keeps lines beside the page (side = "left" or "right") or below it ("bottom"): a tab tree, a reading list, notes. It takes the same title and lines as a float, plus size in pixels, and returns a handle with update, close, focus and is_open. Each side of a window holds one panel; a new one replaces it.
A panel takes keys only while it has focus: :panel-focus (or focus()) gives it focus, j/k move its cursor, and its keys functions get the cursor's line. Escape returns to the page, and clicking a line focuses the panel there.
local function tab_lines()
local lines = {}
for i, tab in ipairs(rt.tabs()) do
lines[i] = { { tostring(i) .. " ", "key" }, { tab.title, tab.current and "accent" or nil } }
end
return lines
end
local tree = rt.ui.panel({
title = "Tabs",
lines = tab_lines(),
keys = { ["<Return>"] = function(_, line) rt.run("tab-select " .. line) end },
})
for _, event in ipairs({ "tab_opened", "tab_closed", "tab_selected", "title_changed" }) do
rt.on(event, function() tree:update({ lines = tab_lines() }) end)
end
rt.keymap.set("normal", "<Space>t", function() tree:focus() end, { desc = "Focus the tab tree" })
For completion and type checking in Neovim, VS Code and other editors using lua-language-server:
dir="$(riptide --paths | sed -n 's/^config: //p')"
mkdir -p "$dir" && riptide --lua-types > "$dir/rt.meta.lua"
The Lua API reference lists every function and setting with its type.
Example config.lua
-- Example config.lua for riptide. Loaded after config.toml.
-- Copy to the config directory (run `riptide --paths` to find it).
--
-- c.<setting> = value set an option (same names as config.toml)
-- rt.get(name) read an option's current value
-- rt.bind(keys, command [, mode])
-- rt.unbind(keys [, mode])
-- rt.platform "linux", "macos" or "windows"
-- rt.config_dir, rt.data_dir, rt.version
-- require("name") loads name.lua or lua/name.lua from the config dir
c.hints.chars = "asdfjkl"
-- One file for every machine.
if rt.platform == "macos" then
c.hints.uppercase = true
end
-- Build settings with code.
local engines = rt.get("url.searchengines")
engines.rs = "https://docs.rs/releases/search?query={}"
engines.crates = "https://crates.io/search?q={}"
c.url.searchengines = engines
for i, page in ipairs({ "news", "mail" }) do
rt.bind("g" .. i, "open -t https://example.com/" .. page)
end
rt.bind("<Ctrl-e>", "mode-leave", "insert")
-- Keep machine-specific settings out of version control:
-- pcall(require, "local")
Plugins
Plugins add commands, key bindings, hooks and more, written in Lua like config.lua. They work like Neovim's: a plugin is a folder with lua/<name>/init.lua, and you set it up from config.lua.
Adding a plugin
The quickest way is the Plugins tab (:plugins): Browse lists the plugins in riptide-plugins (or the repository plugins.catalog names) with what each asks for, and Add installs one. You can also paste a git URL and the folder in it, or run :pack-add <url> [folder]. Plugins added this way are kept in plugins.toml in the config folder, which riptide writes for you:
# plugins.toml
[[plugin]]
src = "https://github.com/joshzcold/riptide-plugins"
subdir = "keepassxc"
opts = { database = "~/Passwords.kdbx" } # options, which you can add by hand
Remove on the tab takes such a plugin out of plugins.toml again. For more control, add plugins in config.lua instead; if both name the same plugin, config.lua's wins:
-- config.lua
rt.pack.add({
"https://github.com/someone/riptide-tab-tools",
{ "https://github.com/someone/reading-list", version = "v1.2", opts = { folder = "~/Reading" } },
{ dir = "~/code/my-plugin", config = function() require("my-plugin").setup() end },
})
A spec is a git URL, or a table:
| Key | |
|---|---|
"url" or src | Where to clone it from. riptide installs it into <data>/pack/<name> the first time, in the background. |
version | A tag, branch or commit to install; by default the newest commit. |
subdir | The plugin's folder in a repository of several, e.g. subdir = "passwords"; its name is the folder's by default. |
dir | A folder on your computer instead (~/ works), for writing your own. |
name | Its name, which require uses; by default the URL's or folder's last part, without a riptide- prefix or a .nvim-style suffix. |
opts | Passed to require(name).setup(opts) once it loads. |
config | A function run once it loads, instead of opts. |
trusted = true | Skip the sandbox and give it everything, as Neovim does. Only for code you vouch for. |
event, cmd, keys | Load it only when needed; see below. |
Plugins load after config.lua has run, so set them up with opts or config rather than calling require straight away.
Loading when needed
A plugin with event, cmd or keys loads only once you need it, which keeps startup quick:
rt.pack.add({
{ "https://github.com/someone/reading-list", cmd = { "read-later", "reading-list" }, opts = {} },
{ dir = "~/code/tab-tools", keys = { "<Space>t", { "<C-t>", mode = "insert" } }, opts = {} },
{ dir = "~/code/history-sync", event = "window_closed", opts = {} },
})
| Key | Loads it |
|---|---|
cmd | when you run one of these commands; it then runs with your arguments |
keys | when you press one of these keys (normal mode unless a mode is given); the keys are pressed again for the plugin's own binding |
event | before the first of these events' hooks run, so the plugin's hooks see it too |
Its permissions are still asked for at startup, so a key or command never stops to ask. The Plugins tab lists what each one waits for, and Load now or :pack-load <name> loads it straight away.
Versions and the lockfile
rt-pack-lock.json in the config folder pins each plugin from git to the commit installed, along with the permissions you approved, so a plugin never changes on its own and the same config gives the same plugins on every computer. Keep it with your dotfiles: on another computer, riptide installs exactly the pinned commits, and if the lockfile moves a plugin to another commit, riptide checks that one out at the next start, or straight away with :pack-restore. Plugins from one repository share a checkout, so they're always on the same commit and update together. Git runs without asking for passwords, so a private repository fails with a message instead of waiting.
| Command | Plugins tab | Does |
|---|---|---|
:pack-check [name] | Check all, Check for updates | fetches and lists each plugin's new commits; nothing changes yet |
| Update, Update all | moves to the commits a check listed, the ones you read | |
:pack-update [name] | fetches and moves straight to the newest commits | |
:pack-sync | Sync | cleans, updates everything and installs what's missing |
:pack-restore | Restore | puts every plugin back on its commit in rt-pack-lock.json |
:pack-clean | Clean | deletes checkouts and lockfile entries of plugins no longer in config.lua |
Every update is recorded in rt-pack-lock.json and reloads your config once; if a new version asks for more permissions, you're asked about those first.
Every plugins.check_interval days (1 by default; 0 turns it off), riptide checks in the background a few seconds after it starts and says which plugins have updates, such as "Plugin updates for pass, passwords; review them on :plugins". It mentions each update once, so a plugin you leave as it is doesn't come up again until it has newer commits, and the check changes nothing.
The plugins page
:plugins opens the Plugins tab of :settings: each plugin with where it comes from, the commit it's on, whether it loaded (and the error if it didn't) and the permissions you approved, with the buttons above.
To review before updating, press Check all (or :pack-check), read each plugin's new commits on the page, then press Update on one or Update all. Revoke forgets the permissions you approved, so the plugin asks again. Remove deletes a plugin's lockfile entry and its checkout, unless another plugin uses it; take it out of config.lua too, or it's installed again at the next start.
Permissions
Plugins run in a sandbox. Without asking, a plugin can react to events, bind keys to its functions, add commands, use timers, show messages, ask you questions (rt.ui, which names the plugin asking), open URLs and keep its own data (rt.store, which other plugins can't read). Anything more is a permission it lists in its riptide-plugin.toml, and the first time it loads, riptide shows them and asks you:
| Permission | Lets it |
|---|---|
spawn | run programs on your computer (rt.spawn) |
files | read and write your files (Lua's io and os) |
commands | run any riptide command (rt.run) and bind keys to command lines; this includes running programs |
settings | read and change your settings (rt.get, rt.set, c) |
clipboard | read and write the clipboard |
keys | see every key you press |
network = ["host", …] | connect to these hosts |
pages = ["*.example.com", …] | type and fill logins into pages on these sites (rt.page); ["*"] is every site |
frames = ["host", …] | show these sites inside its own pages |
Your answer is kept in rt-pack-lock.json in the config folder, so it's asked once; a new version that asks for more asks again, for the new permissions only. Saying no leaves the plugin unloaded. To be asked again, press Revoke on :plugins, or delete its entry in rt-pack-lock.json.
In the sandbox, load only runs text in the plugin's own globals, require only finds the plugin's own modules and those of the plugins it lists in dependencies (not your config's, nor other plugins'), and a plugin's changes to its rt table don't affect anyone else. Every callback, a plugin's included, is stopped after 2 seconds.
Writing a plugin
reading-list/
├── riptide-plugin.toml what it is and what it needs
├── lua/reading-list/
│ └── init.lua require("reading-list")
└── plugin/
└── reading-list.lua runs when it loads (optional)
# riptide-plugin.toml
name = "reading-list"
description = "Save pages to read later"
[permissions]
spawn = true
-- lua/reading-list/init.lua
local M = {}
function M.setup(opts)
local list = rt.store()
rt.keymap.set("normal", "<Space>r", function()
local urls = list.get("urls") or {}
table.insert(urls, rt.url())
list.set("urls", urls)
rt.notify("Saved for later")
end, { desc = "Read later" })
end
return M
Options
A plugin lists its options in riptide-plugin.toml, and the Plugins tab shows a form for them; for a plugin added there, Save options keeps them in plugins.toml and reloads it, so setup(opts) gets them. For one from config.lua, the tab shows them and they go in rt.pack.add's opts.
[[option]]
name = "database"
type = "path" # string (the default), path, number, bool, choice or secret
description = "Your .kdbx file"
required = true
[[option]]
name = "remember"
type = "number"
default = 0 # shown in the form; the plugin applies it when unset
[[option]]
name = "password"
type = "secret"
description = "The database password, so it doesn't ask"
A riptide older than the plugin ignores keys it doesn't know, such as [[option]] before 0.4, with a warning in its log; it still refuses a permission it doesn't know.
A secret option is never written to a file: the form saves it in your OS keyring (the macOS Keychain, Windows' Credential Manager, or the Secret Service, such as GNOME Keyring or KWallet, on Linux), and the plugin reads it when it needs it, with rt.secret.get("password", function(value, err) … end). A plugin can read only its own secret options, and only the ones its manifest declares. The Plugins tab shows whether a secret is set, never its value; Clear takes it out of the keyring. On Linux without a keyring service, saving a secret says so and the plugin can ask instead.
Dependencies
A plugin that builds on another lists it in riptide-plugin.toml:
dependencies = ["passwords"]
It loads once its dependencies have, and may require their modules; their functions run with their own permissions, not the dependent's. You don't have to add a dependency yourself: riptide installs it from the same repository (the folder beside it, for one from subdir) or from the folder beside a dir plugin, and asks for its permissions as usual. Add it to rt.pack.add only to pass it options. A plugin whose dependency is refused or fails doesn't load, and :plugins says why.
Pages
A plugin can ship HTML in a pages/ folder and open it in a tab with rt.ui.page({ path, on_message }). Its pages are served as riptide://<name>.plugin/…, each plugin its own origin, and they talk to the plugin, and only to it, with messages:
-- plugin/reading-list.lua
rt.command("reading-list", function()
rt.ui.page({
path = "index.html",
on_message = function(name, data, page)
if name == "ready" then page:send("urls", rt.store().get("urls") or {}) end
end,
})
end)
// pages/app.js, loaded with <script src="app.js"> from pages/index.html
addEventListener("rtmessage", (e) => {
if (e.detail.name === "urls") render(e.detail.data);
});
rt.send("ready", JSON.stringify({}));
where = "panel" docks the page beside the page area instead, with side (left, right or bottom) and size in pixels: a sidebar that shows a web app in an iframe, say. Clicking a field in it types there, as in a tab, and :panel-focus moves the keyboard to it and back. page:close() closes it.
Pages run only their own files: inline scripts, other sites' scripts and eval are blocked. They may show other sites in iframes only over https and only for the hosts the frames permission lists, and connect to only the hosts network lists. A plugin's pages are served once it has loaded.
Testing
riptide --plugin-test DIR runs the plugin in DIR in a throwaway profile, with the permissions its manifest asks for already approved, and runs its test/*_spec.lua files. It prints TAP and exits with 1 if a test failed, so it works in CI under xvfb-run:
-- test/reading_list_spec.lua
describe("reading-list", function()
it("saves the page", function()
keys("<Space>r")
wait_until(function() return last_message() end)
assert.matches("^Saved", last_message())
end)
end)
| In a spec | |
|---|---|
describe(name, fn), it(name, fn), before_each(fn) | group and name tests |
assert(v), assert.equals(expected, actual), assert.same (deep), assert.truthy, assert.falsy, assert.matches(pattern, s), assert.has_error(fn, pattern) | checks |
keys("<Space>r"), run("open x") | press riptide keys or run a command |
wait(ms), wait_until(fn, timeout), wait_for(event, { pattern, timeout }) | let the browser work; wait_for returns the event |
messages(), last_message(), clear_messages() | what rt.notify showed, the plugin's included |
page("login.html") | the address of a file in the plugin's test/ folder, served as an ordinary web page (http://plugin-test.localhost/…) |
plugin_test.dir | the plugin's folder, e.g. for opts.command = plugin_test.dir .. "/test/bin/fake-tool" |
Secret options are kept in memory for the run, never in your keyring. Each test gets 10 seconds. test/config.lua, if there is one, replaces the default rt.pack.add({ dir = DIR, opts = {} }), to pass other options. riptide-plugin-template is a plugin to start from, with a spec and a GitHub workflow that runs it, and riptide-plugins collects plugins, each installed with subdir.
:help <name> shows a plugin's description, permissions and its doc/<name>.md (or README.md), as text.
The Lua page describes the API, and :help rt.ui.float (any rt. function) shows one function; errors name the plugin's file and line, e.g. reading-list/lua/reading-list/init.lua:7: ….
Themes and colors
ui.theme sets the colors of riptide's own parts: the tab bar, the status bar, completion, prompts, hint labels, and the riptide://history and riptide://downloads pages (those pick up a new theme when they next load). Web pages keep their own colors (see dark mode for those).
| Theme | Look |
|---|---|
riptide | Dark blue and teal (the default) |
riptide-light | The same, light |
gruvbox-dark, gruvbox-light | Gruvbox |
catppuccin-mocha, catppuccin-latte | Catppuccin |
nord | Nord |
dracula | Dracula |
solarized-dark, solarized-light | Solarized |
tokyo-night | Tokyo Night |
:theme nord switches and saves the choice, like :set ui.theme nord; :theme alone lists them, and :theme <Tab> completes the names. While you type or Tab through the names, riptide shows each theme as it's named; Escape goes back to the one you had. In a config file:
ui.theme = "gruvbox-dark"
ui.theme = "auto" follows your desktop's light or dark preference and switches as soon as it changes, using ui.auto_theme.dark and ui.auto_theme.light:
ui.theme = "auto"
"ui.auto_theme.dark" = "tokyo-night"
"ui.auto_theme.light" = "solarized-light"
If you set colors.webpage.preferred_color_scheme to light or dark, auto follows that instead, so riptide matches the pages.
Your own themes
A file in themes/ in the config directory adds a theme named after the file. themes/kanagawa-dragon.toml makes kanagawa-dragon, which :theme, ui.theme and ui.auto_theme.* then accept. Give a palette of base colors, and riptide works out every color from them as it does for the built-in themes (readable text included):
# ~/.config/riptide/themes/kanagawa-dragon.toml
[palette]
base = "#0a0c0f" # status bar and headers: the darkest background
surface = "#181616" # tab bar, completion and prompts
fg = "#c5c9c5"
accent = "#8ba4b0" # https URLs, the selected completion, key hints
yellow = "#c4b28a" # hint labels, prompt frames, matches
red = "#c4746e" # errors
green = "#8a9a7b" # insert mode
blue = "#658594" # passthrough mode, pinned tabs
[colors] # optional: any colors.* setting, without "colors."
"completion.item.selected.bg" = "#2d4f67"
"completion.item.selected.fg" = "#c8c093"
| Palette color | Required | If left out |
|---|---|---|
base, surface, fg, accent, yellow, red, green, blue | yes | |
surface2, surface3 (alternating tabs) | no | surface mixed with a little fg |
muted (descriptions) | no | fg mixed toward surface |
orange (warnings) | no | between red and yellow |
Palette colors are #rrggbb; [colors] takes any CSS color. Theme files are read with the config, so after editing one run :config-source. A file with a mistake is skipped with a message saying what's wrong.
In config.lua, rt.theme defines a theme the same way:
rt.theme("kanagawa-dragon", {
palette = {
base = "#0a0c0f", surface = "#181616", fg = "#c5c9c5", accent = "#8ba4b0",
yellow = "#c4b28a", red = "#c4746e", green = "#8a9a7b", blue = "#658594",
},
colors = { ["completion.item.selected.bg"] = "#2d4f67" },
})
c.ui.theme = "kanagawa-dragon"
Themes from other programs
Two kinds of theme file made for other programs work as they are. Copy one into themes/, and its file name becomes the theme's name:
| File | What's read |
|---|---|
<name>.yaml or .yml: a base16 scheme | base00…base0F, in either the older flat layout or under palette: |
<name>.py: a qutebrowser theme | c.colors.… = … and config.set("colors.…", …) lines, with values as strings, variables (base00 = "#…") or dict entries (palette['bg']). No Python is run. |
A qutebrowser theme's status bar, completion, hint, error, insert and passthrough colors become the palette, and every other colors.* setting riptide also has is used as it is. Many Neovim and terminal themes ship a base16 file; Kanagawa's is in extras/base16/.
Changing single colors
colors.* settings change one color on top of the theme, with qutebrowser's names where there is one. An empty value (the default) uses the theme's. Colors are #rrggbb, #rgb, rgb(…), hsl(…) or a CSS color name:
ui.theme = "nord"
"colors.statusbar.insert.bg" = "#2e7d32"
"colors.hints.bg" = "#ffd54f"
c.colors.tabs.selected.odd.bg = "#000000"
The settings reference lists every colors.* setting. Every built-in theme keeps text readable: each text color has at least 4.5:1 contrast with its background (WCAG AA), which riptide's tests check.
Fonts
fonts.default_family and fonts.default_size set riptide's font, and the per-part settings (fonts.statusbar, fonts.tabs.selected, fonts.tabs.unselected, fonts.completion.entry, fonts.completion.category, fonts.prompts, fonts.hints, fonts.keyhint) take a CSS font in which default_size and default_family stand for those two, as in qutebrowser:
"fonts.default_family" = '"JetBrains Mono", monospace'
"fonts.default_size" = "11pt"
"fonts.tabs.selected" = "bold default_size default_family"
The tab bar, the status bar and the completion rows grow to fit their fonts. statusbar.padding and tabs.padding add room around the text, in pixels like CSS padding:
"statusbar.padding" = "4px 8px"
"tabs.padding" = "2px 6px"
Padding or font sizes set in ui.css resize the bars too.
Pages have their own fonts: fonts.web.family.standard, .fixed, .serif and .sans_serif (empty keeps Chromium's) and fonts.web.size.default, .default_fixed and .minimum in pixels. Pages pick them up when they reload.
Floating command line
ui.overlay.position = "floating" turns the command line into a box near the top of the page, like a command palette: what you type shows in the box, with its completions under it, and the key hints appear there too. ui.overlay.width sets the box's width in pixels.
"ui.overlay.position" = "floating"
"ui.overlay.width" = 900
The default, docked, keeps the list full width above the status bar, as in qutebrowser. Questions have their own setting, prompt.position.
Hint labels
Hint labels take the theme's colors.hints.* and fonts.hints. hints.radius rounds their corners (in pixels, 0 for square) and hints.padding sets the room around the letters:
"hints.radius" = 0
"hints.padding" = "1px 4px"
For anything else, put CSS in hints.css in the config directory. Labels are .label elements and the typed part of a label is .matched; the page's own CSS can't reach them, and yours applies only to them:
.label { box-shadow: 0 1px 3px rgb(0 0 0 / 40%); }
.matched { opacity: 0.5; }
hints.css is read again each time hints are shown.
Custom CSS
ui.css in the config directory is added to riptide's tab bar, status bar and overlay after their own styles, for anything the settings don't cover. The theme's colors are there as CSS variables (--rt-statusbar-bg, --rt-tabs-selected-bg, … one per colors.* setting), so a rule can reuse them:
/* ~/.config/riptide/ui.css */
.tab.selected { border-bottom: 2px solid var(--rt-prompts-border); }
#bar.insert { font-style: italic; }
While a question is up, the overlay's body has a class for what it's about: prompt-dialog (a page's alert, confirm, prompt or leave-page warning), prompt-permission, prompt-login, prompt-download, prompt-certificate or prompt-confirm (riptide asking before quitting, closing a pinned tab or opening another program). Setting --prompt-accent recolors the box's frame and keys:
body.prompt-permission { --prompt-accent: #e06c75; }
body.prompt-download { --prompt-accent: #98c379; }
content.user_stylesheets lists CSS files for web pages; relative paths are in the config directory, and like other content settings it can be set per site:
"content.user_stylesheets" = ["readable.css"]
[per_domain."news.example.com"]
"content.user_stylesheets" = ["readable.css", "news.css"]
Both are read again within a second of being saved: open pages and bars change without a reload.
Commands
Type these after :, or bind them to keys. :help shows the same list in the browser, with your own bindings.
| Command | Default keys | Description |
|---|---|---|
:open | <Ctrl-t> O PP Pp gO go o pP pp | Open a URL or search for text |
:back | H | Go back in history |
:forward | L | Go forward in history |
:reload | <Ctrl-r> <F5> R r | Reload the current page |
:stop | Stop loading the current page | |
:scroll | <Down> <Up> h j k l | Scroll in a direction |
:scroll-page | <Ctrl-b> <Ctrl-d> <Ctrl-f> <Ctrl-u> | Scroll by a multiple of the page size |
:scroll-to-perc | $ 0 G gg | Scroll to a percentage of the page |
:mode-enter | ' <Ctrl-v> V ` i v | Enter a key mode |
:mode-leave | Leave the current mode | |
:cmd-set-text | Preset the command line text | |
:tab-close | <Ctrl-w> d | Close the current tab (--force: a pinned one without asking) |
:tab-pin | <Ctrl-p> | Pin or unpin the current tab (count: tab number) |
:tab-next | <Ctrl-PgDown> J | Switch to the next tab |
:tab-prev | <Ctrl-PgUp> K gT | Switch to the previous tab |
:tab-focus | <Alt-1> <Alt-2> <Alt-3> <Alt-4> <Alt-5> <Alt-6> <Alt-7> <Alt-8> <Alt-9> <Ctrl-Tab> <Ctrl-^> g$ g0 g^ | Select a tab by number, or 'last' |
:tab-move | gJ gK gm | Move the current tab: +, -, start, end or a number |
:tab-only | co | Close all tabs except the current one |
:tab-clone | Duplicate the current tab: :tab-clone [-b] [-w] | |
:tab-give | gD | Move the current tab to window N, or to a new window: :tab-give [N] |
:tab-call | Reopen the current tab in a call window, where screen sharing picks a tab, window or screen | |
:tab-take | Move a tab from another window here: :tab-take <window/tab> | |
:undo | <Ctrl-T> u | Re-open the last closed tab |
:hint | ;I ;O ;b ;d ;f ;h ;i ;o ;r ;t ;x ;y F f | Label elements to follow: [--rapid] [group] [target] [fill text] |
:yank | yD yT yY yd yt yy | Copy the page's url, title or domain to the clipboard |
:set | Show or change an option: :set name [value], :set name! toggles | |
:bind | Show or set a key binding: :bind [--mode m] keys [command] | |
:unbind | Remove a key binding: :unbind [--mode m] keys | |
:config-source | Reload the configuration files | |
:help | <F1> | Show help: :help [-t] [:command | setting | section] |
:version | Show version, paths and loaded config files | |
:report | Report a bug: opens a new GitHub issue with the version filled in | |
:debug-keytester | Show the name and binding of each key you press, until Escape | |
:debug-log-filter | Change the log filter while running, e.g. rt_cef=debug; default goes back to RT_LOG | |
:changelog | Show what changed in each version: :changelog [-t] | |
:quickmark-add | m | Save a quickmark: :quickmark-add <url> <name> |
:quickmark-load | B b | Open a quickmark: :quickmark-load [-t|-b] <name> |
:quickmark-del | Delete a quickmark (default: the current page's) | |
:bookmark-add | M | Bookmark a URL (default: the current page) |
:bookmark-load | gB gb | Open a bookmark: :bookmark-load [-t|-b] <url> |
:bookmark-del | Delete a bookmark (default: the current page) | |
:save | Write config, cookies, quickmarks, bookmarks and the session to disk now: :save [what…] | |
:session-save | Save the open tabs: :session-save [name] | |
:session-load | Replace the open tabs with a saved session | |
:session-delete | Delete a saved session | |
:history-clear | Delete all browsing history (needs --force) | |
:history-import | Import qutebrowser's history: :history-import [path to history.sqlite] | |
:adblock-update | Download the filter lists in content.blocking.adblock.lists | |
:spell-suggest | Suggest fixes for the misspelled word at the cursor (insert mode) | |
:spell-replace | Replace the misspelled word: :spell-replace <word> | |
:spell-add | Add the word from the last :spell-suggest to your dictionary | |
:spell-install | Download spell-check dictionaries (checked against pinned checksums) and turn them on: :spell-install en-US de-DE, or pick from a list without a language | |
:spawn | Run a program: :spawn [-u] [-v] [-m] [-o] [-d] <cmd> [args]; -u runs a userscript | |
:extension-install | Install a Chrome extension: the Web Store page you're on, or a store page address, an id or a .crx file; it loads after a restart | |
:extension-remove | Delete an installed extension, by name or id; it's gone after :restart | |
:extensions | List extensions in :settings, with their popups, options, updates and Remove | |
:extension-open | Open an extension's popup over the page, or its options in a tab: :extension-open <name> [popup|options] | |
:extension-update | Check installed extensions for updates, or update one: :extension-update [name] | |
:open-editor | Edit the focused text field in editor.command (also :edit-text) | |
:edit-text | Edit the focused text field in editor.command (qutebrowser's name for :open-editor) | |
:edit-url | Edit the page's URL in editor.command, then open it: [-t|-b|-w|-p] [-r] [url] | |
:cmd-edit | Edit the command line in editor.command, then put it back: [--run] runs it instead | |
:greasemonkey-reload | Read the scripts in the greasemonkey directories again | |
:close | Close the current window (:quit closes all of them) | |
:tab-select | T gt | Go to a tab in any window: :tab-select <window/tab | text> (T) |
:history | Show the browsing history: :history [-t] | |
:settings | Open the settings page, to browse and change every setting | |
:plugins | Show your plugins, their permissions and updates | |
:pack-check | Check plugins from git for new commits, to review and update on :plugins: :pack-check [name] | |
:pack-update | Update plugins from git to their newest commits, all or one: :pack-update [name] | |
:pack-add | Add a plugin from git, kept in plugins.toml: :pack-add <url> [folder in the repository] | |
:pack-sync | Remove plugins no longer in config.lua, update the rest and install what's missing | |
:pack-clean | Remove the checkouts and lockfile entries of plugins no longer in config.lua | |
:pack-restore | Put every plugin from git back on its commit in rt-pack-lock.json | |
:panel-focus | Focus the next panel a plugin opened, then the page again; Escape also returns to the page | |
:pack-load | Load a plugin that waits for an event, command or key now: :pack-load <name> | |
:recover | Show the tabs open at each recent crash, to reopen some or all of them | |
:crash-report | Show the newest crash report, to check and send as a GitHub issue or by email | |
:selection-follow | <Ctrl-Return> <Return> | Follow the link around the selection, e.g. after a search (Return; -t: new tab) |
:zoom | = | Set the zoom: :zoom [percent] (=; no value: zoom.default) |
:zoom-in | + | Zoom in a level (+; a count zooms further) |
:zoom-out | - | Zoom out a level (-) |
:devtools-focus | Bring this tab's developer tools to the front | |
:bookmark-list | List quickmarks and bookmarks on a page: [-t] in a new tab | |
:quickmark-save | Write the quickmarks file now | |
:quickmarks-reload | Read the quickmarks and bookmarks files again | |
:bookmarks-reload | Read the quickmarks and bookmarks files again | |
:debug-dump-page | Save the page's HTML to a file: :debug-dump-page <file> | |
:debug-clear-ssl-errors | Forget the certificate errors allowed this session | |
:restart | Save the session, quit and start again | |
:devtools | wi | Open the developer tools for this tab (wi) |
:print | Print the page, or save it: :print [--pdf file] | |
:fullscreen | <F11> | Toggle fullscreen (F11) |
:screenshot | Save what the tab shows as an image: :screenshot [--force] file (.png, .jpg or .webp) | |
:view-source | gf | Show the page source in a new tab (gf) |
:jseval | Evaluate a JavaScript expression in the page: :jseval <code> | |
:home | Open the start page | |
:tab-mute | <Alt-m> | Mute or unmute this tab (Alt-m) |
:pip | gp | Float the page's main video in a picture-in-picture window, or bring it back (gp) |
:call-mute | cm | Mute or unmute your microphone in the call, from any tab or window, with the site's own mute key (content.call_mute_keys) |
:share-stop | Stop sharing your screen, a window or a tab, from any tab or window | |
:messages | Show this session's messages | |
:repeat-command | . | Run the last command again (.) |
:theme | Switch the color theme (ui.theme), or list the themes: :theme [name] | |
:cmd-repeat-last | Run the last command again, as . does | |
:cmd-repeat | Run a command several times: :cmd-repeat N command | |
:cmd-run-with-count | Run a command with a count, multiplied by any count typed first: :cmd-run-with-count N command | |
:scroll-px | Scroll by pixels: :scroll-px <dx> <dy> | |
:cmd-later | Run a command later: :cmd-later <ms> <command> | |
:message-info | Show a message: :message-info <text> | |
:message-warning | Show a warning: :message-warning <text> | |
:message-error | Show an error: :message-error <text> | |
:clear-messages | Take the messages off the screen | |
:config-cycle | Cycle a setting: :config-cycle <option> [values…] (no values: toggle) | |
:config-unset | Put a setting back to its default, or forget its value for one site: :config-unset [-u pattern] <option> | |
:config-list-add | Add a value to a list setting: :config-list-add <option> <value> | |
:config-list-remove | Remove a value from a list setting: :config-list-remove <option> <value> | |
:config-dict-add | Set a key in a map setting: :config-dict-add [--replace] <option> <key> <value> | |
:config-dict-remove | Remove a key from a map setting: :config-dict-remove <option> <key> | |
:config-clear | Put every setting back to its default | |
:config-diff | Show the settings that differ from their defaults | |
:config-edit | Edit config.lua (or config.toml) in editor.command, then load it again | |
:config-write-toml | Write the current settings to config.toml: [--force] replaces an existing one | |
:insert-text | Type text into the focused field: :insert-text <text> | |
:fake-key | Send keys to the page: :fake-key [-g] <keys> (-g: to the browser) | |
:click-element | Click an element: :click-element id|css|focused [value] | |
:scroll-to-anchor | Scroll to the element with this id or name | |
:window-only | Close every other window | |
:nop | Do nothing (to make a key do nothing) | |
:navigate | <Ctrl-a> <Ctrl-x> [[ ]] gU gu {{ }} | Go up, prev, next, increment or decrement the URL: :navigate <where> [-t] |
:search | Find text in the page: :search [-r] [text] (no text clears it); / and ? type one | |
:search-next | n | Go to the next match of the last search |
:search-prev | N | Go to the previous match of the last search |
:set-mark | Remember the scroll position as a mark: a-z for this page, A-Z with its URL | |
:jump-mark | Go back to a mark; ' is where the last jump started | |
:macro-record | q | Record keys into a register until macro-record again (q + register) |
:macro-run | @ | Replay a macro (@ + register; @@ repeats the last one; a count repeats it) |
:selection-toggle | Start or stop selecting in caret mode (--line selects whole lines) | |
:selection-reverse | Swap the ends of the selection (caret mode) | |
:download | Download a URL (default: the current page) | |
:download-cancel | Cancel a download (count: its number) | |
:download-open | Open a finished download (count: its number) | |
:download-clear | Remove finished downloads from the list | |
:download-retry | Start a failed or cancelled download again (count: its number) | |
:download-remove | Take a download off the list, cancelling it if it runs (count: its number; --all: every finished one) | |
:download-delete | Delete a finished download's file and take it off the list (count: its number) | |
:downloads | List this session's downloads and their progress | |
:quit | <Ctrl-q> ZQ ZZ | Quit the browser; --save keeps the tabs as the default session |
:prompt-fileselect-external | In a file prompt, pick the folder with fileselect.folder.command (Alt-e) | |
:hint-follow | Follow the hint with this label, or the match waiting for Return (Return in hint mode) | |
:completion-item-del | Delete the selected completion: history entry, quickmark, bookmark or session, or close the tab (Ctrl-d) | |
:completion-item-yank | Yank the selected completion's text: [--sel] for the primary selection (Ctrl-c) |
Default key bindings
Change these with :bind, [bindings.<mode>] in config.toml or rt.bind() in config.lua. :help bindings shows your current bindings, with your changes marked.
normal mode
| Keys | Command |
|---|---|
$ | scroll-to-perc --horizontal 100 |
' | mode-enter jump_mark |
+ | zoom-in |
- | zoom-out |
. | repeat-command |
/ | cmd-set-text / |
0 | scroll-to-perc --horizontal 0 |
: | cmd-set-text : |
;I | hint images tab |
;O | hint links fill :open -t -r {hint-url} |
;b | hint all tab-bg |
;d | hint links download |
;f | hint all tab |
;h | hint all hover |
;i | hint images current |
;o | hint links fill :open {hint-url} |
;r | hint --rapid links tab-bg |
;t | hint inputs |
;x | hint blocks hide |
;y | hint links yank |
<Alt-1> | tab-focus 1 |
<Alt-2> | tab-focus 2 |
<Alt-3> | tab-focus 3 |
<Alt-4> | tab-focus 4 |
<Alt-5> | tab-focus 5 |
<Alt-6> | tab-focus 6 |
<Alt-7> | tab-focus 7 |
<Alt-8> | tab-focus 8 |
<Alt-9> | tab-focus -1 |
<Alt-m> | tab-mute |
<Ctrl-PgDown> | tab-next |
<Ctrl-PgUp> | tab-prev |
<Ctrl-Return> | selection-follow -t |
<Ctrl-T> | undo |
<Ctrl-Tab> | tab-focus last |
<Ctrl-^> | tab-focus last |
<Ctrl-a> | navigate increment |
<Ctrl-b> | scroll-page 0 -1 |
<Ctrl-d> | scroll-page 0 0.5 |
<Ctrl-f> | scroll-page 0 1 |
<Ctrl-p> | tab-pin |
<Ctrl-q> | quit |
<Ctrl-r> | reload -f |
<Ctrl-t> | open -t |
<Ctrl-u> | scroll-page 0 -0.5 |
<Ctrl-v> | mode-enter passthrough |
<Ctrl-w> | tab-close |
<Ctrl-x> | navigate decrement |
<Down> | scroll down |
<Escape> | clear-keychain |
<F11> | fullscreen |
<F1> | help |
<F5> | reload |
<Return> | selection-follow |
<Up> | scroll up |
= | zoom |
? | cmd-set-text ? |
@ | macro-run |
B | cmd-set-text -s :quickmark-load -t |
F | hint all tab |
G | scroll-to-perc |
H | back |
J | tab-next |
K | tab-prev |
L | forward |
M | bookmark-add |
N | search-prev |
O | cmd-set-text -s :open -t |
PP | open -t -- {primary} |
Pp | open -t -- {clipboard} |
R | reload -f |
T | cmd-set-text -s :tab-select |
V | mode-enter caret ;; selection-toggle --line |
ZQ | quit |
ZZ | quit --save |
[[ | navigate prev |
]] | navigate next |
` | mode-enter set_mark |
b | cmd-set-text -s :quickmark-load |
cm | call-mute |
co | tab-only |
d | tab-close |
f | hint |
g$ | tab-focus -1 |
g0 | tab-focus 1 |
gB | cmd-set-text -s :bookmark-load -t |
gD | tab-give |
gJ | tab-move + |
gK | tab-move - |
gO | cmd-set-text :open -t -r {url} |
gT | tab-prev |
gU | navigate up -t |
g^ | tab-focus 1 |
gb | cmd-set-text -s :bookmark-load |
gf | view-source |
gg | scroll-to-perc 0 |
gm | tab-move |
go | cmd-set-text :open {url} |
gp | pip |
gt | cmd-set-text -s :tab-select |
gu | navigate up |
h | scroll left |
i | mode-enter insert |
j | scroll down |
k | scroll up |
l | scroll right |
m | cmd-set-text -s :quickmark-add {url} |
n | search-next |
o | cmd-set-text -s :open |
pP | open -- {primary} |
pp | open -- {clipboard} |
q | macro-record |
r | reload |
u | undo |
v | mode-enter caret |
wi | devtools |
yD | yank -s domain |
yT | yank -s title |
yY | yank -s |
yd | yank domain |
yt | yank title |
yy | yank |
{{ | navigate prev -t |
}} | navigate next -t |
insert mode
| Keys | Command |
|---|---|
<Ctrl-e> | open-editor |
<Escape> | mode-leave |
command mode
| Keys | Command |
|---|---|
<Alt-Backspace> | rl-backward-kill-word |
<Alt-b> | rl-backward-word |
<Alt-d> | rl-kill-word |
<Alt-f> | rl-forward-word |
<Backspace> | rl-backward-delete-char |
<Ctrl-C> | completion-item-yank --sel |
<Ctrl-a> | rl-beginning-of-line |
<Ctrl-b> | rl-backward-char |
<Ctrl-c> | completion-item-yank |
<Ctrl-d> | completion-item-del |
<Ctrl-e> | rl-end-of-line |
<Ctrl-f> | rl-forward-char |
<Ctrl-h> | rl-backward-delete-char |
<Ctrl-k> | rl-kill-line |
<Ctrl-n> | command-history-next |
<Ctrl-p> | command-history-prev |
<Ctrl-u> | rl-unix-line-discard |
<Ctrl-v> | rl-paste |
<Ctrl-w> | rl-rubout |
<Ctrl-y> | rl-yank |
<Delete> | rl-delete-char |
<Down> | command-history-next |
<End> | rl-end-of-line |
<Escape> | mode-leave |
<Home> | rl-beginning-of-line |
<Left> | rl-backward-char |
<Return> | command-accept |
<Right> | rl-forward-char |
<Shift-Insert> | rl-paste --sel |
<Shift-Tab> | completion-item-focus prev |
<Tab> | completion-item-focus next |
<Up> | command-history-prev |
passthrough mode
| Keys | Command |
|---|---|
<Shift-Escape> | mode-leave |
hint mode
| Keys | Command |
|---|---|
<Escape> | mode-leave |
<Return> | hint-follow |
prompt mode
| Keys | Command |
|---|---|
<Alt-Backspace> | rl-backward-kill-word |
<Alt-b> | rl-backward-word |
<Alt-d> | rl-kill-word |
<Alt-e> | prompt-fileselect-external |
<Alt-f> | rl-forward-word |
<Alt-y> | prompt-yank |
<Backspace> | rl-backward-delete-char |
<Ctrl-a> | rl-beginning-of-line |
<Ctrl-b> | rl-backward-char |
<Ctrl-e> | rl-end-of-line |
<Ctrl-f> | rl-forward-char |
<Ctrl-h> | rl-backward-delete-char |
<Ctrl-k> | rl-kill-line |
<Ctrl-u> | rl-unix-line-discard |
<Ctrl-v> | rl-paste |
<Ctrl-w> | rl-filename-rubout |
<Ctrl-x> | prompt-open-download |
<Ctrl-y> | rl-yank |
<Delete> | rl-delete-char |
<End> | rl-end-of-line |
<Escape> | mode-leave |
<Home> | rl-beginning-of-line |
<Left> | rl-backward-char |
<Return> | prompt-accept |
<Right> | rl-forward-char |
<Shift-Insert> | rl-paste --sel |
<Tab> | prompt-complete |
yesno mode
| Keys | Command |
|---|---|
<Alt-y> | prompt-yank |
<Escape> | mode-leave |
<Return> | prompt-accept |
A | prompt-accept --save yes |
N | prompt-accept --save no |
n | prompt-accept no |
y | prompt-accept yes |
set_mark mode
| Keys | Command |
|---|---|
<Escape> | mode-leave |
jump_mark mode
| Keys | Command |
|---|---|
<Escape> | mode-leave |
record_macro mode
| Keys | Command |
|---|---|
<Escape> | mode-leave |
run_macro mode
| Keys | Command |
|---|---|
<Escape> | mode-leave |
caret mode
| Keys | Command |
|---|---|
$ | move-to-end-of-line |
0 | move-to-start-of-line |
<Ctrl-Space> | selection-drop |
<Escape> | mode-leave |
<Return> | yank selection |
<Space> | selection-toggle |
G | move-to-end-of-document |
V | selection-toggle --line |
Y | yank -s selection |
[ | move-to-start-of-prev-block |
] | move-to-start-of-next-block |
b | move-to-prev-word |
e | move-to-end-of-word |
gg | move-to-start-of-document |
h | move-to-prev-char |
j | move-to-next-line |
k | move-to-prev-line |
l | move-to-next-char |
o | selection-reverse |
v | selection-toggle |
w | move-to-next-word |
y | yank selection |
{ | move-to-end-of-prev-block |
} | move-to-end-of-next-block |
Settings
Set these in config.toml (hints.chars = "asdf"), config.lua (c.hints.chars = "asdf") or with :set hints.chars asdf.
| Setting | Type | Default | Description |
|---|---|---|---|
aliases | table<string, string> | {"q":"close","qa":"quit","w":"session-save","wq":"quit --save","wqa":"quit --save"} | Command aliases: name → command. As in qutebrowser, :q closes the window, :qa quits, :w saves the session and :wq saves and quits |
auto_save.interval | integer | 15000 | Milliseconds between crash-recovery saves of the open tabs (0 turns them off) |
auto_save.session | boolean | false | Save the open tabs as the 'default' session on quit, and restore them at startup |
bindings.key_mappings | table<string, string> | {"<Ctrl-6>":"<Ctrl-^>","<Ctrl-[>":"<Escape>","<Ctrl-i>":"<Tab>","<Ctrl-j>":"<Return>","<Ctrl-m>":"<Return>","<Shift-Return>":"<Return>"} | Keys treated as other keys in every mode, before bindings are looked up, e.g. Ctrl-[ as Escape |
changelog_after_upgrade | major | minor | patch | never | minor | Open the changelog in a tab after an upgrade of at least this size: major, minor, patch or never |
colors.completion.category.bg | string | `` | Background of completion category headers; empty uses ui.theme's |
colors.completion.category.fg | string | `` | Text of completion category headers; empty uses ui.theme's |
colors.completion.description.fg | string | `` | Descriptions and details in the completion list; empty uses ui.theme's |
colors.completion.fg | string | `` | Completion list text; empty uses ui.theme's |
colors.completion.item.selected.bg | string | `` | Background of the selected completion; empty uses ui.theme's |
colors.completion.item.selected.fg | string | `` | Text of the selected completion; empty uses ui.theme's |
colors.completion.match.fg | string | `` | The typed text where it appears in completion items; empty uses ui.theme's |
colors.completion.odd.bg | string | `` | Completion list background; empty uses ui.theme's |
colors.hints.bg | string | `` | Background of hint labels; empty uses ui.theme's |
colors.hints.border | string | `` | Border of hint labels; empty uses ui.theme's |
colors.hints.fg | string | `` | Text of hint labels; empty uses ui.theme's |
colors.hints.match.fg | string | `` | The typed part of hint labels; empty uses ui.theme's |
colors.keyhint.suffix.fg | string | `` | The keys still to type in the key hint popup; empty uses ui.theme's |
colors.messages.error.bg | string | `` | Background of error messages; empty uses ui.theme's |
colors.messages.error.fg | string | `` | Text of error messages; empty uses ui.theme's |
colors.messages.warning.bg | string | `` | Background of warnings; empty uses ui.theme's |
colors.messages.warning.fg | string | `` | Text of warnings; empty uses ui.theme's |
colors.prompts.bg | string | `` | Background of prompts; empty uses ui.theme's |
colors.prompts.border | string | `` | Frame, title and keys of floating prompts; empty uses ui.theme's |
colors.prompts.fg | string | `` | Text of prompts; empty uses ui.theme's |
colors.prompts.key.bg | string | `` | Background of a floating prompt's keys; empty uses ui.theme's |
colors.statusbar.insert.bg | string | `` | Status bar background in insert mode; empty uses ui.theme's |
colors.statusbar.insert.fg | string | `` | Status bar text in insert mode; empty uses ui.theme's |
colors.statusbar.normal.bg | string | `` | Status bar background; empty uses ui.theme's |
colors.statusbar.normal.fg | string | `` | Status bar text; empty uses ui.theme's |
colors.statusbar.passthrough.bg | string | `` | Status bar background in passthrough mode; empty uses ui.theme's |
colors.statusbar.passthrough.fg | string | `` | Status bar text in passthrough mode; empty uses ui.theme's |
colors.statusbar.private.bg | string | `` | Status bar background in private windows; empty uses ui.theme's |
colors.statusbar.private.fg | string | `` | Status bar text in private windows; empty uses ui.theme's |
colors.statusbar.url.error.fg | string | `` | The address of a page that failed to load; empty uses ui.theme's |
colors.statusbar.url.success.http.fg | string | `` | An http:// address in the status bar; empty uses ui.theme's |
colors.statusbar.url.success.https.fg | string | `` | An https:// address in the status bar; empty uses ui.theme's |
colors.tabs.bar.bg | string | `` | Tab bar background behind the tabs; empty uses ui.theme's |
colors.tabs.even.bg | string | `` | Background of even-numbered tabs; empty uses ui.theme's |
colors.tabs.indicator.error | string | `` | A tab's indicator when its page failed to load; empty uses ui.theme's |
colors.tabs.indicator.start | string | `` | A tab's loading indicator; empty uses ui.theme's |
colors.tabs.odd.bg | string | `` | Background of odd-numbered tabs; empty uses ui.theme's |
colors.tabs.odd.fg | string | `` | Text of tabs; empty uses ui.theme's |
colors.tabs.pinned.odd.bg | string | `` | Background of pinned tabs; empty uses ui.theme's |
colors.tabs.pinned.odd.fg | string | `` | Text of pinned tabs; empty uses ui.theme's |
colors.tabs.selected.accent | string | `` | Color of the line marking the current tab, any CSS color such as #2ec4b6; empty matches the tab, so no line shows |
colors.tabs.selected.odd.bg | string | `` | Background of the current tab; empty uses ui.theme's |
colors.tabs.selected.odd.fg | string | `` | Text of the current tab; empty uses ui.theme's |
colors.webpage.bg | string | white | Background of a new tab before its page paints, e.g. #1e1e2e so dark themes don't flash white; #rrggbb, white or black |
colors.webpage.darkmode.enabled | boolean | false | Render light pages dark with Chromium's automatic dark mode; applies at once and can be set per site |
colors.webpage.preferred_color_scheme | auto | light | dark | auto | The color scheme pages see in prefers-color-scheme: auto follows the system |
completion.cmd_history_max_items | integer | 100 | How many command lines Up and Down remember |
completion.delay | integer | 0 | Milliseconds to wait after a key press before updating completions |
completion.height | string | 12 | Height of the completion list: rows (12) or a percentage of the window (50%) |
completion.min_chars | integer | 0 | Characters to type after a command before its arguments complete |
completion.open_categories | string[] | ["searchengines","quickmarks","bookmarks","history","filesystem"] | What :open completes from, in order: searchengines, quickmarks, bookmarks, history, filesystem |
completion.quick | boolean | true | When only one command or setting name is left, Tab takes it and moves on to completing the next part |
completion.show | always | auto | never | always | When to show completions: always, only after pressing Tab (auto), or never |
completion.shrink | boolean | true | Shrink the completion list to its items; false keeps it completion.height tall |
completion.timestamp_format | string | %Y-%m-%d %H:%M | strftime format of the last-visit time shown next to history completions; empty hides it |
completion.use_best_match | boolean | false | Return runs the first command that starts with an unknown command name, so :rel runs :reload |
completion.web_history.exclude | string[] | [] | URL globs (e.g. ://.bank.example/*) that :open never suggests from history |
completion.web_history.max_items | integer | 100 | How many history entries :open completion shows (0 turns history completion off) |
confirm_quit | string[] | ["never"] | Ask before quitting: always, multiple-tabs (more than one tab open), downloads (downloads still running), or never |
content.autoplay | boolean | true | Let videos play by themselves; false waits until you interact with the page (after a restart) |
content.blocking.adblock.lists | string[] | ["https://easylist.to/easylist/easylist.txt","https://easylist.to/easylist/easyprivacy.txt","https://ublockorigin.github.io/uAssets/filters/filters.min.txt","https://ublockorigin.github.io/uAssets/filters/privacy.min.txt","https://ublockorigin.github.io/uAssets/filters/quick-fixes.min.txt","https://ublockorigin.github.io/uAssets/filters/unbreak.min.txt"] | Adblock Plus filter lists or hosts files that :adblock-update downloads (https://, or file:// for local lists); uBlock Origin's own lists may use its trusted scriptlets |
content.blocking.adblock.rules | string[] | [] | Your own filter rules, in Adblock Plus syntax (e.g. example.com##.banner); they apply at once, without :adblock-update |
content.blocking.enabled | boolean | true | Block ads and trackers with the filter lists from content.blocking.adblock.lists |
content.blocking.whitelist | string[] | [] | Sites where nothing is blocked, as host names; a host also covers its subdomains |
content.cache.size | integer | 0 | Disk cache size in bytes; 0 lets Chromium choose (takes effect after a restart) |
content.call_mute_keys | table<string, string> | {"*.webex.com":"<Ctrl-m>","*.zoom.us":"<Alt-a>","meet.google.com":"<Ctrl-d>","meet.jit.si":"m","teams.cloud.microsoft":"<Ctrl-Shift-m>","teams.live.com":"<Ctrl-Shift-m>","teams.microsoft.com":"<Ctrl-Shift-m>"} | The key each call site mutes the microphone with, by URL pattern, for :call-mute (cm); keys as in :bind, e.g. <Ctrl-d> |
content.call_sites | string[] | ["meet.google.com","teams.microsoft.com","teams.live.com","teams.cloud.microsoft","*.zoom.us/wc/*","*.zoom.us/j/*","*.webex.com","meet.jit.si","whereby.com"] | Video call sites, as URL patterns, that open in a call window. There, sharing your screen lets you pick a tab, a window or the whole screen; in an ordinary tab it always shares the whole screen. Clear the list to open these sites as ordinary tabs; you then lose that choice unless you use :open --call or :tab-call |
content.canvas_reading | boolean | true | Let pages read back what they drew on a canvas; false blocks a common fingerprinting trick but breaks some sites (after a restart) |
content.cookies.accept | all | no-3rdparty | no-unknown-3rdparty | never | all | Which cookies sites may set: all, none from other sites (no-3rdparty; no-unknown-3rdparty is the same here), or never |
content.cookies.store | boolean | true | Keep cookies after the browser closes; false makes every cookie last only for the session |
content.desktop_capture | ask | true | false | ask | Let sites capture your screen or desktop audio: ask, true or false |
content.dns_prefetch | boolean | true | Look up the hosts of links before you follow them, which is faster but tells your DNS server about them |
content.geolocation | ask | true | false | ask | Let sites know your location: ask, true or false |
content.headers.accept_language | string | `` | Languages sites are asked for, e.g. en-US,en;q=0.9 (also navigator.languages); empty for the system's |
content.headers.custom | table<string, string> | {} | Extra headers sent with every request: name → value |
content.headers.do_not_track | boolean | true | Send DNT: 1 with every request, asking sites not to track you |
content.headers.referer | always | never | same-domain | same-domain | When to send the Referer header: always, never, or only within the same domain and its subdomains |
content.headers.user_agent | string | `` | User agent sent to sites and shown to their scripts; empty for Chromium's own. Can be set per site |
content.images | boolean | true | Load images; can be set per site |
content.javascript.can_close_tabs | boolean | true | Let a page close its own tab with window.close(), as login popups do |
content.javascript.can_open_tabs_automatically | boolean | false | Let pages open tabs and windows without a click (popups); can be set per site |
content.javascript.clipboard | none | access | access-paste | access | What pages may do with the clipboard: nothing, copy with a click (access), or also read it (access-paste); can be set per site |
content.javascript.enabled | boolean | true | Run JavaScript on pages; can be set per site |
content.javascript.log_message.levels | string[] | [] | Console messages from pages shown in the status bar and :messages, by level: debug, info, warning, error (can be set per site) |
content.local_content_can_access_file_urls | boolean | false | Let file:// pages read other local files, which a downloaded page could misuse (after a restart) |
content.media.audio_capture | ask | true | false | ask | Let sites use your microphone: ask, true or false |
content.media.video_capture | ask | true | false | ask | Let sites use your camera: ask, true or false |
content.mouse_lock | ask | true | false | ask | Let sites lock your mouse pointer, as games do: ask, true or false |
content.mute | boolean | false | Mute pages; can be set per site |
content.notifications.app_name | string | riptide | The app name on desktop notifications (presenter = libnotify), which notification services such as dunst and mako can match to style them |
content.notifications.enabled | ask | true | false | ask | Let sites show notifications: ask, true or false |
content.notifications.presenter | auto | libnotify | messages | auto | Where page notifications show: auto (Chromium's desktop notifications), libnotify (desktop notifications riptide sends with notify-send, following the other content.notifications settings) or messages (riptide's status bar) |
content.notifications.show_origin | boolean | true | Show the site a notification came from (presenter = libnotify or messages) |
content.notifications.site_icon | boolean | true | Show the site's icon on desktop notifications (presenter = libnotify) |
content.notifications.timeout | integer | -1 | Milliseconds a desktop notification stays (presenter = libnotify): -1 lets the desktop decide, 0 keeps it until dismissed |
content.notifications.urgency | low | normal | critical | normal | How urgent desktop notifications are (presenter = libnotify): low, normal or critical |
content.pdf_viewer | boolean | true | Show PDFs in the browser; false downloads them instead |
content.prefers_reduced_motion | boolean | false | Tell pages you prefer less motion, so they can tone down animations (after a restart) |
content.proxy | string | system | Proxy: system, none, a proxy URL such as socks5://127.0.0.1:9050, or pac+ and a PAC script's URL |
content.register_protocol_handler | ask | true | false | ask | Let sites register to handle links like mailto: : ask, true or false |
content.tls.certificate_errors | ask | block | load-insecurely | ask | Pages whose TLS certificate isn't trusted: ask, block, or load-insecurely |
content.unknown_url_scheme_policy | ask | allow-all | disallow | ask | Links to schemes the browser can't show (mailto:, magnet:, zoommtg:): ask before handing them to xdg-open, always hand them over, or never |
content.user_stylesheets | string[] | [] | CSS files applied to pages (relative paths are in the config directory); reloaded when they change; can be set per site |
content.webgl | boolean | true | Allow WebGL, which 3D graphics need and fingerprinting scripts use (after a restart) |
content.webrtc_ip_handling_policy | all-interfaces | default-public-and-private-interfaces | default-public-interface-only | disable-non-proxied-udp | all-interfaces | Which IP addresses WebRTC (video calls) may reveal; disable-non-proxied-udp keeps it behind content.proxy |
content.widevine | boolean | false | Allow Widevine DRM: Chromium downloads Google's CDM once (takes effect after a restart) |
crash_report.email | string | `` | Where :crash-report's Email button sends a report; empty hides the button |
downloads.location.directory | string | `` | Where downloads go; empty means the system Downloads folder |
downloads.location.prompt | boolean | true | Ask where to save each download (false saves straight to the directory) |
downloads.location.remember | boolean | true | Start the save prompt in the folder the last download went to |
downloads.location.suggestion | both | path | filename | both | What the save prompt starts with: the folder and file name (both), the folder (path), or the file name |
downloads.open_dispatcher | string | `` | Program that opens downloads (:download-open); {} is the file, or it's added at the end. Empty for the desktop's default |
downloads.remove_finished | integer | -1 | Take finished downloads off the list after this many milliseconds; -1 keeps them |
editor.command | string[] | ["gvim","-f","{file}","-c","normal {line}G{column0}l"] | Editor for :open-editor; fields: {file}, {line}, {column}, {line0}, {column0} |
editor.remove_file | boolean | true | Delete the temporary file after the editor closes; false keeps it, e.g. to recover text |
extensions.load | string[] | [] | Folders of unpacked Chrome extensions to load, besides the ones :extension-install installs (after a restart) |
fileselect.folder.command | string[] | ["xterm","-e","ranger","--choosedir={}"] | Program that picks a folder for fileselect.handler = external; {} is the file it writes the path to |
fileselect.handler | default | external | default | File pickers for upload fields: Chromium's own (default), or the fileselect.*.command programs (external) |
fileselect.multiple_files.command | string[] | ["xterm","-e","ranger","--choosefiles={}"] | Program that picks several files for fileselect.handler = external; {} is the file it writes the paths to, one per line |
fileselect.single_file.command | string[] | ["xterm","-e","ranger","--choosefile={}"] | Program that picks a file for fileselect.handler = external; {} is the file it writes the path to |
fonts.completion.category | string | bold default_size default_family | Font of completion category headers (default_size and default_family stand for those settings) |
fonts.completion.entry | string | default_size default_family | Font of completion entries |
fonts.default_family | string | "DejaVu Sans Mono", Monospace, monospace | Font family that the other fonts.* settings call default_family |
fonts.default_size | string | 10pt | Font size that the other fonts.* settings call default_size, e.g. 10pt or 13px |
fonts.hints | string | bold default_size default_family | Font of hint labels |
fonts.keyhint | string | default_size default_family | Font of the key hint popup |
fonts.prompts | string | default_size default_family | Font of prompts |
fonts.statusbar | string | default_size default_family | Font of the status bar; sizes beyond the bar's height are cut off until bars size to their font |
fonts.tabs.selected | string | default_size default_family | Font of the current tab |
fonts.tabs.unselected | string | default_size default_family | Font of the other tabs |
fonts.web.family.fixed | string | `` | Monospace font for pages (CSS monospace); empty for Chromium's |
fonts.web.family.sans_serif | string | `` | Sans-serif font for pages; empty for Chromium's |
fonts.web.family.serif | string | `` | Serif font for pages; empty for Chromium's |
fonts.web.family.standard | string | `` | Font for pages that don't choose one; empty for Chromium's |
fonts.web.size.default | integer | 16 | Default text size of pages, in pixels |
fonts.web.size.default_fixed | integer | 13 | Default size of monospace text in pages, in pixels |
fonts.web.size.minimum | integer | 0 | Smallest text size pages may use, in pixels (0 for no minimum) |
hints.auto_follow | always | unique-match | full-match | never | unique-match | When a hint is followed without Return: when one is left (unique-match), only when its label is typed in full (full-match), always, or never |
hints.auto_follow_timeout | integer | 0 | Ignore keys for this many milliseconds after following a hint, so extra typing doesn't reach the page |
hints.chars | string | asdfghjkl | Characters used for hint labels |
hints.dictionary | string | /usr/share/dict/words | Word list for hints.mode = word, one word per line |
hints.hide_unmatched_rapid_hints | boolean | true | In rapid hint mode (:hint --rapid), hide the labels that don't match what's typed |
hints.leave_on_load | boolean | true | Leave hint mode when the page starts loading something new |
hints.min_chars | integer | 1 | The shortest hint label, in characters |
hints.mode | letter | number | word | letter | letter: labels from hints.chars; number: numbered labels, and typing letters filters by text; word: dictionary words from each link's text |
hints.next_regexes | string[] | ["\\bnext\\b","\\bmore\\b","\\bnewer\\b","\\b[>→≫]\\b","\\b(>>|»)\\b","\\bcontinue\\b"] | Link texts ]] follows to the next page, as JavaScript regular expressions (case doesn't matter) |
hints.padding | string | 0 3px | Space around a hint label's text, as CSS padding, e.g. 1px 4px |
hints.prev_regexes | string[] | ["\\bprev(ious)?\\b","\\bback\\b","\\bolder\\b","\\b[<←≪]\\b","\\b(<<|«)\\b"] | Link texts [[ follows to the previous page, as JavaScript regular expressions (case doesn't matter) |
hints.radius | integer | 3 | Corner radius of hint labels in pixels; 0 is square |
hints.scatter | boolean | true | Spread hint labels over the alphabet so neighbours differ; false labels in order |
hints.selectors | table<string, string> | {"all":"a, area, textarea, select, input:not([type=hidden]), button, iframe, summary, [contenteditable]:not([contenteditable=false]), [onclick], [onmousedown], [role=link], [role=option], [role=button], [role=tab], [role=checkbox], [role=switch], [role=menuitem], [role=menuitemcheckbox], [role=menuitemradio], [role=treeitem], [aria-haspopup], [tabindex]:not([tabindex='-1'])","blocks":"[class], [id], img, iframe, ins, aside, video, embed, object","images":"img","inputs":"input:not([type]), input[type=text], input[type=search], input[type=email], input[type=url], input[type=tel], input[type=password], input[type=number], input[type=date], input[type=datetime-local], input[type=month], input[type=time], input[type=week], textarea, [contenteditable]:not([contenteditable=false])","links":"a[href], area[href], [role=link][href]","media":"audio, img, video"} | Hint groups for :hint, as CSS selector lists; your entries are added to the built-in all, links, images, media and inputs |
hints.uppercase | boolean | false | Show hint labels in upper case |
input.forward_unbound_keys | all | auto | none | auto | Pass unbound keys to the page in normal mode (auto: all but plain letters and digits) |
input.insert_mode.auto_enter | boolean | true | Enter insert mode when an editable element gets focus |
input.insert_mode.auto_leave | boolean | true | Leave insert mode when focus leaves an editable element |
input.insert_mode.auto_load | boolean | false | Enter insert mode when a page focuses a text field by itself, as autofocus does on load |
input.insert_mode.leave_on_load | boolean | true | Leave insert mode when a new page starts loading |
input.match_counts | boolean | true | Read digits typed before a binding as a count (3j); false lets digits be bindings themselves |
input.media_keys | boolean | true | Let the keyboard's media keys (play, pause, next) control audio and video in pages (after a restart) |
input.mode_override | none | normal | insert | passthrough | none | Mode to enter when a page loads or its tab is focused; set it per site, e.g. passthrough for a web terminal |
input.mouse.rocker_gestures | boolean | false | Hold the right button and click the left to go back, or the other way round to go forward; turns off the page's context menu |
input.partial_timeout | integer | 0 | Milliseconds before a half-typed key chain or count is forgotten; 0 waits forever |
input.spatial_navigation | boolean | false | Move focus between links and fields with the arrow keys, as on a TV (after a restart) |
keyhint.blacklist | string[] | [] | Key chains the key hint popup leaves out, as globs on the whole chain (e.g. g* for every chain starting with g) |
keyhint.delay | integer | 500 | How long after a partial key chain the popup listing its continuations appears, in milliseconds |
messages.timeout | integer | 3000 | Milliseconds before a status bar message clears (0 keeps it) |
new_instance_open_target | tab | tab-bg | window | tab | Where URLs from a second riptide invocation open |
new_instance_open_target_window | first-opened | last-opened | last-focused | last-focused | Which window URLs from a second riptide invocation open in |
plugins.catalog | string | https://github.com/joshzcold/riptide-plugins | The git repository of plugins the Plugins tab's Browse lists, one plugin per folder |
plugins.check_interval | integer | 1 | Every this many days, check plugins from git for new commits in the background and say once which have updates; nothing updates by itself (0: never) |
prompt.position | bottom | center | docked | bottom | Where questions (permissions, logins, downloads, page dialogs) appear: bottom, a box floating near the bottom of the page; center, the same box in the middle; or docked above the status bar |
prompt.width | integer | 640 | Width in pixels of a floating prompt (prompt.position = bottom or center), at most the page's |
scrolling.bar | always | never | overlay | always | Page scrollbars: always, never, or overlay (thin, shown while scrolling; after a restart) |
scrolling.smooth | boolean | false | Animate scrolling by keys instead of jumping |
search.ignore_case | smart | always | never | smart | Case in searches: smart ignores it unless the text has a capital, always, or never |
search.incremental | boolean | true | Search while typing after / or ? |
search.wrap | boolean | true | Go on from the top when a search passes the last match (or from the bottom, searching up) |
search.wrap_messages | boolean | true | Say when a search wraps around the page |
session.default_name | string | `` | Session that :session-save, :wq and auto_save.session use; empty means the last one loaded, or default |
session.lazy_restore | boolean | false | When restoring a session, load background tabs only when they are first shown |
spellcheck.languages | string[] | [] | Spell-check languages such as en-US (empty: off); Chromium downloads each dictionary from Google once |
statusbar.padding | string | 0 4px | Space around the status bar's text, as CSS padding (top right bottom left), e.g. 2px 8px; the bar grows to fit |
statusbar.position | top | bottom | bottom | Where the status bar is |
statusbar.show | always | never | in-mode | always | When to show the status bar: always, only while typing a command or answering a prompt (never), or also outside normal mode and while a message is shown (in-mode) |
statusbar.widgets | string[] | ["keypress","downloads","blocked","muted","media","sharing","zoom","search_match","url","scroll","history","tabs","progress"] | What the right side of the status bar shows, in order: keypress, downloads, blocked (requests the ad blocker stopped on the page), muted, media, sharing (a screen, window or tab being shared from any tab; :share-stop stops it), zoom, search_match, url, scroll, scroll_raw, history, tabs, progress, clock[:strftime format], text:…, lua:<name> (drawn by rt.statusbar.widget) |
tabs.close_mouse_button | middle | right | none | middle | Which mouse button closes a tab clicked in the tab bar |
tabs.close_mouse_button_on_bar | new-tab | close-current | close-last | ignore | new-tab | What tabs.close_mouse_button does on the empty part of the tab bar |
tabs.favicons.show | always | never | pinned | always | Show site icons in the tab bar: always, never, or only on pinned tabs |
tabs.indicator.width | integer | 3 | Width in pixels of the loading indicator at the left of each tab (0 hides it) |
tabs.last_close | ignore | blank | startpage | default-page | close | ignore | What closing the last tab does |
tabs.max_width | integer | -1 | Largest width in pixels of a tab in a top or bottom tab bar (-1 for no limit) |
tabs.min_width | integer | -1 | Smallest width in pixels of a tab in a top or bottom tab bar; tabs that don't fit scroll (-1 for no minimum) |
tabs.mode_on_change | normal | persist | restore | normal | Mode after switching tabs: normal, persist (keep insert/passthrough), or restore (the mode the tab was left in) |
tabs.mousewheel_switching | boolean | true | Switch tabs with the mouse wheel over the tab bar |
tabs.new_position.related | prev | next | first | last | next | Where tabs opened from a page go (popups, hints) |
tabs.new_position.unrelated | prev | next | first | last | last | Where other new tabs go (:open -t) |
tabs.padding | string | 0 4px 0 0 | Space around each tab's title, as CSS padding (top right bottom left); the tab bar grows to fit |
tabs.pinned.close | ask | refuse | close | ask | Closing a pinned tab without --force: ask first, refuse, or just close it |
tabs.pinned.frozen | boolean | true | Keep pinned tabs on their page: :open in a pinned tab opens a new tab |
tabs.pinned.shrink | boolean | true | Shrink pinned tabs to their icon and number |
tabs.position | top | bottom | left | right | top | Where the tab bar is; left and right list the tabs vertically |
tabs.select_on_remove | next | prev | last-used | next | Which tab to show after closing the current one: the next, the previous, or the one used before |
tabs.show | always | never | multiple | switching | always | When to show the tab bar: always, never, with more than one tab, or briefly after switching tabs |
tabs.show_switching_delay | integer | 800 | How long the tab bar stays after switching tabs with tabs.show = switching, in milliseconds |
tabs.tabs_are_windows | boolean | false | Open every tab in its own window and hide the tab bar, for tiling window managers |
tabs.title.alignment | left | center | right | left | Where tab titles sit in their tab: left, center or right |
tabs.title.format | string | {audio}{media}{index}: {current_title} | Tab titles; fields: {index}, {aligned_index}, {current_title}, {current_url}, {host}, {perc}, {audio}, {media} ([A/V] while the page uses a camera or the screen, and a microphone), {private} |
tabs.title.format_pinned | string | {index} | Titles of pinned tabs while tabs.pinned.shrink shrinks them; same fields as tabs.title.format |
tabs.tooltips | boolean | true | Show a tab's title and URL when the mouse rests on it |
tabs.undo_stack_size | integer | 100 | How many closed tabs u can reopen; 0 keeps none |
tabs.width | integer | 200 | Width of the tab bar in pixels when tabs.position is left or right |
tabs.wrap | boolean | true | Wrap around from the last tab to the first (and back) when switching tabs |
ui.auto_theme.dark | string | riptide | The theme ui.theme = auto uses when the desktop (or colors.webpage.preferred_color_scheme) prefers dark |
ui.auto_theme.light | string | riptide-light | The theme ui.theme = auto uses when light is preferred |
ui.overlay.position | docked | floating | docked | Where the command line's completions and the key hints appear: docked above the status bar, or floating, a box near the top of the page that also shows the command |
ui.overlay.width | integer | 800 | Width in pixels of the floating overlay (ui.overlay.position = floating), at most the page's |
ui.theme | string | riptide | Colors of riptide's bars, prompts and hints: riptide, riptide-light, gruvbox, catppuccin, nord, dracula, solarized, tokyo-night or a theme from themes/ in the config directory (:theme); auto follows the light or dark preference |
url.auto_search | naive | schemeless | never | naive | When :open searches: text that doesn't look like an address (naive), anything without a scheme:// (schemeless), or never |
url.default_page | string | https://start.duckduckgo.com/ | Page for :open without a URL |
url.incdec_segments | string[] | ["path","query"] | Parts of the URL Ctrl-a and Ctrl-x change: host, port, path, query, anchor |
url.open_base_url | boolean | false | Open a search engine's home page when :open gets just its name |
url.searchengines | table<string, string> | {"DEFAULT":"https://duckduckgo.com/?q={}"} | Search engines; ':open g rust' uses the 'g' entry, anything else DEFAULT |
url.start_pages | string[] | ["https://start.duckduckgo.com/"] | Pages opened at startup when no URL is given |
url.yank_ignored_parameters | string[] | ["ref","utm_source","utm_medium","utm_campaign","utm_term","utm_content","utm_name","fbclid","gclid"] | Query parameters dropped when yanking a URL, such as tracking tags |
window.hide_decoration | boolean | false | Ask the window manager for no title bar or borders (applies to new windows) |
window.title_format | string | {current_title}{title_sep}Riptide | Window title; fields: {current_title}, {title_sep}, {current_url}, {host}, {mode} |
zoom.default | integer | 100 | Zoom in percent for pages, and what :zoom without a value resets to |
zoom.levels | string[] | ["25%","33%","50%","67%","75%","90%","100%","110%","125%","150%","175%","200%","250%","300%","400%","500%"] | The zoom levels + and - step through, in percent |
Lua API
These are the type definitions riptide --lua-types prints for lua-language-server. They document everything config.lua can use: the c settings proxy and the rt.* functions. Lua explains how to use them.
---@meta
---@diagnostic disable: missing-fields
-- Type definitions for riptide's config.lua, for lua-language-server.
-- Generated by `riptide --lua-types`; do not edit.
---@alias rt.Mode "normal"|"insert"|"command"|"passthrough"|"hint"|"prompt"|"yesno"|"set_mark"|"jump_mark"|"record_macro"|"run_macro"|"caret"
---@alias rt.SettingName "aliases"|"auto_save.interval"|"auto_save.session"|"bindings.key_mappings"|"changelog_after_upgrade"|"colors.completion.category.bg"|"colors.completion.category.fg"|"colors.completion.description.fg"|"colors.completion.fg"|"colors.completion.item.selected.bg"|"colors.completion.item.selected.fg"|"colors.completion.match.fg"|"colors.completion.odd.bg"|"colors.hints.bg"|"colors.hints.border"|"colors.hints.fg"|"colors.hints.match.fg"|"colors.keyhint.suffix.fg"|"colors.messages.error.bg"|"colors.messages.error.fg"|"colors.messages.warning.bg"|"colors.messages.warning.fg"|"colors.prompts.bg"|"colors.prompts.border"|"colors.prompts.fg"|"colors.prompts.key.bg"|"colors.statusbar.insert.bg"|"colors.statusbar.insert.fg"|"colors.statusbar.normal.bg"|"colors.statusbar.normal.fg"|"colors.statusbar.passthrough.bg"|"colors.statusbar.passthrough.fg"|"colors.statusbar.private.bg"|"colors.statusbar.private.fg"|"colors.statusbar.url.error.fg"|"colors.statusbar.url.success.http.fg"|"colors.statusbar.url.success.https.fg"|"colors.tabs.bar.bg"|"colors.tabs.even.bg"|"colors.tabs.indicator.error"|"colors.tabs.indicator.start"|"colors.tabs.odd.bg"|"colors.tabs.odd.fg"|"colors.tabs.pinned.odd.bg"|"colors.tabs.pinned.odd.fg"|"colors.tabs.selected.accent"|"colors.tabs.selected.odd.bg"|"colors.tabs.selected.odd.fg"|"colors.webpage.bg"|"colors.webpage.darkmode.enabled"|"colors.webpage.preferred_color_scheme"|"completion.cmd_history_max_items"|"completion.delay"|"completion.height"|"completion.min_chars"|"completion.open_categories"|"completion.quick"|"completion.show"|"completion.shrink"|"completion.timestamp_format"|"completion.use_best_match"|"completion.web_history.exclude"|"completion.web_history.max_items"|"confirm_quit"|"content.autoplay"|"content.blocking.adblock.lists"|"content.blocking.adblock.rules"|"content.blocking.enabled"|"content.blocking.whitelist"|"content.cache.size"|"content.call_mute_keys"|"content.call_sites"|"content.canvas_reading"|"content.cookies.accept"|"content.cookies.store"|"content.desktop_capture"|"content.dns_prefetch"|"content.geolocation"|"content.headers.accept_language"|"content.headers.custom"|"content.headers.do_not_track"|"content.headers.referer"|"content.headers.user_agent"|"content.images"|"content.javascript.can_close_tabs"|"content.javascript.can_open_tabs_automatically"|"content.javascript.clipboard"|"content.javascript.enabled"|"content.javascript.log_message.levels"|"content.local_content_can_access_file_urls"|"content.media.audio_capture"|"content.media.video_capture"|"content.mouse_lock"|"content.mute"|"content.notifications.app_name"|"content.notifications.enabled"|"content.notifications.presenter"|"content.notifications.show_origin"|"content.notifications.site_icon"|"content.notifications.timeout"|"content.notifications.urgency"|"content.pdf_viewer"|"content.prefers_reduced_motion"|"content.proxy"|"content.register_protocol_handler"|"content.tls.certificate_errors"|"content.unknown_url_scheme_policy"|"content.user_stylesheets"|"content.webgl"|"content.webrtc_ip_handling_policy"|"content.widevine"|"crash_report.email"|"downloads.location.directory"|"downloads.location.prompt"|"downloads.location.remember"|"downloads.location.suggestion"|"downloads.open_dispatcher"|"downloads.remove_finished"|"editor.command"|"editor.remove_file"|"extensions.load"|"fileselect.folder.command"|"fileselect.handler"|"fileselect.multiple_files.command"|"fileselect.single_file.command"|"fonts.completion.category"|"fonts.completion.entry"|"fonts.default_family"|"fonts.default_size"|"fonts.hints"|"fonts.keyhint"|"fonts.prompts"|"fonts.statusbar"|"fonts.tabs.selected"|"fonts.tabs.unselected"|"fonts.web.family.fixed"|"fonts.web.family.sans_serif"|"fonts.web.family.serif"|"fonts.web.family.standard"|"fonts.web.size.default"|"fonts.web.size.default_fixed"|"fonts.web.size.minimum"|"hints.auto_follow"|"hints.auto_follow_timeout"|"hints.chars"|"hints.dictionary"|"hints.hide_unmatched_rapid_hints"|"hints.leave_on_load"|"hints.min_chars"|"hints.mode"|"hints.next_regexes"|"hints.padding"|"hints.prev_regexes"|"hints.radius"|"hints.scatter"|"hints.selectors"|"hints.uppercase"|"input.forward_unbound_keys"|"input.insert_mode.auto_enter"|"input.insert_mode.auto_leave"|"input.insert_mode.auto_load"|"input.insert_mode.leave_on_load"|"input.match_counts"|"input.media_keys"|"input.mode_override"|"input.mouse.rocker_gestures"|"input.partial_timeout"|"input.spatial_navigation"|"keyhint.blacklist"|"keyhint.delay"|"messages.timeout"|"new_instance_open_target"|"new_instance_open_target_window"|"plugins.catalog"|"plugins.check_interval"|"prompt.position"|"prompt.width"|"scrolling.bar"|"scrolling.smooth"|"search.ignore_case"|"search.incremental"|"search.wrap"|"search.wrap_messages"|"session.default_name"|"session.lazy_restore"|"spellcheck.languages"|"statusbar.padding"|"statusbar.position"|"statusbar.show"|"statusbar.widgets"|"tabs.close_mouse_button"|"tabs.close_mouse_button_on_bar"|"tabs.favicons.show"|"tabs.indicator.width"|"tabs.last_close"|"tabs.max_width"|"tabs.min_width"|"tabs.mode_on_change"|"tabs.mousewheel_switching"|"tabs.new_position.related"|"tabs.new_position.unrelated"|"tabs.padding"|"tabs.pinned.close"|"tabs.pinned.frozen"|"tabs.pinned.shrink"|"tabs.position"|"tabs.select_on_remove"|"tabs.show"|"tabs.show_switching_delay"|"tabs.tabs_are_windows"|"tabs.title.alignment"|"tabs.title.format"|"tabs.title.format_pinned"|"tabs.tooltips"|"tabs.undo_stack_size"|"tabs.width"|"tabs.wrap"|"ui.auto_theme.dark"|"ui.auto_theme.light"|"ui.overlay.position"|"ui.overlay.width"|"ui.theme"|"url.auto_search"|"url.default_page"|"url.incdec_segments"|"url.open_base_url"|"url.searchengines"|"url.start_pages"|"url.yank_ignored_parameters"|"window.hide_decoration"|"window.title_format"|"zoom.default"|"zoom.levels"
---@alias rt.Event "startup"|"quit"|"load_started"|"load_finished"|"url_changed"|"title_changed"|"tab_opened"|"tab_closed"|"tab_selected"|"window_opened"|"window_closed"|"mode_changed"|"setting_changed"|"download_started"|"download_finished"
---@class rt
---@field platform "linux"|"macos"|"windows"
---@field version string
---@field config_dir string
---@field data_dir string
rt = {}
---@deprecated `hb` is the old name of `rt`.
hb = rt
---Set an option; `c.name = value` does the same. With `pattern` (e.g.
---`"*.example.com"` or `"https://meet.example.com"`), only for matching pages;
---this works for the content.* permission settings and content.blocking.enabled.
---@param name rt.SettingName
---@param value any
---@param pattern? string
function rt.set(name, value, pattern) end
---Get an option's current value.
---@param name rt.SettingName
---@return any
function rt.get(name) end
---Define a theme for `ui.theme` and `:theme`, like a `themes/<name>.toml`
---file: `palette` holds base, surface, fg, accent, yellow, red, green and blue
---(and optionally surface2, surface3, muted, orange) as `#rrggbb`, and
---`colors` any `colors.*` setting without the `colors.` prefix.
---@param name string a-z, 0-9, `-` and `_`
---@param spec { palette: table<string, string>, colors?: table<string, string> }
function rt.theme(name, spec) end
---Bind keys (qutebrowser notation, e.g. "<Ctrl-x>" or "gg") to a command,
---or to a Lua function that runs when they're pressed.
---@param keys string
---@param command string|fun()
---@param mode? rt.Mode defaults to "normal"
---@return string command what the keys run (`lua-call <n>` for a function)
function rt.bind(keys, command, mode) end
rt.keymap = {}
---Bind keys in one mode or several, as `vim.keymap.set`. `opts.desc` is shown
---in the key hints popup instead of the command.
---@param mode rt.Mode|rt.Mode[]
---@param keys string
---@param rhs string|fun()
---@param opts? { desc?: string }
function rt.keymap.set(mode, keys, rhs, opts) end
---Remove a binding in one mode or several.
---@param mode rt.Mode|rt.Mode[]
---@param keys string
function rt.keymap.del(mode, keys) end
---Remove a binding.
---@param keys string
---@param mode? rt.Mode defaults to "normal"
function rt.unbind(keys, mode) end
---Define a command, e.g. `:wiki rust`; `fn` gets the rest of the line.
---`opts` is a description, or a table with `desc` and `complete`, a function
---that gets what's typed after the command and returns completions: strings,
---or `{ name = "…", desc = "…" }` tables.
---@param name string letters, digits, `-` and `_`
---@param fn fun(args: string)
---@param opts? string|{ desc?: string, complete?: fun(arglead: string): (string|{ name: string, desc?: string })[] }
function rt.command(name, fn, opts) end
---Run `fn` on an event, with a table of what it's about (`e.url`, …):
---- `startup`: riptide has started and loaded config.lua
---- `quit`: riptide is about to quit
---- `load_started`: a tab started loading a page (url)
---- `load_finished`: a tab finished loading a page (url)
---- `url_changed`: a tab's address changed (url)
---- `title_changed`: a tab's title changed (url, title)
---- `tab_opened`: a tab was opened (url)
---- `tab_closed`: a tab was closed (url)
---- `tab_selected`: another tab became the current one (url, index from 1)
---- `window_opened`: a window was opened (private: "true" or "false")
---- `window_closed`: a window was closed
---- `mode_changed`: the mode changed (from, to)
---- `setting_changed`: a setting changed (name, value as text)
---- `download_started`: a download started (url, path)
---- `download_finished`: a download finished (url, path, state: done, failed or cancelled)
---`opts.pattern` only runs it for matching pages (as `:set -u` patterns),
---`opts.group` names it for `rt.off`/`rt.group`, and `opts.once` runs it once.
---`rt.on(event, fn)` works too. Returns an id for `rt.off`.
---@param event rt.Event
---@param opts { pattern?: string, group?: string, once?: boolean }|fun(e: table)
---@param fn? fun(e: table)
---@return integer
function rt.on(event, opts, fn) end
---Call `fn` once, `ms` milliseconds from now. `:stop()` on the result cancels it.
---@param ms integer
---@param fn fun()
---@return { stop: fun(self) }
function rt.defer(ms, fn) end
---Call `fn` every `ms` milliseconds (10 or more) until `:stop()` is called on the result.
---@param ms integer
---@param fn fun()
---@return { stop: fun(self) }
function rt.every(ms, fn) end
---Show a message in the status bar; the same as `rt.message`.
---@param text string
---@param level? "info"|"warning"|"error"
function rt.notify(text, level) end
---Data kept between runs under `name` (letters, digits, `-`, `_`), saved
---as JSON in the data folder on every change. Values are strings, numbers,
---booleans and tables of those.
---@param name? string Required in config.lua; in a plugin, its stores are its own and `name` defaults to "data".
---@return { get: fun(key: string): any, set: fun(key: string, value: any), all: fun(): table, clear: fun() }
function rt.store(name) end
---@class rt.PluginSpec
---@field [1]? string A git URL, the same as `src`.
---@field src? string A git URL to install it from.
---@field dir? string A folder on your computer instead.
---@field name? string Its name for `require`; by default from the URL or folder.
---@field version? string A tag, branch or commit.
---@field subdir? string The plugin's folder in a repository of several plugins.
---@field trusted? boolean Skip the sandbox and allow everything.
---@field opts? table Passed to `require(name).setup(opts)` once it loads.
---@field config? fun() Run once it loads, instead of `opts`.
---@field event? rt.Event|rt.Event[] Load when one of these events fires.
---@field cmd? string|string[] Load when one of these commands runs.
---@field keys? string|(string|{ [1]: string, mode?: string })[] Load when one of these keys is pressed.
rt.statusbar = {}
---A status bar widget: `fn` gives its text each time the bar is drawn,
---where `statusbar.widgets` lists `"lua:<name>"`. It gets 50 ms; one that
---fails or runs longer is removed. `nil` instead of `fn` removes it.
---@param name string Letters, digits, `-` and `_`.
---@param fn? fun(): string|number|nil
function rt.statusbar.widget(name, fn) end
rt.pack = {}
---Add plugins: a git URL, a spec, or a list of them. They load once
---`config.lua` has run, or with `event`, `cmd` or `keys` only when needed.
---@param specs string|rt.PluginSpec|(string|rt.PluginSpec)[]
function rt.pack.add(specs) end
---Remove a hook by the id `rt.on` returned, or every hook in a group.
---@param id integer|string
function rt.off(id) end
---A group name for hooks; `{ clear = true }` first removes the group's
---hooks, so a script that runs again doesn't add them twice.
---@param name string
---@param opts? { clear?: boolean }
---@return string
function rt.group(name, opts) end
---The current page's URL (in callbacks).
---@return string
function rt.url() end
---The current page's title (in callbacks).
---@return string
function rt.title() end
---The current mode, e.g. "normal" (in callbacks).
---@return string
function rt.mode() end
---The current window's tabs, in order (in callbacks).
---@return { index: integer, title: string, url: string, current: boolean, pinned: boolean }[]
function rt.tabs() end
---The count typed before the key, if any (in callbacks).
---@return integer?
function rt.count() end
---Run a command line, e.g. `rt.run("tab-close")` (in callbacks).
---@param line string
function rt.run(line) end
---Open a URL (in callbacks).
---@param url string
---@param target? "current"|"tab"|"tab-bg"|"window"|"private"
function rt.open(url, target) end
---Show a message in the status bar (in callbacks).
---@param text string
---@param level? "info"|"warning"|"error"
function rt.message(text, level) end
---@class rt.SpawnOpts
---@field stdin? string written to the program's standard input
---@field cwd? string the directory to run it in
---@field env? table<string, string> extra environment variables
---@class rt.SpawnResult
---@field code integer|nil the exit code; nil if a signal ended it or it didn't start
---@field stdout string
---@field stderr string
---@field error string|nil why it couldn't run
---Run a program in the background (in callbacks); no shell is involved.
---`argv` is a list, or a command line split like `:spawn` does.
---@param argv string[]|string
---@param opts? rt.SpawnOpts|fun(result: rt.SpawnResult)
---@param callback? fun(result: rt.SpawnResult) called when it exits
function rt.spawn(argv, opts, callback) end
rt.ui = {}
---@class rt.SelectOpts
---@field prompt? string the question shown above the items
---@field format? fun(item: any): string how to show an item; tostring by default
---Pick one of `items` in the prompt area, like `vim.ui.select`: each item has
---a key (1-9, then a-z; at most 35 items), and Return picks the first.
---`on_choice` gets the item and its index, or nil when cancelled.
---@param items any[]
---@param opts? rt.SelectOpts
---@param on_choice fun(item: any|nil, index: integer|nil)
function rt.ui.select(items, opts, on_choice) end
---@class rt.InputOpts
---@field prompt? string the question
---@field default? string text to start with
---@field secret? boolean mask what's typed, for passwords
---Ask for text in the prompt area, like `vim.ui.input`. `on_confirm` gets
---the text, or nil when cancelled.
---@param opts? rt.InputOpts
---@param on_confirm fun(text: string|nil)
function rt.ui.input(opts, on_confirm) end
---@alias rt.Highlight "title"|"muted"|"accent"|"match"|"url"|"key"|"info"|"warning"|"error"
---@class rt.FloatOpts
---@field title? string
---@field lines? (string|(string|{ [1]: string, [2]: rt.Highlight })[])[] Text, or chunks of text with highlights; never HTML.
---@field width? integer The widest it gets, in characters (10–200, default 60).
---@field position? "center"|"top"|"bottom"|"top-right"|"bottom-right"
---@field timeout? integer Close by itself after this many milliseconds.
---@field keys? table<string, fun(float: rt.Float)> Keys it takes in normal mode while it's the newest float with keys; Escape closes it.
---@field on_close? fun() Called when riptide closes it (Escape, its timeout).
---@class rt.Float
---@field id integer
local Float = {}
---Redraw with these options changed.
---@param changes rt.FloatOpts
function Float:update(changes) end
function Float:close() end
---@return boolean
function Float:is_open() end
---A box of text over the page. A plugin's floats show its name.
---@param opts rt.FloatOpts
---@return rt.Float
function rt.ui.float(opts) end
---@class rt.PanelOpts
---@field title? string
---@field lines? (string|(string|{ [1]: string, [2]: rt.Highlight })[])[] Text, or chunks of text with highlights; never HTML.
---@field side? "left"|"right"|"bottom" Default left.
---@field size? integer Width beside the page, or height below it, in pixels (default 300, or 200 below).
---@field keys? table<string, fun(panel: rt.Panel, line: integer)> Keys it takes while focused, with the cursor's line; j/k move the cursor and Escape returns to the page.
---@field on_close? fun() Called when riptide closes it (its window closing, another panel taking its side).
---@class rt.Panel
---@field id integer
local Panel = {}
---Redraw with these options changed.
---@param changes rt.PanelOpts
function Panel:update(changes) end
function Panel:close() end
---Give it the keyboard until Escape.
function Panel:focus() end
---@return boolean
function Panel:is_open() end
---@class rt.PageOpts
---@field path? string A file in the plugin's `pages/` folder (default `index.html`).
---@field where? "tab"|"panel" Default tab.
---@field side? "left"|"right"|"bottom" In a panel; default right.
---@field size? integer In a panel: its width, or height below the page, in pixels (default 400).
---@field on_message? fun(name: string, data: any, page: rt.Page) A message the page sent with `rt.send(name, json)`.
---@class rt.Page
---@field id integer
local Page = {}
---Send the page a message: it gets an `rtmessage` event with `detail.name` and `detail.data`.
---@param name string
---@param data any Anything `rt.json.encode` takes.
function Page:send(name, data) end
---Close its panel, or the tabs showing it.
function Page:close() end
rt.secret = {}
---One of this plugin's secret options (`type = "secret"` in its
---riptide-plugin.toml), from the OS keyring where the Plugins tab keeps it:
---`fn(value)`, `fn(nil)` when it isn't set, or `fn(nil, why)`. Only plugins
---have secret options, and each reads only its own.
---@param option string
---@param fn fun(value: string|nil, err: string|nil)
function rt.secret.get(option, fn) end
---Open one of this plugin's pages in a tab or a panel. Only plugins have pages.
---@param opts? rt.PageOpts
---@return rt.Page
function rt.ui.page(opts) end
---Lines beside or below the page, one panel per side of a window. A plugin's panels show its name.
---@param opts rt.PanelOpts
---@return rt.Panel
function rt.ui.panel(opts) end
---The current tab's page. Plugins need the `pages` permission for the site,
---checked when the action runs; nothing acts on riptide's own pages, and what's
---typed or filled is never logged or kept in history.
rt.page = {}
---Type text into the focused field, as `:insert-text` does.
---@param text string
function rt.page.type(text) end
---Press keys in the page, as `:fake-key` does, e.g. `"<Tab>"`.
---@param keys string
function rt.page.key(keys) end
---@class rt.Login
---@field host string the site the login is for; nothing is filled if the tab has moved on to another
---@field username? string
---@field password? string
---@field submit? boolean submit the form once it's filled
---Fill the page's login form: the focused field's form, or the first with a
---password field.
---@param login rt.Login
function rt.page.fill_login(login) end
---Evaluate a JavaScript expression in the page and get its value, as JSON
---turns it into Lua: `fn(value)`, or `fn(nil, why)`. It runs in the page's
---own world, so the page can see it and change what it returns.
---@param code string An expression; wrap statements in `(() => { … })()`.
---@param fn fun(value: any, err: string|nil)
function rt.page.eval(code, fn) end
---Add a stylesheet to the page, until it next loads.
---@param css string
function rt.page.css(css) end
---The page's selected text: `fn(text)`, or `fn(nil, why)`.
---@param fn fun(text: string|nil, err: string|nil)
function rt.page.selection(fn) end
---@class rt.HintedElement
---@field url string|nil Its link, if it has one.
---@field text string Its text, lowercased.
---Hint the elements `selector` matches, in frames of sites the plugin may act
---on, and hand the one you pick to `action`; nothing is clicked.
---@param opts { selector: string, action: fun(element: rt.HintedElement|nil, err: string|nil) }
function rt.page.hint(opts) end
rt.json = {}
---Parse JSON; `null` becomes nil.
---@param text string
---@return any
function rt.json.decode(text) end
---Write a value as JSON.
---@param value any
---@return string
function rt.json.encode(value) end
---@class rt.c
---@field aliases table<string, string> Command aliases: name → command. As in qutebrowser, :q closes the window, :qa quits, :w saves the session and :wq saves and quits
---@field auto_save rt.c.auto_save
---@field bindings rt.c.bindings
---@field changelog_after_upgrade "major"|"minor"|"patch"|"never" Open the changelog in a tab after an upgrade of at least this size: major, minor, patch or never
---@field colors rt.c.colors
---@field completion rt.c.completion
---@field confirm_quit string[] Ask before quitting: always, multiple-tabs (more than one tab open), downloads (downloads still running), or never
---@field content rt.c.content
---@field crash_report rt.c.crash_report
---@field downloads rt.c.downloads
---@field editor rt.c.editor
---@field extensions rt.c.extensions
---@field fileselect rt.c.fileselect
---@field fonts rt.c.fonts
---@field hints rt.c.hints
---@field input rt.c.input
---@field keyhint rt.c.keyhint
---@field messages rt.c.messages
---@field new_instance_open_target "tab"|"tab-bg"|"window" Where URLs from a second riptide invocation open
---@field new_instance_open_target_window "first-opened"|"last-opened"|"last-focused" Which window URLs from a second riptide invocation open in
---@field plugins rt.c.plugins
---@field prompt rt.c.prompt
---@field scrolling rt.c.scrolling
---@field search rt.c.search
---@field session rt.c.session
---@field spellcheck rt.c.spellcheck
---@field statusbar rt.c.statusbar
---@field tabs rt.c.tabs
---@field ui rt.c.ui
---@field url rt.c.url
---@field window rt.c.window
---@field zoom rt.c.zoom
---@class rt.c.auto_save
---@field interval integer Milliseconds between crash-recovery saves of the open tabs (0 turns them off)
---@field session boolean Save the open tabs as the 'default' session on quit, and restore them at startup
---@class rt.c.bindings
---@field key_mappings table<string, string> Keys treated as other keys in every mode, before bindings are looked up, e.g. Ctrl-[ as Escape
---@class rt.c.colors
---@field completion rt.c.colors.completion
---@field hints rt.c.colors.hints
---@field keyhint rt.c.colors.keyhint
---@field messages rt.c.colors.messages
---@field prompts rt.c.colors.prompts
---@field statusbar rt.c.colors.statusbar
---@field tabs rt.c.colors.tabs
---@field webpage rt.c.colors.webpage
---@class rt.c.colors.completion
---@field category rt.c.colors.completion.category
---@field description rt.c.colors.completion.description
---@field fg string Completion list text; empty uses ui.theme's
---@field item rt.c.colors.completion.item
---@field match rt.c.colors.completion.match
---@field odd rt.c.colors.completion.odd
---@class rt.c.colors.completion.category
---@field bg string Background of completion category headers; empty uses ui.theme's
---@field fg string Text of completion category headers; empty uses ui.theme's
---@class rt.c.colors.completion.description
---@field fg string Descriptions and details in the completion list; empty uses ui.theme's
---@class rt.c.colors.completion.item
---@field selected rt.c.colors.completion.item.selected
---@class rt.c.colors.completion.item.selected
---@field bg string Background of the selected completion; empty uses ui.theme's
---@field fg string Text of the selected completion; empty uses ui.theme's
---@class rt.c.colors.completion.match
---@field fg string The typed text where it appears in completion items; empty uses ui.theme's
---@class rt.c.colors.completion.odd
---@field bg string Completion list background; empty uses ui.theme's
---@class rt.c.colors.hints
---@field bg string Background of hint labels; empty uses ui.theme's
---@field border string Border of hint labels; empty uses ui.theme's
---@field fg string Text of hint labels; empty uses ui.theme's
---@field match rt.c.colors.hints.match
---@class rt.c.colors.hints.match
---@field fg string The typed part of hint labels; empty uses ui.theme's
---@class rt.c.colors.keyhint
---@field suffix rt.c.colors.keyhint.suffix
---@class rt.c.colors.keyhint.suffix
---@field fg string The keys still to type in the key hint popup; empty uses ui.theme's
---@class rt.c.colors.messages
---@field error rt.c.colors.messages.error
---@field warning rt.c.colors.messages.warning
---@class rt.c.colors.messages.error
---@field bg string Background of error messages; empty uses ui.theme's
---@field fg string Text of error messages; empty uses ui.theme's
---@class rt.c.colors.messages.warning
---@field bg string Background of warnings; empty uses ui.theme's
---@field fg string Text of warnings; empty uses ui.theme's
---@class rt.c.colors.prompts
---@field bg string Background of prompts; empty uses ui.theme's
---@field border string Frame, title and keys of floating prompts; empty uses ui.theme's
---@field fg string Text of prompts; empty uses ui.theme's
---@field key rt.c.colors.prompts.key
---@class rt.c.colors.prompts.key
---@field bg string Background of a floating prompt's keys; empty uses ui.theme's
---@class rt.c.colors.statusbar
---@field insert rt.c.colors.statusbar.insert
---@field normal rt.c.colors.statusbar.normal
---@field passthrough rt.c.colors.statusbar.passthrough
---@field private rt.c.colors.statusbar.private
---@field url rt.c.colors.statusbar.url
---@class rt.c.colors.statusbar.insert
---@field bg string Status bar background in insert mode; empty uses ui.theme's
---@field fg string Status bar text in insert mode; empty uses ui.theme's
---@class rt.c.colors.statusbar.normal
---@field bg string Status bar background; empty uses ui.theme's
---@field fg string Status bar text; empty uses ui.theme's
---@class rt.c.colors.statusbar.passthrough
---@field bg string Status bar background in passthrough mode; empty uses ui.theme's
---@field fg string Status bar text in passthrough mode; empty uses ui.theme's
---@class rt.c.colors.statusbar.private
---@field bg string Status bar background in private windows; empty uses ui.theme's
---@field fg string Status bar text in private windows; empty uses ui.theme's
---@class rt.c.colors.statusbar.url
---@field error rt.c.colors.statusbar.url.error
---@field success rt.c.colors.statusbar.url.success
---@class rt.c.colors.statusbar.url.error
---@field fg string The address of a page that failed to load; empty uses ui.theme's
---@class rt.c.colors.statusbar.url.success
---@field http rt.c.colors.statusbar.url.success.http
---@field https rt.c.colors.statusbar.url.success.https
---@class rt.c.colors.statusbar.url.success.http
---@field fg string An http:// address in the status bar; empty uses ui.theme's
---@class rt.c.colors.statusbar.url.success.https
---@field fg string An https:// address in the status bar; empty uses ui.theme's
---@class rt.c.colors.tabs
---@field bar rt.c.colors.tabs.bar
---@field even rt.c.colors.tabs.even
---@field indicator rt.c.colors.tabs.indicator
---@field odd rt.c.colors.tabs.odd
---@field pinned rt.c.colors.tabs.pinned
---@field selected rt.c.colors.tabs.selected
---@class rt.c.colors.tabs.bar
---@field bg string Tab bar background behind the tabs; empty uses ui.theme's
---@class rt.c.colors.tabs.even
---@field bg string Background of even-numbered tabs; empty uses ui.theme's
---@class rt.c.colors.tabs.indicator
---@field error string A tab's indicator when its page failed to load; empty uses ui.theme's
---@field start string A tab's loading indicator; empty uses ui.theme's
---@class rt.c.colors.tabs.odd
---@field bg string Background of odd-numbered tabs; empty uses ui.theme's
---@field fg string Text of tabs; empty uses ui.theme's
---@class rt.c.colors.tabs.pinned
---@field odd rt.c.colors.tabs.pinned.odd
---@class rt.c.colors.tabs.pinned.odd
---@field bg string Background of pinned tabs; empty uses ui.theme's
---@field fg string Text of pinned tabs; empty uses ui.theme's
---@class rt.c.colors.tabs.selected
---@field accent string Color of the line marking the current tab, any CSS color such as #2ec4b6; empty matches the tab, so no line shows
---@field odd rt.c.colors.tabs.selected.odd
---@class rt.c.colors.tabs.selected.odd
---@field bg string Background of the current tab; empty uses ui.theme's
---@field fg string Text of the current tab; empty uses ui.theme's
---@class rt.c.colors.webpage
---@field bg string Background of a new tab before its page paints, e.g. #1e1e2e so dark themes don't flash white; #rrggbb, white or black
---@field darkmode rt.c.colors.webpage.darkmode
---@field preferred_color_scheme "auto"|"light"|"dark" The color scheme pages see in prefers-color-scheme: auto follows the system
---@class rt.c.colors.webpage.darkmode
---@field enabled boolean Render light pages dark with Chromium's automatic dark mode; applies at once and can be set per site
---@class rt.c.completion
---@field cmd_history_max_items integer How many command lines Up and Down remember
---@field delay integer Milliseconds to wait after a key press before updating completions
---@field height string Height of the completion list: rows (12) or a percentage of the window (50%)
---@field min_chars integer Characters to type after a command before its arguments complete
---@field open_categories string[] What :open completes from, in order: searchengines, quickmarks, bookmarks, history, filesystem
---@field quick boolean When only one command or setting name is left, Tab takes it and moves on to completing the next part
---@field show "always"|"auto"|"never" When to show completions: always, only after pressing Tab (auto), or never
---@field shrink boolean Shrink the completion list to its items; false keeps it completion.height tall
---@field timestamp_format string strftime format of the last-visit time shown next to history completions; empty hides it
---@field use_best_match boolean Return runs the first command that starts with an unknown command name, so :rel runs :reload
---@field web_history rt.c.completion.web_history
---@class rt.c.completion.web_history
---@field exclude string[] URL globs (e.g. *://*.bank.example/*) that :open never suggests from history
---@field max_items integer How many history entries :open completion shows (0 turns history completion off)
---@class rt.c.content
---@field autoplay boolean Let videos play by themselves; false waits until you interact with the page (after a restart)
---@field blocking rt.c.content.blocking
---@field cache rt.c.content.cache
---@field call_mute_keys table<string, string> The key each call site mutes the microphone with, by URL pattern, for :call-mute (cm); keys as in :bind, e.g. <Ctrl-d>
---@field call_sites string[] Video call sites, as URL patterns, that open in a call window. There, sharing your screen lets you pick a tab, a window or the whole screen; in an ordinary tab it always shares the whole screen. Clear the list to open these sites as ordinary tabs; you then lose that choice unless you use :open --call or :tab-call
---@field canvas_reading boolean Let pages read back what they drew on a canvas; false blocks a common fingerprinting trick but breaks some sites (after a restart)
---@field cookies rt.c.content.cookies
---@field desktop_capture "ask"|"true"|"false" Let sites capture your screen or desktop audio: ask, true or false
---@field dns_prefetch boolean Look up the hosts of links before you follow them, which is faster but tells your DNS server about them
---@field geolocation "ask"|"true"|"false" Let sites know your location: ask, true or false
---@field headers rt.c.content.headers
---@field images boolean Load images; can be set per site
---@field javascript rt.c.content.javascript
---@field local_content_can_access_file_urls boolean Let file:// pages read other local files, which a downloaded page could misuse (after a restart)
---@field media rt.c.content.media
---@field mouse_lock "ask"|"true"|"false" Let sites lock your mouse pointer, as games do: ask, true or false
---@field mute boolean Mute pages; can be set per site
---@field notifications rt.c.content.notifications
---@field pdf_viewer boolean Show PDFs in the browser; false downloads them instead
---@field prefers_reduced_motion boolean Tell pages you prefer less motion, so they can tone down animations (after a restart)
---@field proxy string Proxy: system, none, a proxy URL such as socks5://127.0.0.1:9050, or pac+ and a PAC script's URL
---@field register_protocol_handler "ask"|"true"|"false" Let sites register to handle links like mailto: : ask, true or false
---@field tls rt.c.content.tls
---@field unknown_url_scheme_policy "ask"|"allow-all"|"disallow" Links to schemes the browser can't show (mailto:, magnet:, zoommtg:): ask before handing them to xdg-open, always hand them over, or never
---@field user_stylesheets string[] CSS files applied to pages (relative paths are in the config directory); reloaded when they change; can be set per site
---@field webgl boolean Allow WebGL, which 3D graphics need and fingerprinting scripts use (after a restart)
---@field webrtc_ip_handling_policy "all-interfaces"|"default-public-and-private-interfaces"|"default-public-interface-only"|"disable-non-proxied-udp" Which IP addresses WebRTC (video calls) may reveal; disable-non-proxied-udp keeps it behind content.proxy
---@field widevine boolean Allow Widevine DRM: Chromium downloads Google's CDM once (takes effect after a restart)
---@class rt.c.content.blocking
---@field adblock rt.c.content.blocking.adblock
---@field enabled boolean Block ads and trackers with the filter lists from content.blocking.adblock.lists
---@field whitelist string[] Sites where nothing is blocked, as host names; a host also covers its subdomains
---@class rt.c.content.blocking.adblock
---@field lists string[] Adblock Plus filter lists or hosts files that :adblock-update downloads (https://, or file:// for local lists); uBlock Origin's own lists may use its trusted scriptlets
---@field rules string[] Your own filter rules, in Adblock Plus syntax (e.g. example.com##.banner); they apply at once, without :adblock-update
---@class rt.c.content.cache
---@field size integer Disk cache size in bytes; 0 lets Chromium choose (takes effect after a restart)
---@class rt.c.content.cookies
---@field accept "all"|"no-3rdparty"|"no-unknown-3rdparty"|"never" Which cookies sites may set: all, none from other sites (no-3rdparty; no-unknown-3rdparty is the same here), or never
---@field store boolean Keep cookies after the browser closes; false makes every cookie last only for the session
---@class rt.c.content.headers
---@field accept_language string Languages sites are asked for, e.g. en-US,en;q=0.9 (also navigator.languages); empty for the system's
---@field custom table<string, string> Extra headers sent with every request: name → value
---@field do_not_track boolean Send DNT: 1 with every request, asking sites not to track you
---@field referer "always"|"never"|"same-domain" When to send the Referer header: always, never, or only within the same domain and its subdomains
---@field user_agent string User agent sent to sites and shown to their scripts; empty for Chromium's own. Can be set per site
---@class rt.c.content.javascript
---@field can_close_tabs boolean Let a page close its own tab with window.close(), as login popups do
---@field can_open_tabs_automatically boolean Let pages open tabs and windows without a click (popups); can be set per site
---@field clipboard "none"|"access"|"access-paste" What pages may do with the clipboard: nothing, copy with a click (access), or also read it (access-paste); can be set per site
---@field enabled boolean Run JavaScript on pages; can be set per site
---@field log_message rt.c.content.javascript.log_message
---@class rt.c.content.javascript.log_message
---@field levels string[] Console messages from pages shown in the status bar and :messages, by level: debug, info, warning, error (can be set per site)
---@class rt.c.content.media
---@field audio_capture "ask"|"true"|"false" Let sites use your microphone: ask, true or false
---@field video_capture "ask"|"true"|"false" Let sites use your camera: ask, true or false
---@class rt.c.content.notifications
---@field app_name string The app name on desktop notifications (presenter = libnotify), which notification services such as dunst and mako can match to style them
---@field enabled "ask"|"true"|"false" Let sites show notifications: ask, true or false
---@field presenter "auto"|"libnotify"|"messages" Where page notifications show: auto (Chromium's desktop notifications), libnotify (desktop notifications riptide sends with notify-send, following the other content.notifications settings) or messages (riptide's status bar)
---@field show_origin boolean Show the site a notification came from (presenter = libnotify or messages)
---@field site_icon boolean Show the site's icon on desktop notifications (presenter = libnotify)
---@field timeout integer Milliseconds a desktop notification stays (presenter = libnotify): -1 lets the desktop decide, 0 keeps it until dismissed
---@field urgency "low"|"normal"|"critical" How urgent desktop notifications are (presenter = libnotify): low, normal or critical
---@class rt.c.content.tls
---@field certificate_errors "ask"|"block"|"load-insecurely" Pages whose TLS certificate isn't trusted: ask, block, or load-insecurely
---@class rt.c.crash_report
---@field email string Where :crash-report's Email button sends a report; empty hides the button
---@class rt.c.downloads
---@field location rt.c.downloads.location
---@field open_dispatcher string Program that opens downloads (:download-open); {} is the file, or it's added at the end. Empty for the desktop's default
---@field remove_finished integer Take finished downloads off the list after this many milliseconds; -1 keeps them
---@class rt.c.downloads.location
---@field directory string Where downloads go; empty means the system Downloads folder
---@field prompt boolean Ask where to save each download (false saves straight to the directory)
---@field remember boolean Start the save prompt in the folder the last download went to
---@field suggestion "both"|"path"|"filename" What the save prompt starts with: the folder and file name (both), the folder (path), or the file name
---@class rt.c.editor
---@field command string[] Editor for :open-editor; fields: {file}, {line}, {column}, {line0}, {column0}
---@field remove_file boolean Delete the temporary file after the editor closes; false keeps it, e.g. to recover text
---@class rt.c.extensions
---@field load string[] Folders of unpacked Chrome extensions to load, besides the ones :extension-install installs (after a restart)
---@class rt.c.fileselect
---@field folder rt.c.fileselect.folder
---@field handler "default"|"external" File pickers for upload fields: Chromium's own (default), or the fileselect.*.command programs (external)
---@field multiple_files rt.c.fileselect.multiple_files
---@field single_file rt.c.fileselect.single_file
---@class rt.c.fileselect.folder
---@field command string[] Program that picks a folder for fileselect.handler = external; {} is the file it writes the path to
---@class rt.c.fileselect.multiple_files
---@field command string[] Program that picks several files for fileselect.handler = external; {} is the file it writes the paths to, one per line
---@class rt.c.fileselect.single_file
---@field command string[] Program that picks a file for fileselect.handler = external; {} is the file it writes the path to
---@class rt.c.fonts
---@field completion rt.c.fonts.completion
---@field default_family string Font family that the other fonts.* settings call default_family
---@field default_size string Font size that the other fonts.* settings call default_size, e.g. 10pt or 13px
---@field hints string Font of hint labels
---@field keyhint string Font of the key hint popup
---@field prompts string Font of prompts
---@field statusbar string Font of the status bar; sizes beyond the bar's height are cut off until bars size to their font
---@field tabs rt.c.fonts.tabs
---@field web rt.c.fonts.web
---@class rt.c.fonts.completion
---@field category string Font of completion category headers (default_size and default_family stand for those settings)
---@field entry string Font of completion entries
---@class rt.c.fonts.tabs
---@field selected string Font of the current tab
---@field unselected string Font of the other tabs
---@class rt.c.fonts.web
---@field family rt.c.fonts.web.family
---@field size rt.c.fonts.web.size
---@class rt.c.fonts.web.family
---@field fixed string Monospace font for pages (CSS monospace); empty for Chromium's
---@field sans_serif string Sans-serif font for pages; empty for Chromium's
---@field serif string Serif font for pages; empty for Chromium's
---@field standard string Font for pages that don't choose one; empty for Chromium's
---@class rt.c.fonts.web.size
---@field default integer Default text size of pages, in pixels
---@field default_fixed integer Default size of monospace text in pages, in pixels
---@field minimum integer Smallest text size pages may use, in pixels (0 for no minimum)
---@class rt.c.hints
---@field auto_follow "always"|"unique-match"|"full-match"|"never" When a hint is followed without Return: when one is left (unique-match), only when its label is typed in full (full-match), always, or never
---@field auto_follow_timeout integer Ignore keys for this many milliseconds after following a hint, so extra typing doesn't reach the page
---@field chars string Characters used for hint labels
---@field dictionary string Word list for hints.mode = word, one word per line
---@field hide_unmatched_rapid_hints boolean In rapid hint mode (:hint --rapid), hide the labels that don't match what's typed
---@field leave_on_load boolean Leave hint mode when the page starts loading something new
---@field min_chars integer The shortest hint label, in characters
---@field mode "letter"|"number"|"word" letter: labels from hints.chars; number: numbered labels, and typing letters filters by text; word: dictionary words from each link's text
---@field next_regexes string[] Link texts ]] follows to the next page, as JavaScript regular expressions (case doesn't matter)
---@field padding string Space around a hint label's text, as CSS padding, e.g. 1px 4px
---@field prev_regexes string[] Link texts [[ follows to the previous page, as JavaScript regular expressions (case doesn't matter)
---@field radius integer Corner radius of hint labels in pixels; 0 is square
---@field scatter boolean Spread hint labels over the alphabet so neighbours differ; false labels in order
---@field selectors table<string, string> Hint groups for :hint, as CSS selector lists; your entries are added to the built-in all, links, images, media and inputs
---@field uppercase boolean Show hint labels in upper case
---@class rt.c.input
---@field forward_unbound_keys "all"|"auto"|"none" Pass unbound keys to the page in normal mode (auto: all but plain letters and digits)
---@field insert_mode rt.c.input.insert_mode
---@field match_counts boolean Read digits typed before a binding as a count (3j); false lets digits be bindings themselves
---@field media_keys boolean Let the keyboard's media keys (play, pause, next) control audio and video in pages (after a restart)
---@field mode_override "none"|"normal"|"insert"|"passthrough" Mode to enter when a page loads or its tab is focused; set it per site, e.g. passthrough for a web terminal
---@field mouse rt.c.input.mouse
---@field partial_timeout integer Milliseconds before a half-typed key chain or count is forgotten; 0 waits forever
---@field spatial_navigation boolean Move focus between links and fields with the arrow keys, as on a TV (after a restart)
---@class rt.c.input.insert_mode
---@field auto_enter boolean Enter insert mode when an editable element gets focus
---@field auto_leave boolean Leave insert mode when focus leaves an editable element
---@field auto_load boolean Enter insert mode when a page focuses a text field by itself, as autofocus does on load
---@field leave_on_load boolean Leave insert mode when a new page starts loading
---@class rt.c.input.mouse
---@field rocker_gestures boolean Hold the right button and click the left to go back, or the other way round to go forward; turns off the page's context menu
---@class rt.c.keyhint
---@field blacklist string[] Key chains the key hint popup leaves out, as globs on the whole chain (e.g. g* for every chain starting with g)
---@field delay integer How long after a partial key chain the popup listing its continuations appears, in milliseconds
---@class rt.c.messages
---@field timeout integer Milliseconds before a status bar message clears (0 keeps it)
---@class rt.c.plugins
---@field catalog string The git repository of plugins the Plugins tab's Browse lists, one plugin per folder
---@field check_interval integer Every this many days, check plugins from git for new commits in the background and say once which have updates; nothing updates by itself (0: never)
---@class rt.c.prompt
---@field position "bottom"|"center"|"docked" Where questions (permissions, logins, downloads, page dialogs) appear: bottom, a box floating near the bottom of the page; center, the same box in the middle; or docked above the status bar
---@field width integer Width in pixels of a floating prompt (prompt.position = bottom or center), at most the page's
---@class rt.c.scrolling
---@field bar "always"|"never"|"overlay" Page scrollbars: always, never, or overlay (thin, shown while scrolling; after a restart)
---@field smooth boolean Animate scrolling by keys instead of jumping
---@class rt.c.search
---@field ignore_case "smart"|"always"|"never" Case in searches: smart ignores it unless the text has a capital, always, or never
---@field incremental boolean Search while typing after / or ?
---@field wrap boolean Go on from the top when a search passes the last match (or from the bottom, searching up)
---@field wrap_messages boolean Say when a search wraps around the page
---@class rt.c.session
---@field default_name string Session that :session-save, :wq and auto_save.session use; empty means the last one loaded, or default
---@field lazy_restore boolean When restoring a session, load background tabs only when they are first shown
---@class rt.c.spellcheck
---@field languages string[] Spell-check languages such as en-US (empty: off); Chromium downloads each dictionary from Google once
---@class rt.c.statusbar
---@field padding string Space around the status bar's text, as CSS padding (top right bottom left), e.g. 2px 8px; the bar grows to fit
---@field position "top"|"bottom" Where the status bar is
---@field show "always"|"never"|"in-mode" When to show the status bar: always, only while typing a command or answering a prompt (never), or also outside normal mode and while a message is shown (in-mode)
---@field widgets string[] What the right side of the status bar shows, in order: keypress, downloads, blocked (requests the ad blocker stopped on the page), muted, media, sharing (a screen, window or tab being shared from any tab; :share-stop stops it), zoom, search_match, url, scroll, scroll_raw, history, tabs, progress, clock[:strftime format], text:…, lua:<name> (drawn by rt.statusbar.widget)
---@class rt.c.tabs
---@field close_mouse_button "middle"|"right"|"none" Which mouse button closes a tab clicked in the tab bar
---@field close_mouse_button_on_bar "new-tab"|"close-current"|"close-last"|"ignore" What tabs.close_mouse_button does on the empty part of the tab bar
---@field favicons rt.c.tabs.favicons
---@field indicator rt.c.tabs.indicator
---@field last_close "ignore"|"blank"|"startpage"|"default-page"|"close" What closing the last tab does
---@field max_width integer Largest width in pixels of a tab in a top or bottom tab bar (-1 for no limit)
---@field min_width integer Smallest width in pixels of a tab in a top or bottom tab bar; tabs that don't fit scroll (-1 for no minimum)
---@field mode_on_change "normal"|"persist"|"restore" Mode after switching tabs: normal, persist (keep insert/passthrough), or restore (the mode the tab was left in)
---@field mousewheel_switching boolean Switch tabs with the mouse wheel over the tab bar
---@field new_position rt.c.tabs.new_position
---@field padding string Space around each tab's title, as CSS padding (top right bottom left); the tab bar grows to fit
---@field pinned rt.c.tabs.pinned
---@field position "top"|"bottom"|"left"|"right" Where the tab bar is; left and right list the tabs vertically
---@field select_on_remove "next"|"prev"|"last-used" Which tab to show after closing the current one: the next, the previous, or the one used before
---@field show "always"|"never"|"multiple"|"switching" When to show the tab bar: always, never, with more than one tab, or briefly after switching tabs
---@field show_switching_delay integer How long the tab bar stays after switching tabs with tabs.show = switching, in milliseconds
---@field tabs_are_windows boolean Open every tab in its own window and hide the tab bar, for tiling window managers
---@field title rt.c.tabs.title
---@field tooltips boolean Show a tab's title and URL when the mouse rests on it
---@field undo_stack_size integer How many closed tabs u can reopen; 0 keeps none
---@field width integer Width of the tab bar in pixels when tabs.position is left or right
---@field wrap boolean Wrap around from the last tab to the first (and back) when switching tabs
---@class rt.c.tabs.favicons
---@field show "always"|"never"|"pinned" Show site icons in the tab bar: always, never, or only on pinned tabs
---@class rt.c.tabs.indicator
---@field width integer Width in pixels of the loading indicator at the left of each tab (0 hides it)
---@class rt.c.tabs.new_position
---@field related "prev"|"next"|"first"|"last" Where tabs opened from a page go (popups, hints)
---@field unrelated "prev"|"next"|"first"|"last" Where other new tabs go (:open -t)
---@class rt.c.tabs.pinned
---@field close "ask"|"refuse"|"close" Closing a pinned tab without --force: ask first, refuse, or just close it
---@field frozen boolean Keep pinned tabs on their page: :open in a pinned tab opens a new tab
---@field shrink boolean Shrink pinned tabs to their icon and number
---@class rt.c.tabs.title
---@field alignment "left"|"center"|"right" Where tab titles sit in their tab: left, center or right
---@field format string Tab titles; fields: {index}, {aligned_index}, {current_title}, {current_url}, {host}, {perc}, {audio}, {media} ([A/V] while the page uses a camera or the screen, and a microphone), {private}
---@field format_pinned string Titles of pinned tabs while tabs.pinned.shrink shrinks them; same fields as tabs.title.format
---@class rt.c.ui
---@field auto_theme rt.c.ui.auto_theme
---@field overlay rt.c.ui.overlay
---@field theme string Colors of riptide's bars, prompts and hints: riptide, riptide-light, gruvbox, catppuccin, nord, dracula, solarized, tokyo-night or a theme from themes/ in the config directory (:theme); auto follows the light or dark preference
---@class rt.c.ui.auto_theme
---@field dark string The theme ui.theme = auto uses when the desktop (or colors.webpage.preferred_color_scheme) prefers dark
---@field light string The theme ui.theme = auto uses when light is preferred
---@class rt.c.ui.overlay
---@field position "docked"|"floating" Where the command line's completions and the key hints appear: docked above the status bar, or floating, a box near the top of the page that also shows the command
---@field width integer Width in pixels of the floating overlay (ui.overlay.position = floating), at most the page's
---@class rt.c.url
---@field auto_search "naive"|"schemeless"|"never" When :open searches: text that doesn't look like an address (naive), anything without a scheme:// (schemeless), or never
---@field default_page string Page for :open without a URL
---@field incdec_segments string[] Parts of the URL Ctrl-a and Ctrl-x change: host, port, path, query, anchor
---@field open_base_url boolean Open a search engine's home page when :open gets just its name
---@field searchengines table<string, string> Search engines; ':open g rust' uses the 'g' entry, anything else DEFAULT
---@field start_pages string[] Pages opened at startup when no URL is given
---@field yank_ignored_parameters string[] Query parameters dropped when yanking a URL, such as tracking tags
---@class rt.c.window
---@field hide_decoration boolean Ask the window manager for no title bar or borders (applies to new windows)
---@field title_format string Window title; fields: {current_title}, {title_sep}, {current_url}, {host}, {mode}
---@class rt.c.zoom
---@field default integer Zoom in percent for pages, and what :zoom without a value resets to
---@field levels string[] The zoom levels + and - step through, in percent
---@type rt.c
c = {}
Changelog
All notable changes to riptide. Generated by git-cliff from Conventional Commits.
0.3.0 - 2026-10-08
Features
- settings: :settings opens a page to browse, change and reset every setting
- tabs: Mark tabs using the camera, microphone or screen
- settings: A clearer settings page with sections, a colors preview and checks while typing
- settings: URLs & search second on the settings page
- sessions: :recover lists each crash's tabs to reopen some or all
- notifications: Libnotify presenter with app name, urgency, timeout and site icon
- settings: A Keys tab to bind, unbind and restore keys, with clash warnings
- settings: A Sites tab to forget saved permission answers and clear site data
- windows: Well-known call sites open in call windows by default
- settings: Edit per-site values on the page, and j/k/Return to move and edit
- windows: Say when a call site opened in a call window
- prompts: Questions belong to their tab, and permission requests show the site and what it wants
- completion: Site icons in :open and tab completion
- adblock: The status bar counts requests blocked on the page
- adblock: Hosts files work as block lists
- completion: Letters with gaps match when nothing contains the text
- spell: :spell-install downloads dictionaries checked against pinned checksums
- tabs: Cm mutes the call from any tab with the site's own key
- spell: :spell-install alone lists the languages to pick from
- tabs: Gp floats the page's video in picture-in-picture
- Chromium's crash dumps are kept locally and listed in :crash-report
- lua: Rt.on with patterns, groups and once, and eleven more events
- lua: Rt.defer and rt.every timers, rt.notify levels, and a time limit per callback
- lua: Rt.keymap with descriptions, and commands that complete their arguments
- lua: Rt.store keeps data between runs
- lua: Sandboxed plugins from a folder, loaded once you approve their permissions
- tabs: Screen shares are marked, and :share-stop ends them from any tab
- lua: Plugins install from git and stay pinned in rt-pack-lock.json
- lua: A Plugins tab with update review, revoke and remove
- lua: Plugins can wait for an event, command or key to load
- lua: Rt.statusbar.widget draws
lua:<name>widgets - lua: Rt.ui.select and rt.ui.input, rt.page to type and fill logins, and rt.json
- lua: Rt.ui.float draws boxes of text over the page
- lua: Builtin plugins, starting with passwords to fill logins from pass, rbw, bw or KeePassXC
- lua: Rt.ui.panel docks lines beside or below the page
- lua: Plugins ship HTML pages that talk only to their own plugin
- Chrome extensions, installed from the Web Store with :extension-install
- lua: Plugin pages can dock in a panel beside the page
- lua: Rt.page.eval, rt.page.css and rt.page.selection
- lua: Rt.page.hint hints a selector's elements and hands the pick to Lua
- An Extensions tab in :settings, :extension-open and extension updates
- help: :help rt.ui.float and :help
<plugin> - Install extensions straight from the Web Store page
- lua: Riptide --plugin-test runs a plugin's specs, and a plugin template
- lua: Subdir installs one plugin from a repository of several
- Extension popups float over the page, as in Chrome
- Drag an extension's popup by its bar
- lua: Plugins declare dependencies, which install and load first
- lua: One checkout per repository, and check, update, sync, restore and clean
- adblock: Scriptlets, $redirect stand-ins and $removeparam
- lua: Riptide checks plugins for updates in the background
- adblock: UBlock Origin's lists by default, with its trusted scriptlets
- adblock: Procedural element hiding (:has-text, :upward, :remove and more)
- adblock: Element hiding inside frames
- lua: Check plugins for updates daily, mentioning each update once
- lua: Add plugins from the Plugins tab or :pack-add, kept in plugins.toml
- adblock: Your own rules, and an element picker that writes them (;x)
Bug fixes
- window: Windows can be resized, so tiling window managers tile them
- cmdline: :q closes the current window and :qa quits, as in qutebrowser
- tabs: Middle-click and Ctrl+click open links in a background tab
- ui: Ctrl+scroll no longer zooms riptide's own bars
- lua: A plugin whose folder is missing shows as Failed on the Plugins tab
- Say plainly what works when an extension has a popup
- lua: Commands added after loading run in the same callback
- extensions: Build on Windows, and wait for the limits link in the e2e test
- lua: Plugin names and tests on Windows
0.2.0 - 2026-10-07
Features
- tabs: Pinned tabs stay where they are and can sit between others
- tabs: Gt lists this window's tabs by number alone
- tabs: A dragged tab shrinks and stays centred on the pointer
- content: Proxy, WebRTC IP policy, DNS prefetch, canvas reading, cache size
- content: Autoplay, WebGL, reduced motion, PDF viewer, window.close and console messages
- release: Ship a .deb with each release
- cmdline: Ctrl-v and Shift-Insert paste into the command line and prompts
- input: Partial_timeout, match_counts and mode_override
- :report, :debug-keytester, :debug-log-filter, editor.remove_file, new_instance_open_target_window
- Scrolling.bar and tabs.close_mouse_button_on_bar
- content: Mailto: and other external links, mouse lock, protocol handlers, file access
- downloads: Prompt-open-download (Ctrl-x) and prompt-yank (Alt-y)
- input: Rocker gestures, spatial navigation and media keys
- config: Say when a changed setting only applies after a restart
- content: Notifications in the status bar, and keep tests off the desktop bus
- tabs: Tabs.tabs_are_windows
- cmdline: Cmd-repeat, cmd-repeat-last and cmd-run-with-count
- hints: Hints inside cross-origin iframes and open shadow roots
- tabs: A tab whose renderer crashed says so, and r reloads it
- prompts: Float prompts in a box near the bottom of the page
- A panic writes a crash report, and the next start says where
- prompts: Amber floating prompts with a button per answer
- theme: Themes and colors.* settings for riptide's bars, prompts and hints
- theme: Fonts for riptide's bars, prompts and hints, and for pages
- theme: Ui.css and content.user_stylesheets, reloaded on change
- theme: Colors.webpage.bg, and dark mode that applies at once and per site
- sessions: Restored tabs keep their back/forward history and scroll position
- completion: Commands and settings match anywhere in their name, with the match highlighted
- theme: Bars and completion rows grow to fit their fonts and padding
- theme: A floating command line, ui.overlay.position and ui.overlay.width
- hints: Hints.radius, hints.padding and a hints.css file shape the labels
- prompts: Prompt.position center, and a CSS class for what each prompt is about
- theme: Ui.theme = auto follows the light or dark preference
- theme: :theme previews each theme while it's typed or picked
- theme: Your own themes from
themes/<name>.tomlin the config directory - window: Riptide's logo as the window icon
- theme: The downloads and history pages take the theme's colors
- windows: Call windows, where screen sharing picks a tab, window or screen
- theme: Import base16 schemes and qutebrowser theme files from themes/
- windows: :tab-call and content.call_sites send calls to call windows
- lua: Rt.theme defines a theme in config.lua
- :crash-report shows the last crash report, to send as a GitHub issue or by email
Bug fixes
- tabs: Place a dragged tab by the pointer, not its centre
- sessions: Keep each crash's tabs, and don't reopen tabs that crash again
- permissions: Alt-y in a permission prompt copies the site's origin
- notifications: Name riptide's desktop file, not Chromium's
- sessions: Crash recovery messages stay until the tabs have loaded
- theme: Readable text in every built-in theme
- theme: The current tab is always the darkest tab
- sessions: Ctrl-C and a closed terminal keep the tabs for crash recovery
- window: Give riptide's windows a WM_CLASS
- windows: Keep ordinary windows' focus as before call windows
- theme: In light themes the current tab is the brightest tab
0.1.0 - 2026-10-06
Features
- Scaffold CEF-based vim-like browser (milestone 0)
- Add ./task wrapper that bootstraps Task
- Add tabs (milestone 3)
- Add hints and yank/paste (milestone 4)
- Add configuration: TOML, Lua and :set/:bind (milestone 5)
- Add history, quickmarks, bookmarks and sessions (milestone 6)
- Add prompts, downloads and permissions (milestone 7)
- ui: Serve UI pages from hb:// with a UI-only message channel
- tabs: Pinned tabs, tab bar mouse support and favicons
- help: Generated help page, :help, :version and F1
- remote: Hand arguments to a running browser from the terminal
- ui: Add window.title_format
- sandbox: Run Chromium's sandbox whenever Linux allows it
- privacy: Stop background calls that only serve Google
- adblock: Block ads and trackers with Adblock Plus filter lists
- help: Add :changelog and an "Updated to" notice
- spell: Spell checking with keyboard-driven suggestions
- spawn: Add :spawn, qutebrowser-style userscripts and :open-editor
- marks: Set and jump to marks with ` and '
- macros: Record keys with q and replay them with @
- caret: Add caret mode for keyboard text selection
- greasemonkey: Run Greasemonkey scripts in matching pages
- search: Find text in the page with /, ?, n and N
- navigate: Go up the URL, to next/prev pages, or change its number
- ui: Add dark mode and preferred color scheme settings
- config: Per-site settings, and save 'always' permission answers
- tabs: Restore each tab's mode when switching back
- tls: Ask before loading pages with untrusted certificates
- widevine: Opt-in Widevine DRM with content.widevine
- windows: Multiple windows and private windows
- lua: Script the browser from config.lua
- clipboard: Yank to and open from the primary selection
- tabs: Pick a tab in any window with :tab-select (T)
- session: Crash-recovery autosave and a history page
- history: Import qutebrowser's history with :history-import
- tabs: Clone tabs and move them between windows
- help: List commands from config.lua on the help page
- hints: Hint elements inside same-origin iframes
- hints: Number hints that filter by text
- downloads: Tab path completion and a :downloads page
- packaging: Build an AppImage next to the tarball
- ui: Stack messages above the status bar
- adblock: Hide ads with the lists' element-hiding rules
- adblock: Re-check element hiding for ads that load late
- spawn: Show output in a tab with :spawn -o, and Y in caret mode
- hints: Run programs and userscripts on hinted links
- search: Follow a search match's link with Return
- lua: Add hb.tabs() for the current window's tabs
- Zoom, devtools, print, fullscreen, view-source, jseval and more
- Scroll-px, cmd-later, fake-key, click-element and more commands
- breaking: Rename hackers-browser to riptide
- tabs: Vertical tabs with tabs.position, and tabs.show
- statusbar: Statusbar.position and statusbar.show
- statusbar: Statusbar.widgets
- keys: A key hint popup for partial key chains
- completion: Delete and yank the selected completion
- editor: Edit-url, cmd-edit and edit-text, and clean yanked URLs
- hints: Hints.auto_follow, hint-follow and hints.selectors
- input: Bindings.key_mappings and input.insert_mode.auto_load
- sessions: Session.lazy_restore and confirm_quit
- screenshot: Save the tab as an image with :screenshot
- content: JavaScript, cookie and user agent settings
- downloads: Retry, remove and delete downloads; external file picker
- greasemonkey: @require and GM_setValue/getValue/deleteValue/listValues
- prompts: Alt-e picks a folder for the download prompt
- userscripts: Run qutebrowser's bundled userscripts unchanged
- config: Config-list/dict add and remove, config-diff, config-clear, config-edit, config-write-toml
- tabs: Tab title format, select_on_remove, wrap, undo stack size, tooltips
- search: Search.wrap and wrap messages; smooth scrolling; zoom.levels
- keys: Readline word commands, rl-yank, caret block moves, selection-drop
- tools: Restart, bookmark-list, mark reloads, debug-dump-page, debug-clear-ssl-errors, devtools-focus
- userscripts: breaking: Name the userscript variables RIPTIDE_*
- completion: Completion.show, height, open_categories and more
- urls: Url.auto_search, url.open_base_url and url.incdec_segments
- hints: Hints.min_chars, scatter, leave_on_load and next/prev regexes
- downloads: Location.suggestion and remember, open_dispatcher, remove_finished
- content: Images, mute, popups, clipboard and protocol handler settings
- tabs: Mark the current tab by making it darker, with an accent line
- tabs: Title alignment, min and max tab width, indicator width, close button
- completion: Shrink, timestamp_format, delay, quick and use_best_match
- Update CEF to 154.0.33 (Chromium 154.0.8037.94)
- hints: Word hints from hints.dictionary, and hints.hide_unmatched_rapid_hints
- tabs: No accent line on the current tab by default; colors.tabs.selected.accent sets one
- sessions: Session.default_name and :save
- window: Window.hide_decoration and changelog_after_upgrade
- greasemonkey: GM_xmlhttpRequest and GM_openInTab
- lua: Rt.spawn runs programs and calls back with their output
- content: Accept_language, do_not_track, referer and custom headers
- tabs: Gt tab picker, pinned-tab prompt, live drag, gD, :set values
- tabs: Gt tab picker, pinned-tab prompt, live drag, gD, :set values
Bug fixes
- Build on Windows, where CEF event flags are i32
- tabs: Focus a new tab again once its browser exists
- keys: Send keys that land in a UI view on to the current tab
- remote: Build valid file URLs from Windows paths
- ui: Leave the port and user info out of {host}
- permissions: Make per-site settings override Chromium's saved answers
- tabs: U reopens a tab closed before its page loaded
- content: Private windows crashed after content settings applied
Building
Requirements: Rust 1.88+ (edition 2024). The smoke test also needs Xvfb and xdotool.
Tasks run through Task. The ./task wrapper uses your installed task if there is one. Otherwise it downloads a pinned, checksum-verified release into .bin/. Arguments pass straight through.
./task setup # once: download CEF (~1.5 GB) into $CEF_PATH, default ~/.local/share/cef
./task run # build and launch; or: ./task run -- example.com
./task # list all tasks
./task setup reads the pinned cef crate version from Cargo.lock and fetches the matching CEF build. It skips the download when that version is already installed, and build, run and lint run it automatically. Set CEF_PATH to keep the binaries somewhere else.
The build copies libcef.so and Chromium's resources next to the binary. The binary finds them through an $ORIGIN rpath.
Logging goes to stderr and uses the RT_LOG filter, e.g. RT_LOG=rt_cef=trace ./task run. Browser data and cef.log live in ~/.local/share/riptide/.
Without Task
git clone --depth 1 --branch cef-v154.4.0+154.0.33 https://github.com/tauri-apps/cef-rs /tmp/cef-rs
(cd /tmp/cef-rs && cargo run -p export-cef-dir -- --force "$HOME/.local/share/cef")
export CEF_PATH="$HOME/.local/share/cef"
cargo build && ./target/debug/riptide
Without CEF_PATH, the cef-dll-sys build script downloads the binaries into target/ instead.
Documentation
./task docs builds this book into docs/book/book/, and ./task docs-serve serves it at http://localhost:3000 with live reload. Both use scripts/mdbook.sh, which downloads a pinned, checksum-verified mdBook into .bin/ if the right version isn't installed. See Writing documentation.
Architecture
Riptide is one Rust workspace around CEF's prebuilt Chromium. The project plan records the reasoning behind each decision and the lessons learned per milestone; this page is the map.
Crates
| Crate | Purpose |
|---|---|
crates/riptide | The riptide binary: main(), which hands subprocesses to CEF and starts the browser. |
crates/rt-core | Modes, key parsing, bindings, commands, the command line, settings, URL guessing and the help data. No CEF dependency; unit tested. |
crates/rt-config | Config paths per platform, the command line, TOML/Lua/autoconfig loading, the single-instance socket protocol, and the generated Lua types, settings docs and reference pages. |
crates/rt-storage | History (SQLite), quickmarks and bookmarks (qutebrowser formats), sessions (TOML). |
crates/rt-adblock | Ad and tracker blocking with Adblock Plus filter lists (Brave's adblock-rust). resources/ubo.json holds uBlock Origin's scriptlets and $redirect stand-ins; refresh it with scripts/update-adblock-resources.sh (needs Node) when uBlock Origin releases. |
crates/rt-cef | CEF integration: window layout, handlers, renderer-process bindings, the riptide:// pages and the status bar and completion UI. |
The rule that keeps this testable: rt-core never depends on CEF. CEF is an adapter that turns events into rt-core inputs and carries out the actions rt-core returns.
The core loop
Key event (OnPreKeyEvent, before the page sees it)
→ rt_core::Engine: mode, counts, multi-key sequences (gg, ;y)
→ a Command (:scroll down, :open -t …)
→ handled inside the engine (modes, command line), or
returned as Effect::Run { command, count } for rt-cef to carry out on a tab, window, UI or storage
Engine takes a Key and returns KeyOutcome { consumed, effects }, so every binding and mode transition is unit tested without a browser.
Processes
There is a single executable. CEF launches its renderer, GPU and utility processes by running it again, and main() dispatches them through cef::execute_process.
- Browser process: windows, tabs, modes, commands, config and storage.
- Renderer processes: a Rust
RenderProcessHandlerthat runs our page scripts (crates/rt-cef/js/: hints, scrolling, caret mode, navigation, spell checking, the editor) and reports results back through process messages. Pages can't forge those replies, and no global function is exposed to page scripts.
The window
Each window uses CEF Views in the Alloy runtime style. Every tab is its own BrowserView, and only the current one is visible. The tab bar, status bar, completion, prompts and messages are HTML pages (crates/rt-cef/ui/) served by the browser itself at riptide://ui/…, in their own views. Only riptide://ui/ pages get the rt.send() channel to Rust, and the browser accepts only the messages each page is allowed to send. Web pages can't link to, frame or redirect to riptide:// addresses.
Where to look
- A new command:
crates/rt-core/src/command.rs(COMMANDSand the parser), then itsEffecthandling incrates/rt-cef/src/shell.rs. - A new setting:
crates/rt-core/src/settings.rs. Types, docs and:helppick it up from the registry. - A default binding:
crates/rt-core/src/keymap.rs. - Page-side behaviour:
crates/rt-cef/js/and its Rust side incrates/rt-cef/src/.
CEF pitfalls
Things CEF, Chromium and the cef crate do that cost time to find out. Add to this page when you hit one, next to the area it belongs to.
Views and windows
- A preferred size with a 0 width or height counts as unset. The view then takes its large default and squeezes the page out of the layout. Always return a non-empty size (
window::bar_size). - Dropping the last reference to a
BrowserViewcloses its browser synchronously, which re-enters our handlers. The shell's state is a UI-threadRefCell, so never drop CEF objects, or call CEF methods that fire callbacks, while it's borrowed (shell::with). - The
cefcrate answers 0 forcan_resize,can_maximizeandcan_minimizeunless the delegate implements them, where CEF's own default is 1. Windows then ask for a fixed size and tiling window managers float them. Both window delegates return 1. - A Chrome-style
BrowserViewdoesn't exist until it's added to a window, so callingset_focusableon it first crashes. A Chrome-style window takes one Chrome-style view, which must be added before any Alloy view (the first view sets the window's profile). Popups must match their opener's style. Seewindow::create_callandtabs::add_call_view. - Overlay views are opaque and only as big as their bounds. Rounded corners show square ones behind them, and there's no dimming the page behind a box.
- A hidden view pauses
requestAnimationFrame. A page that measures itself before it's shown must do it synchronously (ui/float.html). - A window doesn't finish closing while any browser in it is open, overlay views included. Floats and panels are closed in the window's
can_close(float::close_window,panel::close_window), or:quithangs.
Keys and input
- The command line is a Rust-owned buffer; keys are consumed in
OnPreKeyEvent, so UI pages never need keyboard focus. - Chromium ignores input to a page while it shows a JavaScript dialog, so prompt keys go through the status bar's browser, which gets focus for the duration.
- A new tab can miss focus requested before its browser existed;
on_after_createdfocuses it again. send_key_eventto a hidden tab is dropped (keys go to the window's focused view), and DevTools'Input.dispatchKeyEventreports success without reaching the page in windowed browsers. The first key a page ever gets starts something that drops keys for about 400 ms (client::send_when_ready).- Under Xvfb, a key sent just as a page finishes loading can be lost below Views. The e2e harness waits two animation frames after each load.
- Middle-clicks and Ctrl+clicks come through
OnOpenURLFromTab, not popups, and Alloy loads them in the same tab if it isn't handled. - Ctrl+wheel over a bar zooms every
riptide://uipage (Chromium saves zoom per host). The bar pages refuse it, andon_load_endresets a saved zoom for non-tab roles.
Pages and navigation
- Loading a
data:error page fromOnLoadErroradds a history entry, sobackloops into the error. Draw into Chromium's own error document fromOnLoadEndinstead. BrowserHost::Findwithfind_next = falsenever activates or scrolls to a match. Every call passestrue; a repeated search first callsStopFinding.- CEF passes a browser's original
extra_infotoon_browser_createdagain on reload, which would undo a:greasemonkey-reload. Script lists carry a generation number. Page.addScriptToEvaluateOnNewDocumentsilently does nothing withoutPage.enablefirst (adblock::before_navigation).Notificationis defined after the context is created, so the renderer's stand-in takes its place on the first microtask,DOMContentLoadedandload.- A page starting to load clears every status message, so startup messages wait for the first load (
shell::show_message_after_load). - A stored default for Chromium's
PROTOCOL_HANDLERScontent setting fails aCHECKwhen a private window's profile inherits it.
Profiles, preferences and services
- Chromium names its profile folder
Defaultwhatevercache_pathsays, socache_pathpoints there. - Services start within 100 ms, so privacy preferences are written into
Local StateandDefault/Preferencesbefore CEF starts.SetPreferencefromon_context_initializedis too late, and through thecefcrate it fails silently unless the error out-string is non-empty (an emptyCefStringis passed as NULL). - Feature names in
libcef.sostrings carry akprefix that Chromium strips (kAimEnabled→AimEnabled). - Chrome's login prompt swallows HTTP auth unless
--disable-chrome-login-promptis set (cef#3603). GetAuthCredentialsruns on the IO thread; the prompt is posted to the UI thread.- Chromium saves permission answers per site, but camera and microphone requests go through a separate API that isn't saved; riptide keeps its own per-site answers. Chromium also holds non-media permission prompts from hidden tabs until they show.
The cef crate
CefStringList::clonecopies the opaque C struct, so iterating a clone is always empty; read lists through the C API.- An empty
CefStringis passed to C as NULL.
Extensions
- Manifest V2 is gone in Chromium 154: MV2 extensions silently don't load.
- Alloy tabs are invisible to
chrome.tabs.query, though messages reach them (sender.tabis set). Popups and keyboard commands that act on "the active tab" can't find riptide's tabs. - With a
declarativeNetRequestextension, a page opened during startup may never start loading;extensions::after_startupreloads such tabs. - Letting Chromium finish a
.crxdownload hands it to its own installer, which deletes the file. riptide cancels the download and fetches the URL itself.
Crashes
chrome://crashwith the sandbox on leaves the tab loading instead of crashing it; the test channel'sCrashTababorts the renderer instead.- Crashpad reads
crash_reporter.cfgnext to the executable, and keeps dumps with no size or age limit on Linux (rt_storage::crash_reports::dumpsprunes them). - A panic that reaches CEF's C callers panics again ("cannot unwind"); only the first panic on a thread is reported.
Building and releasing
- CEF's
libcef.socarries debug info (1.4 GB); stripping it gives 260 MB. - Inside an AppImage,
chrome-sandboxcan't be setuid, so the sandbox needs user namespaces there. - A tag pushed with a workflow's own token doesn't trigger other workflows, and GitHub Actions can't bypass a ruleset on a personal repository; see Releasing.
- mdBook's smart punctuation turns
--forceinto an en dash, so it's off.
Testing
- Unix socket paths must fit in about 108 bytes, so the e2e harness uses a short
XDG_RUNTIME_DIR. - CEF's zygote leaves the process group; the harness also stops the helpers that carry its
--user-data-dir. cargo test -p rt-e2edrives the builttarget/debug/riptideand doesn't rebuild it: runcargo buildfirst, or./task e2e.
Testing
| Command | What it runs |
|---|---|
./task test | Unit tests for rt-core, rt-config, rt-storage, rt-adblock and rt-cef (modes, keys, commands, settings, config files, paths for all three platforms, history, marks, sessions, and the CEF layer's decisions); no browser needed. rt-cef's tests link CEF, so they run on Linux only. |
./task e2e | End-to-end tests in crates/rt-e2e: real browsers, each on its own Xvfb display, driven through the test channel. ./task e2e -- tabs runs only the tests whose names contain tabs. |
./task smoke | A short check with real X11 input (xdotool keys and clicks, window focus) that the e2e tests can't give: typing into a field, a trusted click, tab keys, a second window, the bundled help page, and :wq with a restart. build-release.yml also runs it against the release packages. |
./task lint | cargo fmt --check, clippy -D warnings, ShellCheck on the scripts, actionlint on the workflows, and cargo-deny (below) |
./task check | All of the above |
The e2e and smoke tests use temporary profiles, so they never touch your browsing data or your browser.
End-to-end tests
Debug builds (and release builds with --features test-control) answer test requests on the command socket. Release builds refuse them. There are four requests:
| Request | Does |
|---|---|
keys | Presses keys such as 5j, <Escape> or :open x<Return>. They go through the same engine path as typed keys, and to the page when the engine doesn't use them. |
run | Runs a command line, as : would. |
state | Returns JSON with the mode, windows, tabs (URL, title, pinned, loading, mode), the status bar, completion and the prompt. |
eval | Runs JavaScript in a tab and returns its string result. |
evalbar | Runs JavaScript in the window's tabbar, statusbar or completion overlay page (Browser::eval_bar). |
crates/rt-e2e wraps them in a Browser that starts riptide with a scratch --basedir, its own Xvfb display, runtime directory and command socket, and a local HTTP server for the fixture pages in crates/rt-e2e/pages/. It stops only its own processes when the test ends. Its browsers run without the desktop's session bus and keep plugins' secrets in memory (RIPTIDE_SECRET_STORE=memory), so a test never touches your keyring. When a test fails, the end of the browser log is printed and the profile is kept in /tmp/rt-e2e-* for a look.
#![allow(unused)]
fn main() {
#[test]
#[ignore = "starts a browser; run with ./task e2e"]
fn d_closes_the_tab() {
let b = Browser::start("page.html");
b.run(&format!("open -t {}", b.url("second.html")));
b.wait_until("the second tab loads", |s| s.tabs().len() == 2 && s.tab().title == "second");
b.keys("d");
b.wait_until("one tab is left", |s| s.tabs().len() == 1);
}
}
- Wait, don't sleep:
wait_until,wait_modeandwait_evalpoll until the state matches, and fail with the last state after 15 seconds. Uses.tab().is_loaded(&url)to wait for a page: a new tab knows its URL before it starts loading, so checking the URL alone isn't enough. - Start options:
Browser::launch().toml("…").lua("…").file("data/greasemonkey/x.user.js", "…").script("config/userscripts/us", "…").start("page.html")writes the profile before the browser starts (scriptmakes the file executable). Config and files can use{server}(the fixture server),{pages}(the fixture directory) and{scratch}(an empty directory for the test's own files, alsob.scratch()). - Files and second invocations:
wait_file(path)polls until a file has content, e.g. a download or a userscript's output.invoke(&[url, ":cmd"])runsriptideagain on the same profile, which hands its arguments to the running browser. - Restarts:
config_dir()anddata_dir()give the profile's paths. After:quitandwait_exit(), acrash()(SIGKILL) or aterminate()(SIGTERM),restart()starts the browser again on the same profile, for testing sessions and crash recovery. - Hints:
follow_hint("hint links tab", |h| h.url.as_deref() == Some(&url))starts hints and presses the label of the element you pick by its text or link.state().hintslists the labels on screen. - Painting:
start()andopen()wait until the page has drawn a frame. Under load Chromium drops keys sent to a page that has loaded but not painted, so callwait_painted()after navigating some other way. - Every test is
#[ignore]d, so a plaincargo testnever starts browsers../task e2eruns them with--ignored, two at a time (E2E_THREADSchanges that). - Clicks that should count as the user's go through hints (
follow_hint). Insert mode ignores a script'sfocus(), so pages can't switch it on, and that includes test scripts. - The smoke test stays for what needs real X11 input (xdotool) and for checking the release packages. New behaviour gets an e2e test.
Unit tests in the CEF layer
Code in crates/rt-cef that decides something without needing CEF, such as where the tab bar goes, what a setting says to do, or which download a count means, is written as a plain function next to the code that uses it, and tested there. The CEF callback then only gathers its inputs and acts on the answer:
#![allow(unused)]
fn main() {
fn decide(setting: &str) -> Decision { … } // pure, tested in this file
pub fn certificate_error(…, callback: Callback) -> bool {
match decide(&setting) { // CEF glue around it
Decision::Load => callback.cont(),
…
}
}
}
When the logic isn't specific to CEF and other crates could use it, it goes in rt-core instead (for example rt_core::html::escape).
Linters
./task lint runs pinned versions of each tool through scripts/tool.sh, which downloads them into .bin/ and checks their checksums. That way a new upstream release never adds warnings to CI without warning. To upgrade one, change its version and checksums there.
| Tool | Checks | Config |
|---|---|---|
| rustfmt, clippy | Rust formatting and lints, with warnings as errors. Every crate also takes the shared lints in [workspace.lints]: no dbg!, todo! or unimplemented!, and a // SAFETY: comment on each unsafe block | Cargo.toml |
| ShellCheck | scripts/*.sh and task | # shellcheck disable=… comments, each with its reason |
| actionlint | .github/workflows/*.yml, including ShellCheck on their run: blocks | — |
| typos | Spelling in code, docs, scripts and pages | _typos.toml, for words that are right where they are |
| Biome | crates/rt-cef/js/*.js, the scripts riptide runs in pages, with warnings as errors; lint only, no formatting | biome.json |
| cargo-deny | Dependency licenses (each must be GPL-3.0-compatible), RustSec security advisories, yanked crates, and where crates come from | deny.toml |
A dependency with a license that isn't in deny.toml fails the check. Add the license only after checking that it's compatible with GPL-3.0.
Security advisories aren't part of ./task lint, because a new one can appear any day without anything in the repository changing. .github/workflows/audit.yml checks them daily and whenever Cargo.lock or deny.toml changes; run it yourself with scripts/tool.sh cargo-deny check advisories. A failing advisory is fixed by updating the crate, or listed in deny.toml's ignore with a reason.
Testing by hand
The local-testing skill describes how to drive the browser on a separate Xvfb display with a scratch --basedir, without touching a browser you're running yourself.
CI
.github/workflows/check.yml runs on every push to main and every pull request:
| Job | Runs |
|---|---|
| commit messages | scripts/check-commits.sh on the new commits |
| linux | ./task lint, ./task test, ./task smoke (Xvfb, cached CEF download) |
| macos, windows | cargo build, the unit tests, and --version/--paths |
| docs | builds this book and checks its internal links and anchors with lychee |
macOS and Windows are built and unit-tested but can't run the browser yet; packaging (M10) adds the app bundle and installer they need.
.github/workflows/docs.yml publishes the book to GitHub Pages on every push to main, and checks its external links weekly.
Other scheduled jobs:
| Workflow | When | Does |
|---|---|---|
audit.yml | daily, and when Cargo.lock or deny.toml changes | RustSec security advisories (cargo-deny) |
cef-update.yml | weekly | Opens an "Update CEF to X" issue when crates.io has a newer cef than Cargo.lock pins |
nightly.yml | daily | The rolling nightly pre-release (see Releasing) |
| Dependabot | weekly | Pull requests that update the GitHub Actions the workflows use |
Contributing
Commit messages
Commits follow Conventional Commits, which the changelog is generated from (feat and fix appear in it; docs, test, ci and chore don't): feat(tabs): add pinned tabs, fix: …, docs: …, ci: …. Run ./task hooks once to check messages locally before CI does.
Before you push
./task checkruns everything CI does: the linters, unit tests, the e2e tests and the smoke test.- Generated files must be current:
UPDATE_LUA_TYPES=1 cargo test -p rt-configrewritesdocs/lua/rt.meta.lua,docs/settings.mdand the generated reference pages after you add or change a command, setting or default binding. - User-visible changes need documentation in the same change; see Writing documentation.
Writing documentation
This book is the documentation. It's built with mdBook from docs/book/ and published to https://joshzcold.github.io/riptide/ on every push to main. A change that users or contributors would notice updates the book in the same commit.
Where things go
| Change | Update |
|---|---|
| A new or changed command, setting or default binding | Nothing by hand: regenerate the reference (below). Add or update a guide page only if the feature needs explaining beyond its one-line description. |
| New user-visible behaviour (a mode, a prompt, a page, a file riptide reads) | The matching page under docs/book/src/guide/ or configuration/. Add a page to SUMMARY.md if no existing one fits. |
| How riptide is built, structured, tested or released | The matching page under docs/book/src/dev/. |
| A pitfall or lesson learned (a CEF quirk, a threading rule) | CEF pitfalls, or the developer guide page for its area. |
| Plans and what's left to do | docs/PLAN.md only; it isn't published. Finished work is one line there, documented in the guides. |
Refactors, internal-only fixes and test-only changes don't need documentation.
Generated pages
These are written by code and checked by unit tests, so CI fails when they're stale. Never edit them by hand:
| File | Generated from |
|---|---|
docs/book/src/reference/commands.md | COMMANDS and the default keymap, through rt_core::help::build (the same data as :help) |
docs/book/src/reference/bindings.md | Keymap::defaults() |
docs/settings.md (included by the settings page) | the settings registry |
docs/lua/rt.meta.lua (included by the Lua API page) | the settings registry and the rt.* API |
Regenerate all of them with:
UPDATE_LUA_TYPES=1 cargo test -p rt-config
To change their text, change the description in the registry (crates/rt-core/src/command.rs, settings.rs, keymap.rs) or the generator (crates/rt-config/src/reference.rs, lua_types.rs). The changelog page includes CHANGELOG.md, which git-cliff generates from commit messages.
Building and previewing
./task docs # build into docs/book/book/
./task docs-serve # serve at http://localhost:3000, rebuilding on save
Style
User guide and configuration pages:
- Write for someone using the browser, in the second person ("
:setchanges a setting"), starting with the task and then how to do it. - Show one short example per feature: the keys, or a
config.lua/config.tomlsnippet. - Use plain language, and name settings and commands exactly, in backticks.
Developer guide pages:
- Explain why as well as what, and link to the code (
crates/rt-cef/src/shell.rs). - Keep instructions runnable: commands should work when pasted.
Both:
- Keep sentences short, and use a table or list when there are three or more parallel items.
- Link to other pages rather than repeating them, and to the reference for exhaustive lists.
- Write links to repository files outside the book as full GitHub URLs, since the site can't serve them.
For AI agents
.claude/skills/docs-user/ and .claude/skills/docs-dev/ turn these rules into checklists for agents, and AGENTS.md points other tools at them. When the rules here change, update the skills too.
Releasing
Releases are cut from GitHub Actions; nothing needs to run locally.
Cutting a release
main only takes changes through pull requests, so a release is two of them.
- Open Actions → release → Run workflow on
main. - Leave Version empty to let git-cliff work out the next version from the Conventional Commits since the last tag. In 0.x, a
featbumps the minor version and anything else the patch version. Or type one, such as0.3.0or0.3.0-rc.1(anything with a-is published as a pre-release). - Run it with Dry run ticked first (the default). It builds and tests everything, then keeps the packages and release notes as a
release-dry-runartifact for a day, without pushing or publishing. - Run it again with Dry run unticked. It commits
chore(release): vX.Y.Z(the version inCargo.tomlandCargo.lock, andCHANGELOG.md) to arelease/vX.Y.Zbranch and opens a pull request. - Merge the pull request. That's the release: the push to
mainbuilds, smoke-tests, tags and publishes it. - Merge the second pull request,
chore(release): update the AUR and Nix packages for vX.Y.Z, which points the packages at the new tarball.
If Actions isn't allowed to create pull requests (Settings → Actions → General → Allow GitHub Actions to create and approve pull requests), the run's summary has a link to open each one instead. Pull requests opened by the workflow don't trigger CI; the release build runs the smoke tests after the merge anyway.
.github/workflows/release.yml:
| Job | Does |
|---|---|
| plan | From Actions: prepares the release pull request, or for a dry run, the version to build. On every push to main: releases the commit if Cargo.toml's version has no tag yet (a merged release pull request), and otherwise does nothing. |
| build | Runs build-release.yml on the release commit (below). |
| publish | Writes SHA256SUMS, records build provenance for every package, and creates the GitHub release, which tags the commit, with that version's section of the changelog. Then scripts/update-packages.sh points the AUR and Nix packages at the new tarball, in a pull request. Pre-releases leave the packages alone, and a dry run only prints the change. |
Because a new version in Cargo.toml is what triggers a release, change it only through the release pull request.
What gets built
build-release.yml is shared by releases and nightlies:
- Linux (x86_64):
cargo build --release, thenscripts/package-linux.shpacks the stripped binary and CEF runtime intoriptide-<version>-linux-x86_64.tar.gz, an AppImage andriptide_<version>_amd64.deb. The whole smoke test then runs against the unpacked tarball, the extracted AppImage, and the.debinstalled with apt (with its setuid sandbox), so a broken package never ships../task package,./task appimageand./task debbuild the same files locally.- The
.deb'sDepends:comes fromdpkg-shlibdeps, so it names the libraries of the distribution it's built on. CI builds on Ubuntu 24.04, which makes it suit Ubuntu 24.04+ and Debian 13+.
- The
- macOS and Windows (experimental):
scripts/package-experimental.shpacks the binary with the CEF framework (macOS,.tar.gz) or runtime files (Windows,.zip). Neither runs the browser yet: they need the app bundle and installer work in M10. If either fails to build, the release goes ahead without it.
Nightly builds
.github/workflows/nightly.yml runs at 07:00 UTC. If main changed since the last nightly, it builds it the same way and replaces the rolling nightly pre-release, with files named riptide-nightly-<date>-<commit>-…. Running it by hand builds even if nothing changed. It never becomes the "latest" release.
Verifying a download
Every published package has a SHA256SUMS entry and a signed build provenance attestation that ties it to the workflow run and commit that built it:
sha256sum -c SHA256SUMS --ignore-missing
gh attestation verify riptide-0.2.0-linux-x86_64.tar.gz --repo joshzcold/riptide
After a release
Packages:
- The AUR
PKGBUILD(packaging/aur/) and the Nix package (packaging/nix/package.nix, used by the rootflake.nix) are updated by the publish job's pull request. If that step fails, runscripts/update-packages.sh X.Y.Z riptide-X.Y.Z-linux-x86_64.tar.gzwith the released tarball and open a pull request with the result. - To check the Nix package, run
nix build .#riptide, thenBIN=result/bin/riptide scripts/smoke-test.sh.
In the browser:
In the browser, :changelog shows the changelog it was built with, and the first start after an update says so in the status bar (and opens it, as changelog_after_upgrade decides). The documentation site isn't tied to releases: it's rebuilt from main on every push.