Last active
March 11, 2026 11:18
-
-
Save dmitriid/523a5229c9a17a1cf4ad9182f4c58cf5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # tammy — a friendly wrapper around tmux. | |
| # | |
| # tmux is powerful but its CLI is full of cryptic flags (-h means horizontal | |
| # but splits vertically, -s means source in one command and session in another, | |
| # etc.). tammy replaces all of that with human-readable subcommands like | |
| # "tammy split right", "tammy connect", and "tammy resize down 5". | |
| # | |
| # It also handles the annoying context-dependent differences (e.g. attach vs | |
| # switch-client depending on whether you're already inside tmux) so you don't | |
| # have to think about it. If fzf is available, session selection is interactive. | |
| # | |
| set -euo pipefail | |
| SCRIPT_NAME="tammy" | |
| usage() { | |
| cat <<EOF | |
| Usage: $SCRIPT_NAME <command> [args] | |
| Commands: | |
| new <name> Create a new tmux session | |
| connect [name] Attach to an existing session (interactive if no name) | |
| detach Detach from current session | |
| kill [name] Kill a session (interactive if no name) | |
| rename <old> <new> Rename a session | |
| list, ls List all tmux sessions | |
| popup <command> Run command in a tmux popup | |
| split <right|left|bottom|down|up|top> Split current pane | |
| swap <up|down|left|right> Swap current pane with neighbor | |
| move <up|down|left|right> Move current pane to that edge | |
| zoom Toggle current pane fullscreen | |
| close Close current pane | |
| resize <up|top|down|bottom|left|right> <n> Resize current pane by n cells | |
| reload Reload tmux config | |
| help Show this help | |
| EOF | |
| } | |
| _pick_session() { | |
| local sessions | |
| sessions=$(tmux list-sessions -F "#{session_name}" 2>/dev/null) || { | |
| echo "No tmux sessions running." >&2; return 1 | |
| } | |
| if command -v fzf &>/dev/null; then | |
| echo "$sessions" | fzf --prompt="Session: " | |
| else | |
| local count | |
| count=$(echo "$sessions" | wc -l) | |
| if [ "$count" -eq 1 ]; then | |
| echo "$sessions" | |
| return | |
| fi | |
| echo "Available sessions:" >&2 | |
| local i=1 | |
| while IFS= read -r s; do | |
| echo " $i) $s" >&2 | |
| i=$((i + 1)) | |
| done <<< "$sessions" | |
| printf "Select session [1-%d]: " "$count" >&2 | |
| read -r choice | |
| local picked | |
| picked=$(echo "$sessions" | sed -n "${choice}p") | |
| if [ -z "$picked" ]; then | |
| echo "Invalid selection." >&2; return 1 | |
| fi | |
| echo "$picked" | |
| fi | |
| } | |
| cmd_new() { | |
| local name="${1:?Usage: $SCRIPT_NAME new <name>}" | |
| if [ -n "${TMUX:-}" ]; then | |
| tmux new-session -d -s "$name" | |
| tmux switch-client -t "$name" | |
| else | |
| tmux new-session -s "$name" | |
| fi | |
| } | |
| cmd_connect() { | |
| local name="${1:-}" | |
| if [ -z "$name" ]; then | |
| name=$(_pick_session) || exit 1 | |
| fi | |
| if [ -n "${TMUX:-}" ]; then | |
| tmux switch-client -t "$name" | |
| else | |
| tmux attach-session -t "$name" | |
| fi | |
| } | |
| cmd_detach() { | |
| tmux detach-client | |
| } | |
| cmd_list() { | |
| tmux list-sessions 2>/dev/null || echo "No tmux sessions running." | |
| } | |
| cmd_kill() { | |
| local name="${1:-}" | |
| if [ -z "$name" ]; then | |
| name=$(_pick_session) || exit 1 | |
| fi | |
| tmux kill-session -t "$name" | |
| echo "Killed session: $name" | |
| } | |
| cmd_rename() { | |
| local old="${1:?Usage: $SCRIPT_NAME rename <old> <new>}" | |
| local new="${2:?Usage: $SCRIPT_NAME rename <old> <new>}" | |
| tmux rename-session -t "$old" "$new" | |
| echo "Renamed session: $old -> $new" | |
| } | |
| cmd_popup() { | |
| local cmd="${1:?Usage: $SCRIPT_NAME popup <command>}" | |
| shift | |
| tmux popup -E "$cmd" "$@" | |
| } | |
| cmd_split() { | |
| local dir="${1:?Usage: $SCRIPT_NAME split <right|left|bottom|up>}" | |
| case "$dir" in | |
| right) tmux split-window -h ;; | |
| left) tmux split-window -hb ;; | |
| bottom|down) tmux split-window -v ;; | |
| up|top) tmux split-window -vb ;; | |
| *) echo "Unknown direction: $dir (use right|left|bottom|down|up|top)" >&2; exit 1 ;; | |
| esac | |
| } | |
| cmd_swap() { | |
| local dir="${1:?Usage: $SCRIPT_NAME swap <up|down|left|right>}" | |
| case "$dir" in | |
| up|top) tmux swap-pane -U ;; | |
| down|bottom) tmux swap-pane -D ;; | |
| left) tmux swap-pane -s '{left-of}' ;; | |
| right) tmux swap-pane -s '{right-of}' ;; | |
| *) echo "Unknown direction: $dir (use up|down|left|right)" >&2; exit 1 ;; | |
| esac | |
| } | |
| cmd_move() { | |
| local dir="${1:?Usage: $SCRIPT_NAME move <up|down|left|right>}" | |
| local pane_id | |
| pane_id=$(tmux display-message -p '#{pane_id}') | |
| case "$dir" in | |
| up|top) tmux move-pane -vb -s "$pane_id" ;; | |
| down|bottom) tmux move-pane -v -s "$pane_id" ;; | |
| left) tmux move-pane -hb -s "$pane_id" ;; | |
| right) tmux move-pane -h -s "$pane_id" ;; | |
| *) echo "Unknown direction: $dir (use up|down|left|right)" >&2; exit 1 ;; | |
| esac | |
| } | |
| cmd_zoom() { | |
| tmux resize-pane -Z | |
| } | |
| cmd_close() { | |
| local pane_count | |
| pane_count=$(tmux list-panes | wc -l) | |
| if [ "$pane_count" -le 1 ]; then | |
| echo "Only one pane in this window. Use 'tmux kill-window' to close the window." >&2 | |
| exit 1 | |
| fi | |
| tmux kill-pane | |
| } | |
| cmd_resize() { | |
| local dir="${1:?Usage: $SCRIPT_NAME resize <up|down|left|right> <n>}" | |
| local n="${2:?Usage: $SCRIPT_NAME resize <up|down|left|right> <n>}" | |
| case "$dir" in | |
| up|top) tmux resize-pane -U "$n" ;; | |
| down|bottom) tmux resize-pane -D "$n" ;; | |
| left) tmux resize-pane -L "$n" ;; | |
| right) tmux resize-pane -R "$n" ;; | |
| *) echo "Unknown direction: $dir (use up|top|down|bottom|left|right)" >&2; exit 1 ;; | |
| esac | |
| } | |
| cmd_reload() { | |
| tmux source-file ~/.tmux.conf | |
| echo "Reloaded ~/.tmux.conf" | |
| } | |
| command="${1:-help}" | |
| shift || true | |
| case "$command" in | |
| new) cmd_new "$@" ;; | |
| connect) cmd_connect "$@" ;; | |
| detach) cmd_detach ;; | |
| kill) cmd_kill "$@" ;; | |
| rename) cmd_rename "$@" ;; | |
| list|ls) cmd_list ;; | |
| popup) cmd_popup "$@" ;; | |
| split) cmd_split "$@" ;; | |
| swap) cmd_swap "$@" ;; | |
| move) cmd_move "$@" ;; | |
| zoom) cmd_zoom ;; | |
| close) cmd_close ;; | |
| resize) cmd_resize "$@" ;; | |
| reload) cmd_reload ;; | |
| help|--help|-h) usage ;; | |
| *) echo "Unknown command: $command" >&2; usage >&2; exit 1 ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment