aboutsummaryrefslogtreecommitdiff
path: root/dotfiles/common/.local/bin/tmux-util
blob: 2fa861df0f5c4793b7f70cbef8e1c73b7064e8fa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/bin/bash
# tmux-util — small utilities for managing tmux sessions.
#
# Subcommands:
#   go <name>      attach to <name> if it exists, otherwise create it
#   pick           fzf-driven session switcher
#   ls             opinionated session listing (attached, idle, windows, cwd)
#   find <pattern> locate panes whose foreground process matches <pattern>
#   reap           gracefully exit every unattached session (skipping aiv-*)
#   rename         fzf-pick a session, prompt for a new name, rename it
#
# Run with no arguments to print this help.

set -uo pipefail

# -----------------------------------------------------------------------------
# Usage
# -----------------------------------------------------------------------------

usage() {
    cat <<'EOF'
Usage: tmux-util <subcommand> [args]

Subcommands:
  go <name>      Attach to session <name>; create it (in $PWD) if missing.
  pick           Fzf-driven session switcher.
  ls             List sessions with attached / idle / window / cwd columns.
  find <pattern> Locate panes whose foreground process matches <pattern>.
  reap           Send SIGHUP to every unattached session's panes and close
                 the session. Sessions matching $TMUX_UTIL_REAP_SKIP
                 (default: ^aiv-) are skipped.
  rename         Pick a session via fzf, prompt for a new name, rename it.

Run with no arguments to print this help.
EOF
}

# -----------------------------------------------------------------------------
# ls
# -----------------------------------------------------------------------------

# Render seconds as a short human duration (e.g. "5s", "12m", "3h", "2d").
format_idle() {
    local s="$1"
    if [ "$s" -lt 60 ]; then
        echo "${s}s"
    elif [ "$s" -lt 3600 ]; then
        echo "$((s / 60))m"
    elif [ "$s" -lt 86400 ]; then
        echo "$((s / 3600))h"
    else
        echo "$((s / 86400))d"
    fi
}

cmd_ls() {
    local sessions
    sessions=$(tmux list-sessions \
        -F '#{session_attached} #{session_name} #{session_activity} #{session_windows}' \
        2>/dev/null || true)

    if [ -z "$sessions" ]; then
        echo "No tmux sessions."
        return 0
    fi

    local now="${TMUX_UTIL_NOW:-$(date +%s)}"

    {
        printf 'STATE\tNAME\tIDLE\tWINDOWS\tCWD\n'
        local attached name activity windows state idle_str cwd
        while IFS=' ' read -r attached name activity windows; do
            [ -n "$name" ] || continue
            state="detached"
            [ "$attached" -gt 0 ] && state="attached"
            idle_str=$(format_idle "$((now - activity))")
            cwd=$(tmux display -p -t "$name" '#{pane_current_path}' 2>/dev/null || echo "?")
            # Replace leading $HOME with ~ for display.
            if [ -n "${HOME:-}" ] && [ "${cwd#$HOME}" != "$cwd" ]; then
                cwd="~${cwd#$HOME}"
            fi
            printf '%s\t%s\t%s\t%s\t%s\n' "$state" "$name" "$idle_str" "$windows" "$cwd"
        done <<<"$sessions"
    } | column -t -s $'\t'
}

# -----------------------------------------------------------------------------
# Reap
# -----------------------------------------------------------------------------

cmd_reap() {
    local skip="${TMUX_UTIL_REAP_SKIP:-^aiv-}"
    local sessions
    sessions=$(tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null \
               | awk '$2 == 0 {print $1}' \
               | grep -vE "$skip" || true)

    if [ -z "$sessions" ]; then
        echo "No unattached sessions to reap."
        return 0
    fi

    local s pids
    while IFS= read -r s; do
        [ -n "$s" ] || continue
        echo "Reaping: $s"
        pids=$(tmux list-panes -s -t "$s" -F '#{pane_pid}' 2>/dev/null)
        if [ -n "$pids" ]; then
            echo "$pids" | xargs -r kill -HUP
        fi
        # Give the session a moment to wind down naturally.
        local i
        for i in 1 2 3; do
            tmux has-session -t "$s" 2>/dev/null || break
            sleep 1
        done
        if tmux has-session -t "$s" 2>/dev/null; then
            echo "  still alive, force killing"
            tmux kill-session -t "$s"
        fi
    done <<<"$sessions"
}

# -----------------------------------------------------------------------------
# Dispatch
# -----------------------------------------------------------------------------

main() {
    if [ "$#" -eq 0 ]; then
        usage
        return 0
    fi

    local sub="$1"
    shift

    case "$sub" in
        -h|--help|help)
            usage
            ;;
        reap)
            cmd_reap "$@"
            ;;
        ls)
            cmd_ls "$@"
            ;;
        go|pick|find|rename)
            echo "tmux-util: subcommand '$sub' is not implemented yet" >&2
            return 2
            ;;
        *)
            echo "tmux-util: unknown subcommand: $sub" >&2
            usage >&2
            return 2
            ;;
    esac
}

main "$@"