@@ -1,18 +1,27 @@ |
| 1 | 1 | //! GUI application state and event loop |
| 2 | 2 | |
| 3 | | -use super::{graph::{DataSeries, LineGraph}, header::HeaderBar, theme::Theme}; |
| 3 | +use super::{ |
| 4 | + graph::{DataSeries, LineGraph}, |
| 5 | + header::HeaderBar, |
| 6 | + process_list::ProcessList, |
| 7 | + tabs::{Tab, TabBar, TAB_BAR_HEIGHT}, |
| 8 | + theme::Theme, |
| 9 | +}; |
| 4 | 10 | use anyhow::Result; |
| 5 | | -use gartk_core::{InputEvent, Key, Rect}; |
| 6 | | -use gartk_render::Renderer; |
| 11 | +use gartk_core::{InputEvent, Key, Point, Rect}; |
| 12 | +use gartk_render::{Renderer, TextStyle}; |
| 7 | 13 | use gartk_x11::{Connection, EventLoop, EventLoopConfig, Window, WindowConfig}; |
| 8 | | -use gartop_ipc::{Command, CpuStats, MemoryStats, Response, StatusInfo}; |
| 14 | +use gartop_ipc::{Command, CpuStats, MemoryStats, ProcessInfo, Response, SortField, StatusInfo}; |
| 9 | 15 | use std::io::{BufRead, BufReader, Write}; |
| 10 | 16 | use std::os::unix::net::UnixStream; |
| 11 | 17 | use std::time::Instant; |
| 12 | 18 | use x11rb::protocol::xproto::{ConnectionExt, ImageFormat}; |
| 13 | 19 | |
| 14 | 20 | /// Header bar height. |
| 15 | | -const HEADER_HEIGHT: u32 = 36; |
| 21 | +const HEADER_HEIGHT: u32 = 40; |
| 22 | + |
| 23 | +/// Graph height. |
| 24 | +const GRAPH_HEIGHT: u32 = 150; |
| 16 | 25 | |
| 17 | 26 | /// Default window dimensions. |
| 18 | 27 | const DEFAULT_WIDTH: u32 = 800; |
@@ -28,6 +37,8 @@ pub struct App { |
| 28 | 37 | gc: u32, |
| 29 | 38 | theme: Theme, |
| 30 | 39 | header: HeaderBar, |
| 40 | + tab_bar: TabBar, |
| 41 | + process_list: ProcessList, |
| 31 | 42 | should_quit: bool, |
| 32 | 43 | width: u32, |
| 33 | 44 | height: u32, |
@@ -38,6 +49,7 @@ pub struct App { |
| 38 | 49 | memory_stats: Option<MemoryStats>, |
| 39 | 50 | cpu_history: Vec<CpuStats>, |
| 40 | 51 | memory_history: Vec<MemoryStats>, |
| 52 | + processes: Vec<ProcessInfo>, |
| 41 | 53 | } |
| 42 | 54 | |
| 43 | 55 | impl App { |
@@ -73,8 +85,10 @@ impl App { |
| 73 | 85 | let theme = Theme::default(); |
| 74 | 86 | let renderer = Renderer::new(width, height)?; |
| 75 | 87 | |
| 76 | | - // Create header bar |
| 88 | + // Create components |
| 77 | 89 | let header = HeaderBar::new(Rect::new(0, 0, width, HEADER_HEIGHT)); |
| 90 | + let tab_bar = TabBar::new(Rect::new(0, HEADER_HEIGHT as i32, width, TAB_BAR_HEIGHT)); |
| 91 | + let process_list = Self::create_process_list(width, height); |
| 78 | 92 | |
| 79 | 93 | // Check if daemon is available |
| 80 | 94 | let daemon_available = Self::check_daemon(); |
@@ -85,19 +99,29 @@ impl App { |
| 85 | 99 | gc, |
| 86 | 100 | theme, |
| 87 | 101 | header, |
| 102 | + tab_bar, |
| 103 | + process_list, |
| 88 | 104 | should_quit: false, |
| 89 | 105 | width, |
| 90 | 106 | height, |
| 91 | 107 | daemon_available, |
| 92 | | - last_refresh: Instant::now() - std::time::Duration::from_secs(10), // force immediate refresh |
| 108 | + last_refresh: Instant::now() - std::time::Duration::from_secs(10), |
| 93 | 109 | status: None, |
| 94 | 110 | cpu_stats: None, |
| 95 | 111 | memory_stats: None, |
| 96 | 112 | cpu_history: Vec::new(), |
| 97 | 113 | memory_history: Vec::new(), |
| 114 | + processes: Vec::new(), |
| 98 | 115 | }) |
| 99 | 116 | } |
| 100 | 117 | |
| 118 | + /// Create process list with correct bounds. |
| 119 | + fn create_process_list(width: u32, height: u32) -> ProcessList { |
| 120 | + let content_start = HEADER_HEIGHT + TAB_BAR_HEIGHT + GRAPH_HEIGHT + 24; // 24 for graph label |
| 121 | + let list_height = height.saturating_sub(content_start); |
| 122 | + ProcessList::new(Rect::new(0, content_start as i32, width, list_height)) |
| 123 | + } |
| 124 | + |
| 101 | 125 | /// Check if daemon is available by attempting a connection. |
| 102 | 126 | fn check_daemon() -> bool { |
| 103 | 127 | let path = gartop_ipc::socket_path(); |
@@ -114,17 +138,14 @@ impl App { |
| 114 | 138 | } |
| 115 | 139 | |
| 116 | 140 | /// Send a command to the daemon and get response. |
| 117 | | - /// Creates a fresh connection for each command since daemon closes after response. |
| 118 | 141 | fn send_command(&self, cmd: &Command) -> Option<Response> { |
| 119 | 142 | let path = gartop_ipc::socket_path(); |
| 120 | 143 | let mut stream = UnixStream::connect(&path).ok()?; |
| 121 | 144 | |
| 122 | | - // Send command |
| 123 | 145 | let json = serde_json::to_string(cmd).ok()?; |
| 124 | 146 | writeln!(stream, "{}", json).ok()?; |
| 125 | 147 | stream.flush().ok()?; |
| 126 | 148 | |
| 127 | | - // Read response |
| 128 | 149 | let mut reader = BufReader::new(&stream); |
| 129 | 150 | let mut line = String::new(); |
| 130 | 151 | reader.read_line(&mut line).ok()?; |
@@ -174,7 +195,22 @@ impl App { |
| 174 | 195 | } |
| 175 | 196 | } |
| 176 | 197 | |
| 177 | | - // Update daemon availability based on whether any command succeeded |
| 198 | + // Get processes sorted by current tab's resource |
| 199 | + let sort_field = match self.tab_bar.active() { |
| 200 | + Tab::Cpu => SortField::Cpu, |
| 201 | + Tab::Memory => SortField::Memory, |
| 202 | + }; |
| 203 | + if let Some(resp) = self.send_command(&Command::GetProcesses { |
| 204 | + sort_by: Some(sort_field), |
| 205 | + limit: Some(100), |
| 206 | + }) { |
| 207 | + if resp.success { |
| 208 | + self.processes = resp.data.and_then(|d| serde_json::from_value(d).ok()).unwrap_or_default(); |
| 209 | + self.process_list.set_processes(self.processes.clone()); |
| 210 | + self.process_list.set_sort(sort_field); |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 178 | 214 | self.daemon_available = any_success; |
| 179 | 215 | self.last_refresh = Instant::now(); |
| 180 | 216 | } |
@@ -198,157 +234,164 @@ impl App { |
| 198 | 234 | // Render header |
| 199 | 235 | self.header.render(&self.renderer, &self.theme)?; |
| 200 | 236 | |
| 201 | | - // Render main content area |
| 202 | | - let content_y = HEADER_HEIGHT as i32; |
| 203 | | - let content_height = self.height.saturating_sub(HEADER_HEIGHT); |
| 204 | | - let content_rect = Rect::new(0, content_y, self.width, content_height); |
| 237 | + // Render tab bar |
| 238 | + self.tab_bar.render(&self.renderer, &self.theme)?; |
| 205 | 239 | |
| 206 | | - self.renderer.fill_rect(content_rect, self.theme.panel_bg)?; |
| 240 | + // Render content area |
| 241 | + let content_y = (HEADER_HEIGHT + TAB_BAR_HEIGHT) as i32; |
| 242 | + let content_height = self.height.saturating_sub(HEADER_HEIGHT + TAB_BAR_HEIGHT); |
| 207 | 243 | |
| 208 | | - // Show connection status or stats |
| 209 | | - self.render_content(content_rect)?; |
| 244 | + if !self.daemon_available { |
| 245 | + self.render_not_connected(content_y)?; |
| 246 | + } else { |
| 247 | + self.render_tab_content(content_y, content_height)?; |
| 248 | + } |
| 210 | 249 | |
| 211 | 250 | self.renderer.flush(); |
| 212 | 251 | Ok(()) |
| 213 | 252 | } |
| 214 | 253 | |
| 215 | | - /// Render main content area. |
| 216 | | - fn render_content(&self, bounds: Rect) -> Result<()> { |
| 217 | | - use gartk_render::TextStyle; |
| 218 | | - |
| 254 | + /// Render "not connected" message. |
| 255 | + fn render_not_connected(&self, content_y: i32) -> Result<()> { |
| 219 | 256 | let style = TextStyle { |
| 220 | 257 | font_family: "monospace".to_string(), |
| 221 | | - font_size: 12.0, |
| 222 | | - color: self.theme.text, |
| 258 | + font_size: 13.0, |
| 259 | + color: self.theme.swap_color, |
| 223 | 260 | ..Default::default() |
| 224 | 261 | }; |
| 225 | | - |
| 226 | 262 | let dim_style = TextStyle { |
| 227 | 263 | color: self.theme.text_secondary, |
| 228 | 264 | ..style.clone() |
| 229 | 265 | }; |
| 230 | 266 | |
| 231 | | - let padding = 12; |
| 232 | | - let graph_height = 120u32; |
| 233 | | - let text_line_height = 18.0; |
| 267 | + self.renderer.text("Not connected to daemon", 20.0, content_y as f64 + 40.0, &style)?; |
| 268 | + self.renderer.text("Run: gartop daemon", 20.0, content_y as f64 + 65.0, &dim_style)?; |
| 269 | + Ok(()) |
| 270 | + } |
| 234 | 271 | |
| 235 | | - if !self.daemon_available { |
| 236 | | - self.renderer.text( |
| 237 | | - "Not connected to daemon", |
| 238 | | - padding as f64, |
| 239 | | - bounds.y as f64 + 30.0, |
| 240 | | - &TextStyle { |
| 241 | | - color: self.theme.swap_color, |
| 242 | | - ..style.clone() |
| 243 | | - }, |
| 244 | | - )?; |
| 245 | | - self.renderer.text( |
| 246 | | - "Run: gartop daemon", |
| 247 | | - padding as f64, |
| 248 | | - bounds.y as f64 + 50.0, |
| 249 | | - &dim_style, |
| 250 | | - )?; |
| 251 | | - return Ok(()); |
| 252 | | - } |
| 272 | + /// Render the content for the active tab. |
| 273 | + fn render_tab_content(&self, content_y: i32, _content_height: u32) -> Result<()> { |
| 274 | + let padding = 12; |
| 275 | + let graph_width = self.width - (padding * 2) as u32; |
| 253 | 276 | |
| 254 | 277 | // Get Cairo context for graph rendering |
| 255 | 278 | let ctx = self.renderer.context()?; |
| 256 | | - let graph = LineGraph::default(); |
| 279 | + let graph = LineGraph { |
| 280 | + fill_opacity: 0.25, |
| 281 | + ..LineGraph::default() |
| 282 | + }; |
| 257 | 283 | |
| 258 | | - let mut y = bounds.y + padding as i32; |
| 259 | | - let graph_width = bounds.width - (padding * 2) as u32; |
| 284 | + let mut y = content_y + padding as i32; |
| 285 | + |
| 286 | + match self.tab_bar.active() { |
| 287 | + Tab::Cpu => { |
| 288 | + // CPU label |
| 289 | + let cpu_label = if let Some(cpu) = &self.cpu_stats { |
| 290 | + format!("CPU: {:.1}%", cpu.usage_percent) |
| 291 | + } else { |
| 292 | + "CPU: --".to_string() |
| 293 | + }; |
| 294 | + self.renderer.text( |
| 295 | + &cpu_label, |
| 296 | + padding as f64, |
| 297 | + y as f64 + 14.0, |
| 298 | + &TextStyle { |
| 299 | + font_family: "monospace".to_string(), |
| 300 | + font_size: 12.0, |
| 301 | + color: self.theme.cpu_color, |
| 302 | + ..Default::default() |
| 303 | + }, |
| 304 | + )?; |
| 305 | + |
| 306 | + // Per-core info on right |
| 307 | + if let Some(cpu) = &self.cpu_stats { |
| 308 | + let cores = cpu.per_core.len(); |
| 309 | + let core_info = format!("{} cores", cores); |
| 310 | + self.renderer.text( |
| 311 | + &core_info, |
| 312 | + (self.width - 80) as f64, |
| 313 | + y as f64 + 14.0, |
| 314 | + &TextStyle { |
| 315 | + font_family: "monospace".to_string(), |
| 316 | + font_size: 10.0, |
| 317 | + color: self.theme.text_secondary, |
| 318 | + ..Default::default() |
| 319 | + }, |
| 320 | + )?; |
| 321 | + } |
| 322 | + y += 20; |
| 260 | 323 | |
| 261 | | - // CPU Section |
| 262 | | - let cpu_label = if let Some(cpu) = &self.cpu_stats { |
| 263 | | - format!("CPU: {:.1}%", cpu.usage_percent) |
| 264 | | - } else { |
| 265 | | - "CPU: --".to_string() |
| 266 | | - }; |
| 267 | | - self.renderer.text( |
| 268 | | - &cpu_label, |
| 269 | | - padding as f64, |
| 270 | | - y as f64 + 14.0, |
| 271 | | - &TextStyle { color: self.theme.cpu_color, ..style.clone() }, |
| 272 | | - )?; |
| 273 | | - y += 20; |
| 274 | | - |
| 275 | | - // CPU Graph |
| 276 | | - let cpu_rect = Rect::new(padding as i32, y, graph_width, graph_height); |
| 277 | | - let mut cpu_series = DataSeries::new("CPU", self.theme.cpu_color); |
| 278 | | - cpu_series.set_values(self.cpu_history.iter().map(|s| s.usage_percent).collect()); |
| 279 | | - graph.render(&ctx, cpu_rect, &[cpu_series], &self.theme); |
| 280 | | - y += graph_height as i32 + padding as i32; |
| 281 | | - |
| 282 | | - // Memory Section |
| 283 | | - let mem_label = if let Some(mem) = &self.memory_stats { |
| 284 | | - format!( |
| 285 | | - "Memory: {:.1}% ({} / {})", |
| 286 | | - mem.usage_percent, |
| 287 | | - format_bytes(mem.used), |
| 288 | | - format_bytes(mem.total) |
| 289 | | - ) |
| 290 | | - } else { |
| 291 | | - "Memory: --".to_string() |
| 292 | | - }; |
| 293 | | - self.renderer.text( |
| 294 | | - &mem_label, |
| 295 | | - padding as f64, |
| 296 | | - y as f64 + 14.0, |
| 297 | | - &TextStyle { color: self.theme.memory_color, ..style.clone() }, |
| 298 | | - )?; |
| 299 | | - y += 20; |
| 300 | | - |
| 301 | | - // Memory Graph |
| 302 | | - let mem_rect = Rect::new(padding as i32, y, graph_width, graph_height); |
| 303 | | - let mut mem_series = DataSeries::new("Memory", self.theme.memory_color); |
| 304 | | - let mut swap_series = DataSeries::new("Swap", self.theme.swap_color); |
| 305 | | - mem_series.set_values(self.memory_history.iter().map(|s| s.usage_percent).collect()); |
| 306 | | - swap_series.set_values(self.memory_history.iter().map(|s| { |
| 307 | | - if s.swap_total > 0 { |
| 308 | | - (s.swap_used as f64 / s.swap_total as f64) * 100.0 |
| 309 | | - } else { |
| 310 | | - 0.0 |
| 324 | + // CPU Graph |
| 325 | + let graph_rect = Rect::new(padding as i32, y, graph_width, GRAPH_HEIGHT); |
| 326 | + let mut cpu_series = DataSeries::new("CPU", self.theme.cpu_color); |
| 327 | + cpu_series.set_values(self.cpu_history.iter().map(|s| s.usage_percent).collect()); |
| 328 | + graph.render(&ctx, graph_rect, &[cpu_series], &self.theme); |
| 311 | 329 | } |
| 312 | | - }).collect()); |
| 313 | | - graph.render(&ctx, mem_rect, &[mem_series, swap_series], &self.theme); |
| 314 | | - y += graph_height as i32 + padding as i32; |
| 315 | | - |
| 316 | | - // Additional stats text |
| 317 | | - if let Some(cpu) = &self.cpu_stats { |
| 318 | | - let cores_to_show = cpu.per_core.len().min(8); |
| 319 | | - let mut core_line = String::from("Cores: "); |
| 320 | | - for (i, usage) in cpu.per_core.iter().take(cores_to_show).enumerate() { |
| 321 | | - if i > 0 { |
| 322 | | - core_line.push_str(" | "); |
| 330 | + |
| 331 | + Tab::Memory => { |
| 332 | + // Memory label |
| 333 | + let mem_label = if let Some(mem) = &self.memory_stats { |
| 334 | + format!( |
| 335 | + "Memory: {:.1}% ({} / {})", |
| 336 | + mem.usage_percent, |
| 337 | + format_bytes(mem.used), |
| 338 | + format_bytes(mem.total) |
| 339 | + ) |
| 340 | + } else { |
| 341 | + "Memory: --".to_string() |
| 342 | + }; |
| 343 | + self.renderer.text( |
| 344 | + &mem_label, |
| 345 | + padding as f64, |
| 346 | + y as f64 + 14.0, |
| 347 | + &TextStyle { |
| 348 | + font_family: "monospace".to_string(), |
| 349 | + font_size: 12.0, |
| 350 | + color: self.theme.memory_color, |
| 351 | + ..Default::default() |
| 352 | + }, |
| 353 | + )?; |
| 354 | + |
| 355 | + // Swap info on right |
| 356 | + if let Some(mem) = &self.memory_stats { |
| 357 | + let swap_pct = if mem.swap_total > 0 { |
| 358 | + (mem.swap_used as f64 / mem.swap_total as f64) * 100.0 |
| 359 | + } else { |
| 360 | + 0.0 |
| 361 | + }; |
| 362 | + let swap_info = format!("Swap: {:.1}%", swap_pct); |
| 363 | + self.renderer.text( |
| 364 | + &swap_info, |
| 365 | + (self.width - 80) as f64, |
| 366 | + y as f64 + 14.0, |
| 367 | + &TextStyle { |
| 368 | + font_family: "monospace".to_string(), |
| 369 | + font_size: 10.0, |
| 370 | + color: self.theme.swap_color, |
| 371 | + ..Default::default() |
| 372 | + }, |
| 373 | + )?; |
| 323 | 374 | } |
| 324 | | - core_line.push_str(&format!("{}:{:.0}%", i, usage)); |
| 325 | | - } |
| 326 | | - if cpu.per_core.len() > cores_to_show { |
| 327 | | - core_line.push_str(&format!(" (+{} more)", cpu.per_core.len() - cores_to_show)); |
| 375 | + y += 20; |
| 376 | + |
| 377 | + // Memory Graph |
| 378 | + let graph_rect = Rect::new(padding as i32, y, graph_width, GRAPH_HEIGHT); |
| 379 | + let mut mem_series = DataSeries::new("Memory", self.theme.memory_color); |
| 380 | + let mut swap_series = DataSeries::new("Swap", self.theme.swap_color); |
| 381 | + mem_series.set_values(self.memory_history.iter().map(|s| s.usage_percent).collect()); |
| 382 | + swap_series.set_values(self.memory_history.iter().map(|s| { |
| 383 | + if s.swap_total > 0 { |
| 384 | + (s.swap_used as f64 / s.swap_total as f64) * 100.0 |
| 385 | + } else { |
| 386 | + 0.0 |
| 387 | + } |
| 388 | + }).collect()); |
| 389 | + graph.render(&ctx, graph_rect, &[mem_series, swap_series], &self.theme); |
| 328 | 390 | } |
| 329 | | - self.renderer.text(&core_line, padding as f64, y as f64 + text_line_height, &dim_style)?; |
| 330 | | - y += text_line_height as i32 + 4; |
| 331 | 391 | } |
| 332 | 392 | |
| 333 | | - if let Some(mem) = &self.memory_stats { |
| 334 | | - let swap_percent = if mem.swap_total > 0 { |
| 335 | | - (mem.swap_used as f64 / mem.swap_total as f64) * 100.0 |
| 336 | | - } else { |
| 337 | | - 0.0 |
| 338 | | - }; |
| 339 | | - self.renderer.text( |
| 340 | | - &format!( |
| 341 | | - "Swap: {:.1}% ({} / {}) | Available: {}", |
| 342 | | - swap_percent, |
| 343 | | - format_bytes(mem.swap_used), |
| 344 | | - format_bytes(mem.swap_total), |
| 345 | | - format_bytes(mem.available) |
| 346 | | - ), |
| 347 | | - padding as f64, |
| 348 | | - y as f64 + text_line_height, |
| 349 | | - &dim_style, |
| 350 | | - )?; |
| 351 | | - } |
| 393 | + // Render process list |
| 394 | + self.process_list.render(&self.renderer, &self.theme)?; |
| 352 | 395 | |
| 353 | 396 | Ok(()) |
| 354 | 397 | } |
@@ -390,11 +433,48 @@ impl App { |
| 390 | 433 | self.width = width; |
| 391 | 434 | self.height = height; |
| 392 | 435 | self.renderer.resize(width, height)?; |
| 436 | + |
| 437 | + // Update component bounds |
| 393 | 438 | self.header = HeaderBar::new(Rect::new(0, 0, width, HEADER_HEIGHT)); |
| 439 | + self.tab_bar.set_bounds(Rect::new(0, HEADER_HEIGHT as i32, width, TAB_BAR_HEIGHT)); |
| 440 | + self.process_list = Self::create_process_list(width, height); |
| 441 | + self.process_list.set_processes(self.processes.clone()); |
| 394 | 442 | |
| 395 | 443 | Ok(()) |
| 396 | 444 | } |
| 397 | 445 | |
| 446 | + /// Handle mouse click. |
| 447 | + fn handle_click(&mut self, pos: Point) -> bool { |
| 448 | + // Check tab bar |
| 449 | + if let Some(tab) = self.tab_bar.on_click(pos) { |
| 450 | + if tab != self.tab_bar.active() { |
| 451 | + self.tab_bar.set_active(tab); |
| 452 | + // Re-sort processes for new tab |
| 453 | + let sort_field = match tab { |
| 454 | + Tab::Cpu => SortField::Cpu, |
| 455 | + Tab::Memory => SortField::Memory, |
| 456 | + }; |
| 457 | + self.process_list.set_sort(sort_field); |
| 458 | + // Force refresh to get re-sorted processes |
| 459 | + self.last_refresh = Instant::now() - std::time::Duration::from_secs(10); |
| 460 | + return true; |
| 461 | + } |
| 462 | + } |
| 463 | + |
| 464 | + // Check process list |
| 465 | + if self.process_list.on_click(pos).is_some() { |
| 466 | + return true; |
| 467 | + } |
| 468 | + |
| 469 | + false |
| 470 | + } |
| 471 | + |
| 472 | + /// Handle scroll. |
| 473 | + fn handle_scroll(&mut self, delta: i32) -> bool { |
| 474 | + self.process_list.on_scroll(delta); |
| 475 | + true |
| 476 | + } |
| 477 | + |
| 398 | 478 | /// Run the GUI event loop. |
| 399 | 479 | pub fn run(mut self) -> Result<()> { |
| 400 | 480 | let config = EventLoopConfig { |
@@ -416,15 +496,61 @@ impl App { |
| 416 | 496 | ev_loop.request_redraw(); |
| 417 | 497 | } |
| 418 | 498 | |
| 499 | + InputEvent::MousePress(mouse_event) => { |
| 500 | + let pos = Point::new(mouse_event.position.x, mouse_event.position.y); |
| 501 | + if self.handle_click(pos) { |
| 502 | + ev_loop.request_redraw(); |
| 503 | + } |
| 504 | + } |
| 505 | + |
| 506 | + InputEvent::MouseMove(mouse_event) => { |
| 507 | + let pos = Point::new(mouse_event.position.x, mouse_event.position.y); |
| 508 | + if self.tab_bar.on_mouse_move(pos) { |
| 509 | + ev_loop.request_redraw(); |
| 510 | + } |
| 511 | + } |
| 512 | + |
| 513 | + InputEvent::Scroll(scroll_event) => { |
| 514 | + let delta = if scroll_event.delta_y > 0 { -1 } else { 1 }; |
| 515 | + if self.handle_scroll(delta) { |
| 516 | + ev_loop.request_redraw(); |
| 517 | + } |
| 518 | + } |
| 519 | + |
| 419 | 520 | InputEvent::Key(key_event) if key_event.pressed => { |
| 420 | 521 | match key_event.key { |
| 421 | 522 | Key::Escape | Key::Char('q') => { |
| 422 | 523 | self.should_quit = true; |
| 423 | 524 | } |
| 424 | 525 | Key::Char('r') => { |
| 425 | | - // Force refresh |
| 426 | 526 | self.last_refresh = Instant::now() - std::time::Duration::from_secs(10); |
| 427 | 527 | } |
| 528 | + Key::Char('1') => { |
| 529 | + self.tab_bar.set_active(Tab::Cpu); |
| 530 | + self.process_list.set_sort(SortField::Cpu); |
| 531 | + self.last_refresh = Instant::now() - std::time::Duration::from_secs(10); |
| 532 | + ev_loop.request_redraw(); |
| 533 | + } |
| 534 | + Key::Char('2') => { |
| 535 | + self.tab_bar.set_active(Tab::Memory); |
| 536 | + self.process_list.set_sort(SortField::Memory); |
| 537 | + self.last_refresh = Instant::now() - std::time::Duration::from_secs(10); |
| 538 | + ev_loop.request_redraw(); |
| 539 | + } |
| 540 | + Key::Tab => { |
| 541 | + let new_tab = match self.tab_bar.active() { |
| 542 | + Tab::Cpu => Tab::Memory, |
| 543 | + Tab::Memory => Tab::Cpu, |
| 544 | + }; |
| 545 | + self.tab_bar.set_active(new_tab); |
| 546 | + let sort_field = match new_tab { |
| 547 | + Tab::Cpu => SortField::Cpu, |
| 548 | + Tab::Memory => SortField::Memory, |
| 549 | + }; |
| 550 | + self.process_list.set_sort(sort_field); |
| 551 | + self.last_refresh = Instant::now() - std::time::Duration::from_secs(10); |
| 552 | + ev_loop.request_redraw(); |
| 553 | + } |
| 428 | 554 | _ => {} |
| 429 | 555 | } |
| 430 | 556 | } |
@@ -434,14 +560,12 @@ impl App { |
| 434 | 560 | } |
| 435 | 561 | |
| 436 | 562 | InputEvent::Idle => { |
| 437 | | - // Periodic refresh |
| 438 | 563 | if self.last_refresh.elapsed().as_secs_f64() >= REFRESH_INTERVAL { |
| 439 | 564 | if self.daemon_available { |
| 440 | 565 | self.refresh_data(); |
| 441 | 566 | self.update_header(); |
| 442 | 567 | ev_loop.request_redraw(); |
| 443 | 568 | } else { |
| 444 | | - // Try reconnecting |
| 445 | 569 | self.daemon_available = Self::check_daemon(); |
| 446 | 570 | if self.daemon_available { |
| 447 | 571 | ev_loop.request_redraw(); |
@@ -454,7 +578,6 @@ impl App { |
| 454 | 578 | _ => {} |
| 455 | 579 | } |
| 456 | 580 | |
| 457 | | - // Render if needed |
| 458 | 581 | if ev_loop.needs_redraw() { |
| 459 | 582 | if let Err(e) = self.render() { |
| 460 | 583 | tracing::error!("Render error: {}", e); |