Last active
August 19, 2026 22:46
-
-
Save combatwombat/141db9cf13cdded0e63377edfd62b1aa to your computer and use it in GitHub Desktop.
rtop - top for dummies
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 | |
| # rtop — quick iStat-style CPU + memory overview | |
| # | |
| # Works on Linux (reads /proc) and macOS/BSD (sysctl/top/vm_stat). | |
| # On macOS, stock bash is 3.2; watch-mode's single-key quit needs bash 4+ | |
| # (install via Homebrew for it), otherwise it falls back to Ctrl-C to quit. | |
| # | |
| # Usage: | |
| # ./rtop.sh One-shot glance | |
| # ./rtop.sh -w [secs] Watch mode, repaint roughly every [secs], default 2. | |
| # On macOS add ~2s/frame, top needs two CPU samples. | |
| # ./rtop.sh -h This help | |
| # | |
| # Put an alias in .bashrc/.bash_profile for easier usage: | |
| # alias rtop="/path/to/rtop.sh" | |
| case $1 in | |
| -h|--help) | |
| # print the header comment block (everything after the shebang, up to the code) | |
| awk 'NR==1{next} /^#/{sub(/^# ?/,""); print; next} {exit}' "$0" | |
| exit 0 ;; | |
| esac | |
| # coloured bar: $1=percent(0-100), $2=width | |
| _bar() { | |
| local pct=$1 width=${2:-20} filled i out='' color reset=$'\e[0m' | |
| (( pct < 0 )) && pct=0; (( pct > 100 )) && pct=100 | |
| (( filled = (pct * width + 50) / 100 )) | |
| if (( pct >= 90 )); then color=$'\e[31m' # red | |
| elif (( pct >= 70 )); then color=$'\e[33m' # yellow | |
| else color=$'\e[32m' # green | |
| fi | |
| for ((i = 0; i < width; i++)); do | |
| (( i < filled )) && out+='█' || out+='░' | |
| done | |
| printf '%s%s%s' "$color" "$out" "$reset" | |
| } | |
| # GiB with one decimal, no bc (arg in kB) | |
| _gib() { printf '%d.%d' $(( $1 / 1048576 )) $(( ($1 % 1048576) * 10 / 1048576 )); } | |
| # online CPU count, portable | |
| _ncpu() { | |
| local n | |
| n=$(nproc 2>/dev/null) \ | |
| || n=$(sysctl -n hw.ncpu 2>/dev/null) \ | |
| || n=$(getconf _NPROCESSORS_ONLN 2>/dev/null) | |
| [[ $n =~ ^[0-9]+$ ]] && echo "$n" || echo 1 | |
| } | |
| # macOS "1234.50M" / "2.00G" -> kB (integer, keeps 2 decimals of the unit) | |
| _unit_to_kb() { | |
| local s=$1 int frac v | |
| local unit=${s: -1} n=${s%?} # note: separate line; local expands RHS before assigning | |
| [[ $n == *.* ]] && { int=${n%.*}; frac=${n#*.}; } || { int=$n; frac=0; } | |
| frac=${frac}00; frac=${frac:0:2} | |
| v=$(( 10#${int:-0} * 100 + 10#${frac:-0} )) | |
| case $unit in | |
| G) echo $(( v * 1048576 / 100 )) ;; | |
| M) echo $(( v * 1024 / 100 )) ;; | |
| K|k) echo $(( v / 100 )) ;; | |
| *) echo 0 ;; | |
| esac | |
| } | |
| # wait $1 seconds; watch mode overrides this to also listen for a quit key. | |
| # returns 9 to signal "quit requested". | |
| _wait() { sleep "$1"; } | |
| # render one frame; $1 = CPU sample window in seconds (default 0.3) | |
| _render() { | |
| local window=${1:-0.3} | |
| local cpu=0 load='?' cores total=0 avail memfree=0 buffers=0 cached=0 | |
| local memused=0 mempct=0 swtotal=0 swfree=0 swused=0 swpct=0 | |
| # ---- CPU + load ---- | |
| if [[ -r /proc/stat ]]; then | |
| # Linux: sample /proc/stat twice over the window | |
| read -r _ u1 n1 s1 i1 w1 irq1 sirq1 st1 _ < /proc/stat | |
| local idle1=$(( i1 + w1 )) tot1=$(( u1 + n1 + s1 + i1 + w1 + irq1 + sirq1 + st1 )) | |
| _wait "$window" || return 9 # 9 = quit key pressed during the sample window | |
| read -r _ u2 n2 s2 i2 w2 irq2 sirq2 st2 _ < /proc/stat | |
| local idle2=$(( i2 + w2 )) tot2=$(( u2 + n2 + s2 + i2 + w2 + irq2 + sirq2 + st2 )) | |
| local dtot=$(( tot2 - tot1 )) didle=$(( idle2 - idle1 )) | |
| (( dtot > 0 )) && cpu=$(( ( (dtot - didle) * 100 + dtot / 2 ) / dtot )) | |
| read -r load _ < /proc/loadavg | |
| else | |
| # macOS/BSD: top -l 2 gives a real delta on its 2nd sample; parse idle% | |
| local secs=${window%.*} line idle | |
| [[ $secs =~ ^[0-9]+$ ]] && (( secs >= 1 )) || secs=1 | |
| line=$(top -l 2 -n 0 -s "$secs" 2>/dev/null | grep 'CPU usage' | tail -1) | |
| idle=${line##*, }; idle=${idle%%[.% ]*} # "…, 90.62% idle" -> "90" | |
| [[ $idle =~ ^[0-9]+$ ]] && cpu=$(( 100 - idle )) | |
| load=$(sysctl -n vm.loadavg 2>/dev/null); load=${load#* }; load=${load%% *} | |
| [[ -n $load ]] || load='?' | |
| fi | |
| cores=$(_ncpu) | |
| # ---- memory + swap (kB throughout) ---- | |
| if [[ -r /proc/meminfo ]]; then | |
| # "used" = total - MemAvailable (cache counts as free; right for a server) | |
| while read -r key val _; do | |
| case $key in | |
| MemTotal:) total=$val ;; | |
| MemAvailable:) avail=$val ;; | |
| MemFree:) memfree=$val ;; | |
| Buffers:) buffers=$val ;; | |
| Cached:) cached=$val ;; | |
| SwapTotal:) swtotal=$val ;; | |
| SwapFree:) swfree=$val ;; | |
| esac | |
| done < /proc/meminfo | |
| : "${avail:=$(( memfree + buffers + cached ))}" # pre-3.14 kernels | |
| memused=$(( total - avail )) | |
| (( swtotal > 0 )) && swused=$(( swtotal - swfree )) | |
| else | |
| # macOS: "used" = active + wired + compressed (Activity Monitor's number) | |
| local pagesize active=0 wired=0 comp=0 line sw swt swu | |
| pagesize=$(sysctl -n hw.pagesize 2>/dev/null); : "${pagesize:=4096}" | |
| total=$(( $(sysctl -n hw.memsize 2>/dev/null || echo 0) / 1024 )) | |
| while IFS= read -r line; do | |
| case $line in | |
| "Pages active:"*) active=${line##*:} ;; | |
| "Pages wired down:"*) wired=${line##*:} ;; | |
| "Pages occupied by compressor:"*) comp=${line##*:} ;; | |
| esac | |
| done < <(vm_stat 2>/dev/null) | |
| active=${active//[!0-9]/}; wired=${wired//[!0-9]/}; comp=${comp//[!0-9]/} | |
| memused=$(( (${active:-0} + ${wired:-0} + ${comp:-0}) * pagesize / 1024 )) | |
| sw=$(sysctl -n vm.swapusage 2>/dev/null) | |
| if [[ $sw == *total*used* ]]; then | |
| swt=${sw#*total = }; swt=${swt%% *} | |
| swu=${sw#*used = }; swu=${swu%% *} | |
| swtotal=$(_unit_to_kb "$swt"); swused=$(_unit_to_kb "$swu") | |
| fi | |
| fi | |
| (( total > 0 )) && mempct=$(( memused * 100 / total )) | |
| printf 'CPU %s %3d%% load %s (%d cores)\n' "$(_bar "$cpu")" "$cpu" "$load" "$cores" | |
| printf 'MEM %s %3d%% %s / %s GiB\n' \ | |
| "$(_bar "$mempct")" "$mempct" "$(_gib "$memused")" "$(_gib "$total")" | |
| if (( swtotal > 0 )); then | |
| swpct=$(( swused * 100 / swtotal )) | |
| printf 'SWAP %s %3d%% %s / %s GiB\n' \ | |
| "$(_bar "$swpct")" "$swpct" "$(_gib "$swused")" "$(_gib "$swtotal")" | |
| fi | |
| } | |
| # ---- watch mode ------------------------------------------------------------ | |
| if [[ $1 == -w || $1 == --watch ]]; then | |
| interval=${2:-2} | |
| hint='(Ctrl-C quits)' | |
| stty_saved=$(stty -g 2>/dev/null) | |
| cleanup() { [[ -n $stty_saved ]] && stty "$stty_saved" 2>/dev/null; printf '\e[?25h\n'; exit 0; } | |
| trap cleanup INT TERM | |
| # single-key 'q' quit needs bash 4+ (read -N / fractional -t); else Ctrl-C | |
| if (( ${BASH_VERSINFO[0]:-0} >= 4 )); then | |
| stty -echo -icanon min 0 time 0 2>/dev/null # unbuffered single-key reads | |
| _wait() { | |
| local key | |
| read -rt "$1" -N 1 key 2>/dev/null && [[ $key == q || $key == Q ]] && return 9 | |
| return 0 | |
| } | |
| hint='(q quits)' | |
| fi | |
| _paint() { | |
| printf '\e[H\e[1m rtop \e[0m %s \e[2m%s\e[0m\n\n%s\n' \ | |
| "$(date '+%H:%M:%S')" "$hint" "$frame" | |
| } | |
| printf '\e[?25l\e[2J' # hide cursor, clear once | |
| if [[ -r /proc/stat ]]; then | |
| # Linux: the sample window spans the whole interval (q caught during it) | |
| while true; do | |
| frame=$(_render "$interval") | |
| (( $? == 9 )) && break | |
| _paint | |
| done | |
| else | |
| # macOS/BSD: top does its own ~1s sample, then pace/listen for the interval | |
| while true; do | |
| frame=$(_render 1) | |
| _paint | |
| _wait "$interval" || break | |
| done | |
| fi | |
| cleanup | |
| else | |
| _render | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment