@@ -46,6 +46,12 @@ pub struct App { |
| 46 | 46 | last_refresh: Instant, |
| 47 | 47 | refresh_interval: f64, |
| 48 | 48 | show_legend: bool, |
| 49 | + /// Freeze mode - pause process list updates for navigation |
| 50 | + frozen: bool, |
| 51 | + /// Search mode - filter processes by name |
| 52 | + search_mode: bool, |
| 53 | + /// Search query string |
| 54 | + search_query: String, |
| 49 | 55 | status: Option<StatusInfo>, |
| 50 | 56 | cpu_stats: Option<CpuStats>, |
| 51 | 57 | memory_stats: Option<MemoryStats>, |
@@ -118,6 +124,9 @@ impl App { |
| 118 | 124 | last_refresh: Instant::now() - std::time::Duration::from_secs(10), |
| 119 | 125 | refresh_interval, |
| 120 | 126 | show_legend, |
| 127 | + frozen: false, |
| 128 | + search_mode: false, |
| 129 | + search_query: String::new(), |
| 121 | 130 | status: None, |
| 122 | 131 | cpu_stats: None, |
| 123 | 132 | memory_stats: None, |
@@ -241,21 +250,24 @@ impl App { |
| 241 | 250 | } |
| 242 | 251 | } |
| 243 | 252 | |
| 244 | | - // Get processes sorted by current tab's resource |
| 245 | | - let sort_field = match self.tab_bar.active() { |
| 246 | | - Tab::Cpu => SortField::Cpu, |
| 247 | | - Tab::Memory => SortField::Memory, |
| 248 | | - Tab::Network => SortField::NetConnections, |
| 249 | | - Tab::Disk => SortField::DiskTotal, |
| 250 | | - }; |
| 251 | | - if let Some(resp) = self.send_command(&Command::GetProcesses { |
| 252 | | - sort_by: Some(sort_field), |
| 253 | | - limit: Some(100), |
| 254 | | - }) { |
| 255 | | - if resp.success { |
| 256 | | - self.processes = resp.data.and_then(|d| serde_json::from_value(d).ok()).unwrap_or_default(); |
| 257 | | - self.process_list.set_process_count(self.processes.len()); |
| 258 | | - self.process_list.set_sort(sort_field); |
| 253 | + // Get processes sorted by current tab's resource (skip when frozen) |
| 254 | + if !self.frozen { |
| 255 | + let sort_field = match self.tab_bar.active() { |
| 256 | + Tab::Cpu => SortField::Cpu, |
| 257 | + Tab::Memory => SortField::Memory, |
| 258 | + Tab::Network => SortField::NetConnections, |
| 259 | + Tab::Disk => SortField::DiskTotal, |
| 260 | + }; |
| 261 | + if let Some(resp) = self.send_command(&Command::GetProcesses { |
| 262 | + sort_by: Some(sort_field), |
| 263 | + limit: Some(100), |
| 264 | + }) { |
| 265 | + if resp.success { |
| 266 | + self.processes = resp.data.and_then(|d| serde_json::from_value(d).ok()).unwrap_or_default(); |
| 267 | + // Sync selection - tracks process by PID across list reorders |
| 268 | + self.process_list.sync_selection(&self.processes); |
| 269 | + self.process_list.set_sort(sort_field); |
| 270 | + } |
| 259 | 271 | } |
| 260 | 272 | } |
| 261 | 273 | |
@@ -293,6 +305,17 @@ impl App { |
| 293 | 305 | // Render header |
| 294 | 306 | self.header.render(&self.renderer, &self.theme)?; |
| 295 | 307 | |
| 308 | + // Freeze indicator |
| 309 | + if self.frozen { |
| 310 | + let freeze_style = TextStyle { |
| 311 | + font_family: "monospace".to_string(), |
| 312 | + font_size: 10.0, |
| 313 | + color: self.theme.swap_color, |
| 314 | + ..Default::default() |
| 315 | + }; |
| 316 | + self.renderer.text("PAUSED", 80.0, (HEADER_HEIGHT as f64 * 0.4) + 6.0, &freeze_style)?; |
| 317 | + } |
| 318 | + |
| 296 | 319 | // Render tab bar |
| 297 | 320 | self.tab_bar.render(&self.renderer, &self.theme)?; |
| 298 | 321 | |
@@ -583,8 +606,34 @@ impl App { |
| 583 | 606 | } |
| 584 | 607 | } |
| 585 | 608 | |
| 586 | | - // Render process list (pass reference, no clone) |
| 587 | | - self.process_list.render(&self.renderer, &self.theme, &self.processes)?; |
| 609 | + // Search bar (above process list when active) |
| 610 | + if self.search_mode || !self.search_query.is_empty() { |
| 611 | + let search_y = (HEADER_HEIGHT + TAB_BAR_HEIGHT + SECTION_GAP + 20 + GRAPH_HEIGHT + SECTION_GAP) as i32 - 20; |
| 612 | + let search_style = TextStyle { |
| 613 | + font_family: "monospace".to_string(), |
| 614 | + font_size: 11.0, |
| 615 | + color: if self.search_mode { self.theme.text } else { self.theme.text_secondary }, |
| 616 | + ..Default::default() |
| 617 | + }; |
| 618 | + let search_text = format!("/{}{}", self.search_query, if self.search_mode { "_" } else { "" }); |
| 619 | + self.renderer.text(&search_text, CONTENT_PADDING as f64, search_y as f64, &search_style)?; |
| 620 | + } |
| 621 | + |
| 622 | + // Filter processes if search query is active |
| 623 | + let display_processes: Vec<ProcessInfo> = if self.search_query.is_empty() { |
| 624 | + self.processes.clone() |
| 625 | + } else { |
| 626 | + let query = self.search_query.to_lowercase(); |
| 627 | + self.processes |
| 628 | + .iter() |
| 629 | + .filter(|p| p.name.to_lowercase().contains(&query) || |
| 630 | + p.cmdline.to_lowercase().contains(&query)) |
| 631 | + .cloned() |
| 632 | + .collect() |
| 633 | + }; |
| 634 | + |
| 635 | + // Render process list with filtered processes |
| 636 | + self.process_list.render(&self.renderer, &self.theme, &display_processes)?; |
| 588 | 637 | |
| 589 | 638 | Ok(()) |
| 590 | 639 | } |
@@ -705,6 +754,24 @@ impl App { |
| 705 | 754 | true |
| 706 | 755 | } |
| 707 | 756 | |
| 757 | + /// Kill a process by PID. |
| 758 | + fn kill_process(&mut self, pid: i32, signal: i32) { |
| 759 | + tracing::info!("Sending signal {} to process {}", signal, pid); |
| 760 | + if let Some(resp) = self.send_command(&Command::KillProcess { |
| 761 | + pid, |
| 762 | + signal: Some(signal), |
| 763 | + }) { |
| 764 | + if resp.success { |
| 765 | + tracing::info!("Process {} killed", pid); |
| 766 | + // Clear selection and trigger refresh |
| 767 | + self.process_list.clear_selection(); |
| 768 | + self.last_refresh = Instant::now() - std::time::Duration::from_secs(10); |
| 769 | + } else { |
| 770 | + tracing::warn!("Failed to kill process {}: {:?}", pid, resp.error); |
| 771 | + } |
| 772 | + } |
| 773 | + } |
| 774 | + |
| 708 | 775 | /// Run the GUI event loop. |
| 709 | 776 | pub fn run(mut self) -> Result<()> { |
| 710 | 777 | let config = EventLoopConfig { |
@@ -748,51 +815,143 @@ impl App { |
| 748 | 815 | } |
| 749 | 816 | |
| 750 | 817 | InputEvent::Key(key_event) if key_event.pressed => { |
| 751 | | - match key_event.key { |
| 752 | | - Key::Escape | Key::Char('q') => { |
| 753 | | - self.should_quit = true; |
| 754 | | - } |
| 755 | | - Key::Char('r') => { |
| 756 | | - self.last_refresh = Instant::now() - std::time::Duration::from_secs(10); |
| 757 | | - } |
| 758 | | - Key::Char('1') => { |
| 759 | | - self.tab_bar.set_active(Tab::Cpu); |
| 760 | | - self.sort_processes(SortField::Cpu); |
| 761 | | - ev_loop.request_redraw(); |
| 762 | | - } |
| 763 | | - Key::Char('2') => { |
| 764 | | - self.tab_bar.set_active(Tab::Memory); |
| 765 | | - self.sort_processes(SortField::Memory); |
| 766 | | - ev_loop.request_redraw(); |
| 767 | | - } |
| 768 | | - Key::Char('3') => { |
| 769 | | - self.tab_bar.set_active(Tab::Network); |
| 770 | | - self.sort_processes(SortField::NetConnections); |
| 771 | | - ev_loop.request_redraw(); |
| 772 | | - } |
| 773 | | - Key::Char('4') => { |
| 774 | | - self.tab_bar.set_active(Tab::Disk); |
| 775 | | - self.sort_processes(SortField::DiskTotal); |
| 776 | | - ev_loop.request_redraw(); |
| 818 | + // Search mode input handling |
| 819 | + if self.search_mode { |
| 820 | + match key_event.key { |
| 821 | + Key::Escape => { |
| 822 | + if self.search_query.is_empty() { |
| 823 | + self.search_mode = false; |
| 824 | + } else { |
| 825 | + self.search_query.clear(); |
| 826 | + } |
| 827 | + ev_loop.request_redraw(); |
| 828 | + } |
| 829 | + Key::Return => { |
| 830 | + self.search_mode = false; |
| 831 | + ev_loop.request_redraw(); |
| 832 | + } |
| 833 | + Key::Backspace => { |
| 834 | + self.search_query.pop(); |
| 835 | + ev_loop.request_redraw(); |
| 836 | + } |
| 837 | + Key::Char(c) => { |
| 838 | + if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' { |
| 839 | + self.search_query.push(c); |
| 840 | + ev_loop.request_redraw(); |
| 841 | + } |
| 842 | + } |
| 843 | + _ => {} |
| 777 | 844 | } |
| 778 | | - Key::Tab => { |
| 779 | | - let new_tab = match self.tab_bar.active() { |
| 780 | | - Tab::Cpu => Tab::Memory, |
| 781 | | - Tab::Memory => Tab::Network, |
| 782 | | - Tab::Network => Tab::Disk, |
| 783 | | - Tab::Disk => Tab::Cpu, |
| 784 | | - }; |
| 785 | | - self.tab_bar.set_active(new_tab); |
| 786 | | - let sort_field = match new_tab { |
| 787 | | - Tab::Cpu => SortField::Cpu, |
| 788 | | - Tab::Memory => SortField::Memory, |
| 789 | | - Tab::Network => SortField::NetConnections, |
| 790 | | - Tab::Disk => SortField::DiskTotal, |
| 791 | | - }; |
| 792 | | - self.sort_processes(sort_field); |
| 793 | | - ev_loop.request_redraw(); |
| 845 | + } else { |
| 846 | + // Normal mode key handling |
| 847 | + match key_event.key { |
| 848 | + Key::Escape => { |
| 849 | + if !self.search_query.is_empty() { |
| 850 | + // Clear search filter first |
| 851 | + self.search_query.clear(); |
| 852 | + ev_loop.request_redraw(); |
| 853 | + } else { |
| 854 | + self.should_quit = true; |
| 855 | + } |
| 856 | + } |
| 857 | + Key::Char('q') => { |
| 858 | + self.should_quit = true; |
| 859 | + } |
| 860 | + Key::Char('/') => { |
| 861 | + self.search_mode = true; |
| 862 | + ev_loop.request_redraw(); |
| 863 | + } |
| 864 | + Key::Char('r') => { |
| 865 | + self.last_refresh = Instant::now() - std::time::Duration::from_secs(10); |
| 866 | + } |
| 867 | + Key::Char('1') => { |
| 868 | + self.tab_bar.set_active(Tab::Cpu); |
| 869 | + self.sort_processes(SortField::Cpu); |
| 870 | + ev_loop.request_redraw(); |
| 871 | + } |
| 872 | + Key::Char('2') => { |
| 873 | + self.tab_bar.set_active(Tab::Memory); |
| 874 | + self.sort_processes(SortField::Memory); |
| 875 | + ev_loop.request_redraw(); |
| 876 | + } |
| 877 | + Key::Char('3') => { |
| 878 | + self.tab_bar.set_active(Tab::Network); |
| 879 | + self.sort_processes(SortField::NetConnections); |
| 880 | + ev_loop.request_redraw(); |
| 881 | + } |
| 882 | + Key::Char('4') => { |
| 883 | + self.tab_bar.set_active(Tab::Disk); |
| 884 | + self.sort_processes(SortField::DiskTotal); |
| 885 | + ev_loop.request_redraw(); |
| 886 | + } |
| 887 | + Key::Tab => { |
| 888 | + let new_tab = match self.tab_bar.active() { |
| 889 | + Tab::Cpu => Tab::Memory, |
| 890 | + Tab::Memory => Tab::Network, |
| 891 | + Tab::Network => Tab::Disk, |
| 892 | + Tab::Disk => Tab::Cpu, |
| 893 | + }; |
| 894 | + self.tab_bar.set_active(new_tab); |
| 895 | + let sort_field = match new_tab { |
| 896 | + Tab::Cpu => SortField::Cpu, |
| 897 | + Tab::Memory => SortField::Memory, |
| 898 | + Tab::Network => SortField::NetConnections, |
| 899 | + Tab::Disk => SortField::DiskTotal, |
| 900 | + }; |
| 901 | + self.sort_processes(sort_field); |
| 902 | + ev_loop.request_redraw(); |
| 903 | + } |
| 904 | + // Freeze mode toggle (pause process list updates) |
| 905 | + Key::Char('f') => { |
| 906 | + self.frozen = !self.frozen; |
| 907 | + ev_loop.request_redraw(); |
| 908 | + } |
| 909 | + // Process list navigation |
| 910 | + Key::Down | Key::Char('j') => { |
| 911 | + self.process_list.select_next(&self.processes); |
| 912 | + ev_loop.request_redraw(); |
| 913 | + } |
| 914 | + Key::Up | Key::Char('k') => { |
| 915 | + self.process_list.select_prev(&self.processes); |
| 916 | + ev_loop.request_redraw(); |
| 917 | + } |
| 918 | + Key::Home => { |
| 919 | + self.process_list.select_first(&self.processes); |
| 920 | + ev_loop.request_redraw(); |
| 921 | + } |
| 922 | + Key::End => { |
| 923 | + self.process_list.select_last(&self.processes); |
| 924 | + ev_loop.request_redraw(); |
| 925 | + } |
| 926 | + Key::PageDown => { |
| 927 | + // Move selection down by visible rows |
| 928 | + for _ in 0..10 { |
| 929 | + self.process_list.select_next(&self.processes); |
| 930 | + } |
| 931 | + ev_loop.request_redraw(); |
| 932 | + } |
| 933 | + Key::PageUp => { |
| 934 | + // Move selection up by visible rows |
| 935 | + for _ in 0..10 { |
| 936 | + self.process_list.select_prev(&self.processes); |
| 937 | + } |
| 938 | + ev_loop.request_redraw(); |
| 939 | + } |
| 940 | + // Kill selected process (K = SIGTERM, X = SIGKILL) |
| 941 | + Key::Char('K') => { |
| 942 | + if let Some(pid) = self.process_list.selected_pid() { |
| 943 | + self.kill_process(pid, 15); // SIGTERM |
| 944 | + ev_loop.request_redraw(); |
| 945 | + } |
| 946 | + } |
| 947 | + Key::Char('X') => { |
| 948 | + if let Some(pid) = self.process_list.selected_pid() { |
| 949 | + self.kill_process(pid, 9); // SIGKILL |
| 950 | + ev_loop.request_redraw(); |
| 951 | + } |
| 952 | + } |
| 953 | + _ => {} |
| 794 | 954 | } |
| 795 | | - _ => {} |
| 796 | 955 | } |
| 797 | 956 | } |
| 798 | 957 | |