mirror of
https://github.com/theniceboy/.config.git
synced 2026-02-22 21:05:57 +08:00
tmux status update (right)
This commit is contained in:
parent
4051d27f71
commit
76bc5b83b4
3 changed files with 275 additions and 32 deletions
29
tmux/tmux-status/mem_usage.sh
Executable file
29
tmux/tmux-status/mem_usage.sh
Executable file
|
|
@ -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 <session_name> <window_index> <pane_id>
|
||||||
|
# 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"
|
||||||
171
tmux/tmux-status/mem_usage_cache.py
Executable file
171
tmux/tmux-status/mem_usage_cache.py
Executable file
|
|
@ -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()
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# hide entire right status if terminal width is below threshold
|
|
||||||
min_width=${TMUX_RIGHT_MIN_WIDTH:-90}
|
min_width=${TMUX_RIGHT_MIN_WIDTH:-90}
|
||||||
width=$(tmux display-message -p '#{client_width}' 2>/dev/null || true)
|
width=$(tmux display-message -p '#{client_width}' 2>/dev/null || true)
|
||||||
if [[ -z "${width:-}" || "$width" == "0" ]]; then
|
if [[ -z "${width:-}" || "$width" == "0" ]]; then
|
||||||
|
|
@ -21,27 +20,46 @@ if [[ -z "$status_bg" || "$status_bg" == "default" ]]; then
|
||||||
status_bg=black
|
status_bg=black
|
||||||
fi
|
fi
|
||||||
|
|
||||||
segment_bg="#3b4252"
|
|
||||||
segment_fg="#eceff4"
|
segment_fg="#eceff4"
|
||||||
# Host (domain) colors to mirror left active style
|
|
||||||
host_bg="${TMUX_THEME_COLOR:-#b294bb}"
|
host_bg="${TMUX_THEME_COLOR:-#b294bb}"
|
||||||
host_fg="#1d1f21"
|
host_fg="#1d1f21"
|
||||||
separator=""
|
separator=""
|
||||||
right_cap="█"
|
right_cap="█"
|
||||||
hostname=$(hostname -s 2>/dev/null || hostname 2>/dev/null || printf 'host')
|
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_bg="#2e3440"
|
||||||
rainbarf_segment=""
|
rainbarf_segment=""
|
||||||
rainbarf_toggle="${TMUX_RAINBARF:-1}"
|
rainbarf_toggle="${TMUX_RAINBARF:-1}"
|
||||||
|
|
||||||
case "$rainbarf_toggle" in
|
case "$rainbarf_toggle" in
|
||||||
0|false|FALSE|off|OFF|no|NO)
|
0|false|FALSE|off|OFF|no|NO) rainbarf_toggle="0" ;;
|
||||||
rainbarf_toggle="0"
|
*) rainbarf_toggle="1" ;;
|
||||||
;;
|
|
||||||
*)
|
|
||||||
rainbarf_toggle="1"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [[ "$rainbarf_toggle" == "1" ]] && command -v rainbarf >/dev/null 2>&1; then
|
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 --no-battery --no-remaining --no-bolt --tmux --rgb 2>/dev/null || true)
|
||||||
rainbarf_output=${rainbarf_output//$'\n'/}
|
rainbarf_output=${rainbarf_output//$'\n'/}
|
||||||
|
|
@ -52,37 +70,62 @@ if [[ "$rainbarf_toggle" == "1" ]] && command -v rainbarf >/dev/null 2>&1; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Notes count for current window (red if > 0, hidden otherwise)
|
# Notes
|
||||||
notes_segment=""
|
notes_output=""
|
||||||
notes_count_script="$HOME/.config/tmux/tmux-status/notes_count.sh"
|
notes_count_script="$HOME/.config/tmux/tmux-status/notes_count.sh"
|
||||||
if [[ -x "$notes_count_script" ]]; then
|
if [[ -x "$notes_count_script" ]]; then
|
||||||
notes_output=$("$notes_count_script" 2>/dev/null || true)
|
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
|
fi
|
||||||
|
|
||||||
# Build a connector into the hostname segment using host colors
|
# --- Segment building (left to right: rainbarf | pane_mem | win_mem | notes | host) ---
|
||||||
host_connector_bg="$status_bg"
|
|
||||||
if [[ -n "$notes_segment" ]]; then
|
# Track the rightmost background for connector chaining
|
||||||
host_connector_bg="#cc6666"
|
prev_bg="$status_bg"
|
||||||
elif [[ -n "$rainbarf_segment" ]]; then
|
[[ -n "$rainbarf_segment" ]] && prev_bg="$rainbarf_bg"
|
||||||
host_connector_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
|
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_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")
|
"$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" \
|
"$rainbarf_segment" \
|
||||||
|
"$mem_pane_segment" \
|
||||||
|
"$mem_win_segment" \
|
||||||
|
"$mem_total_segment" \
|
||||||
"$notes_segment" \
|
"$notes_segment" \
|
||||||
"$host_prefix" \
|
"$host_prefix" \
|
||||||
"$hostname" \
|
"$hostname" \
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue