Skip to content

Instantly share code, notes, and snippets.

@gatopeich
Last active May 19, 2026 10:41
Show Gist options
  • Select an option

  • Save gatopeich/844c634051d087d09b217e09d2158da2 to your computer and use it in GitHub Desktop.

Select an option

Save gatopeich/844c634051d087d09b217e09d2158da2 to your computer and use it in GitHub Desktop.
Like Claude Squad, without fuss: A vertical TMUX sidebar to track all your Agent sessions at a glance
#!/usr/bin/env python3.12
"""
tmux-tabs — vertical window sidebar for tmux.
Shows all tmux windows in a narrow left pane with bell highlights and terminal
titles. Click any window to switch to it. Automatically follows focus when a
new window is opened (e.g. via F2 in byobu).
Usage:
Launch in a 15%-wide left split:
tmux split-window -hb -p 15 tmux-tabs
Recommended: bind to a key in ~/.tmux.conf:
bind -n F9 split-window -hb -p 15 tmux-tabs
Quit: press q inside the sidebar.
Requirements:
pip install --user textual (python3.12)
Colors are read live from the tmux theme (window-status-style, etc.) so the
sidebar matches your current byobu/tmux color scheme automatically.
"""
import os
import subprocess
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.reactive import reactive
from textual.widgets import Label
def tmux(*args):
return subprocess.run(["tmux", *args], capture_output=True, text=True).stdout.strip()
def pane_var(pane, fmt):
return tmux("display-message", "-p", "-t", pane, fmt)
def tmux_windows():
out = tmux("list-windows", "-F",
"#{window_index}|#{window_name}|#{window_bell_flag}|#{window_flags}|#{pane_title}")
return [
{"index": idx, "name": name, "bell": bell == "1",
"active": "*" in flags, "last": "-" in flags, "title": title}
for line in out.splitlines()
for idx, name, bell, flags, title in [line.split("|", 4)]
]
def parse_style(style):
return {k: v or True for k, _, v in (p.strip().partition("=") for p in style.split(","))}
def tmux_styles():
keys = {"normal": "window-status-style", "current": "window-status-current-style",
"bell": "window-status-bell-style", "status": "status-style"}
return {name: parse_style(tmux("show-options", "-gv", key)) for name, key in keys.items()}
def style_to_css(s, base=None):
bg = s.get("bg", base.get("bg", "default") if base else "default")
fg = s.get("fg", base.get("fg", "default") if base else "default")
if s.get("reverse"):
bg, fg = fg, bg
bold = "\n text-style: bold;" if s.get("bold") else ""
return f"background: {bg};\n color: {fg};{bold}"
class WindowItem(Label):
def __init__(self, win, app_ref):
self.win_index = win["index"]
self._app_ref = app_ref
icon = "🔔" if win["bell"] else ("▶" if win["active"] else " ")
title = win["title"].lstrip("✳").strip()
label = f"{icon} [b]{win['name']}[/b]\n [dim]{title}[/dim]" if title else f"{icon} [b]{win['name']}[/b]"
classes = "window" + (" bell" if win["bell"] else "") + (" active" if win["active"] else "")
super().__init__(label, classes=classes, markup=True)
def on_click(self):
self._app_ref.switch_to(self.win_index)
class TmuxTabs(App):
DEFAULT_CSS = ""
BINDINGS = [("q", "quit", "Quit")]
windows: reactive[list] = reactive([], recompose=True)
_pane_id: str = ""
_current_window: str = ""
@classmethod
def build_css(cls):
s = tmux_styles()
base = s["status"]
bg = s["normal"].get("bg", base.get("bg", "#94cdba"))
fg = s["normal"].get("fg", base.get("fg", "black"))
normal, bell, current = (style_to_css(s[k], base) for k in ("normal", "bell", "current"))
return f"""
* {{ scrollbar-background: {bg}; scrollbar-color: {fg}; scrollbar-size: 0 0; }}
Screen {{ background: {bg}; color: {fg}; layers: base; }}
#windows {{ height: 1fr; overflow-y: auto; background: {bg}; align: left middle; }}
.window {{ padding: 0 1; height: 2; width: 100%; {normal} }}
.window .dim {{ color: {fg}; text-style: dim; }}
.window:hover {{ background: {fg}; color: {bg}; }}
.bell {{ {bell} }}
.bell:hover {{ {bell} text-style: bold reverse; }}
.active {{ {current} }}
.active:hover {{ {current} }}
"""
def compose(self) -> ComposeResult:
yield Vertical(*[WindowItem(w, self) for w in self.windows], id="windows")
def on_mount(self):
self._pane_id = os.environ["TMUX_PANE"]
self._current_window = pane_var(self._pane_id, "#{window_index}")
tmux("select-pane", "-t", self._pane_id, "-T", "tmux-tabs")
self.windows = tmux_windows()
self.set_interval(1.0, self.poll)
def poll(self):
self.windows = tmux_windows()
# If our window has only us, the content pane was closed — move to another window
if pane_var(self._pane_id, "#{window_panes}") == "1":
target = (next((w["index"] for w in self.windows if w.get("last")), None)
or next((w["index"] for w in self.windows if not w["active"]), None))
if target:
self.switch_to(target)
win_width = int(pane_var(self._pane_id, "#{window_width}"))
if int(pane_var(self._pane_id, "#{pane_width}")) >= win_width * 0.9:
tmux("resize-pane", "-t", self._pane_id, "-x", str(win_width // 4))
return
active = next((w["index"] for w in self.windows if w["active"]), None)
if active and active != self._current_window:
self.switch_to(active)
def switch_to(self, win_index):
# Resolve target window's active pane (window-number target is ambiguous from client context)
target_pane = tmux("list-panes", "-t", win_index, "-f", "#{pane_active}", "-F", "#{pane_id}")
width = pane_var(self._pane_id, "#{pane_width}")
tmux("move-pane", "-h", "-s", self._pane_id, "-t", target_pane, "-d", "-b")
tmux("select-window", "-t", win_index)
tmux("resize-pane", "-t", self._pane_id, "-x", width)
tmux("select-pane", "-t", target_pane)
self._current_window = win_index
if __name__ == "__main__":
os.environ.setdefault("TEXTUAL_THEME", "")
TmuxTabs.CSS = TmuxTabs.build_css()
TmuxTabs().run(inline=False)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment