Learn Tmux - Custom Scripts, Lightweight Plugins & Status Widgets
Series/Learn Tmux/Episode 17
Episode 17 of 28

Learn Tmux - Custom Scripts, Lightweight Plugins & Status Widgets

Building your own tmux helpers via shell functions, designing lightweight status widgets with external scripts, splitting the config into source-file modules, and keeping everything under version control.

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Introduction

In episode 16 we built a plugin foundation with TPM — declaring plugins, installing via prefix + I, updating via prefix + U, and choosing must-have plugins for clipboard, persistence, and status widgets. But there are times when plugins feel like buying pre-made furniture: fine for general needs, but not quite right for your own specific requirements.

In this episode we flip the perspective. Instead of always adding plugins, we learn to build our own: shell functions to automate tmux, lightweight status widgets written in a few lines, and a config structure split into modules so it's easy to maintain and version. This is the skill that separates ordinary tmux users from engineers who truly control their workflow.

All examples refer to tmux 3.7b as the latest stable version. The final target is simple: a config that is thin, clear, and portable to any machine without drama.

Why Build Your Own Helpers

Before writing the first line, it's important to understand when a plugin is worth using and when your own script makes more sense. The two aren't enemies — they're tools with different trade-offs.

FactorPlugin (via TPM)Your Own Script / Function
MaintenanceFollows the ecosystem, updates via prefix + UMaintained by you
CostFolder + state per pluginOne small file
CustomizationFits the plugin author's designExactly your needs
PortabilityDepends on plugin prerequisitesDepends on the tools you choose
Use CaseCommon problems with large communitiesSpecific needs / team internal

The analogy is choosing between buying a ready-made tool and building your own in a workshop. For already-common problems — clipboard, persistence — plugins are the right call because the community has tested them millions of times. But when you need a command that only makes sense in your team's own workflow, e.g. "create a session named after the project folder", a 15-line script is far lighter than a plugin supporting a hundred features you'll never use.

Shell Functions for tmux Automation

The most natural bridge between tmux and the shell is the function. Shell functions can run from anywhere, read the environment, and call tmux commands non-interactively — exactly what we learned from the CLI side in episodes 13 and 14.

The most useful example is a function that creates a per-project session. Save the following function in your shell config, e.g. ~/.zshrc or ~/.bashrc:

Fungsi mkt di shell config
mkt() {
  local name="${1:-$(basename "$PWD")}"
  tmux has-session -t "$name" 2>/dev/null || tmux new-session -d -s "$name"
  tmux switch-client -t "$name"
}

Let's break down each line:

  • Line two stores the session name. If you call mkt backend, the name used is backend; if no argument is given, the name comes from basename "$PWD" — so in the folder /home/devnull/work/api-gateway, the session is automatically named api-gateway.
  • Line three is the core of the logic: tmux has-session -t "$name" checks whether the session already exists. If it doesn't (marked by ||), a new detached session is created with tmux new-session -d -s "$name".
  • Line four switches the current client to that session with tmux switch-client -t "$name".

The key to this pattern is the -d flag on new-session. Without -d, tmux will try to attach the client to the new session directly, which creates a conflict inside an already running tmux. Creating a detached session then explicitly moving the client is a safe pattern in both contexts: from inside tmux and from a plain shell.

Tip

The has-session || new-session pattern is the most important idiom in tmux automation. You'll see it repeatedly in tmuxp scripts, sessionizers, and various other tools. Once you understand this idiom, nearly all session automation opens up to you.

Lightweight Status Widgets

The tmux status bar supports two ways to display dynamic data: format variables #{...} evaluated by the server, and shell commands #(...) that execute a command. The two are often confused, yet their behavior differs.

Format variables evaluate fast because they're computed internally by the server. Shell commands #() spawn a new process each time they're evaluated — flexible, but there's a cost. The evaluation frequency is controlled by the status-interval option.

The simplest widget example is showing the git branch in the status bar:

Status right dengan git branch
set -g status-interval 5
set -g status-right '#[fg=green]#(git rev-parse --abbrev-ref HEAD 2>/dev/null)#[default] %H:%M'

Note the use of 2>/dev/null — when you're in a directory that isn't a git repo, git rev-parse outputs an error to stderr that can break the status bar. Discarding it makes the widget stay quiet when not relevant.

External Scripts for Widgets

When a single #(...) line gets too long or needs logic, move it to an external script. A healthy convention is to keep all widgets in one directory, e.g. ~/.config/tmux/widgets.

LinuxWidget beban sistem
#!/usr/bin/env bash
if [[ -r /proc/loadavg ]]; then
  read -r load _ < /proc/loadavg
else
  load="$(uptime | awk -F'load average: ' '{print $2}' | cut -d, -f1)"
fi
echo "#[fg=yellow]load ${load}#[default]"

This script reads the load average with one portability trick: on Linux, the data is read directly from /proc/loadavg; on platforms without that file, the value is taken from uptime output. The result is printed with a color code format the status bar understands directly.

To make it executable, set the execute permission, then reference its path in the config:

Set izin dan referensi script
chmod +x ~/.config/tmux/widgets/loadavg.sh
set -g status-left-length 40
set -g status-left '#[fg=cyan]#(~/.config/tmux/widgets/loadavg.sh)#[default]'

Warning

Every time status-interval elapses, tmux re-runs all #() widgets. Heavy logic like network calls or parsing large files becomes an unnecessary CPU burden. Keep widgets thin — display values, don't do heavy work inside them.

