|
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
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. |
||
|---|---|---|
| .forgejo | ||
| docs | ||
| legacy | ||
| src | ||
| tests | ||
| .gitignore | ||
| .releaserc.json | ||
| Makefile | ||
| README.md | ||
| session-switcher.tmux | ||
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/pluginsthat noset -g @pluginline names, and that includes the symlinkmake installcreates. It removes the link, not the repo —make installputs 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_idis immutable, so keying on it survives a rename — including one done outside the popup with tmux's ownprefix + $.- But ids are handed out from zero again when the server dies, in whatever
order
tmux-resurrectreplays 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.