| 1 | //! Header bar component |
| 2 | |
| 3 | use gartk_core::Rect; |
| 4 | use gartk_render::{Renderer, TextStyle}; |
| 5 | use super::theme::Theme; |
| 6 | |
| 7 | /// Header bar showing app title and stats summary. |
| 8 | pub struct HeaderBar { |
| 9 | bounds: Rect, |
| 10 | uptime: String, |
| 11 | cpu_usage: f32, |
| 12 | memory_usage: f32, |
| 13 | net_rate: f64, // Combined rx+tx bytes/sec |
| 14 | disk_rate: f64, // Combined read+write bytes/sec |
| 15 | max_temp: Option<f64>, // Maximum temperature reading |
| 16 | gpu_usage: Option<f64>, // GPU utilization percentage |
| 17 | } |
| 18 | |
| 19 | impl HeaderBar { |
| 20 | /// Create a new header bar. |
| 21 | pub fn new(bounds: Rect) -> Self { |
| 22 | Self { |
| 23 | bounds, |
| 24 | uptime: String::from("0s"), |
| 25 | cpu_usage: 0.0, |
| 26 | memory_usage: 0.0, |
| 27 | net_rate: 0.0, |
| 28 | disk_rate: 0.0, |
| 29 | max_temp: None, |
| 30 | gpu_usage: None, |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | /// Update header with current stats. |
| 35 | pub fn update(&mut self, uptime_secs: u64, cpu: f32, memory: f32, net_rate: f64, disk_rate: f64) { |
| 36 | self.uptime = format_uptime(uptime_secs); |
| 37 | self.cpu_usage = cpu; |
| 38 | self.memory_usage = memory; |
| 39 | self.net_rate = net_rate; |
| 40 | self.disk_rate = disk_rate; |
| 41 | } |
| 42 | |
| 43 | /// Update temperature reading. |
| 44 | pub fn update_temp(&mut self, max_temp: Option<f64>) { |
| 45 | self.max_temp = max_temp; |
| 46 | } |
| 47 | |
| 48 | /// Update GPU usage. |
| 49 | pub fn update_gpu(&mut self, gpu_usage: Option<f64>) { |
| 50 | self.gpu_usage = gpu_usage; |
| 51 | } |
| 52 | |
| 53 | /// Render the header bar. |
| 54 | pub fn render(&self, renderer: &Renderer, theme: &Theme) -> anyhow::Result<()> { |
| 55 | // Background - use panel_bg for seamless transition to tab bar |
| 56 | renderer.fill_rect(self.bounds, theme.panel_bg)?; |
| 57 | |
| 58 | // Title |
| 59 | let title_style = TextStyle { |
| 60 | font_family: "monospace".to_string(), |
| 61 | font_size: 14.0, |
| 62 | color: theme.text, |
| 63 | ..Default::default() |
| 64 | }; |
| 65 | // Position text in upper portion of header (leave room below for visual separation) |
| 66 | let text_y = self.bounds.y as f64 + (self.bounds.height as f64 * 0.4) + 6.0; |
| 67 | renderer.text("gartop", 16.0, text_y, &title_style)?; |
| 68 | |
| 69 | // Stats summary on right side |
| 70 | let stats_style = TextStyle { |
| 71 | font_family: "monospace".to_string(), |
| 72 | font_size: 12.0, |
| 73 | color: theme.text_secondary, |
| 74 | ..Default::default() |
| 75 | }; |
| 76 | |
| 77 | // CPU indicator |
| 78 | let cpu_text = format!("CPU: {:.1}%", self.cpu_usage); |
| 79 | let cpu_style = TextStyle { |
| 80 | color: theme.cpu_color, |
| 81 | ..stats_style.clone() |
| 82 | }; |
| 83 | |
| 84 | // Memory indicator |
| 85 | let mem_text = format!("MEM: {:.1}%", self.memory_usage); |
| 86 | let mem_style = TextStyle { |
| 87 | color: theme.memory_color, |
| 88 | ..stats_style.clone() |
| 89 | }; |
| 90 | |
| 91 | // Network indicator |
| 92 | let net_text = format!("NET: {}", format_rate(self.net_rate)); |
| 93 | let net_style = TextStyle { |
| 94 | color: theme.network_color, |
| 95 | ..stats_style.clone() |
| 96 | }; |
| 97 | |
| 98 | // Disk indicator |
| 99 | let disk_text = format!("DISK: {}", format_rate(self.disk_rate)); |
| 100 | let disk_style = TextStyle { |
| 101 | color: theme.disk_color, |
| 102 | ..stats_style.clone() |
| 103 | }; |
| 104 | |
| 105 | // Temperature indicator (if available) |
| 106 | let temp_text = self.max_temp |
| 107 | .map(|t| format!("TEMP: {:.0}°C", t)) |
| 108 | .unwrap_or_default(); |
| 109 | let temp_style = TextStyle { |
| 110 | color: theme.temp_color, |
| 111 | ..stats_style.clone() |
| 112 | }; |
| 113 | |
| 114 | // GPU indicator (if available) |
| 115 | let gpu_text = self.gpu_usage |
| 116 | .map(|u| format!("GPU: {:.0}%", u)) |
| 117 | .unwrap_or_default(); |
| 118 | let gpu_style = TextStyle { |
| 119 | color: theme.gpu_color, |
| 120 | ..stats_style.clone() |
| 121 | }; |
| 122 | |
| 123 | // Uptime |
| 124 | let uptime_text = format!("up {}", self.uptime); |
| 125 | |
| 126 | // Position from right side |
| 127 | let right_margin = 16.0; |
| 128 | let spacing = 16.0; |
| 129 | let y = self.bounds.y as f64 + (self.bounds.height as f64 * 0.4) + 6.0; |
| 130 | |
| 131 | let uptime_width = renderer.measure_text(&uptime_text, &stats_style)?.width as f64; |
| 132 | let gpu_width = if self.gpu_usage.is_some() { |
| 133 | renderer.measure_text(&gpu_text, &gpu_style)?.width as f64 |
| 134 | } else { |
| 135 | 0.0 |
| 136 | }; |
| 137 | let temp_width = if self.max_temp.is_some() { |
| 138 | renderer.measure_text(&temp_text, &temp_style)?.width as f64 |
| 139 | } else { |
| 140 | 0.0 |
| 141 | }; |
| 142 | let disk_width = renderer.measure_text(&disk_text, &disk_style)?.width as f64; |
| 143 | let net_width = renderer.measure_text(&net_text, &net_style)?.width as f64; |
| 144 | let mem_width = renderer.measure_text(&mem_text, &mem_style)?.width as f64; |
| 145 | let cpu_width = renderer.measure_text(&cpu_text, &cpu_style)?.width as f64; |
| 146 | |
| 147 | let right_edge = (self.bounds.x + self.bounds.width as i32) as f64; |
| 148 | |
| 149 | let uptime_x = right_edge - right_margin - uptime_width; |
| 150 | let gpu_x = if self.gpu_usage.is_some() { |
| 151 | uptime_x - spacing - gpu_width |
| 152 | } else { |
| 153 | uptime_x |
| 154 | }; |
| 155 | let temp_x = if self.max_temp.is_some() { |
| 156 | gpu_x - spacing - temp_width |
| 157 | } else { |
| 158 | gpu_x |
| 159 | }; |
| 160 | let disk_x = temp_x - spacing - disk_width; |
| 161 | let net_x = disk_x - spacing - net_width; |
| 162 | let mem_x = net_x - spacing - mem_width; |
| 163 | let cpu_x = mem_x - spacing - cpu_width; |
| 164 | |
| 165 | renderer.text(&cpu_text, cpu_x, y, &cpu_style)?; |
| 166 | renderer.text(&mem_text, mem_x, y, &mem_style)?; |
| 167 | renderer.text(&net_text, net_x, y, &net_style)?; |
| 168 | renderer.text(&disk_text, disk_x, y, &disk_style)?; |
| 169 | if self.max_temp.is_some() { |
| 170 | renderer.text(&temp_text, temp_x, y, &temp_style)?; |
| 171 | } |
| 172 | if self.gpu_usage.is_some() { |
| 173 | renderer.text(&gpu_text, gpu_x, y, &gpu_style)?; |
| 174 | } |
| 175 | renderer.text(&uptime_text, uptime_x, y, &stats_style)?; |
| 176 | |
| 177 | Ok(()) |
| 178 | } |
| 179 | |
| 180 | /// Get header height. |
| 181 | pub fn height(&self) -> u32 { |
| 182 | self.bounds.height |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | /// Format uptime seconds to human-readable string. |
| 187 | fn format_uptime(secs: u64) -> String { |
| 188 | if secs < 60 { |
| 189 | format!("{}s", secs) |
| 190 | } else if secs < 3600 { |
| 191 | format!("{}m {}s", secs / 60, secs % 60) |
| 192 | } else if secs < 86400 { |
| 193 | format!("{}h {}m", secs / 3600, (secs % 3600) / 60) |
| 194 | } else { |
| 195 | format!("{}d {}h", secs / 86400, (secs % 86400) / 3600) |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | /// Format rate (bytes/second) to compact human-readable string. |
| 200 | fn format_rate(bytes_per_sec: f64) -> String { |
| 201 | const KIB: f64 = 1024.0; |
| 202 | const MIB: f64 = KIB * 1024.0; |
| 203 | const GIB: f64 = MIB * 1024.0; |
| 204 | |
| 205 | if bytes_per_sec >= GIB { |
| 206 | format!("{:.1}G/s", bytes_per_sec / GIB) |
| 207 | } else if bytes_per_sec >= MIB { |
| 208 | format!("{:.1}M/s", bytes_per_sec / MIB) |
| 209 | } else if bytes_per_sec >= KIB { |
| 210 | format!("{:.0}K/s", bytes_per_sec / KIB) |
| 211 | } else if bytes_per_sec > 0.0 { |
| 212 | format!("{:.0}B/s", bytes_per_sec) |
| 213 | } else { |
| 214 | "0".to_string() |
| 215 | } |
| 216 | } |
| 217 |