Rust · 7083 bytes Raw Blame History
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