Control browser tabs from the command line using D-Bus IPC.
- D-Bus Architecture - Fast, reliable inter-process communication
- Multi-Browser Support - Firefox, Zen, Chrome, Brave work simultaneously
- Core Commands - List, close, activate, and open tabs across browsers
- Desktop Switching - Automatic window focus across virtual desktops
- Rofi Integration - Quick tab switching with rofi scripts
- Multiple Output Formats - TSV, JSON, simple
- Clean Architecture - Minimal dependencies, production ready
yay -S tabctl
# or
paru -S tabctlAfter installation, set up native messaging:
tabctl install- Build the binaries:
git clone https://github.com/slastra/tabctl.git
cd tabctl
go build -o tabctl ./cmd/tabctl
go build -o tabctl-mediator ./cmd/tabctl-mediator- Install native messaging host:
./tabctl install- Install the browser extension:
- Firefox / Zen: https://addons.mozilla.org/en-US/firefox/addon/tabctl1/
- Chrome / Chromium / Brave / Helium: https://chromewebstore.google.com/detail/tabctl/baomblllgemcgbignhpbipgiofmjdhpn
- Restart browser to activate native messaging
# List all tabs from all browsers
tabctl list
# List tabs from specific browser
tabctl list --browser Firefox
tabctl list --browser Brave
# Activate a tab (switches desktop if needed!)
tabctl activate firefox.1.2 # Firefox tab
tabctl activate brave.1234.5678 # Brave tab
tabctl activate helium.1874583011.1874583012 # Helium tab
# Close tabs
tabctl close firefox.1.2 firefox.1.3
echo "brave.1234.5678" | tabctl close
# Open URLs in new tabs (prints the new tab IDs)
tabctl open https://example.com https://github.com
echo "https://example.com" | tabctl open
tabctl open --browser Firefox https://example.com # when multiple browsers are connected
# Show mediator/extension versions and protocol compatibility
tabctl statusTab IDs are prefixed with the lowercased browser name so multiple browsers (e.g. Brave + Helium) stay distinguishable:
<browser>.<window_id>.<tab_id>- Examples:
firefox.1.2,helium.1874583011.1874583012,brave.999.42,chrome.123.45
Tab IDs are ephemeral — list prints them and activate/close consume
them; they aren't meant to be stored.
# JSON output
tabctl list --format json
# Simple format (titles only — display-only, cannot be mapped back to tab IDs)
tabctl list --format simple
# Custom delimiter
tabctl list --delimiter ","Quick tab switching with rofi (includes desktop switching):
# From a source checkout
./scripts/rofi-tabctl-wmctrl.sh # X11 (wmctrl)
./scripts/rofi-tabctl-niri.sh # Niri (Wayland)
./scripts/rofi-tabctl-hyprland.sh # Hyprland (Wayland), with favicons
# AUR installs ship the scripts here
/usr/share/tabctl/scripts/rofi-tabctl-wmctrl.sh
/usr/share/tabctl/scripts/rofi-tabctl-niri.sh
/usr/share/tabctl/scripts/rofi-tabctl-hyprland.shAdd to your window manager keybindings for instant access. The niri and
hyprland scripts also render per-tab favicons (needs jq, curl, and
imagemagick). They use your default rofi theme — style them via your
normal rofi config. In the Hyprland script, Enter switches to the
highlighted tab and Ctrl+w closes it (the menu reopens so you can clear
several tabs in a row).
Browser Extension ← Native Messaging → tabctl-mediator ← D-Bus → tabctl CLI
- tabctl - Command-line interface
- tabctl-mediator - Native messaging host with D-Bus server
- Browser Extensions - Firefox (Manifest V2) and Chrome (Manifest V3) extensions
- D-Bus Services -
dev.slastra.TabCtl.Firefox,dev.slastra.TabCtl.Brave
The native-messaging protocol is a JSON-RPC 2.0 subset with a version
handshake. Because the extension updates through the browser stores and the
tabctl binaries update through the AUR, the two can briefly be out of
sync after a release — update both to matching versions together.
Both sides surface a mismatch so it never fails silently:
- If the mediator is newer,
tabctl statusreports it and any tab command fails with a clear "update" error. - If the extension is newer (it handshakes but the mediator is too old to answer), the extension shows a red ! badge on its toolbar icon — hover it for the "update the tabctl package" hint.
The most common failure: the CLI reached the session bus, but no mediator is registered on it. In order of likelihood:
-
The extension isn't installed or is disabled. The mediator is launched by the browser through the extension — no extension, no mediator. Install it from the store:
- Firefox / Zen: https://addons.mozilla.org/en-US/firefox/addon/tabctl1/
- Chrome / Chromium / Brave / Helium: https://chromewebstore.google.com/detail/tabctl/baomblllgemcgbignhpbipgiofmjdhpn
Note: loading the extension as a temporary add-on (about:debugging) only lasts until the browser restarts.
-
The browser wasn't restarted after
tabctl installwrote the native messaging manifest. -
The mediator crashed. Check its log (see below).
Diagnostics:
# Is a mediator registered on the bus?
busctl --user list | grep -i tabctl
# Is the mediator process alive?
pgrep -af tabctl-mediator
# What does the mediator log say?
tail ~/.local/state/tabctl/mediator-*.logA different error, cannot connect to D-Bus session bus, means the
session bus itself is unreachable — check DBUS_SESSION_BUS_ADDRESS.
- Check extension is enabled in browser
- Verify native messaging host:
ls ~/.mozilla/native-messaging-hosts/tabctl_mediator.json ls ~/.config/*/NativeMessagingHosts/tabctl_mediator.json
- Check mediator is running:
ps aux | grep tabctl-mediator
-
Check D-Bus registration:
dbus-send --session --print-reply --dest=org.freedesktop.DBus \ /org/freedesktop/DBus org.freedesktop.DBus.ListNames | grep TabCtl -
Check logs (one file per browser):
tail -f ~/.local/state/tabctl/mediator-firefox.log
- Go 1.19+
- D-Bus session bus
- Browser with native messaging support
make build
# or
go build -o tabctl ./cmd/tabctl
go build -o tabctl-mediator ./cmd/tabctl-mediatorgo test ./...MIT - See LICENSE file for details
Inspired by BroTab, rewritten in Go with a D-Bus architecture and a Manifest v3 Chrome extension.