1#!/usr/bin/env bash
2set -euo pipefail
3
4# System Monitoring Dashboard
5# Terminal-based real-time system monitoring with color-coded alerts
6# Cross-platform support for Linux and macOS
7
8# ============================================================================
9# Configuration
10# ============================================================================
11
12REFRESH_INTERVAL=${REFRESH_INTERVAL:-2} # seconds
13
14# Alert thresholds
15CPU_WARNING=70
16CPU_CRITICAL=90
17MEM_WARNING=75
18MEM_CRITICAL=90
19DISK_WARNING=80
20DISK_CRITICAL=95
21
22# Platform detection
23OS_TYPE=$(uname -s)
24
25# Colors
26RED='\033[0;31m'
27GREEN='\033[0;32m'
28YELLOW='\033[1;33m'
29BLUE='\033[0;34m'
30CYAN='\033[0;36m'
31BOLD='\033[1m'
32NC='\033[0m'
33
34# ============================================================================
35# Terminal Control
36# ============================================================================
37
38# Save cursor position and hide cursor
39init_terminal() {
40 tput smcup # Save screen
41 tput civis # Hide cursor
42 clear
43}
44
45# Restore terminal on exit
46cleanup_terminal() {
47 tput rmcup # Restore screen
48 tput cnorm # Show cursor
49 clear
50}
51
52# Position cursor at row, col
53move_cursor() {
54 local row=$1
55 local col=$2
56 tput cup "$row" "$col"
57}
58
59# Get terminal dimensions
60get_terminal_size() {
61 TERM_ROWS=$(tput lines)
62 TERM_COLS=$(tput cols)
63}
64
65# ============================================================================
66# Metric Collection Functions
67# ============================================================================
68
69get_cpu_usage() {
70 if [[ "$OS_TYPE" == "Linux" ]]; then
71 # Linux: use top or /proc/stat
72 if command -v top &> /dev/null; then
73 top -bn2 -d 0.1 | grep '^%Cpu' | tail -n1 | awk '{print int(100 - $8)}'
74 else
75 echo "0"
76 fi
77 elif [[ "$OS_TYPE" == "Darwin" ]]; then
78 # macOS: use top
79 top -l 2 -n 0 -F | grep 'CPU usage' | tail -n1 | awk '{print int($3 + $5)}'
80 else
81 echo "0"
82 fi
83}
84
85get_memory_usage() {
86 if [[ "$OS_TYPE" == "Linux" ]]; then
87 # Linux: use /proc/meminfo
88 if [[ -f /proc/meminfo ]]; then
89 local total
90 local available
91 total=$(grep MemTotal /proc/meminfo | awk '{print $2}')
92 available=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
93 echo $(( (total - available) * 100 / total ))
94 else
95 echo "0"
96 fi
97 elif [[ "$OS_TYPE" == "Darwin" ]]; then
98 # macOS: use vm_stat
99 local page_size
100 local free_pages
101 local active_pages
102 local inactive_pages
103 local wired_pages
104
105 page_size=$(vm_stat | grep 'page size' | awk '{print $8}')
106 free_pages=$(vm_stat | grep 'Pages free' | awk '{print $3}' | tr -d '.')
107 active_pages=$(vm_stat | grep 'Pages active' | awk '{print $3}' | tr -d '.')
108 inactive_pages=$(vm_stat | grep 'Pages inactive' | awk '{print $3}' | tr -d '.')
109 wired_pages=$(vm_stat | grep 'Pages wired' | awk '{print $4}' | tr -d '.')
110
111 local used=$((active_pages + inactive_pages + wired_pages))
112 local total=$((used + free_pages))
113
114 echo $((used * 100 / total))
115 else
116 echo "0"
117 fi
118}
119
120get_disk_usage() {
121 # Get disk usage for root filesystem
122 df -h / | awk 'NR==2 {print int($5)}'
123}
124
125get_load_average() {
126 if [[ "$OS_TYPE" == "Linux" ]] || [[ "$OS_TYPE" == "Darwin" ]]; then
127 uptime | awk -F'load average:' '{print $2}' | awk '{print $1, $2, $3}' | tr -d ','
128 else
129 echo "0.00 0.00 0.00"
130 fi
131}
132
133get_top_processes() {
134 local count=${1:-5}
135
136 if [[ "$OS_TYPE" == "Linux" ]]; then
137 ps aux --sort=-%cpu | head -n $((count + 1)) | tail -n $count | awk '{printf "%-15s %5s%% %5s%%\n", substr($11,1,15), $3, $4}'
138 elif [[ "$OS_TYPE" == "Darwin" ]]; then
139 ps aux -r | head -n $((count + 1)) | tail -n $count | awk '{printf "%-15s %5s%% %5s%%\n", substr($11,1,15), $3, $4}'
140 fi
141}
142
143# ============================================================================
144# Display Functions
145# ============================================================================
146
147get_color() {
148 local value=$1
149 local warning=$2
150 local critical=$3
151
152 if [[ $value -ge $critical ]]; then
153 echo -e "$RED"
154 elif [[ $value -ge $warning ]]; then
155 echo -e "$YELLOW"
156 else
157 echo -e "$GREEN"
158 fi
159}
160
161draw_box() {
162 local row=$1
163 local col=$2
164 local width=$3
165 local height=$4
166 local title=$5
167
168 # Top border
169 move_cursor "$row" "$col"
170 echo -ne "${CYAN}┌"
171 printf '─%.0s' $(seq 1 $((width - 2)))
172 echo -ne "┐${NC}"
173
174 # Title
175 move_cursor "$row" $((col + 2))
176 echo -ne "${BOLD}${title}${NC}"
177
178 # Side borders
179 local i
180 for ((i = 1; i < height - 1; i++)); do
181 move_cursor $((row + i)) "$col"
182 echo -ne "${CYAN}│${NC}"
183 move_cursor $((row + i)) $((col + width - 1))
184 echo -ne "${CYAN}│${NC}"
185 done
186
187 # Bottom border
188 move_cursor $((row + height - 1)) "$col"
189 echo -ne "${CYAN}└"
190 printf '─%.0s' $(seq 1 $((width - 2)))
191 echo -ne "┘${NC}"
192}
193
194draw_bar() {
195 local value=$1
196 local max_width=$2
197 local warning=$3
198 local critical=$4
199
200 local filled=$((value * max_width / 100))
201 local color
202 color=$(get_color "$value" "$warning" "$critical")
203
204 echo -ne "$color"
205 printf '█%.0s' $(seq 1 "$filled")
206 echo -ne "$NC"
207 printf '░%.0s' $(seq 1 $((max_width - filled)))
208}
209
210draw_metric_panel() {
211 local row=$1
212 local col=$2
213 local width=$3
214 local title=$4
215 local value=$5
216 local warning=$6
217 local critical=$7
218
219 draw_box "$row" "$col" "$width" 5 "$title"
220
221 # Value
222 move_cursor $((row + 2)) $((col + 2))
223 local color
224 color=$(get_color "$value" "$warning" "$critical")
225 printf "%s%3d%%%s" "$color" "$value" "$NC"
226
227 # Progress bar
228 move_cursor $((row + 3)) $((col + 2))
229 draw_bar "$value" $((width - 4)) "$warning" "$critical"
230}
231
232draw_header() {
233 move_cursor 0 0
234 echo -ne "${BOLD}${CYAN}"
235 printf '═%.0s' $(seq 1 "$TERM_COLS")
236 echo -ne "$NC"
237
238 move_cursor 0 2
239 echo -ne "${BOLD}System Monitor${NC}"
240
241 move_cursor 0 $((TERM_COLS - 30))
242 echo -ne "${BOLD}$(date '+%Y-%m-%d %H:%M:%S')${NC}"
243
244 move_cursor 1 0
245 echo -ne "${BOLD}${CYAN}"
246 printf '═%.0s' $(seq 1 "$TERM_COLS")
247 echo -ne "$NC"
248}
249
250draw_dashboard() {
251 # Collect metrics
252 local cpu_usage
253 local mem_usage
254 local disk_usage
255 local load_avg
256
257 cpu_usage=$(get_cpu_usage)
258 mem_usage=$(get_memory_usage)
259 disk_usage=$(get_disk_usage)
260 load_avg=$(get_load_average)
261
262 # Clear and draw
263 clear
264 draw_header
265
266 # Calculate layout
267 local panel_width=$((TERM_COLS / 2 - 3))
268
269 # Top row - 4 metric panels
270 local panel_w=$((TERM_COLS / 4 - 2))
271 draw_metric_panel 3 2 "$panel_w" "CPU" "$cpu_usage" "$CPU_WARNING" "$CPU_CRITICAL"
272 draw_metric_panel 3 $((panel_w + 3)) "$panel_w" "Memory" "$mem_usage" "$MEM_WARNING" "$MEM_CRITICAL"
273 draw_metric_panel 3 $((panel_w * 2 + 4)) "$panel_w" "Disk" "$disk_usage" "$DISK_WARNING" "$DISK_CRITICAL"
274
275 # Load average
276 draw_box 3 $((panel_w * 3 + 5)) "$panel_w" 5 "Load Avg"
277 move_cursor 5 $((panel_w * 3 + 7))
278 echo -ne "$load_avg"
279
280 # Process list
281 local proc_row=9
282 draw_box "$proc_row" 2 $((TERM_COLS - 4)) 12 "Top Processes (CPU)"
283
284 move_cursor $((proc_row + 2)) 4
285 printf "${BOLD}%-15s %5s %5s${NC}" "COMMAND" "CPU%" "MEM%"
286
287 local line=0
288 while IFS= read -r process; do
289 move_cursor $((proc_row + 3 + line)) 4
290 echo -ne "$process"
291 ((line++))
292 done < <(get_top_processes 8)
293
294 # Status line
295 move_cursor $((TERM_ROWS - 2)) 2
296 echo -ne "${BOLD}Press Ctrl+C to exit | Refresh: ${REFRESH_INTERVAL}s${NC}"
297}
298
299# ============================================================================
300# Main Loop
301# ============================================================================
302
303main() {
304 # Initialize
305 init_terminal
306 trap cleanup_terminal EXIT INT TERM
307
308 # Main monitoring loop
309 while true; do
310 get_terminal_size
311
312 # Ensure terminal is large enough
313 if [[ $TERM_ROWS -lt 24 ]] || [[ $TERM_COLS -lt 80 ]]; then
314 clear
315 move_cursor 2 2
316 echo -e "${RED}Terminal too small!${NC}"
317 move_cursor 3 2
318 echo "Minimum size: 80x24"
319 move_cursor 4 2
320 echo "Current size: ${TERM_COLS}x${TERM_ROWS}"
321 sleep 1
322 continue
323 fi
324
325 # Draw dashboard
326 draw_dashboard
327
328 # Wait for refresh interval
329 sleep "$REFRESH_INTERVAL"
330 done
331}
332
333main "$@"