diff --git a/.tmux.conf b/.tmux.conf index c1aafea..9e50355 100644 --- a/.tmux.conf +++ b/.tmux.conf @@ -20,7 +20,6 @@ set -g @rose_pine_variant 'main' set -g @plugin 'tmux-plugins/tmux-cpu' set -g @plugin 'tmux-plugins/tmux-battery' set -g @plugin 'tmux-plugins/tmux-prefix-highlight' -set -g @plugin 'xamut/tmux-network-bandwidth' set -g @plugin 'tmux-plugins/tmux-online-status' # Git enhancement @@ -63,13 +62,26 @@ set -g @batt_icon_status_discharging 'BAT' # set -g @catppuccin_window_default_text "#W" # Rose Pine configuration -set -g @rose_pine_host 'on' +set -g @rose_pine_host 'off' +# The left column repeated the active window name that the window list already +# shows; with every window auto-renamed to 'claude' that read as claude claude. +set -g @rose_pine_disable_active_window_menu 'on' set -g @rose_pine_user 'on' set -g @rose_pine_directory 'on' set -g @rose_pine_date_time '%H:%M' # Custom status modules for Rose Pine -set -g @rose_pine_status_right_append_section '#(cd #{pane_current_path} && git branch --show-current 2>/dev/null) | CPU: #{cpu_percentage} | #{battery_icon} #{battery_percentage}' +set -g @rose_pine_status_right_append_section '#(cd #{pane_current_path} && git branch --show-current 2>/dev/null) | CPU: #{cpu_percentage} #(cat ~/.statmon/temp) | #{battery_icon} #{battery_percentage} | #(cat ~/.statmon/net)' + +# Rainbow throbber. A daemon writes frames into @throb_line and forces a status +# redraw, so animating costs nothing extra: the #() commands above stay cached +# on status-interval instead of re-running per frame. Empty when not spawned. +bind T run-shell 'throb --toggle' + +# Moving your attention turns the page: focus changes advance the rotation, +# so the thought under the wave tracks where you actually are. +set-hook -g pane-focus-in 'run-shell -b "throb --next"' +set-hook -g after-select-window 'run-shell -b "throb --next"' # Window configuration set -g @rose_pine_window_status_separator " | " @@ -84,5 +96,19 @@ set -g pane-border-style fg=default,bg=default set -g pane-active-border-style fg=default,bg=default set -g pane-border-lines single +# claude code's process name is its bare version string; show 'claude' instead +set -g automatic-rename-format '#{?#{m:2.*,#{pane_current_command}},claude,#{pane_current_command}}' + # Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf) run '~/.tmux/plugins/tpm/tpm' + +# After tpm, so a theme that rewrites status-format can't strand the splice. +# The throbber gets its own centre chunk instead of riding the right column: +# tmux then recentres it in the gap the window list and right column leave, +# per client, on every redraw, including the ones a pane or window switch +# triggers: that is when the directory and branch change the gap's width. +run-shell 'throb --mount' + +# --on is a no-op while the daemons live; this revives them after a server crash. +run-shell 'throb --on' +run-shell 'statmon --on' diff --git a/bin/statmon b/bin/statmon new file mode 100755 index 0000000..0fa4034 --- /dev/null +++ b/bin/statmon @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# One sampler behind the tmux status line. Status-line #() jobs get killed and +# respawned by redraw storms and one-shot clients, orphaning their children +# (macmon, sleep) into a fork pile; this daemon samples on its own clock and +# the status line only ever cats the result files. +# +# statmon --on start (no-op while running) +# statmon --off stop +# statmon status +# +# Results land in ~/.statmon/{temp,net}. + +set -u +STORE="${STATMON_HOME:-$HOME/.statmon}" +PIDFILE="$STORE/daemon.pid" +INTERVAL=15 +TEMP_KILL_AFTER=10 +mkdir -p "$STORE" + +running() { [ -s "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; } + +# macmon can hang when its parent context is odd; sample it with a watchdog so +# at most one attempt is ever in flight. +sample_temp() { + macmon pipe -s1 -i 500 /dev/null | + jq -r '"\(.temp.cpu_temp_avg | round)°C"' >"$STORE/temp.new" & + local sampler=$! + ( sleep "$TEMP_KILL_AFTER"; kill -9 "$sampler" 2>/dev/null ) & + local watchdog=$! + wait "$sampler" 2>/dev/null && [ -s "$STORE/temp.new" ] && mv "$STORE/temp.new" "$STORE/temp" + kill "$watchdog" 2>/dev/null +} + +counters() { + netstat -ib | awk '$3 ~ /= 1024 && i < 4) { b /= 1024; i++ } + printf (i == 1 ? "%d%s" : "%.1f%s"), b, u[i] + }' +} + +sample_net() { + local now rx tx prev rx0 tx0 t0 dt + now=$(date +%s) + read -r rx tx < <(counters) + prev=$(cat "$STORE/net.raw" 2>/dev/null || echo "") + printf '%s %s %s' "$now" "$rx" "$tx" >"$STORE/net.raw" + [ -z "$prev" ] && return 0 + read -r t0 rx0 tx0 <<<"$prev" + dt=$(( now - t0 )) + [ "$dt" -lt 1 ] && return 0 + printf '↓%s ↑%s' "$(human $(( (rx - rx0) / dt )))" "$(human $(( (tx - tx0) / dt )))" >"$STORE/net" +} + +case "${1:-}" in + --on) + running && exit 0 + ( + trap 'rm -f "$PIDFILE"' EXIT + while tmux has-session 2>/dev/null; do + sample_temp + sample_net + sleep "$INTERVAL" + done + ) >/dev/null 2>&1 & + echo $! >"$PIDFILE" + exit 0 ;; + --off) + running && kill "$(cat "$PIDFILE")" 2>/dev/null + rm -f "$PIDFILE" + exit 0 ;; + *) + running && echo "running (pid $(cat "$PIDFILE"))" || echo "off" + exit 0 ;; +esac diff --git a/install.sh b/install.sh index f285a73..759a73c 100755 --- a/install.sh +++ b/install.sh @@ -17,7 +17,7 @@ fi # Install required tools echo "Installing required tools..." -brew install starship btop tmux alacritty bat jq || true +brew install starship btop tmux alacritty bat jq macmon || true # Install Nerd Font echo "Installing Fira Code Nerd Font..." @@ -118,6 +118,9 @@ ln -sf "$root/.gitconfig-skalar" ~/.gitconfig-skalar ln -sf "$root/.gitconfig-personal" ~/.gitconfig-personal ln -sf "$root/.claude/statusline.sh" ~/.claude/statusline.sh +mkdir -p ~/.local/bin +ln -sf "$root/bin/statmon" ~/.local/bin/statmon + # Configure git echo "Configuring git..." git config --global core.excludesfile ~/.globalgitignore