No description
Find a file
alex 413cd6c4e6
All checks were successful
release / guard (push) Has been skipped
release / test (push) Successful in 56s
release / scan (push) Successful in 21s
release / bundle-check (push) Successful in 24s
release / release (push) Successful in 51s
release / backmerge (push) Has been skipped
merge(develop): publish the plugin from CI, installable with TPM or a drop-in bundle
Everything this repository does beyond the switcher itself, landing on main in one reviewed
merge. main was seeded at the last commit before CI existed, so this is the first change it has
ever taken and the first release it will cut.

Two things make the plugin installable by someone who is not the person who built it. A drop-in
bundle per architecture carries the entry point and a built binary and nothing else, so an
install needs no compiler and no clone; deliberately no src/, since the entry point rebuilds
only when the source is newer than the binary and with no source that test is false. And the
repository being reachable makes set -g @plugin work, which is also what stops TPM's clean
from deleting the plugin directory -- the standing wart with the symlink install.

Two gates now stand between a merge and a publish. scan runs gitleaks across the commit graph
rather than the working tree, because a secret committed and deleted in a later commit is still
published, and a tree-only scan reports clean on exactly that case. bundle-check unpacks what
would be shipped and asserts the layout the entry point resolves against, then checks the name
against the ELF header and that SHA256SUMS covers every artefact.

The rest is the release engine itself: semantic-release deriving the version from these
commits, a merge-message template so a pull request title becomes a Conventional subject, and
the back-merge that keeps develop from falling behind main -- which is load-bearing, since rc
numbering is computed from the newest tag reachable from HEAD.

The two feat commits are what set the version. With no stable tag in the repository this cuts
1.0.0 rather than a minor bump.

Verified rather than assumed: v1.0.0-rc.1 was cut from develop by this pipeline and carries the
bundle, the source tarball and SHA256SUMS; make test passes; gitleaks reports no findings
across the whole history.
2026-09-25 20:03:44 +00:00
.forgejo docs: make the repository standalone, with no reference to sibling repos 2026-09-25 19:10:00 +02:00
docs docs: make the repository standalone, with no reference to sibling repos 2026-09-25 19:10:00 +02:00
legacy feat: add a compiled tmux session switcher 2026-09-25 16:00:00 +02:00
src feat: add a compiled tmux session switcher 2026-09-25 16:00:00 +02:00
tests docs: make the repository standalone, with no reference to sibling repos 2026-09-25 19:10:00 +02:00
.gitignore chore: ignore key material so it cannot be committed 2026-09-25 18:00:20 +02:00
.releaserc.json ci: write merge subjects as merge(<branch>) and teach the release engine the type 2026-09-25 17:22:20 +02:00
Makefile docs: make the repository standalone, with no reference to sibling repos 2026-09-25 19:10:00 +02:00
README.md docs: install with TPM, now that the repo is somewhere TPM can reach 2026-09-25 18:51:00 +02:00
session-switcher.tmux feat: open the switcher by clicking the session name 2026-09-25 16:06:20 +02:00

tmux-session-switcher

A modal session switcher for tmux, shown in a popup, that switches the client as you move the highlight — so the session you are considering renders full-size behind the popup instead of in choose-tree's cramped preview pane. Esc puts you back where you started.

It also gives tmux something it does not have on its own: a stable, manual session order, so prefix + w then 3 always lands on the same session.

 1  Blog Api        6w  *  ~/Desktop/blog-api
 2  NotesApp        8w     ~/Desktop/notesapp
>3  Shop Back       4w     ~/Desktop/shop/backend
 4  Análisis Técnico 3w    ~/Desktop/analisis_tecnico

Keys

key
1–9, 0 jump straight to that slot and switch
j / k, arrows down / up
J / K move the highlighted session down / up in the order
gg / G first / last
enter switch to the highlighted session
a add a session (prompts for a name, created detached)
r rename the highlighted session
esc / q cancel, returning to the session you opened from
wheel / click move the highlight; click the highlighted row to take it

Outside the popup, left-clicking the session name at the far left of the status line opens it too (needs set -g mouse on).

Install

With TPM

Declare it like any other plugin, using the full clone URL — TPM tries a plugin name as a git URL before falling back to assuming GitHub:

set -g @plugin 'https://git.piqueras.me/alex/tmux-session-switcher'

then prefix + I. Put it with the other @plugin lines; unlike the methods below it does not need its own run-shell, and being a declared plugin is what makes it survive TPM's clean.

TPM clones the source, so this path compiles on first load and needs a C compiler and make. If the machine has neither, see Without a compiler.

From a working copy

For development, or to run a revision that is not released yet. Clone it wherever you keep projects, then link it into tmux's plugins directory:

git clone <this repo> ~/Desktop/tmux-session-switcher
make -C ~/Desktop/tmux-session-switcher install

and load it from tmux.conf, before the tpm line:

run-shell '~/.config/tmux/plugins/tmux-session-switcher/session-switcher.tmux'

