I use tmux as my daily terminal multiplexer. Over time I've built a small set of shell helpers and keybindings that make session management nearly frictionless β both locally and on remote machines.
Every project gets its own tmux session, named after the directory. If I'm in ~/code/myapp, the session is called myapp. Dots get replaced with underscores so tmux doesn't complain (e.g. my.project becomes my_project). This convention means I never have to think about session names β the directory is the name.
I have a handful of fish shell functions that all follow the same pattern: attach if the session exists, create it if it doesn't.
-
tmβ The one I use most. It attaches to (or creates) a session named after the current directory. It also sets the terminal title with a laptop emoji so I can spot it in my tab bar. -
tpβ A session picker. With no arguments, it pops up an fzf list of all running sessions. Pick one and you're in. Press Escape and it falls back to creating a session for the current directory. You can also pass a name directly:tp workattaches to (or creates) theworksession. -
tvβ Liketm, but the session starts with neovim running. I use this when I know I'm about to edit code. Pass a filename to open it directly:tv src/main.rs. -
tnβ Just a quick alias fortmux new-session -s. For when I want an explicit name that doesn't match a directory.
I also work on a remote MacBook M3 Pro Max (called m3pro in my SSH config). For that I have a separate script called tms that does the same session management over SSH.
Running tms without arguments fetches the list of tmux sessions from the remote machine and presents them in fzf. I can pick one to attach, or choose "Create new session" to start fresh. If fzf isn't available, it falls back to a numbered menu.
The terminal title gets a link emoji (π) prefix so I can immediately tell which tabs are remote vs. local. Fish completions auto-suggest remote session names as I type.
I remapped the prefix from the default Ctrl+B to Ctrl+S β it's easier to reach and I never need to send Ctrl+S to anything. Pressing the prefix twice (Ctrl+S Ctrl+S) sends a literal Ctrl+S through if I ever do need it.
Everything else follows vim conventions:
| Binding | Action |
|---|---|
prefix + h/j/k/l |
Navigate between panes |
prefix + H/J/K/L |
Resize panes (repeatable) |
prefix + d |
Split horizontally |
prefix + s |
Split vertically |
prefix + z |
Zoom (toggle fullscreen) a pane |
prefix + x |
Kill a pane |
prefix + i |
Pull the last pane back into this window |
prefix + e |
Open scrollback in neovim |
prefix + o |
Fuzzy session switcher (sessionx plugin) |
prefix + g |
Reload tmux config |
Alt+k |
Clear screen and scrollback (like Cmd+K on macOS) |
New splits open in the same directory as the current pane, which seems obvious but isn't the default.
One binding I'm particularly happy with is Alt+k. In a plain shell, it clears the screen and scrollback β just like hitting Cmd+K in a native macOS terminal. But if a program is running in the pane (neovim, a dev server, etc.), it sends Ctrl+L instead so it doesn't blow away whatever the program is doing. It detects this by checking ps for processes that aren't shells.
I keep plugins minimal and managed through TPM:
- tmux-resurrect and tmux-continuum β Save and restore sessions across restarts. I don't auto-restore on startup (it can be slow), but having the option to manually restore is a lifesaver.
- tmux-sessionx β A fuzzy session switcher bound to
prefix + o. This is the in-tmux complement to my shell-leveltpfunction. - tmux-fuzzback β Search through scrollback with fzf in a popup. Great for finding that error message that scrolled past.
- tmux-fzf-url β Pick URLs from scrollback with fzf and open them. Saves a lot of copy-pasting.
escape-time 0β Without this, pressing Escape in neovim has a noticeable delay. This is a must for vim users.base-index 1β Windows start at 1, not 0. My keyboard's number row starts at 1, so should my windows.mouse onβ I use the keyboard for everything, but mouse support is nice for the occasional scroll or pane resize.history-limit 100000β A generous scrollback buffer. Memory is cheap, losing context isn't.- Copy mode uses vi keys β
yandEnterboth yank the selection and exit copy mode. Clipboard passthrough is enabled so OSC 52 works for copying to the system clipboard from remote sessions.
I also keep a zm function around as a zellij equivalent of tm β same "attach or create by directory name" pattern. I switch between multiplexers occasionally and having the same muscle memory helps.
Local workflow:
cd ~/code/myproject
tm β attached to "myproject" session
tp β fzf pick any session
tp work β attach/create "work" session
tv β "myproject" session with neovim
tv src/main.rs β "myproject" session, neovim opens file
Remote workflow:
tms β fzf pick from m3pro's sessions
tms deploy β attach/create "deploy" on m3pro
All the configuration lives in a dotfiles repo managed by a setup script that symlinks everything into place. The tmux config goes to ~/.tmux.conf, the fish functions to ~/.config/fish/conf.d/tmux.fish, and the tms script to ~/.config/scripts/tms.