diff --git a/tmux/tmux-status/mem_usage.sh b/tmux/tmux-status/mem_usage.sh new file mode 100755 index 0000000..366bd12 --- /dev/null +++ b/tmux/tmux-status/mem_usage.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail +# Reads mem cache and prints pane_mem and window_mem as raw values. +# Usage: mem_usage.sh +# Output: two lines - pane mem, window mem (e.g. "120M\n450M") + +CACHE_FILE="/tmp/tmux-mem-usage.json" + +# Trigger cache refresh in background +python3 "$HOME/.config/tmux/tmux-status/mem_usage_cache.py" &>/dev/null & +disown 2>/dev/null + +session="$1" +window_idx="$2" +pane_id="$3" + +if [[ ! -f "$CACHE_FILE" ]]; then + printf '\n' + exit 0 +fi + +wkey="${session}:${window_idx}" +read -r pane_val win_val total_val < <( + jq -r --arg pid "$pane_id" --arg wk "$wkey" \ + '[(.pane[$pid] // ""), (.window[$wk] // ""), (.total // "")] | join(" ")' \ + "$CACHE_FILE" 2>/dev/null || echo "" +) + +printf '%s\n%s\n%s\n' "$pane_val" "$win_val" "$total_val" diff --git a/tmux/tmux-status/mem_usage_cache.py b/tmux/tmux-status/mem_usage_cache.py new file mode 100755 index 0000000..ad70457 --- /dev/null +++ b/tmux/tmux-status/mem_usage_cache.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +Runs top once, maps all process memory to tmux panes/windows, writes JSON cache. +Intended to be called periodically (e.g. every status refresh); self-throttles to 10s. +""" +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +import time + +CACHE_FILE = "/tmp/tmux-mem-usage.json" +LOCK_FILE = "/tmp/tmux-mem-usage.lock" +STALE_SECONDS = 5 + + +def is_fresh() -> bool: + try: + return (time.time() - os.path.getmtime(CACHE_FILE)) < STALE_SECONDS + except OSError: + return False + + +def run(cmd: str) -> str: + r = subprocess.run(cmd, shell=True, capture_output=True, text=True) + return r.stdout + + +def parse_mem(s: str) -> float: + s = s.strip() + if not s or s == "-": + return 0.0 + m = re.match(r"([\d.]+)([KMGB]?)", s, re.IGNORECASE) + if not m: + return 0.0 + v = float(m.group(1)) + u = m.group(2).upper() + if u == "K": + return v / 1024 + if u == "G": + return v * 1024 + return v + + +def fmt(mb: float) -> str: + if mb >= 1024: + return f"{mb / 1024:.1f}G" + return f"{mb:.0f}M" + + +def get_top_mem() -> dict[int, float]: + out = run("top -l 1 -o mem -n 9999 -stats pid,mem,cmprs 2>/dev/null") + result: dict[int, float] = {} + for line in out.strip().split("\n"): + parts = line.split() + if len(parts) >= 3 and parts[0].isdigit(): + pid = int(parts[0]) + result[pid] = parse_mem(parts[1]) + parse_mem(parts[2]) + return result + + +def get_ppid_map() -> dict[int, int]: + out = run("ps -eo pid,ppid") + m: dict[int, int] = {} + for line in out.strip().split("\n"): + parts = line.split() + if len(parts) >= 2 and parts[0].isdigit() and parts[1].isdigit(): + m[int(parts[0])] = int(parts[1]) + return m + + +def get_tmux_panes() -> list[dict]: + out = run( + "tmux list-panes -a -F '#{session_name}\t#{window_index}\t#{window_id}\t#{pane_id}\t#{pane_pid}' 2>/dev/null" + ) + panes = [] + for line in out.strip().split("\n"): + if not line: + continue + parts = line.split("\t") + if len(parts) >= 5 and parts[4].isdigit(): + panes.append( + { + "session": parts[0], + "window_idx": parts[1], + "window_id": parts[2], + "pane_id": parts[3], + "pane_pid": int(parts[4]), + } + ) + return panes + + +def build_children_map(ppid_map: dict[int, int]) -> dict[int, list[int]]: + children: dict[int, list[int]] = {} + for pid, ppid in ppid_map.items(): + children.setdefault(ppid, []).append(pid) + return children + + +def get_descendants(pid: int, children_map: dict[int, list[int]]) -> set[int]: + result = set() + stack = [pid] + while stack: + p = stack.pop() + for c in children_map.get(p, []): + if c not in result: + result.add(c) + stack.append(c) + return result + + +def main(): + if is_fresh(): + return + + fd = None + try: + fd = os.open(LOCK_FILE, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + except FileExistsError: + try: + if (time.time() - os.path.getmtime(LOCK_FILE)) > 30: + os.unlink(LOCK_FILE) + except OSError: + pass + return + try: + _generate() + finally: + os.close(fd) + try: + os.unlink(LOCK_FILE) + except OSError: + pass + + +def _generate(): + top_mem = get_top_mem() + ppid_map = get_ppid_map() + children_map = build_children_map(ppid_map) + panes = get_tmux_panes() + + pane_mem: dict[str, float] = {} + window_mem: dict[str, float] = {} + + for pane in panes: + desc = get_descendants(pane["pane_pid"], children_map) + desc.add(pane["pane_pid"]) + total = sum(top_mem.get(p, 0) for p in desc) + pane_mem[pane["pane_id"]] = total + + wkey = f"{pane['session']}:{pane['window_idx']}" + window_mem[wkey] = window_mem.get(wkey, 0) + total + + total_tmux = sum(pane_mem.values()) + + pane_fmt = {k: fmt(v) for k, v in pane_mem.items()} + window_fmt = {k: fmt(v) for k, v in window_mem.items()} + + data = {"ts": time.time(), "pane": pane_fmt, "window": window_fmt, "total": fmt(total_tmux)} + tmp = CACHE_FILE + ".tmp" + with open(tmp, "w") as f: + json.dump(data, f) + os.replace(tmp, CACHE_FILE) + + +if __name__ == "__main__": + main() diff --git a/tmux/tmux-status/right.sh b/tmux/tmux-status/right.sh index 3f98c71..56ae72d 100755 --- a/tmux/tmux-status/right.sh +++ b/tmux/tmux-status/right.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -# hide entire right status if terminal width is below threshold min_width=${TMUX_RIGHT_MIN_WIDTH:-90} width=$(tmux display-message -p '#{client_width}' 2>/dev/null || true) if [[ -z "${width:-}" || "$width" == "0" ]]; then @@ -21,27 +20,46 @@ if [[ -z "$status_bg" || "$status_bg" == "default" ]]; then status_bg=black fi -segment_bg="#3b4252" segment_fg="#eceff4" -# Host (domain) colors to mirror left active style host_bg="${TMUX_THEME_COLOR:-#b294bb}" host_fg="#1d1f21" separator="" right_cap="█" hostname=$(hostname -s 2>/dev/null || hostname 2>/dev/null || printf 'host') + +# --- Data gathering --- + +# Memory usage +mem_pane_bg="#5e81ac" +mem_pane_fg="#eceff4" +mem_win_bg="#4c566a" +mem_win_fg="#eceff4" +mem_total_bg="#3b4252" +mem_total_fg="#eceff4" +mem_pane_val="" +mem_win_val="" +mem_total_val="" +mem_script="$HOME/.config/tmux/tmux-status/mem_usage.sh" +if [[ -x "$mem_script" ]]; then + IFS=$'\t' read -r _sess _widx _pid < <( + tmux display-message -p '#{session_name} #{window_index} #{pane_id}' 2>/dev/null || echo "" + ) + if [[ -n "${_sess:-}" && -n "${_widx:-}" && -n "${_pid:-}" ]]; then + mem_output=$("$mem_script" "$_sess" "$_widx" "$_pid" 2>/dev/null || true) + mem_pane_val=$(sed -n '1p' <<< "$mem_output") + mem_win_val=$(sed -n '2p' <<< "$mem_output") + mem_total_val=$(sed -n '3p' <<< "$mem_output") + fi +fi + +# Rainbarf rainbarf_bg="#2e3440" rainbarf_segment="" rainbarf_toggle="${TMUX_RAINBARF:-1}" - case "$rainbarf_toggle" in - 0|false|FALSE|off|OFF|no|NO) - rainbarf_toggle="0" - ;; - *) - rainbarf_toggle="1" - ;; + 0|false|FALSE|off|OFF|no|NO) rainbarf_toggle="0" ;; + *) rainbarf_toggle="1" ;; esac - if [[ "$rainbarf_toggle" == "1" ]] && command -v rainbarf >/dev/null 2>&1; then rainbarf_output=$(rainbarf --no-battery --no-remaining --no-bolt --tmux --rgb 2>/dev/null || true) rainbarf_output=${rainbarf_output//$'\n'/} @@ -52,37 +70,62 @@ if [[ "$rainbarf_toggle" == "1" ]] && command -v rainbarf >/dev/null 2>&1; then fi fi -# Notes count for current window (red if > 0, hidden otherwise) -notes_segment="" +# Notes +notes_output="" notes_count_script="$HOME/.config/tmux/tmux-status/notes_count.sh" if [[ -x "$notes_count_script" ]]; then notes_output=$("$notes_count_script" 2>/dev/null || true) - if [[ -n "$notes_output" ]]; then - notes_connector_bg="$status_bg" - if [[ -n "$rainbarf_segment" ]]; then - notes_connector_bg="$rainbarf_bg" - fi - notes_bg="#cc6666" - notes_fg="#1d1f21" - notes_segment=$(printf '#[fg=%s,bg=%s]%s#[fg=%s,bg=%s,bold]%s#[default]' \ - "$notes_bg" "$notes_connector_bg" "$separator" \ - "$notes_fg" "$notes_bg" "$notes_output") - fi fi -# Build a connector into the hostname segment using host colors -host_connector_bg="$status_bg" -if [[ -n "$notes_segment" ]]; then - host_connector_bg="#cc6666" -elif [[ -n "$rainbarf_segment" ]]; then - host_connector_bg="$rainbarf_bg" +# --- Segment building (left to right: rainbarf | pane_mem | win_mem | notes | host) --- + +# Track the rightmost background for connector chaining +prev_bg="$status_bg" +[[ -n "$rainbarf_segment" ]] && prev_bg="$rainbarf_bg" + +mem_pane_segment="" +if [[ -n "$mem_pane_val" ]]; then + mem_pane_segment=$(printf '#[fg=%s,bg=%s]%s#[fg=%s,bg=%s] %s ' \ + "$mem_pane_bg" "$prev_bg" "$separator" \ + "$mem_pane_fg" "$mem_pane_bg" "$mem_pane_val") + prev_bg="$mem_pane_bg" fi + +mem_win_segment="" +if [[ -n "$mem_win_val" ]]; then + mem_win_segment=$(printf '#[fg=%s,bg=%s]%s#[fg=%s,bg=%s] %s ' \ + "$mem_win_bg" "$prev_bg" "$separator" \ + "$mem_win_fg" "$mem_win_bg" "$mem_win_val") + prev_bg="$mem_win_bg" +fi + +mem_total_segment="" +if [[ -n "$mem_total_val" ]]; then + mem_total_segment=$(printf '#[fg=%s,bg=%s]%s#[fg=%s,bg=%s] %s ' \ + "$mem_total_bg" "$prev_bg" "$separator" \ + "$mem_total_fg" "$mem_total_bg" "$mem_total_val") + prev_bg="$mem_total_bg" +fi + +notes_segment="" +if [[ -n "$notes_output" ]]; then + notes_bg="#cc6666" + notes_fg="#1d1f21" + notes_segment=$(printf '#[fg=%s,bg=%s]%s#[fg=%s,bg=%s,bold]%s#[default]' \ + "$notes_bg" "$prev_bg" "$separator" \ + "$notes_fg" "$notes_bg" "$notes_output") + prev_bg="$notes_bg" +fi + host_prefix=$(printf '#[fg=%s,bg=%s]%s#[fg=%s,bg=%s] ' \ - "$host_bg" "$host_connector_bg" "$separator" \ + "$host_bg" "$prev_bg" "$separator" \ "$host_fg" "$host_bg") -printf '%s%s%s%s #[fg=%s,bg=%s]%s' \ +printf '%s%s%s%s%s%s%s #[fg=%s,bg=%s]%s' \ "$rainbarf_segment" \ + "$mem_pane_segment" \ + "$mem_win_segment" \ + "$mem_total_segment" \ "$notes_segment" \ "$host_prefix" \ "$hostname" \