Then tmux source-file ~/.config/tmux/tmux.conf.

Needs a C compiler and make. The plugin builds itself on first load and whenever the source is newer than the binary, in the background, so it never holds up tmux's startup.

Without a compiler

Each release carries a drop-in bundle per architecture — the plugin and a prebuilt binary, and nothing else. Unpack the one for your machine into tmux's plugins directory:

mkdir -p ~/.config/tmux/plugins
tar -xzf tmux-session-switcher-<version>-plugin-linux-amd64.tar.gz \
    -C ~/.config/tmux/plugins

then add the same run-shell line above. Nothing is compiled: the bundle carries no source, and the entry point only rebuilds when the source is newer than the binary.

The bundle is also the fix if you installed with TPM on a machine with no C compiler — drop its bin/session-switcher into the cloned plugin directory.

If you use TPM alongside a working copy: its clean command (prefix + alt-u) deletes every directory under ~/.config/tmux/plugins that no set -g @plugin line names, and that includes the symlink make install creates. It removes the link, not the repo — make install puts it back. Installing with TPM instead avoids this entirely, which is why that is listed first.

Options

Set these before the run-shell line:

set -g @session-switcher-key         'w'          # prefix + this key
set -g @session-switcher-width       '60%'        # popup size
set -g @session-switcher-height      '50%'
set -g @session-switcher-title       ' sessions '
set -g @session-switcher-debounce-ms '35'
set -g @session-switcher-status-click 'on'        # 'off' to unbind the click

status-click binds MouseDown1StatusLeft, so a left click on the session name opens the popup. It needs no status-left changes: tmux's default status-format already wraps status-left in a #[range=left] region, and a click inside one fires the StatusLeft mouse key. The range ends where the rendered text ends, so clicking the empty middle of the bar does nothing. tmux binds only MouseDown3StatusLeft (its session menu) by default, so button 1 there was free; set this to off if you want it back.

debounce-ms is how long the highlight must sit still before the client is actually switched to it. Holding j therefore costs one tmux call when you stop, not one per row crossed. Raise it if your machine is slow and the background redraws feel busy; 0 switches immediately.

The session order

tmux has no native session ordering — list-sessions is alphabetical — so the order lives in ${XDG_DATA_HOME:-~/.local/share}/tmux/session-order, keyed by session id and stamped with the tmux server pid:

#server 1493
$3	NotesApp
$1	home

Both the id and the name are stored, and here is why:

  • session_id is immutable, so keying on it survives a rename — including one done outside the popup with tmux's own prefix + $.
  • But ids are handed out from zero again when the server dies, in whatever order tmux-resurrect replays its save file. Keying on the id alone would then map saved slots onto the wrong sessions after every restart, silently.
  • So: same server pid → trust the ids. Different pid → the server restarted, fall back to matching by name and re-stamp with the fresh ids.

The file is rewritten on every listing, which also prunes dead sessions and absorbs renames made elsewhere. Sessions it has never heard of join the end, alphabetically.

Why it is compiled

The original was bash driving fzf --no-input, re-invoking itself for every binding (legacy/session-switcher.sh, kept for reference). It worked, but each keypress cost one or more process spawns, and on a busy machine the popup went visibly laggy. J/K were the worst: the reorder ran one tmux display-message per session just to re-fetch names it already had.

Here the list, the ordering, the gg double-tap and the rendering are all in-process. tmux is executed once at startup — list-sessions and display-message chained into a single invocation — once per action that changes tmux state, and once per settled preview switch.

Median of 5 runs, 11 sessions, both implementations driven through a real pty:

bash + fzf compiled
popup open, idle 44.0 ms 5.2 ms 8.5×
reorder (J), idle 22.2 ms 0.7 ms 32×
popup open, one busy core 237.5 ms 27.9 ms 8.5×
reorder (J), one busy core 107.9 ms 0.8 ms 135×

The reorder cost of the old version scaled with the number of sessions; on a loaded server with 11 sessions it measured 155–225 ms per keypress.

Tests

make test

48 assertions against a throwaway tmux server (-L swtest, -f /dev/null) with its own scratch $XDG_DATA_HOME, so your real sessions and your real order file are never touched. Needs script(1) from util-linux, which is what gives that server a client with a pty for switch-client to act on.

Releases

Versions are derived from the commit messages, not written down anywhere: a feat: bumps the minor, a fix: the patch, a BREAKING CHANGE: footer the major. Pushing to main publishes a stable release, pushing to develop publishes a release candidate, and a push of nothing but docs: publishes nothing.

Each release carries a reproducible source tarball, a ready-to-use plugin bundle per architecture and SHA256SUMS. See docs/releases.md.

Notes

  • Session names and paths are separated by tabs in the tmux format strings. It has to be a tab: tmux runs format output through vis(3), so any other control byte comes back as the literal text of its octal escape.
  • The popup needs an attached client; without one the switcher exits with a message rather than silently switching nothing.