Modular Configuration with source-file

As keybindings, themes, plugins, and widgets accumulate, a single ~/.tmux.conf can swell to hundreds of lines. That's where source-file comes in: a command to load another config file, similar to include in programming languages.

The modular pattern splits the config by concern:

LinuxStruktur konfigurasi modular
~/.config/tmux/
  tmux.conf
  keys.conf
  plugins.conf
  theme.conf
  widgets.conf

In tmux 3.7b, the config can live at ~/.config/tmux/tmux.conf following the XDG standard — tmux uses it automatically as long as ~/.tmux.conf doesn't exist. The main file then becomes just an entry point that loads other modules:

tmux.conf dengan source-file
set -g base-index 1
set -g mouse on
 
source-file ~/.config/tmux/keys.conf
source-file ~/.config/tmux/theme.conf
source-file ~/.config/tmux/plugins.conf
source-file ~/.config/tmux/widgets.conf

The benefit of this split is real in the real world: when one person changes the theme, git diff only touches theme.conf; when adding a keybinding, only keys.conf. Team collaboration is cleaner and merge conflicts are easier to resolve. Remember to reload with prefix + r whenever one of the modules changes.

Modular Theme

One of the most commonly separated modules is the theme, because visual changes happen most often. Here's an example theme.conf with the Catppuccin Mocha palette:

theme.conf
set -g status-style 'bg=#1e1e2e,fg=#cdd6f4'
set -g window-status-current-style 'fg=#89b4fa'
set -g pane-border-style 'fg=#45475a'
set -g pane-active-border-style 'fg=#89b4fa'
ColorValueRole
#1e1e2eMantleStatus bar background
#cdd6f4TextDefault text color
#89b4faBlueAccent / active color
#45475aSurfaceInactive pane border

Since the theme only contains options, moving it to a separate file doesn't change behavior at all — it only simplifies maintenance. The same pattern applies to keys.conf and widgets.conf.

Reproducible Layouts with tmuxp

In episode 14 we already touched tmuxp as a declarative tool for arranging sessions. Now we use it more deeply to complete the modular config: a layout stored as YAML can be re-run anytime and produce an identical structure.

tmuxp.yaml
session_name: web
windows:
  - window_name: editor
    panes:
      - shell_command: nvim
  - window_name: server
    panes:
      - shell_command: npm run dev

This YAML file describes a session named web with two windows: editor opening nvim, and server running the dev server. Compare that to rewriting lines of tmux new-window and tmux send-keys by hand — a declarative file is far easier to read, review, and version.

Load layout tmuxp
tmuxp load tmuxp.yaml

Because tmuxp only uses tmux commands under the hood, the same layout works on any machine as long as tmux is installed — ideal for teams that want to standardize their workspace structure.

Versioning the Config

All the effort above is wasted without versioning. A config not managed by version control will start to differ between machines, and those differences are the source of the hardest-to-trace bugs.

The most common approach is keeping the config in a dotfiles repo and symlinking it to the location tmux reads:

Symlink konfigurasi ke repo dotfiles
mkdir -p ~/dotfiles/tmux
git init ~/dotfiles
ln -s ~/dotfiles/tmux/tmux.conf ~/.tmux.conf

With a symlink, you only have one source of truth: the file inside the repo. Every change can be committed, reviewed, and rolled back. For a cleaner setup with many tools at once, tools like GNU Stow can manage symlinks automatically — but for a single tmux config, ln -s is enough.

Common Pitfalls

  1. Forgetting chmod +x on widget scripts. A non-executable script only shows an error or produces nothing in the status bar. Always verify by running the script directly from the terminal before wiring it into the config.
  2. Putting heavy logic in #() widgets. Widgets are re-executed every status-interval. Network calls, big loops, or parsing huge files inside a widget will make the status bar slow and burden the CPU.
  3. Using absolute paths in source-file. source-file /home/user/.config/tmux/theme.conf will break on another machine. Use ~ or $HOME so the config stays portable.
  4. Editing modular files but forgetting to reload. Split files aren't reloaded automatically. After changing theme.conf, you still have to press prefix + r for the changes to show.
  5. Having ~/.tmux.conf and ~/.config/tmux/tmux.conf at the same time. tmux prioritizes ~/.tmux.conf; the XDG file is only used when the legacy file doesn't exist. These two files will shadow each other and confuse. Pick one.
  6. Versioning without a symlink. Copying files into a repo and editing two different locations only creates two sources of truth. Use a symlink so the repo is the only source.

Conclusion

This episode completed your mindset: plugins are great, but the ability to build your own is a long-term asset. You can now write shell functions like mkt() that create per-project sessions, design lightweight widgets with #() and external scripts, split the config into organized source-file modules, describe layouts with tmuxp, and keep everything in sync via versioning.

Key points to take away:

  • The has-session || new-session idiom is the foundation of session automation.
  • #() widgets are flexible but must stay thin; raise status-interval to reduce load.
  • Modular configs use source-file and XDG, not piling hundreds of lines into one file.
  • tmuxp makes layouts declarative, and symlinks make versioning a single source of truth.

With a solid local foundation, it's time to bring tmux to the most defining scenario: remote machines. In episode 18 we'll cover remote development and SSH workflows — surviving dropped connections, using mosh for slow networks, traversing jump hosts, and handling nested tmux when the server also runs tmux. See you in episode 18!