1use crossterm::event::{KeyCode, KeyModifiers};
4use std::time::{Duration, Instant};
5
6use trueno_viz::monitor::collectors::{
7 BatteryCollector, CpuCollector, DiskCollector, MemoryCollector, NetworkCollector,
8 ProcessCollector, SensorCollector,
9};
10use trueno_viz::monitor::types::Collector;
11
12#[cfg(feature = "nvidia")]
13use trueno_viz::monitor::collectors::NvidiaGpuCollector;
14
15#[cfg(target_os = "linux")]
16use trueno_viz::monitor::collectors::AmdGpuCollector;
17
18#[cfg(target_os = "macos")]
19use trueno_viz::monitor::collectors::AppleGpuCollector;
20
21use crate::analyzers::{ContainerAnalyzer, DiskEntropyAnalyzer, DiskIoAnalyzer, GpuProcessAnalyzer, NetworkStatsAnalyzer, PsiAnalyzer, SensorHealthAnalyzer, StorageAnalyzer, SwapAnalyzer, ThrashingSeverity};
22use crate::state::{PanelType, ProcessSortColumn, SignalType};
23
24#[inline]
29fn contains_ignore_case(haystack: &str, needle_lower: &str) -> bool {
30 if needle_lower.is_empty() {
31 return true;
32 }
33 if haystack.len() < needle_lower.len() {
34 return false;
35 }
36
37 let needle_bytes = needle_lower.as_bytes();
39 let haystack_bytes = haystack.as_bytes();
40
41 'outer: for i in 0..=(haystack_bytes.len() - needle_bytes.len()) {
42 for (j, &nb) in needle_bytes.iter().enumerate() {
43 let hb = haystack_bytes[i + j];
44 let hb_lower = if hb.is_ascii_uppercase() { hb + 32 } else { hb };
46 if hb_lower != nb {
47 continue 'outer;
48 }
49 }
50 return true;
51 }
52 false
53}
54
55#[derive(Debug, Clone)]
57pub struct MockGpuData {
58 pub name: String,
59 pub gpu_util: f64,
60 pub vram_used: u64,
61 pub vram_total: u64,
62 pub temperature: f64,
63 pub power_watts: u32,
64 pub power_limit_watts: u32,
65 pub clock_mhz: u32,
66 pub history: Vec<f64>,
67}
68
69#[derive(Debug, Clone)]
71pub struct MockBatteryData {
72 pub percent: f64,
73 pub charging: bool,
74 pub time_remaining_mins: Option<u32>,
75 pub power_watts: f64,
76 pub health_percent: f64,
77 pub cycle_count: u32,
78}
79
80#[derive(Debug, Clone)]
82pub struct MockSensorData {
83 pub name: String,
84 pub label: String,
85 pub value: f64,
86 pub max: Option<f64>,
87 pub crit: Option<f64>,
88 pub sensor_type: MockSensorType,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq)]
92pub enum MockSensorType {
93 Temperature,
94 Fan,
95 Voltage,
96 Power,
97}
98
99#[derive(Debug, Clone)]
101pub struct MockContainerData {
102 pub name: String,
103 pub status: String,
104 pub cpu_percent: f64,
105 pub mem_used: u64,
106 pub mem_limit: u64,
107}
108
109#[derive(Debug, Clone, Copy)]
111pub struct PanelVisibility {
112 pub cpu: bool,
113 pub memory: bool,
114 pub disk: bool,
115 pub network: bool,
116 pub process: bool,
117 pub gpu: bool,
118 pub battery: bool,
119 pub sensors: bool,
120 pub files: bool,
121}
122
123impl Default for PanelVisibility {
124 fn default() -> Self {
125 Self {
126 cpu: true,
127 memory: true,
128 disk: true,
129 network: true,
130 process: true,
131 gpu: true,
132 battery: true,
133 sensors: true,
134 files: false, }
136 }
137}
138
139#[derive(Debug, Clone, Default)]
141pub struct CpuCoreState {
142 pub user: f64,
143 pub system: f64,
144 pub iowait: f64,
145 pub idle: f64,
146}
147
148impl CpuCoreState {
149 pub fn total_busy(&self) -> f64 {
151 self.user + self.system + self.iowait
152 }
153}
154
155#[derive(Debug, Clone, Default)]
157pub struct TopProcessForCore {
158 pub pid: u32,
159 pub name: String,
160 pub cpu_percent: f64,
161}
162
163#[derive(Debug, Clone, Default)]
165pub struct MemoryBreakdown {
166 pub used_bytes: u64,
167 pub cached_bytes: u64,
168 pub buffers_bytes: u64,
169 pub free_bytes: u64,
170}
171
172#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
174pub enum SwapTrend {
175 Rising,
176 #[default]
177 Stable,
178 Falling,
179}
180
181impl SwapTrend {
182 pub fn symbol(&self) -> &'static str {
184 match self {
185 SwapTrend::Rising => "↑",
186 SwapTrend::Stable => "→",
187 SwapTrend::Falling => "↓",
188 }
189 }
190}
191
192#[derive(Debug, Clone, Default)]
194pub struct TopMemConsumer {
195 pub pid: u32,
196 pub name: String,
197 pub mem_bytes: u64,
198 pub mem_percent: f64,
199}
200
201#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
203pub enum DiskHealth {
204 #[default]
205 Good,
206 Warning,
207 Critical,
208 Unknown,
209}
210
211impl DiskHealth {
212 pub fn symbol(&self) -> &'static str {
214 match self {
215 DiskHealth::Good => "✓",
216 DiskHealth::Warning => "⚠",
217 DiskHealth::Critical => "✗",
218 DiskHealth::Unknown => "?",
219 }
220 }
221}
222
223#[derive(Debug, Clone, Default)]
225pub struct DiskHealthStatus {
226 pub device: String,
227 pub status: DiskHealth,
228 pub temperature: Option<f64>,
229 pub reallocated_sectors: u64,
230}
231
232pub struct App {
234 pub cpu: CpuCollector,
236 pub memory: MemoryCollector,
237 pub disk: DiskCollector,
238 pub network: NetworkCollector,
239 pub process: ProcessCollector,
240 pub sensors: SensorCollector,
241 pub battery: BatteryCollector,
242
243 #[cfg(feature = "nvidia")]
244 pub nvidia_gpu: NvidiaGpuCollector,
245
246 #[cfg(target_os = "linux")]
247 pub amd_gpu: AmdGpuCollector,
248
249 #[cfg(target_os = "macos")]
250 pub apple_gpu: AppleGpuCollector,
251
252 pub swap_analyzer: SwapAnalyzer,
254 pub disk_io_analyzer: DiskIoAnalyzer,
255 pub storage_analyzer: StorageAnalyzer,
256 pub connection_analyzer: crate::analyzers::ConnectionAnalyzer,
257 pub treemap_analyzer: crate::analyzers::TreemapAnalyzer,
258 pub gpu_process_analyzer: GpuProcessAnalyzer,
259 pub psi_analyzer: PsiAnalyzer,
260 pub container_analyzer: ContainerAnalyzer,
261 pub network_stats: NetworkStatsAnalyzer,
262 pub disk_entropy: DiskEntropyAnalyzer,
263 pub process_extra: crate::analyzers::ProcessExtraAnalyzer,
264 pub file_analyzer: crate::analyzers::FileAnalyzer,
265 pub sensor_health: SensorHealthAnalyzer,
266
267 pub cpu_history: Vec<f64>,
269 pub mem_history: Vec<f64>,
270 pub mem_available_history: Vec<f64>,
271 pub mem_cached_history: Vec<f64>,
272 pub mem_free_history: Vec<f64>,
273 pub swap_history: Vec<f64>,
274 pub net_rx_history: Vec<f64>,
275 pub net_tx_history: Vec<f64>,
276 pub per_core_percent: Vec<f64>,
277
278 pub per_core_history: Vec<Vec<f64>>,
281 pub per_core_state: Vec<CpuCoreState>,
283 pub freq_history: Vec<f64>,
285 pub top_process_per_core: Vec<TopProcessForCore>,
287 pub thermal_throttle_active: Option<bool>,
289
290 pub mem_pressure_history: Vec<f64>,
293 pub mem_reclaim_rate: f64,
295 pub top_mem_consumers: Vec<TopMemConsumer>,
297 pub swap_trend: SwapTrend,
299
300 pub disk_latency_history: Vec<f64>,
303 pub disk_read_iops: f64,
305 pub disk_write_iops: f64,
307 pub disk_queue_depth: f64,
309 pub disk_health: Vec<DiskHealthStatus>,
311
312 pub mem_total: u64,
314 pub mem_used: u64,
315 pub mem_available: u64,
316 pub mem_cached: u64,
317 pub mem_free: u64,
318 pub swap_total: u64,
319 pub swap_used: u64,
320
321 pub net_rx_total: u64,
323 pub net_tx_total: u64,
324 pub net_interface_ip: String,
325
326 pub net_rx_peak: f64,
328 pub net_tx_peak: f64,
329 pub net_rx_peak_time: Instant,
330 pub net_tx_peak_time: Instant,
331
332 pub net_errors: u64,
335 pub net_drops: u64,
337 pub net_established: u64,
339 pub net_listening: u64,
341
342 pub panels: PanelVisibility,
344 pub process_selected: usize,
345 pub process_scroll_offset: usize,
346 pub sort_column: ProcessSortColumn,
347 pub sort_descending: bool,
348 pub filter: String,
349 pub show_filter_input: bool,
350 pub show_help: bool,
351 pub show_tree: bool,
352
353 pub show_signal_menu: bool,
355 pub pending_signal: Option<(u32, String, SignalType)>, pub signal_result: Option<(bool, String, Instant)>, pub focused_panel: Option<PanelType>,
360 pub exploded_panel: Option<PanelType>,
361
362 pub files_view_mode: crate::state::FilesViewMode,
364
365 pub frame_id: u64,
367 pub last_collect: Instant,
368 pub avg_frame_time_us: u64,
369 pub max_frame_time_us: u64,
370 pub show_fps: bool,
371
372 pub deterministic: bool,
374
375 pub mock_gpus: Vec<MockGpuData>,
377 pub mock_battery: Option<MockBatteryData>,
378 pub mock_sensors: Vec<MockSensorData>,
379 pub mock_containers: Vec<MockContainerData>,
380}
381
382impl App {
383 pub fn thrashing_severity(&self) -> ThrashingSeverity {
385 self.swap_analyzer.detect_thrashing()
386 }
387
388 pub fn has_zram(&self) -> bool {
390 self.swap_analyzer.has_zram()
391 }
392
393 pub fn zram_ratio(&self) -> f64 {
395 self.swap_analyzer.zram_compression_ratio()
396 }
397}
398
399impl App {
400 pub fn new(deterministic: bool, show_fps: bool) -> Self {
402 use trueno_viz::monitor::debug::{self, Level, TimingGuard};
403
404 debug::log(Level::Debug, "app", "Initializing CPU collector...");
405 let _t = TimingGuard::new("app", "CpuCollector::new");
406 let cpu = CpuCollector::new();
407 drop(_t);
408 debug::log(Level::Info, "app", &format!("CPU: {} cores", cpu.core_count()));
409
410 debug::log(Level::Debug, "app", "Initializing Memory collector...");
411 let memory = MemoryCollector::new();
412
413 debug::log(Level::Debug, "app", "Initializing Disk collector...");
414 let disk = DiskCollector::new();
415
416 debug::log(Level::Debug, "app", "Initializing Network collector...");
417 let network = NetworkCollector::new();
418
419 debug::log(Level::Debug, "app", "Initializing Process collector...");
420 let process = ProcessCollector::new();
421
422 debug::log(Level::Debug, "app", "Initializing Sensors collector...");
423 let sensors = SensorCollector::new();
424
425 debug::log(Level::Debug, "app", "Initializing Battery collector...");
426 let battery = BatteryCollector::new();
427
428 #[cfg(feature = "nvidia")]
429 let nvidia_gpu = {
430 debug::log(Level::Debug, "app", "Initializing NVIDIA GPU collector...");
431 NvidiaGpuCollector::new()
432 };
433
434 #[cfg(target_os = "linux")]
435 let amd_gpu = {
436 debug::log(Level::Debug, "app", "Initializing AMD GPU collector...");
437 AmdGpuCollector::new()
438 };
439
440 #[cfg(target_os = "macos")]
441 let apple_gpu = {
442 debug::log(Level::Debug, "app", "Initializing Apple GPU collector...");
443 let _t = TimingGuard::new("app", "AppleGpuCollector::new");
444 let g = AppleGpuCollector::new();
445 drop(_t);
446 debug::log(Level::Info, "app", &format!("Apple GPU: {} devices", g.gpus().len()));
447 g
448 };
449
450 debug::log(Level::Debug, "app", "All collectors initialized");
451
452 let mut app = Self {
453 cpu,
454 memory,
455 disk,
456 network,
457 process,
458 sensors,
459 battery,
460
461 #[cfg(feature = "nvidia")]
462 nvidia_gpu,
463
464 #[cfg(target_os = "linux")]
465 amd_gpu,
466
467 #[cfg(target_os = "macos")]
468 apple_gpu,
469
470 swap_analyzer: SwapAnalyzer::default(),
472 disk_io_analyzer: DiskIoAnalyzer::default(),
473 storage_analyzer: StorageAnalyzer::default(),
474 connection_analyzer: crate::analyzers::ConnectionAnalyzer::default(),
475 treemap_analyzer: crate::analyzers::TreemapAnalyzer::new("/"),
476 gpu_process_analyzer: GpuProcessAnalyzer::default(),
477 psi_analyzer: PsiAnalyzer::default(),
478 container_analyzer: ContainerAnalyzer::default(),
479 network_stats: NetworkStatsAnalyzer::default(),
480 disk_entropy: DiskEntropyAnalyzer::new(),
481 process_extra: crate::analyzers::ProcessExtraAnalyzer::new(),
482 file_analyzer: crate::analyzers::FileAnalyzer::new(),
483 sensor_health: SensorHealthAnalyzer::default(),
484
485 cpu_history: Vec::with_capacity(300),
486 mem_history: Vec::with_capacity(300),
487 mem_available_history: Vec::with_capacity(300),
488 mem_cached_history: Vec::with_capacity(300),
489 mem_free_history: Vec::with_capacity(300),
490 swap_history: Vec::with_capacity(300),
491 net_rx_history: Vec::with_capacity(300),
492 net_tx_history: Vec::with_capacity(300),
493 per_core_percent: Vec::new(),
494
495 per_core_history: Vec::new(),
497 per_core_state: Vec::new(),
498 freq_history: Vec::with_capacity(60),
499 top_process_per_core: Vec::new(),
500 thermal_throttle_active: None,
501
502 mem_pressure_history: Vec::with_capacity(60),
504 mem_reclaim_rate: 0.0,
505 top_mem_consumers: Vec::new(),
506 swap_trend: SwapTrend::Stable,
507
508 disk_latency_history: Vec::with_capacity(60),
510 disk_read_iops: 0.0,
511 disk_write_iops: 0.0,
512 disk_queue_depth: 0.0,
513 disk_health: Vec::new(),
514
515 mem_total: 0,
516 mem_used: 0,
517 mem_available: 0,
518 mem_cached: 0,
519 mem_free: 0,
520 swap_total: 0,
521 swap_used: 0,
522
523 net_rx_total: 0,
524 net_tx_total: 0,
525 net_interface_ip: String::new(),
526
527 net_rx_peak: 0.0,
528 net_tx_peak: 0.0,
529 net_rx_peak_time: Instant::now(),
530 net_tx_peak_time: Instant::now(),
531
532 net_errors: 0,
534 net_drops: 0,
535 net_established: 0,
536 net_listening: 0,
537
538 panels: PanelVisibility::default(),
539 process_selected: 0,
540 process_scroll_offset: 0,
541 sort_column: ProcessSortColumn::Cpu,
542 sort_descending: true,
543 filter: String::new(),
544 show_filter_input: false,
545 show_help: false,
546 show_tree: false,
547
548 show_signal_menu: false,
549 pending_signal: None,
550 signal_result: None,
551
552 focused_panel: None,
553 exploded_panel: None,
554
555 files_view_mode: crate::state::FilesViewMode::default(),
556
557 frame_id: 0,
558 last_collect: Instant::now(),
559 avg_frame_time_us: 0,
560 max_frame_time_us: 0,
561 show_fps,
562
563 deterministic,
564
565 mock_gpus: Vec::new(),
567 mock_battery: None,
568 mock_sensors: Vec::new(),
569 mock_containers: Vec::new(),
570 };
571
572 debug::log(Level::Debug, "app", "Initial metrics collection (1/2)...");
574 app.collect_metrics();
575 debug::log(Level::Debug, "app", "Initial metrics collection (2/2)...");
576 app.collect_metrics();
577 debug::log(Level::Info, "app", "App initialization complete");
578
579 app
580 }
581
582 pub fn new_mock() -> Self {
587 Self {
588 cpu: CpuCollector::default(),
589 memory: MemoryCollector::default(),
590 disk: DiskCollector::default(),
591 network: NetworkCollector::default(),
592 process: ProcessCollector::default(),
593 sensors: SensorCollector::default(),
594 battery: BatteryCollector::default(),
595
596 #[cfg(feature = "nvidia")]
597 nvidia_gpu: NvidiaGpuCollector::default(),
598
599 #[cfg(target_os = "linux")]
600 amd_gpu: AmdGpuCollector::default(),
601
602 #[cfg(target_os = "macos")]
603 apple_gpu: AppleGpuCollector::default(),
604
605 swap_analyzer: SwapAnalyzer::new(),
606 disk_io_analyzer: DiskIoAnalyzer::new(),
607 storage_analyzer: StorageAnalyzer::new(),
608 connection_analyzer: crate::analyzers::ConnectionAnalyzer::new(),
609 treemap_analyzer: crate::analyzers::TreemapAnalyzer::new("/tmp"),
610 gpu_process_analyzer: GpuProcessAnalyzer::new(),
611 psi_analyzer: PsiAnalyzer::new(),
612 container_analyzer: ContainerAnalyzer::new(),
613 network_stats: NetworkStatsAnalyzer::new(),
614 disk_entropy: DiskEntropyAnalyzer::new(),
615 process_extra: crate::analyzers::ProcessExtraAnalyzer::new(),
616 file_analyzer: crate::analyzers::FileAnalyzer::new(),
617 sensor_health: SensorHealthAnalyzer::new(),
618
619 cpu_history: vec![0.25, 0.30, 0.35, 0.40, 0.45, 0.50, 0.45, 0.40],
621 mem_history: vec![0.60, 0.61, 0.62, 0.63, 0.64, 0.65, 0.64, 0.63],
622 mem_available_history: vec![0.40, 0.39, 0.38, 0.37, 0.36, 0.35, 0.36, 0.37],
623 mem_cached_history: vec![0.20, 0.21, 0.22, 0.23, 0.22, 0.21, 0.20, 0.21],
624 mem_free_history: vec![0.10, 0.09, 0.08, 0.07, 0.08, 0.09, 0.10, 0.09],
625 swap_history: vec![0.05, 0.06, 0.07, 0.08, 0.07, 0.06, 0.05, 0.06],
626 net_rx_history: vec![0.01, 0.02, 0.03, 0.04, 0.03, 0.02, 0.01, 0.02],
627 net_tx_history: vec![0.005, 0.01, 0.015, 0.02, 0.015, 0.01, 0.005, 0.01],
628 per_core_percent: vec![25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0, 60.0],
629
630 per_core_history: {
632 let mut histories = Vec::with_capacity(8);
633 for i in 0..8 {
634 let mut h = Vec::with_capacity(60);
635 let base = 20.0 + (i as f64 * 5.0);
636 for j in 0..6 {
637 h.push(base + (j as f64 * 0.5));
638 }
639 histories.push(h);
640 }
641 histories
642 },
643 per_core_state: vec![
644 CpuCoreState { user: 20.0, system: 3.0, iowait: 2.0, idle: 75.0 },
645 CpuCoreState { user: 25.0, system: 3.0, iowait: 2.0, idle: 70.0 },
646 CpuCoreState { user: 28.0, system: 4.0, iowait: 3.0, idle: 65.0 },
647 CpuCoreState { user: 32.0, system: 5.0, iowait: 3.0, idle: 60.0 },
648 CpuCoreState { user: 35.0, system: 6.0, iowait: 4.0, idle: 55.0 },
649 CpuCoreState { user: 38.0, system: 7.0, iowait: 5.0, idle: 50.0 },
650 CpuCoreState { user: 42.0, system: 8.0, iowait: 5.0, idle: 45.0 },
651 CpuCoreState { user: 45.0, system: 9.0, iowait: 6.0, idle: 40.0 },
652 ],
653 freq_history: {
654 let mut h = Vec::with_capacity(60);
655 h.extend_from_slice(&[3200.0, 3400.0, 3600.0, 3800.0, 4000.0, 4200.0]);
656 h
657 },
658 top_process_per_core: vec![
659 TopProcessForCore { pid: 1234, name: "firefox".to_string(), cpu_percent: 15.0 },
660 TopProcessForCore { pid: 5678, name: "chrome".to_string(), cpu_percent: 12.0 },
661 TopProcessForCore { pid: 9012, name: "code".to_string(), cpu_percent: 10.0 },
662 TopProcessForCore { pid: 3456, name: "rustc".to_string(), cpu_percent: 25.0 },
663 TopProcessForCore { pid: 7890, name: "cargo".to_string(), cpu_percent: 20.0 },
664 TopProcessForCore { pid: 1111, name: "node".to_string(), cpu_percent: 8.0 },
665 TopProcessForCore { pid: 2222, name: "python".to_string(), cpu_percent: 6.0 },
666 TopProcessForCore { pid: 3333, name: "java".to_string(), cpu_percent: 5.0 },
667 ],
668 thermal_throttle_active: Some(false),
669
670 mem_pressure_history: {
672 let mut h = Vec::with_capacity(60);
673 h.extend_from_slice(&[5.0, 8.0, 12.0, 15.0, 10.0, 7.0]);
674 h
675 },
676 mem_reclaim_rate: 1250.0, top_mem_consumers: vec![
678 TopMemConsumer { pid: 1234, name: "firefox".to_string(), mem_bytes: 2 * 1024 * 1024 * 1024, mem_percent: 12.5 },
679 TopMemConsumer { pid: 5678, name: "chrome".to_string(), mem_bytes: 1500 * 1024 * 1024, mem_percent: 9.4 },
680 TopMemConsumer { pid: 9012, name: "code".to_string(), mem_bytes: 800 * 1024 * 1024, mem_percent: 5.0 },
681 TopMemConsumer { pid: 3456, name: "slack".to_string(), mem_bytes: 600 * 1024 * 1024, mem_percent: 3.8 },
682 ],
683 swap_trend: SwapTrend::Stable,
684
685 disk_latency_history: {
687 let mut h = Vec::with_capacity(60);
688 h.extend_from_slice(&[2.5, 3.0, 4.5, 8.0, 5.5, 3.2]);
689 h
690 },
691 disk_read_iops: 1250.0,
692 disk_write_iops: 850.0,
693 disk_queue_depth: 2.5,
694 disk_health: vec![
695 DiskHealthStatus {
696 device: "nvme0n1".to_string(),
697 status: DiskHealth::Good,
698 temperature: Some(42.0),
699 reallocated_sectors: 0,
700 },
701 ],
702
703 mem_total: 16 * 1024 * 1024 * 1024, mem_used: 10 * 1024 * 1024 * 1024, mem_available: 6 * 1024 * 1024 * 1024, mem_cached: 3 * 1024 * 1024 * 1024, mem_free: 2 * 1024 * 1024 * 1024, swap_total: 4 * 1024 * 1024 * 1024, swap_used: 500 * 1024 * 1024, net_rx_total: 1024 * 1024 * 1024, net_tx_total: 512 * 1024 * 1024, net_interface_ip: "192.168.1.100".to_string(),
714
715 net_rx_peak: 100_000_000.0, net_tx_peak: 50_000_000.0, net_rx_peak_time: Instant::now(),
718 net_tx_peak_time: Instant::now(),
719
720 net_errors: 5,
722 net_drops: 2,
723 net_established: 42,
724 net_listening: 15,
725
726 panels: PanelVisibility::default(),
727 process_selected: 0,
728 process_scroll_offset: 0,
729 sort_column: ProcessSortColumn::Cpu,
730 sort_descending: true,
731 filter: String::new(),
732 show_filter_input: false,
733 show_help: false,
734 show_tree: false,
735
736 show_signal_menu: false,
737 pending_signal: None,
738 signal_result: None,
739
740 focused_panel: None,
741 exploded_panel: None,
742
743 files_view_mode: crate::state::FilesViewMode::default(),
744
745 frame_id: 100,
746 last_collect: Instant::now(),
747 avg_frame_time_us: 1000,
748 max_frame_time_us: 2000,
749 show_fps: false,
750
751 deterministic: true,
752
753 mock_gpus: vec![
755 MockGpuData {
756 name: "NVIDIA RTX 4090".to_string(),
757 gpu_util: 75.0,
758 vram_used: 20 * 1024 * 1024 * 1024, vram_total: 24 * 1024 * 1024 * 1024, temperature: 72.0,
761 power_watts: 350,
762 power_limit_watts: 450,
763 clock_mhz: 2520,
764 history: vec![0.65, 0.70, 0.75, 0.80, 0.75, 0.70, 0.75, 0.80],
765 },
766 MockGpuData {
767 name: "NVIDIA RTX 3080".to_string(),
768 gpu_util: 45.0,
769 vram_used: 6 * 1024 * 1024 * 1024, vram_total: 10 * 1024 * 1024 * 1024, temperature: 65.0,
772 power_watts: 220,
773 power_limit_watts: 320,
774 clock_mhz: 1950,
775 history: vec![0.40, 0.45, 0.50, 0.45, 0.40, 0.45, 0.50, 0.45],
776 },
777 ],
778 mock_battery: Some(MockBatteryData {
779 percent: 72.5,
780 charging: false,
781 time_remaining_mins: Some(185),
782 power_watts: 15.2,
783 health_percent: 94.0,
784 cycle_count: 342,
785 }),
786 mock_sensors: vec![
787 MockSensorData {
788 name: "coretemp/temp1".to_string(),
789 label: "Package".to_string(),
790 value: 65.0,
791 max: Some(100.0),
792 crit: Some(105.0),
793 sensor_type: MockSensorType::Temperature,
794 },
795 MockSensorData {
796 name: "coretemp/temp2".to_string(),
797 label: "Core 0".to_string(),
798 value: 62.0,
799 max: Some(100.0),
800 crit: Some(105.0),
801 sensor_type: MockSensorType::Temperature,
802 },
803 MockSensorData {
804 name: "coretemp/temp3".to_string(),
805 label: "Core 1".to_string(),
806 value: 64.0,
807 max: Some(100.0),
808 crit: Some(105.0),
809 sensor_type: MockSensorType::Temperature,
810 },
811 MockSensorData {
812 name: "nct6798/fan1".to_string(),
813 label: "CPU Fan".to_string(),
814 value: 1200.0,
815 max: Some(3000.0),
816 crit: None,
817 sensor_type: MockSensorType::Fan,
818 },
819 MockSensorData {
820 name: "nct6798/fan2".to_string(),
821 label: "Chassis Fan".to_string(),
822 value: 800.0,
823 max: Some(2000.0),
824 crit: None,
825 sensor_type: MockSensorType::Fan,
826 },
827 MockSensorData {
828 name: "nct6798/in0".to_string(),
829 label: "Vcore".to_string(),
830 value: 1.25,
831 max: Some(1.50),
832 crit: None,
833 sensor_type: MockSensorType::Voltage,
834 },
835 ],
836 mock_containers: vec![
837 MockContainerData {
838 name: "nginx-proxy".to_string(),
839 status: "running".to_string(),
840 cpu_percent: 2.5,
841 mem_used: 128 * 1024 * 1024, mem_limit: 512 * 1024 * 1024, },
844 MockContainerData {
845 name: "postgres-db".to_string(),
846 status: "running".to_string(),
847 cpu_percent: 8.2,
848 mem_used: 512 * 1024 * 1024, mem_limit: 2 * 1024 * 1024 * 1024, },
851 MockContainerData {
852 name: "redis-cache".to_string(),
853 status: "running".to_string(),
854 cpu_percent: 1.1,
855 mem_used: 64 * 1024 * 1024, mem_limit: 256 * 1024 * 1024, },
858 ],
859 }
860 }
861
862 pub fn collect_metrics(&mut self) {
864 use trueno_viz::monitor::debug::{self, Level};
865
866 self.frame_id += 1;
867
868 if self.deterministic {
870 return;
871 }
872
873 let is_first = self.frame_id <= 2;
874
875 if is_first { debug::log(Level::Trace, "collect", "cpu..."); }
877 if self.cpu.is_available() {
878 if let Ok(metrics) = self.cpu.collect() {
879 if let Some(total) = metrics.get_gauge("cpu.total") {
880 Self::push_to_history(&mut self.cpu_history, total / 100.0);
881 }
882
883 self.per_core_percent.clear();
885 for i in 0..self.cpu.core_count() {
886 if let Some(percent) = metrics.get_gauge(&format!("cpu.core.{i}")) {
887 self.per_core_percent.push(percent);
888 }
889 }
890 }
891 }
892
893 if is_first { debug::log(Level::Trace, "collect", "memory..."); }
895 if self.memory.is_available() {
896 if let Ok(metrics) = self.memory.collect() {
897 if let Some(total) = metrics.get_counter("memory.total") {
899 self.mem_total = total;
900 }
901 if let Some(used) = metrics.get_counter("memory.used") {
902 self.mem_used = used;
903 }
904 if let Some(available) = metrics.get_counter("memory.available") {
905 self.mem_available = available;
906 }
907 if let Some(cached) = metrics.get_counter("memory.cached") {
908 self.mem_cached = cached;
909 }
910 if let Some(free) = metrics.get_counter("memory.free") {
911 self.mem_free = free;
912 }
913 if let Some(swap_total) = metrics.get_counter("memory.swap.total") {
914 self.swap_total = swap_total;
915 }
916 if let Some(swap_used) = metrics.get_counter("memory.swap.used") {
917 self.swap_used = swap_used;
918 }
919
920 if self.mem_total > 0 {
922 let total = self.mem_total as f64;
923
924 if let Some(percent) = metrics.get_gauge("memory.used.percent") {
926 Self::push_to_history(&mut self.mem_history, percent / 100.0);
927 }
928
929 let avail_pct = self.mem_available as f64 / total;
931 Self::push_to_history(&mut self.mem_available_history, avail_pct);
932
933 let cached_pct = self.mem_cached as f64 / total;
935 Self::push_to_history(&mut self.mem_cached_history, cached_pct);
936
937 let free_pct = self.mem_free as f64 / total;
939 Self::push_to_history(&mut self.mem_free_history, free_pct);
940 }
941
942 if let Some(swap_percent) = metrics.get_gauge("memory.swap.percent") {
944 Self::push_to_history(&mut self.swap_history, swap_percent / 100.0);
945 }
946 }
947 }
948
949 if is_first { debug::log(Level::Trace, "collect", "network..."); }
951 if self.network.is_available() {
952 let _ = self.network.collect();
953 if let Some(iface) = self.network.current_interface() {
954 if let Some(rates) = self.network.all_rates().get(iface) {
955 let rx_norm = (rates.rx_bytes_per_sec / 1_000_000_000.0).min(1.0);
957 let tx_norm = (rates.tx_bytes_per_sec / 1_000_000_000.0).min(1.0);
958 Self::push_to_history(&mut self.net_rx_history, rx_norm);
959 Self::push_to_history(&mut self.net_tx_history, tx_norm);
960
961 self.net_rx_total += rates.rx_bytes_per_sec as u64;
964 self.net_tx_total += rates.tx_bytes_per_sec as u64;
965
966 if rates.rx_bytes_per_sec > self.net_rx_peak {
968 self.net_rx_peak = rates.rx_bytes_per_sec;
969 self.net_rx_peak_time = Instant::now();
970 }
971 if rates.tx_bytes_per_sec > self.net_tx_peak {
972 self.net_tx_peak = rates.tx_bytes_per_sec;
973 self.net_tx_peak_time = Instant::now();
974 }
975 }
976 }
977 }
978
979 if is_first { debug::log(Level::Trace, "collect", "disk..."); }
981 if self.disk.is_available() {
982 let _ = self.disk.collect();
983 }
984
985 if is_first { debug::log(Level::Trace, "collect", "process..."); }
987 if self.process.is_available() {
988 let _ = self.process.collect();
989 }
990
991 if is_first { debug::log(Level::Trace, "collect", "sensors..."); }
993 if self.sensors.is_available() {
994 let _ = self.sensors.collect();
995 }
996
997 if is_first { debug::log(Level::Trace, "collect", "battery..."); }
999 if self.battery.is_available() {
1000 let _ = self.battery.collect();
1001 }
1002
1003 if is_first { debug::log(Level::Trace, "collect", "gpu..."); }
1005 #[cfg(feature = "nvidia")]
1006 if self.nvidia_gpu.is_available() {
1007 let _ = self.nvidia_gpu.collect();
1008 }
1009
1010 #[cfg(target_os = "linux")]
1011 if self.amd_gpu.is_available() {
1012 let _ = self.amd_gpu.collect();
1013 }
1014
1015 #[cfg(target_os = "macos")]
1016 if self.apple_gpu.is_available() {
1017 let _ = self.apple_gpu.collect();
1018 }
1019
1020 if is_first { debug::log(Level::Trace, "collect", "swap_analyzer..."); }
1022 self.swap_analyzer.collect();
1023
1024 if is_first { debug::log(Level::Trace, "collect", "disk_io_analyzer..."); }
1025 self.disk_io_analyzer.collect();
1026
1027 if is_first { debug::log(Level::Trace, "collect", "disk_entropy..."); }
1029 let mount_paths: Vec<String> = self.disk.mounts().iter().map(|m| m.mount_point.clone()).collect();
1030 self.disk_entropy.collect(&mount_paths);
1031
1032 if is_first { debug::log(Level::Trace, "collect", "storage_analyzer..."); }
1033 self.storage_analyzer.collect();
1034
1035 if is_first { debug::log(Level::Trace, "collect", "connection_analyzer..."); }
1036 self.connection_analyzer.collect();
1037
1038 if is_first { debug::log(Level::Trace, "collect", "treemap_analyzer..."); }
1039 self.treemap_analyzer.collect();
1040
1041 if is_first { debug::log(Level::Trace, "collect", "gpu_process_analyzer..."); }
1042 self.gpu_process_analyzer.collect();
1043
1044 if is_first { debug::log(Level::Trace, "collect", "psi_analyzer..."); }
1045 self.psi_analyzer.collect();
1046
1047 if is_first { debug::log(Level::Trace, "collect", "container_analyzer..."); }
1048 self.container_analyzer.collect();
1049
1050 #[cfg(target_os = "linux")]
1052 {
1053 if is_first { debug::log(Level::Trace, "collect", "network_stats..."); }
1054 self.network_stats.collect();
1055 }
1056
1057 if is_first { debug::log(Level::Trace, "collect", "process_extra..."); }
1059 let pids: Vec<u32> = self.process.processes().keys().copied().collect();
1060 let cpu_percents: std::collections::HashMap<u32, f64> = self.process.processes()
1061 .iter()
1062 .map(|(&pid, p)| (pid, p.cpu_percent))
1063 .collect();
1064 self.process_extra.collect(&pids, &cpu_percents);
1065
1066 if is_first { debug::log(Level::Trace, "collect", "file_analyzer..."); }
1068 self.file_analyzer.collect("/");
1069
1070 if is_first { debug::log(Level::Trace, "collect", "sensor_health..."); }
1072 let _ = self.sensor_health.collect();
1073
1074 self.last_collect = Instant::now();
1075 }
1076
1077 fn push_to_history(history: &mut Vec<f64>, value: f64) {
1078 history.push(value);
1079 if history.len() > 300 {
1080 history.remove(0);
1081 }
1082 }
1083
1084 pub fn update_frame_stats(&mut self, frame_times: &[Duration]) {
1086 if frame_times.is_empty() {
1087 return;
1088 }
1089
1090 let total: u128 = frame_times.iter().map(|d| d.as_micros()).sum();
1091 self.avg_frame_time_us = (total / frame_times.len() as u128) as u64;
1092 self.max_frame_time_us = frame_times
1093 .iter()
1094 .map(|d| d.as_micros() as u64)
1095 .max()
1096 .unwrap_or(0);
1097 }
1098
1099 pub fn handle_key(&mut self, code: KeyCode, modifiers: KeyModifiers) -> bool {
1101 if self.pending_signal.is_some() {
1103 match code {
1104 KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
1105 self.confirm_signal();
1106 }
1107 KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
1108 self.cancel_signal();
1109 }
1110 _ => {}
1111 }
1112 return false;
1113 }
1114
1115 if self.show_signal_menu {
1117 match code {
1118 KeyCode::Esc => {
1119 self.show_signal_menu = false;
1120 }
1121 KeyCode::Char('x') => {
1122 self.show_signal_menu = false;
1123 self.request_signal(SignalType::Term);
1124 }
1125 KeyCode::Char('K') => {
1126 self.show_signal_menu = false;
1127 self.request_signal(SignalType::Kill);
1128 }
1129 KeyCode::Char('H') => {
1130 self.show_signal_menu = false;
1131 self.request_signal(SignalType::Hup);
1132 }
1133 KeyCode::Char('i') => {
1134 self.show_signal_menu = false;
1135 self.request_signal(SignalType::Int);
1136 }
1137 KeyCode::Char('p') => {
1138 self.show_signal_menu = false;
1139 self.request_signal(SignalType::Stop);
1140 }
1141 KeyCode::Char('c') => {
1142 self.show_signal_menu = false;
1143 self.request_signal(SignalType::Cont);
1144 }
1145 _ => {}
1146 }
1147 return false;
1148 }
1149
1150 if self.show_filter_input {
1152 match code {
1153 KeyCode::Esc => {
1154 self.show_filter_input = false;
1155 self.filter.clear();
1156 }
1157 KeyCode::Enter => {
1158 self.show_filter_input = false;
1159 }
1160 KeyCode::Backspace => {
1161 self.filter.pop();
1162 }
1163 KeyCode::Char(c) => {
1164 self.filter.push(c);
1165 }
1166 _ => {}
1167 }
1168 return false;
1169 }
1170
1171 if code == KeyCode::Esc {
1173 if self.exploded_panel.is_some() {
1174 self.exploded_panel = None;
1175 return false;
1176 }
1177 if self.focused_panel.is_some() {
1178 self.focused_panel = None;
1179 return false;
1180 }
1181 return true; }
1183
1184 if code == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL) {
1186 return true;
1187 }
1188
1189 if self.exploded_panel.is_some() {
1192 match code {
1193 KeyCode::Enter | KeyCode::Char('z') => {
1195 self.exploded_panel = None;
1196 return false;
1197 }
1198 _ => {}
1200 }
1201 }
1202 else if self.focused_panel.is_some() {
1204 match code {
1205 KeyCode::Enter | KeyCode::Char('z') => {
1207 if let Some(panel) = self.focused_panel {
1208 self.exploded_panel = Some(panel);
1209 }
1210 return false;
1211 }
1212 KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down => {
1214 self.navigate_panel_focus(code);
1215 return false;
1216 }
1217 KeyCode::Char('h') => {
1219 self.navigate_panel_focus(KeyCode::Left);
1220 return false;
1221 }
1222 KeyCode::Char('l') => {
1223 self.navigate_panel_focus(KeyCode::Right);
1224 return false;
1225 }
1226 KeyCode::Char('j') => {
1227 self.navigate_panel_focus(KeyCode::Down);
1228 return false;
1229 }
1230 KeyCode::Char('k') => {
1231 self.navigate_panel_focus(KeyCode::Up);
1232 return false;
1233 }
1234 _ => {}
1235 }
1236 }
1237 else {
1239 match code {
1240 KeyCode::Char('h') => {
1241 self.focused_panel = Some(self.first_visible_panel());
1242 return false;
1243 }
1244 KeyCode::Char('l') => {
1245 self.focused_panel = Some(self.first_visible_panel());
1246 return false;
1247 }
1248 _ => {}
1249 }
1250 }
1251
1252 match code {
1253 KeyCode::Char('q') => return true,
1255
1256 KeyCode::Char('?') | KeyCode::F(1) => self.show_help = !self.show_help,
1258
1259 KeyCode::Char('1') => self.panels.cpu = !self.panels.cpu,
1261 KeyCode::Char('2') => self.panels.memory = !self.panels.memory,
1262 KeyCode::Char('3') => self.panels.disk = !self.panels.disk,
1263 KeyCode::Char('4') => self.panels.network = !self.panels.network,
1264 KeyCode::Char('5') => self.panels.process = !self.panels.process,
1265 KeyCode::Char('6') => self.panels.gpu = !self.panels.gpu,
1266 KeyCode::Char('7') => self.panels.battery = !self.panels.battery,
1267 KeyCode::Char('8') => self.panels.sensors = !self.panels.sensors,
1268 KeyCode::Char('9') => self.panels.files = !self.panels.files,
1269
1270 KeyCode::Char('v') => self.files_view_mode = self.files_view_mode.next(),
1272
1273 KeyCode::Down if self.focused_panel.is_none() || self.exploded_panel.is_some() => {
1275 self.navigate_process(1)
1276 }
1277 KeyCode::Up if self.focused_panel.is_none() || self.exploded_panel.is_some() => {
1278 self.navigate_process(-1)
1279 }
1280 KeyCode::Char('j') if self.exploded_panel.is_some() => self.navigate_process(1),
1282 KeyCode::Char('k') if self.exploded_panel.is_some() => self.navigate_process(-1),
1283 KeyCode::PageDown => self.navigate_process(10),
1284 KeyCode::PageUp => self.navigate_process(-10),
1285 KeyCode::Home | KeyCode::Char('g') => self.process_selected = 0,
1286 KeyCode::Char('G') => {
1287 let count = self.process_count();
1288 if count > 0 {
1289 self.process_selected = count - 1;
1290 }
1291 }
1292
1293 KeyCode::Tab | KeyCode::Char('s') => {
1295 self.sort_column = self.sort_column.next();
1296 }
1297 KeyCode::Char('r') => self.sort_descending = !self.sort_descending,
1298
1299 KeyCode::Char('t') => self.show_tree = !self.show_tree,
1301
1302 KeyCode::Char('X') if self.focused_panel.is_none() || self.exploded_panel == Some(PanelType::Process) => {
1305 self.request_signal(SignalType::Kill);
1307 }
1308 KeyCode::Char('x') if self.focused_panel.is_none() => {
1309 self.request_signal(SignalType::Term);
1311 }
1312
1313 KeyCode::Char('z') if self.focused_panel.is_none() && self.exploded_panel.is_none() => {
1315 self.focused_panel = Some(self.first_visible_panel());
1316 }
1317
1318 KeyCode::Char('f') | KeyCode::Char('/') => {
1320 self.show_filter_input = true;
1321 }
1322 KeyCode::Delete => self.filter.clear(),
1323
1324 KeyCode::Char('0') => {
1326 self.panels = PanelVisibility::default();
1327 self.focused_panel = None;
1328 self.exploded_panel = None;
1329 }
1330
1331 _ => {}
1332 }
1333
1334 false
1335 }
1336
1337 fn navigate_panel_focus(&mut self, direction: KeyCode) {
1339 let visible = self.visible_panels();
1340 if visible.is_empty() {
1341 return;
1342 }
1343
1344 let current = self.focused_panel.unwrap_or_else(|| self.first_visible_panel());
1345 let current_idx = visible.iter().position(|&p| p == current).unwrap_or(0);
1346
1347 let new_idx = match direction {
1348 KeyCode::Left | KeyCode::Up => {
1349 if current_idx == 0 {
1350 visible.len() - 1
1351 } else {
1352 current_idx - 1
1353 }
1354 }
1355 KeyCode::Right | KeyCode::Down => {
1356 if current_idx >= visible.len() - 1 {
1357 0
1358 } else {
1359 current_idx + 1
1360 }
1361 }
1362 _ => current_idx,
1363 };
1364
1365 self.focused_panel = Some(visible[new_idx]);
1366 }
1367
1368 pub fn visible_panels(&self) -> Vec<PanelType> {
1370 let mut visible = Vec::new();
1371 if self.panels.cpu {
1372 visible.push(PanelType::Cpu);
1373 }
1374 if self.panels.memory {
1375 visible.push(PanelType::Memory);
1376 }
1377 if self.panels.disk {
1378 visible.push(PanelType::Disk);
1379 }
1380 if self.panels.network {
1381 visible.push(PanelType::Network);
1382 }
1383 if self.panels.process {
1384 visible.push(PanelType::Process);
1385 }
1386 if self.panels.gpu && self.has_gpu() {
1387 visible.push(PanelType::Gpu);
1388 }
1389 if self.panels.battery && self.battery.is_available() {
1390 visible.push(PanelType::Battery);
1391 }
1392 if self.panels.sensors && self.sensors.is_available() {
1393 visible.push(PanelType::Sensors);
1394 }
1395 if self.panels.files {
1396 visible.push(PanelType::Files);
1397 }
1398 visible
1399 }
1400
1401 fn first_visible_panel(&self) -> PanelType {
1403 self.visible_panels().first().copied().unwrap_or(PanelType::Cpu)
1404 }
1405
1406 pub fn is_panel_visible(&self, panel: PanelType) -> bool {
1408 match panel {
1409 PanelType::Cpu => self.panels.cpu,
1410 PanelType::Memory => self.panels.memory,
1411 PanelType::Disk => self.panels.disk,
1412 PanelType::Network => self.panels.network,
1413 PanelType::Process => self.panels.process,
1414 PanelType::Gpu => self.panels.gpu && self.has_gpu(),
1415 PanelType::Battery => self.panels.battery && self.battery.is_available(),
1416 PanelType::Sensors => self.panels.sensors && self.sensors.is_available(),
1417 PanelType::Files => self.panels.files,
1418 }
1419 }
1420
1421 fn navigate_process(&mut self, delta: isize) {
1422 let count = self.process_count();
1423 if count == 0 {
1424 return;
1425 }
1426
1427 let current = self.process_selected;
1428 let new = if delta > 0 {
1429 (current + delta as usize).min(count - 1)
1430 } else {
1431 current.saturating_sub((-delta) as usize)
1432 };
1433 self.process_selected = new;
1434 }
1435
1436 fn process_count(&self) -> usize {
1437 let filter_lower = self.filter.to_lowercase();
1438 self.process
1439 .processes()
1440 .values()
1441 .filter(|p| {
1442 if filter_lower.is_empty() {
1443 true
1444 } else {
1445 contains_ignore_case(&p.name, &filter_lower)
1447 || contains_ignore_case(&p.cmdline, &filter_lower)
1448 }
1449 })
1450 .count()
1451 }
1452
1453 pub fn sorted_processes(&self) -> Vec<&trueno_viz::monitor::collectors::process::ProcessInfo> {
1455 let filter_lower = self.filter.to_lowercase();
1457
1458 let mut procs: Vec<_> = self
1459 .process
1460 .processes()
1461 .values()
1462 .filter(|p| {
1463 if filter_lower.is_empty() {
1464 true
1465 } else {
1466 contains_ignore_case(&p.name, &filter_lower)
1468 || contains_ignore_case(&p.cmdline, &filter_lower)
1469 }
1470 })
1471 .collect();
1472
1473 procs.sort_by(|a, b| {
1474 let cmp = match self.sort_column {
1475 ProcessSortColumn::Pid => a.pid.cmp(&b.pid),
1476 ProcessSortColumn::Name => a.name.cmp(&b.name),
1477 ProcessSortColumn::Cpu => a
1478 .cpu_percent
1479 .partial_cmp(&b.cpu_percent)
1480 .unwrap_or(std::cmp::Ordering::Equal),
1481 ProcessSortColumn::Mem => a
1482 .mem_percent
1483 .partial_cmp(&b.mem_percent)
1484 .unwrap_or(std::cmp::Ordering::Equal),
1485 ProcessSortColumn::State => a.state.as_char().cmp(&b.state.as_char()),
1486 ProcessSortColumn::User => a.user.cmp(&b.user),
1487 ProcessSortColumn::Threads => a.threads.cmp(&b.threads),
1488 };
1489 if self.sort_descending {
1490 cmp.reverse()
1491 } else {
1492 cmp
1493 }
1494 });
1495
1496 procs
1497 }
1498
1499 pub fn has_gpu(&self) -> bool {
1501 #[cfg(feature = "nvidia")]
1502 if self.nvidia_gpu.is_available() {
1503 return true;
1504 }
1505
1506 #[cfg(target_os = "linux")]
1507 if self.amd_gpu.is_available() {
1508 return true;
1509 }
1510
1511 #[cfg(target_os = "macos")]
1512 if self.apple_gpu.is_available() {
1513 return true;
1514 }
1515
1516 false
1517 }
1518
1519 #[cfg(unix)]
1521 pub fn send_signal(&mut self, pid: u32, signal: SignalType) -> Result<(), String> {
1522 use std::process::Command;
1523
1524 let signal_num = signal.number();
1525 let result = Command::new("kill")
1526 .arg(format!("-{}", signal_num))
1527 .arg(pid.to_string())
1528 .output();
1529
1530 match result {
1531 Ok(output) => {
1532 if output.status.success() {
1533 self.signal_result = Some((
1534 true,
1535 format!("Sent {} to PID {}", signal.name(), pid),
1536 Instant::now(),
1537 ));
1538 Ok(())
1539 } else {
1540 let stderr = String::from_utf8_lossy(&output.stderr);
1541 let msg = format!("Failed to send {} to {}: {}", signal.name(), pid, stderr.trim());
1542 self.signal_result = Some((false, msg.clone(), Instant::now()));
1543 Err(msg)
1544 }
1545 }
1546 Err(e) => {
1547 let msg = format!("Failed to execute kill: {}", e);
1548 self.signal_result = Some((false, msg.clone(), Instant::now()));
1549 Err(msg)
1550 }
1551 }
1552 }
1553
1554 #[cfg(not(unix))]
1555 pub fn send_signal(&mut self, _pid: u32, _signal: SignalType) -> Result<(), String> {
1556 Err("Signal sending not supported on this platform".to_string())
1557 }
1558
1559 pub fn selected_process(&self) -> Option<(u32, String)> {
1561 let procs = self.sorted_processes();
1562 procs.get(self.process_selected).map(|p| (p.pid, p.name.clone()))
1563 }
1564
1565 pub fn request_signal(&mut self, signal: SignalType) {
1567 if let Some((pid, name)) = self.selected_process() {
1568 self.pending_signal = Some((pid, name, signal));
1569 }
1570 }
1571
1572 pub fn confirm_signal(&mut self) {
1574 if let Some((pid, _name, signal)) = self.pending_signal.take() {
1575 let _ = self.send_signal(pid, signal);
1576 }
1577 }
1578
1579 pub fn cancel_signal(&mut self) {
1581 self.pending_signal = None;
1582 }
1583
1584 pub fn clear_old_signal_result(&mut self) {
1586 if let Some((_, _, timestamp)) = &self.signal_result {
1587 if timestamp.elapsed() > Duration::from_secs(3) {
1588 self.signal_result = None;
1589 }
1590 }
1591 }
1592}
1593
1594#[cfg(test)]
1595mod tests {
1596 use super::*;
1597 use crossterm::event::{KeyCode, KeyModifiers};
1598
1599 #[test]
1600 fn test_panel_visibility_default() {
1601 let vis = PanelVisibility::default();
1602 assert!(vis.cpu);
1603 assert!(vis.memory);
1604 assert!(vis.disk);
1605 assert!(vis.network);
1606 assert!(vis.process);
1607 assert!(vis.gpu);
1608 assert!(vis.battery);
1609 assert!(vis.sensors);
1610 assert!(!vis.files); }
1612
1613 #[test]
1614 fn test_mock_app_creation() {
1615 let app = App::new_mock();
1616 assert!(app.deterministic);
1617 assert_eq!(app.frame_id, 100);
1618 assert_eq!(app.avg_frame_time_us, 1000);
1619 assert_eq!(app.max_frame_time_us, 2000);
1620 assert!(!app.show_fps);
1621 }
1622
1623 #[test]
1624 fn test_mock_app_history_populated() {
1625 let app = App::new_mock();
1626 assert_eq!(app.cpu_history.len(), 8);
1627 assert_eq!(app.mem_history.len(), 8);
1628 assert_eq!(app.per_core_percent.len(), 8);
1629 }
1630
1631 #[test]
1632 fn test_mock_app_memory_values() {
1633 let app = App::new_mock();
1634 assert_eq!(app.mem_total, 16 * 1024 * 1024 * 1024);
1635 assert_eq!(app.mem_used, 10 * 1024 * 1024 * 1024);
1636 assert_eq!(app.mem_available, 6 * 1024 * 1024 * 1024);
1637 }
1638
1639 #[test]
1640 fn test_update_frame_stats_empty() {
1641 let mut app = App::new_mock();
1642 app.update_frame_stats(&[]);
1643 }
1645
1646 #[test]
1647 fn test_update_frame_stats_single() {
1648 let mut app = App::new_mock();
1649 app.update_frame_stats(&[Duration::from_micros(500)]);
1650 assert_eq!(app.avg_frame_time_us, 500);
1651 assert_eq!(app.max_frame_time_us, 500);
1652 }
1653
1654 #[test]
1655 fn test_update_frame_stats_multiple() {
1656 let mut app = App::new_mock();
1657 let times = vec![
1658 Duration::from_micros(100),
1659 Duration::from_micros(200),
1660 Duration::from_micros(300),
1661 ];
1662 app.update_frame_stats(×);
1663 assert_eq!(app.avg_frame_time_us, 200); assert_eq!(app.max_frame_time_us, 300);
1665 }
1666
1667 #[test]
1668 fn test_visible_panels_default() {
1669 let app = App::new_mock();
1670 let visible = app.visible_panels();
1671 assert!(visible.contains(&PanelType::Cpu));
1673 assert!(visible.contains(&PanelType::Memory));
1674 assert!(visible.contains(&PanelType::Disk));
1675 assert!(visible.contains(&PanelType::Network));
1676 assert!(visible.contains(&PanelType::Process));
1677 assert!(!visible.contains(&PanelType::Files)); }
1679
1680 #[test]
1681 fn test_visible_panels_with_files() {
1682 let mut app = App::new_mock();
1683 app.panels.files = true;
1684 let visible = app.visible_panels();
1685 assert!(visible.contains(&PanelType::Files));
1686 }
1687
1688 #[test]
1689 fn test_visible_panels_all_disabled() {
1690 let mut app = App::new_mock();
1691 app.panels.cpu = false;
1692 app.panels.memory = false;
1693 app.panels.disk = false;
1694 app.panels.network = false;
1695 app.panels.process = false;
1696 app.panels.gpu = false;
1697 app.panels.battery = false;
1698 app.panels.sensors = false;
1699 app.panels.files = false;
1700 let visible = app.visible_panels();
1701 assert!(visible.is_empty());
1702 }
1703
1704 #[test]
1705 fn test_first_visible_panel_default() {
1706 let app = App::new_mock();
1707 let first = app.first_visible_panel();
1708 assert_eq!(first, PanelType::Cpu);
1709 }
1710
1711 #[test]
1712 fn test_first_visible_panel_when_cpu_disabled() {
1713 let mut app = App::new_mock();
1714 app.panels.cpu = false;
1715 let first = app.first_visible_panel();
1716 assert_eq!(first, PanelType::Memory);
1717 }
1718
1719 #[test]
1720 fn test_is_panel_visible() {
1721 let app = App::new_mock();
1722 assert!(app.is_panel_visible(PanelType::Cpu));
1723 assert!(app.is_panel_visible(PanelType::Memory));
1724 assert!(!app.is_panel_visible(PanelType::Files));
1725 }
1726
1727 #[test]
1728 fn test_handle_key_quit_q() {
1729 let mut app = App::new_mock();
1730 let quit = app.handle_key(KeyCode::Char('q'), KeyModifiers::NONE);
1731 assert!(quit);
1732 }
1733
1734 #[test]
1735 fn test_handle_key_quit_ctrl_c() {
1736 let mut app = App::new_mock();
1737 let quit = app.handle_key(KeyCode::Char('c'), KeyModifiers::CONTROL);
1738 assert!(quit);
1739 }
1740
1741 #[test]
1742 fn test_handle_key_quit_esc() {
1743 let mut app = App::new_mock();
1744 let quit = app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
1745 assert!(quit);
1746 }
1747
1748 #[test]
1749 fn test_handle_key_help_toggle() {
1750 let mut app = App::new_mock();
1751 assert!(!app.show_help);
1752 app.handle_key(KeyCode::Char('?'), KeyModifiers::NONE);
1753 assert!(app.show_help);
1754 app.handle_key(KeyCode::Char('?'), KeyModifiers::NONE);
1755 assert!(!app.show_help);
1756 }
1757
1758 #[test]
1759 fn test_handle_key_panel_toggles() {
1760 let mut app = App::new_mock();
1761
1762 assert!(app.panels.cpu);
1764 app.handle_key(KeyCode::Char('1'), KeyModifiers::NONE);
1765 assert!(!app.panels.cpu);
1766
1767 assert!(app.panels.memory);
1769 app.handle_key(KeyCode::Char('2'), KeyModifiers::NONE);
1770 assert!(!app.panels.memory);
1771
1772 assert!(!app.panels.files);
1774 app.handle_key(KeyCode::Char('9'), KeyModifiers::NONE);
1775 assert!(app.panels.files);
1776 }
1777
1778 #[test]
1779 fn test_handle_key_filter_mode() {
1780 let mut app = App::new_mock();
1781 assert!(!app.show_filter_input);
1782
1783 app.handle_key(KeyCode::Char('/'), KeyModifiers::NONE);
1785 assert!(app.show_filter_input);
1786
1787 app.handle_key(KeyCode::Char('t'), KeyModifiers::NONE);
1789 app.handle_key(KeyCode::Char('e'), KeyModifiers::NONE);
1790 app.handle_key(KeyCode::Char('s'), KeyModifiers::NONE);
1791 app.handle_key(KeyCode::Char('t'), KeyModifiers::NONE);
1792 assert_eq!(app.filter, "test");
1793
1794 app.handle_key(KeyCode::Backspace, KeyModifiers::NONE);
1796 assert_eq!(app.filter, "tes");
1797
1798 app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
1800 assert!(!app.show_filter_input);
1801 assert_eq!(app.filter, "");
1802 }
1803
1804 #[test]
1805 fn test_handle_key_filter_enter_confirm() {
1806 let mut app = App::new_mock();
1807 app.handle_key(KeyCode::Char('f'), KeyModifiers::NONE);
1808 app.handle_key(KeyCode::Char('a'), KeyModifiers::NONE);
1809 app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
1810 assert!(!app.show_filter_input);
1811 assert_eq!(app.filter, "a"); }
1813
1814 #[test]
1815 fn test_handle_key_sort_toggle() {
1816 let mut app = App::new_mock();
1817 assert_eq!(app.sort_column, ProcessSortColumn::Cpu);
1818
1819 app.handle_key(KeyCode::Tab, KeyModifiers::NONE);
1820 assert_eq!(app.sort_column, ProcessSortColumn::Mem);
1821
1822 app.handle_key(KeyCode::Char('s'), KeyModifiers::NONE);
1823 assert_eq!(app.sort_column, ProcessSortColumn::State);
1824 }
1825
1826 #[test]
1827 fn test_handle_key_sort_reverse() {
1828 let mut app = App::new_mock();
1829 assert!(app.sort_descending);
1830 app.handle_key(KeyCode::Char('r'), KeyModifiers::NONE);
1831 assert!(!app.sort_descending);
1832 }
1833
1834 #[test]
1835 fn test_handle_key_tree_toggle() {
1836 let mut app = App::new_mock();
1837 assert!(!app.show_tree);
1838 app.handle_key(KeyCode::Char('t'), KeyModifiers::NONE);
1839 assert!(app.show_tree);
1840 }
1841
1842 #[test]
1843 fn test_handle_key_reset_view() {
1844 let mut app = App::new_mock();
1845 app.panels.cpu = false;
1846 app.focused_panel = Some(PanelType::Memory);
1847 app.exploded_panel = Some(PanelType::Disk);
1848
1849 app.handle_key(KeyCode::Char('0'), KeyModifiers::NONE);
1850
1851 assert!(app.panels.cpu);
1852 assert!(app.focused_panel.is_none());
1853 assert!(app.exploded_panel.is_none());
1854 }
1855
1856 #[test]
1857 fn test_handle_key_focus_start_h() {
1858 let mut app = App::new_mock();
1859 assert!(app.focused_panel.is_none());
1860
1861 app.handle_key(KeyCode::Char('h'), KeyModifiers::NONE);
1862 assert!(app.focused_panel.is_some());
1863 }
1864
1865 #[test]
1866 fn test_handle_key_focus_start_l() {
1867 let mut app = App::new_mock();
1868 assert!(app.focused_panel.is_none());
1869
1870 app.handle_key(KeyCode::Char('l'), KeyModifiers::NONE);
1871 assert!(app.focused_panel.is_some());
1872 }
1873
1874 #[test]
1875 fn test_handle_key_focus_navigation() {
1876 let mut app = App::new_mock();
1877 app.focused_panel = Some(PanelType::Cpu);
1878
1879 app.handle_key(KeyCode::Right, KeyModifiers::NONE);
1881 assert_eq!(app.focused_panel, Some(PanelType::Memory));
1882
1883 app.handle_key(KeyCode::Left, KeyModifiers::NONE);
1885 assert_eq!(app.focused_panel, Some(PanelType::Cpu));
1886 }
1887
1888 #[test]
1889 fn test_handle_key_explode_panel() {
1890 let mut app = App::new_mock();
1891 app.focused_panel = Some(PanelType::Cpu);
1892
1893 app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
1895 assert_eq!(app.exploded_panel, Some(PanelType::Cpu));
1896
1897 app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
1899 assert!(app.exploded_panel.is_none());
1900 }
1901
1902 #[test]
1903 fn test_handle_key_explode_with_z() {
1904 let mut app = App::new_mock();
1905 app.focused_panel = Some(PanelType::Memory);
1906
1907 app.handle_key(KeyCode::Char('z'), KeyModifiers::NONE);
1908 assert_eq!(app.exploded_panel, Some(PanelType::Memory));
1909 }
1910
1911 #[test]
1912 fn test_handle_key_esc_unexplode() {
1913 let mut app = App::new_mock();
1914 app.exploded_panel = Some(PanelType::Cpu);
1915
1916 let quit = app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
1917 assert!(!quit);
1918 assert!(app.exploded_panel.is_none());
1919 }
1920
1921 #[test]
1922 fn test_handle_key_esc_unfocus() {
1923 let mut app = App::new_mock();
1924 app.focused_panel = Some(PanelType::Cpu);
1925
1926 let quit = app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
1927 assert!(!quit);
1928 assert!(app.focused_panel.is_none());
1929 }
1930
1931 #[test]
1932 fn test_handle_key_files_view_mode() {
1933 let mut app = App::new_mock();
1934 use crate::state::FilesViewMode;
1935
1936 assert_eq!(app.files_view_mode, FilesViewMode::Size);
1937 app.handle_key(KeyCode::Char('v'), KeyModifiers::NONE);
1938 assert_eq!(app.files_view_mode, FilesViewMode::Entropy);
1939 }
1940
1941 #[test]
1942 fn test_navigate_panel_focus_wrap_right() {
1943 let mut app = App::new_mock();
1944 app.panels.memory = false;
1946 app.panels.disk = false;
1947 app.panels.network = false;
1948 app.panels.process = false;
1949 app.panels.gpu = false;
1950 app.panels.battery = false;
1951 app.panels.sensors = false;
1952 app.panels.files = false;
1953
1954 app.focused_panel = Some(PanelType::Cpu);
1955 app.navigate_panel_focus(KeyCode::Right);
1956 assert_eq!(app.focused_panel, Some(PanelType::Cpu));
1958 }
1959
1960 #[test]
1961 fn test_navigate_panel_focus_wrap_left() {
1962 let mut app = App::new_mock();
1963 app.focused_panel = Some(PanelType::Cpu);
1964 app.navigate_panel_focus(KeyCode::Left);
1965 assert!(app.focused_panel.is_some());
1967 }
1968
1969 #[test]
1970 fn test_navigate_panel_focus_empty() {
1971 let mut app = App::new_mock();
1972 app.panels.cpu = false;
1973 app.panels.memory = false;
1974 app.panels.disk = false;
1975 app.panels.network = false;
1976 app.panels.process = false;
1977 app.panels.gpu = false;
1978 app.panels.battery = false;
1979 app.panels.sensors = false;
1980
1981 app.navigate_panel_focus(KeyCode::Right);
1982 }
1984
1985 #[test]
1986 fn test_navigate_process_empty() {
1987 let mut app = App::new_mock();
1988 app.navigate_process(1);
1990 app.navigate_process(-1);
1991 }
1993
1994 #[test]
1995 fn test_process_count_empty() {
1996 let app = App::new_mock();
1997 assert_eq!(app.process_count(), 0);
1998 }
1999
2000 #[test]
2001 fn test_sorted_processes_empty() {
2002 let app = App::new_mock();
2003 let procs = app.sorted_processes();
2004 assert!(procs.is_empty());
2005 }
2006
2007 #[test]
2008 fn test_has_gpu_mock() {
2009 let app = App::new_mock();
2010 let _has_gpu = app.has_gpu();
2013 }
2014
2015 #[test]
2016 fn test_thrashing_severity() {
2017 let app = App::new_mock();
2018 let severity = app.thrashing_severity();
2019 assert_eq!(severity, ThrashingSeverity::None);
2020 }
2021
2022 #[test]
2023 fn test_has_zram() {
2024 let app = App::new_mock();
2025 let _has = app.has_zram();
2026 }
2028
2029 #[test]
2030 fn test_zram_ratio() {
2031 let app = App::new_mock();
2032 let ratio = app.zram_ratio();
2033 assert!(ratio >= 0.0);
2034 }
2035
2036 #[test]
2037 fn test_selected_process_none() {
2038 let app = App::new_mock();
2039 assert!(app.selected_process().is_none());
2040 }
2041
2042 #[test]
2043 fn test_request_signal_no_process() {
2044 let mut app = App::new_mock();
2045 app.request_signal(SignalType::Term);
2046 assert!(app.pending_signal.is_none()); }
2048
2049 #[test]
2050 fn test_cancel_signal() {
2051 let mut app = App::new_mock();
2052 app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2053 app.cancel_signal();
2054 assert!(app.pending_signal.is_none());
2055 }
2056
2057 #[test]
2058 fn test_confirm_signal_none() {
2059 let mut app = App::new_mock();
2060 app.confirm_signal(); }
2063
2064 #[test]
2065 fn test_clear_old_signal_result_none() {
2066 let mut app = App::new_mock();
2067 app.clear_old_signal_result();
2068 }
2070
2071 #[test]
2072 fn test_clear_old_signal_result_recent() {
2073 let mut app = App::new_mock();
2074 app.signal_result = Some((true, "test".to_string(), Instant::now()));
2075 app.clear_old_signal_result();
2076 assert!(app.signal_result.is_some()); }
2078
2079 #[test]
2080 fn test_signal_menu_handling() {
2081 let mut app = App::new_mock();
2082 app.show_signal_menu = true;
2083
2084 let quit = app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2086 assert!(!quit);
2087 assert!(!app.show_signal_menu);
2088 }
2089
2090 #[test]
2091 fn test_signal_menu_keys() {
2092 let mut app = App::new_mock();
2093
2094 app.show_signal_menu = true;
2096 app.handle_key(KeyCode::Char('x'), KeyModifiers::NONE);
2097 assert!(!app.show_signal_menu);
2098
2099 app.show_signal_menu = true;
2100 app.handle_key(KeyCode::Char('K'), KeyModifiers::NONE);
2101 assert!(!app.show_signal_menu);
2102
2103 app.show_signal_menu = true;
2104 app.handle_key(KeyCode::Char('H'), KeyModifiers::NONE);
2105 assert!(!app.show_signal_menu);
2106 }
2107
2108 #[test]
2109 fn test_pending_signal_confirmation() {
2110 let mut app = App::new_mock();
2111 app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2112
2113 let quit = app.handle_key(KeyCode::Char('y'), KeyModifiers::NONE);
2115 assert!(!quit);
2116 assert!(app.pending_signal.is_none());
2117 }
2118
2119 #[test]
2120 fn test_pending_signal_cancel() {
2121 let mut app = App::new_mock();
2122 app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2123
2124 let quit = app.handle_key(KeyCode::Char('n'), KeyModifiers::NONE);
2126 assert!(!quit);
2127 assert!(app.pending_signal.is_none());
2128 }
2129
2130 #[test]
2131 fn test_pending_signal_esc_cancels() {
2132 let mut app = App::new_mock();
2133 app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2134
2135 let quit = app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2136 assert!(!quit);
2137 assert!(app.pending_signal.is_none());
2138 }
2139
2140 #[test]
2141 fn test_process_navigation_keys() {
2142 let mut app = App::new_mock();
2143
2144 app.process_selected = 5;
2146 app.handle_key(KeyCode::Home, KeyModifiers::NONE);
2147 assert_eq!(app.process_selected, 0);
2148
2149 app.process_selected = 5;
2151 app.handle_key(KeyCode::Char('g'), KeyModifiers::NONE);
2152 assert_eq!(app.process_selected, 0);
2153 }
2154
2155 #[test]
2156 fn test_delete_clears_filter() {
2157 let mut app = App::new_mock();
2158 app.filter = "test".to_string();
2159 app.handle_key(KeyCode::Delete, KeyModifiers::NONE);
2160 assert!(app.filter.is_empty());
2161 }
2162
2163 #[test]
2164 fn test_hjkl_focus_navigation() {
2165 let mut app = App::new_mock();
2166 app.focused_panel = Some(PanelType::Cpu);
2167
2168 app.handle_key(KeyCode::Char('l'), KeyModifiers::NONE);
2170 assert_eq!(app.focused_panel, Some(PanelType::Memory));
2171
2172 app.handle_key(KeyCode::Char('h'), KeyModifiers::NONE);
2174 assert_eq!(app.focused_panel, Some(PanelType::Cpu));
2175 }
2176
2177 #[test]
2178 fn test_jk_process_nav_in_explode() {
2179 let mut app = App::new_mock();
2180 app.exploded_panel = Some(PanelType::Process);
2181
2182 app.handle_key(KeyCode::Char('j'), KeyModifiers::NONE);
2184 app.handle_key(KeyCode::Char('k'), KeyModifiers::NONE);
2185 }
2187
2188 #[test]
2189 fn test_z_starts_focus() {
2190 let mut app = App::new_mock();
2191 assert!(app.focused_panel.is_none());
2192 assert!(app.exploded_panel.is_none());
2193
2194 app.handle_key(KeyCode::Char('z'), KeyModifiers::NONE);
2195 assert!(app.focused_panel.is_some());
2196 }
2197
2198 #[test]
2200 fn test_collect_metrics_real() {
2201 let mut app = App::new(false, false);
2202 app.collect_metrics();
2204 assert!(app.frame_id >= 1);
2206 }
2207
2208 #[test]
2210 fn test_collect_metrics_cycles() {
2211 let mut app = App::new(false, false);
2212 let initial_frame = app.frame_id;
2213 app.collect_metrics();
2214 app.collect_metrics();
2215 assert_eq!(app.frame_id, initial_frame + 2);
2216 }
2217
2218 #[test]
2220 fn test_collect_metrics_history() {
2221 let mut app = App::new(false, false);
2222 let initial_cpu_len = app.cpu_history.len();
2223 app.collect_metrics();
2224 assert!(app.cpu_history.len() >= initial_cpu_len);
2226 }
2227
2228 #[test]
2230 fn test_push_to_history() {
2231 let mut history = Vec::new();
2232 App::push_to_history(&mut history, 0.5);
2233 assert_eq!(history.len(), 1);
2234 assert_eq!(history[0], 0.5);
2235
2236 for i in 0..400 {
2238 App::push_to_history(&mut history, i as f64 / 400.0);
2239 }
2240 assert_eq!(history.len(), 300);
2242 }
2243
2244 #[test]
2249 fn test_collect_metrics_performance() {
2250 use std::time::Instant;
2251
2252 let mut app = App::new(false, false);
2253 let iterations = 5;
2254 let start = Instant::now();
2255
2256 for _ in 0..iterations {
2257 app.collect_metrics();
2258 }
2259
2260 let elapsed = start.elapsed();
2261 let avg_ms = elapsed.as_millis() / iterations as u128;
2262
2263 assert!(avg_ms < 5000, "collect_metrics too slow: {}ms avg", avg_ms);
2266 }
2267
2268 #[test]
2270 fn test_history_push_performance() {
2271 use std::time::Instant;
2272
2273 let mut history = Vec::new();
2274 let iterations = 10000;
2275 let start = Instant::now();
2276
2277 for i in 0..iterations {
2278 App::push_to_history(&mut history, i as f64 / iterations as f64);
2279 }
2280
2281 let elapsed = start.elapsed();
2282 let per_op_ns = elapsed.as_nanos() / iterations as u128;
2283
2284 assert!(per_op_ns < 1000, "push_to_history too slow: {}ns per op", per_op_ns);
2286 }
2287
2288 #[test]
2291 fn test_app_mock_creation_performance() {
2292 use std::time::Instant;
2293
2294 let iterations = 50;
2295 let start = Instant::now();
2296
2297 for _ in 0..iterations {
2298 let _ = App::new_mock();
2299 }
2300
2301 let elapsed = start.elapsed();
2302 let avg_us = elapsed.as_micros() / iterations as u128;
2303
2304 assert!(avg_us < 100000, "new_mock too slow: {}us avg", avg_us);
2307 }
2308
2309 #[test]
2312 fn test_visible_panels_files_enabled() {
2313 let mut app = App::new_mock();
2314 app.panels.files = true;
2315 let panels = app.visible_panels();
2316 assert!(panels.contains(&PanelType::Files));
2317 }
2318
2319 #[test]
2320 fn test_cancel_signal_with_pending() {
2321 let mut app = App::new_mock();
2322 app.pending_signal = Some((1234, "test_proc".to_string(), SignalType::Term));
2323 app.cancel_signal();
2324 assert!(app.pending_signal.is_none());
2325 }
2326
2327 #[test]
2328 fn test_pending_signal_hup() {
2329 let mut app = App::new_mock();
2330 app.pending_signal = Some((1234, "proc".to_string(), SignalType::Hup));
2331 assert!(app.pending_signal.is_some());
2332 if let Some((pid, name, signal)) = &app.pending_signal {
2333 assert_eq!(*pid, 1234);
2334 assert_eq!(name, "proc");
2335 assert_eq!(*signal, SignalType::Hup);
2336 }
2337 }
2338
2339 #[test]
2340 fn test_pending_signal_int() {
2341 let mut app = App::new_mock();
2342 app.pending_signal = Some((5678, "daemon".to_string(), SignalType::Int));
2343 if let Some((_, _, signal)) = &app.pending_signal {
2344 assert_eq!(*signal, SignalType::Int);
2345 }
2346 }
2347
2348 #[test]
2349 fn test_pending_signal_usr1() {
2350 let mut app = App::new_mock();
2351 app.pending_signal = Some((1000, "app".to_string(), SignalType::Usr1));
2352 if let Some((_, _, signal)) = &app.pending_signal {
2353 assert_eq!(*signal, SignalType::Usr1);
2354 }
2355 }
2356
2357 #[test]
2358 fn test_pending_signal_usr2() {
2359 let mut app = App::new_mock();
2360 app.pending_signal = Some((2000, "service".to_string(), SignalType::Usr2));
2361 if let Some((_, _, signal)) = &app.pending_signal {
2362 assert_eq!(*signal, SignalType::Usr2);
2363 }
2364 }
2365
2366 #[test]
2367 fn test_pending_signal_stop() {
2368 let mut app = App::new_mock();
2369 app.pending_signal = Some((3000, "worker".to_string(), SignalType::Stop));
2370 if let Some((_, _, signal)) = &app.pending_signal {
2371 assert_eq!(*signal, SignalType::Stop);
2372 }
2373 }
2374
2375 #[test]
2376 fn test_pending_signal_cont() {
2377 let mut app = App::new_mock();
2378 app.pending_signal = Some((4000, "bg_task".to_string(), SignalType::Cont));
2379 if let Some((_, _, signal)) = &app.pending_signal {
2380 assert_eq!(*signal, SignalType::Cont);
2381 }
2382 }
2383
2384 #[test]
2385 fn test_pending_signal_kill() {
2386 let mut app = App::new_mock();
2387 app.pending_signal = Some((9999, "zombie".to_string(), SignalType::Kill));
2388 if let Some((_, _, signal)) = &app.pending_signal {
2389 assert_eq!(*signal, SignalType::Kill);
2390 }
2391 }
2392
2393 #[test]
2394 fn test_signal_menu_key_i_int() {
2395 let mut app = App::new_mock();
2396 app.process_selected = 0;
2397 app.show_signal_menu = true;
2398 app.handle_key(KeyCode::Char('i'), KeyModifiers::NONE);
2399 assert!(!app.show_signal_menu);
2400 }
2401
2402 #[test]
2403 fn test_signal_menu_key_p_stop() {
2404 let mut app = App::new_mock();
2405 app.process_selected = 0;
2406 app.show_signal_menu = true;
2407 app.handle_key(KeyCode::Char('p'), KeyModifiers::NONE);
2408 assert!(!app.show_signal_menu);
2409 }
2410
2411 #[test]
2412 fn test_signal_menu_key_c_cont() {
2413 let mut app = App::new_mock();
2414 app.process_selected = 0;
2415 app.show_signal_menu = true;
2416 app.handle_key(KeyCode::Char('c'), KeyModifiers::NONE);
2417 assert!(!app.show_signal_menu);
2418 }
2419
2420 #[test]
2421 fn test_signal_menu_key_unknown_ignored() {
2422 let mut app = App::new_mock();
2423 app.show_signal_menu = true;
2424 app.handle_key(KeyCode::Char('z'), KeyModifiers::NONE);
2426 assert!(app.show_signal_menu);
2427 }
2428
2429 #[test]
2430 fn test_filter_input_escape_clears() {
2431 let mut app = App::new_mock();
2432 app.show_filter_input = true;
2433 app.filter = "test".to_string();
2434 app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2435 assert!(!app.show_filter_input);
2436 assert!(app.filter.is_empty());
2437 }
2438
2439 #[test]
2440 fn test_filter_input_backspace() {
2441 let mut app = App::new_mock();
2442 app.show_filter_input = true;
2443 app.filter = "test".to_string();
2444 app.handle_key(KeyCode::Backspace, KeyModifiers::NONE);
2445 assert_eq!(app.filter, "tes");
2446 }
2447
2448 #[test]
2449 fn test_filter_input_char_append() {
2450 let mut app = App::new_mock();
2451 app.show_filter_input = true;
2452 app.filter = "te".to_string();
2453 app.handle_key(KeyCode::Char('s'), KeyModifiers::NONE);
2454 app.handle_key(KeyCode::Char('t'), KeyModifiers::NONE);
2455 assert_eq!(app.filter, "test");
2456 }
2457
2458 #[test]
2459 fn test_filter_input_enter_closes() {
2460 let mut app = App::new_mock();
2461 app.show_filter_input = true;
2462 app.filter = "search".to_string();
2463 app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
2464 assert!(!app.show_filter_input);
2465 assert_eq!(app.filter, "search"); }
2467
2468 #[test]
2471 fn test_mock_gpus_populated() {
2472 let app = App::new_mock();
2473 assert!(!app.mock_gpus.is_empty(), "mock_gpus should be populated");
2474 assert_eq!(app.mock_gpus.len(), 2, "should have 2 mock GPUs");
2475 assert!(app.mock_gpus[0].name.contains("RTX"));
2476 }
2477
2478 #[test]
2479 fn test_mock_battery_populated() {
2480 let app = App::new_mock();
2481 assert!(app.mock_battery.is_some(), "mock_battery should be populated");
2482 let bat = app.mock_battery.as_ref().expect("battery");
2483 assert!(bat.percent > 0.0);
2484 assert!(bat.health_percent > 0.0);
2485 }
2486
2487 #[test]
2488 fn test_mock_sensors_populated() {
2489 let app = App::new_mock();
2490 assert!(!app.mock_sensors.is_empty(), "mock_sensors should be populated");
2491 assert!(app.mock_sensors.len() >= 3);
2492 }
2493
2494 #[test]
2495 fn test_mock_containers_populated() {
2496 let app = App::new_mock();
2497 assert!(!app.mock_containers.is_empty(), "mock_containers should be populated");
2498 assert_eq!(app.mock_containers.len(), 3);
2499 }
2500
2501 #[test]
2504 fn test_panel_toggle_keys() {
2505 let mut app = App::new_mock();
2506
2507 let original = app.panels.disk;
2509 app.handle_key(KeyCode::Char('3'), KeyModifiers::NONE);
2510 assert_ne!(app.panels.disk, original);
2511
2512 let original = app.panels.network;
2514 app.handle_key(KeyCode::Char('4'), KeyModifiers::NONE);
2515 assert_ne!(app.panels.network, original);
2516
2517 let original = app.panels.process;
2519 app.handle_key(KeyCode::Char('5'), KeyModifiers::NONE);
2520 assert_ne!(app.panels.process, original);
2521
2522 let original = app.panels.gpu;
2524 app.handle_key(KeyCode::Char('6'), KeyModifiers::NONE);
2525 assert_ne!(app.panels.gpu, original);
2526
2527 let original = app.panels.battery;
2529 app.handle_key(KeyCode::Char('7'), KeyModifiers::NONE);
2530 assert_ne!(app.panels.battery, original);
2531
2532 let original = app.panels.sensors;
2534 app.handle_key(KeyCode::Char('8'), KeyModifiers::NONE);
2535 assert_ne!(app.panels.sensors, original);
2536 }
2537
2538 #[test]
2539 fn test_navigation_when_focused() {
2540 let mut app = App::new_mock();
2541 app.focused_panel = Some(PanelType::Cpu);
2542
2543 app.handle_key(KeyCode::Char('h'), KeyModifiers::NONE);
2545 app.handle_key(KeyCode::Char('l'), KeyModifiers::NONE);
2547 app.handle_key(KeyCode::Char('j'), KeyModifiers::NONE);
2549 app.handle_key(KeyCode::Char('k'), KeyModifiers::NONE);
2551 }
2552
2553 #[test]
2554 fn test_navigation_when_not_focused() {
2555 let mut app = App::new_mock();
2556 app.focused_panel = None;
2557
2558 app.handle_key(KeyCode::Char('h'), KeyModifiers::NONE);
2560 assert!(app.focused_panel.is_some());
2561
2562 app.focused_panel = None;
2564 app.handle_key(KeyCode::Char('l'), KeyModifiers::NONE);
2565 assert!(app.focused_panel.is_some());
2566 }
2567
2568 #[test]
2569 fn test_process_navigation_pageup_pagedown() {
2570 let mut app = App::new_mock();
2571 app.process_selected = 5;
2572
2573 app.handle_key(KeyCode::PageDown, KeyModifiers::NONE);
2575 app.handle_key(KeyCode::PageUp, KeyModifiers::NONE);
2576 }
2577
2578 #[test]
2579 fn test_process_navigation_home_end() {
2580 let mut app = App::new_mock();
2581 app.process_selected = 5;
2582
2583 app.handle_key(KeyCode::Char('g'), KeyModifiers::NONE);
2585 assert_eq!(app.process_selected, 0);
2586
2587 app.handle_key(KeyCode::Char('G'), KeyModifiers::NONE);
2589 }
2590
2591 #[test]
2592 fn test_process_navigation_arrow_keys() {
2593 let mut app = App::new_mock();
2594 app.focused_panel = None;
2595 app.exploded_panel = None;
2596 app.process_selected = 5;
2597
2598 app.handle_key(KeyCode::Down, KeyModifiers::NONE);
2600 app.handle_key(KeyCode::Up, KeyModifiers::NONE);
2601 }
2602
2603 #[test]
2604 fn test_process_navigation_with_exploded_panel() {
2605 let mut app = App::new_mock();
2606 app.exploded_panel = Some(PanelType::Process);
2607 app.process_selected = 0;
2608
2609 app.handle_key(KeyCode::Char('j'), KeyModifiers::NONE);
2611 app.handle_key(KeyCode::Char('k'), KeyModifiers::NONE);
2612 }
2613
2614 #[test]
2615 fn test_help_toggle() {
2616 let mut app = App::new_mock();
2617 assert!(!app.show_help);
2618
2619 app.handle_key(KeyCode::Char('?'), KeyModifiers::NONE);
2620 assert!(app.show_help);
2621
2622 app.handle_key(KeyCode::Char('?'), KeyModifiers::NONE);
2623 assert!(!app.show_help);
2624 }
2625
2626 #[test]
2627 fn test_f1_help_toggle() {
2628 let mut app = App::new_mock();
2629 assert!(!app.show_help);
2630
2631 app.handle_key(KeyCode::F(1), KeyModifiers::NONE);
2632 assert!(app.show_help);
2633 }
2634
2635 #[test]
2636 fn test_quit_key() {
2637 let mut app = App::new_mock();
2638 let quit = app.handle_key(KeyCode::Char('q'), KeyModifiers::NONE);
2639 assert!(quit);
2640 }
2641
2642 #[test]
2643 fn test_esc_clears_focus() {
2644 let mut app = App::new_mock();
2645 app.focused_panel = Some(PanelType::Cpu);
2646
2647 app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2648 assert!(app.focused_panel.is_none());
2649 }
2650
2651 #[test]
2652 fn test_ctrl_c_returns_true() {
2653 let mut app = App::new_mock();
2654 let quit = app.handle_key(KeyCode::Char('c'), KeyModifiers::CONTROL);
2655 assert!(quit);
2656 }
2657
2658 #[test]
2659 fn test_view_mode_cycle() {
2660 let mut app = App::new_mock();
2661 let original = app.files_view_mode;
2662
2663 app.handle_key(KeyCode::Char('v'), KeyModifiers::NONE);
2664 assert_ne!(app.files_view_mode, original);
2666 }
2667
2668 #[test]
2669 fn test_pending_signal_confirm() {
2670 let mut app = App::new_mock();
2671 app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2672
2673 app.handle_key(KeyCode::Char('y'), KeyModifiers::NONE);
2675 assert!(app.pending_signal.is_none());
2676 }
2677
2678 #[test]
2679 fn test_pending_signal_cancel_with_n_key() {
2680 let mut app = App::new_mock();
2681 app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2682
2683 app.handle_key(KeyCode::Char('n'), KeyModifiers::NONE);
2685 assert!(app.pending_signal.is_none());
2686 }
2687
2688 #[test]
2689 fn test_panel_explode_toggle() {
2690 let mut app = App::new_mock();
2691 app.focused_panel = Some(PanelType::Cpu);
2692 app.exploded_panel = None;
2693
2694 app.handle_key(KeyCode::Char('z'), KeyModifiers::NONE);
2696 assert!(app.exploded_panel.is_some());
2697
2698 app.handle_key(KeyCode::Char('z'), KeyModifiers::NONE);
2699 assert!(app.exploded_panel.is_none());
2700 }
2701
2702 #[test]
2703 fn test_signal_menu_keys_huk() {
2704 let mut app = App::new_mock();
2705 app.show_signal_menu = true;
2706 app.process_selected = 0;
2707
2708 app.handle_key(KeyCode::Char('H'), KeyModifiers::NONE);
2710
2711 app.show_signal_menu = true;
2712 app.handle_key(KeyCode::Char('u'), KeyModifiers::NONE);
2714
2715 app.show_signal_menu = true;
2716 app.handle_key(KeyCode::Char('U'), KeyModifiers::NONE);
2718 }
2720
2721 #[test]
2724 fn test_signal_menu_all_signal_types() {
2725 let mut app = App::new_mock();
2726
2727 for (key, _) in [
2729 ('x', SignalType::Term),
2730 ('K', SignalType::Kill),
2731 ('i', SignalType::Int),
2732 ('p', SignalType::Stop),
2733 ('c', SignalType::Cont),
2734 ] {
2735 app.show_signal_menu = true;
2736 app.pending_signal = None;
2737 app.handle_key(KeyCode::Char(key), KeyModifiers::NONE);
2738 assert!(!app.show_signal_menu);
2739 }
2740 }
2741
2742 #[test]
2743 fn test_signal_menu_esc_closes_menu() {
2744 let mut app = App::new_mock();
2745 app.show_signal_menu = true;
2746
2747 app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2748 assert!(!app.show_signal_menu);
2749 }
2750
2751 #[test]
2752 fn test_filter_input_backspace_removal() {
2753 let mut app = App::new_mock();
2754 app.show_filter_input = true;
2755 app.filter = "test".to_string();
2756
2757 app.handle_key(KeyCode::Backspace, KeyModifiers::NONE);
2758 assert_eq!(app.filter, "tes");
2759 }
2760
2761 #[test]
2762 fn test_filter_input_esc_clears_text() {
2763 let mut app = App::new_mock();
2764 app.show_filter_input = true;
2765 app.filter = "test".to_string();
2766
2767 app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2768 assert!(!app.show_filter_input);
2769 assert!(app.filter.is_empty());
2770 }
2771
2772 #[test]
2773 fn test_filter_input_enter_preserves() {
2774 let mut app = App::new_mock();
2775 app.show_filter_input = true;
2776 app.filter = "test".to_string();
2777
2778 app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
2779 assert!(!app.show_filter_input);
2780 assert_eq!(app.filter, "test"); }
2782
2783 #[test]
2784 fn test_filter_input_add_char() {
2785 let mut app = App::new_mock();
2786 app.show_filter_input = true;
2787 app.filter = String::new();
2788
2789 app.handle_key(KeyCode::Char('a'), KeyModifiers::NONE);
2790 assert_eq!(app.filter, "a");
2791 }
2792
2793 #[test]
2794 fn test_pending_signal_enter_confirms() {
2795 let mut app = App::new_mock();
2796 app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2797
2798 app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
2799 assert!(app.pending_signal.is_none());
2800 }
2801
2802 #[test]
2803 fn test_pending_signal_capital_y_confirms() {
2804 let mut app = App::new_mock();
2805 app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2806
2807 app.handle_key(KeyCode::Char('Y'), KeyModifiers::NONE);
2808 assert!(app.pending_signal.is_none());
2809 }
2810
2811 #[test]
2812 fn test_pending_signal_capital_n_cancels() {
2813 let mut app = App::new_mock();
2814 app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2815
2816 app.handle_key(KeyCode::Char('N'), KeyModifiers::NONE);
2817 assert!(app.pending_signal.is_none());
2818 }
2819
2820 #[test]
2821 fn test_pending_signal_esc_cancels_prompt() {
2822 let mut app = App::new_mock();
2823 app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2824
2825 app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2826 assert!(app.pending_signal.is_none());
2827 }
2828
2829 #[test]
2830 fn test_exploded_panel_enter_exits() {
2831 let mut app = App::new_mock();
2832 app.exploded_panel = Some(PanelType::Cpu);
2833
2834 app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
2835 assert!(app.exploded_panel.is_none());
2836 }
2837
2838 #[test]
2839 fn test_focused_panel_arrow_navigation() {
2840 let mut app = App::new_mock();
2841 app.focused_panel = Some(PanelType::Cpu);
2842 app.exploded_panel = None;
2843
2844 app.handle_key(KeyCode::Left, KeyModifiers::NONE);
2846 app.handle_key(KeyCode::Right, KeyModifiers::NONE);
2847 app.handle_key(KeyCode::Up, KeyModifiers::NONE);
2848 app.handle_key(KeyCode::Down, KeyModifiers::NONE);
2849 }
2850
2851 #[test]
2852 fn test_focused_panel_hjkl_navigation() {
2853 let mut app = App::new_mock();
2854 app.focused_panel = Some(PanelType::Memory);
2855 app.exploded_panel = None;
2856
2857 app.handle_key(KeyCode::Char('h'), KeyModifiers::NONE);
2859 app.handle_key(KeyCode::Char('l'), KeyModifiers::NONE);
2860 }
2861
2862 #[test]
2863 fn test_focused_panel_enter_explodes() {
2864 let mut app = App::new_mock();
2865 app.focused_panel = Some(PanelType::Disk);
2866 app.exploded_panel = None;
2867
2868 app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
2869 assert_eq!(app.exploded_panel, Some(PanelType::Disk));
2870 }
2871
2872 #[test]
2873 fn test_unfocused_process_navigation_jk() {
2874 let mut app = App::new_mock();
2875 app.focused_panel = None;
2876 app.exploded_panel = None;
2877 app.process_selected = 5;
2878
2879 app.handle_key(KeyCode::Char('j'), KeyModifiers::NONE);
2880 app.handle_key(KeyCode::Char('k'), KeyModifiers::NONE);
2881 }
2882
2883 #[test]
2884 fn test_push_to_history_overflow() {
2885 let mut history = Vec::new();
2886 for i in 0..305 {
2887 App::push_to_history(&mut history, i as f64);
2888 }
2889 assert_eq!(history.len(), 300);
2890 assert_eq!(history[0], 5.0); }
2892
2893 #[test]
2894 fn test_panel_visibility_all_fields() {
2895 let vis = PanelVisibility {
2896 cpu: false,
2897 memory: false,
2898 disk: false,
2899 network: false,
2900 process: false,
2901 gpu: false,
2902 battery: false,
2903 sensors: false,
2904 files: true,
2905 };
2906 assert!(!vis.cpu);
2907 assert!(vis.files);
2908 }
2909
2910 #[test]
2911 fn test_mock_gpu_data_debug() {
2912 let gpu = MockGpuData {
2913 name: "Test GPU".to_string(),
2914 gpu_util: 50.0,
2915 vram_used: 1000,
2916 vram_total: 8000,
2917 temperature: 65.0,
2918 power_watts: 150,
2919 power_limit_watts: 300,
2920 clock_mhz: 1500,
2921 history: vec![0.5],
2922 };
2923 let debug = format!("{:?}", gpu);
2924 assert!(debug.contains("Test GPU"));
2925 }
2926
2927 #[test]
2928 fn test_mock_battery_data_debug() {
2929 let bat = MockBatteryData {
2930 percent: 75.0,
2931 charging: true,
2932 time_remaining_mins: Some(120),
2933 power_watts: 45.0,
2934 health_percent: 95.0,
2935 cycle_count: 500,
2936 };
2937 let debug = format!("{:?}", bat);
2938 assert!(debug.contains("75"));
2939 }
2940
2941 #[test]
2942 fn test_mock_sensor_data_debug() {
2943 let sensor = MockSensorData {
2944 name: "cpu/temp1".to_string(),
2945 label: "CPU".to_string(),
2946 value: 65.0,
2947 max: Some(90.0),
2948 crit: Some(100.0),
2949 sensor_type: MockSensorType::Temperature,
2950 };
2951 let debug = format!("{:?}", sensor);
2952 assert!(debug.contains("cpu/temp1"));
2953 }
2954
2955 #[test]
2956 fn test_mock_container_data_debug() {
2957 let container = MockContainerData {
2958 name: "nginx".to_string(),
2959 status: "running".to_string(),
2960 cpu_percent: 5.0,
2961 mem_used: 100_000_000,
2962 mem_limit: 1_000_000_000,
2963 };
2964 let debug = format!("{:?}", container);
2965 assert!(debug.contains("nginx"));
2966 }
2967
2968 #[test]
2969 fn test_mock_sensor_type_equality() {
2970 assert_eq!(MockSensorType::Temperature, MockSensorType::Temperature);
2971 assert_ne!(MockSensorType::Temperature, MockSensorType::Fan);
2972 assert_ne!(MockSensorType::Voltage, MockSensorType::Power);
2973 }
2974
2975 #[test]
2976 fn test_toggle_panel_9_files_toggle() {
2977 let mut app = App::new_mock();
2978 let initial = app.panels.files;
2979
2980 app.handle_key(KeyCode::Char('9'), KeyModifiers::NONE);
2981 assert_ne!(app.panels.files, initial);
2982 }
2983
2984 #[test]
2991 fn test_filter_no_hang_with_5000_items() {
2992 use jugar_probar::tui_load::{DataGenerator, TuiLoadTest};
2993 use std::time::{Duration, Instant};
2994
2995 let generator = DataGenerator::new(5000);
2997 let items = generator.generate();
2998
2999 let timeout = Duration::from_millis(1000);
3001 let filters = ["", "a", "sys", "chrome", "nonexistent_long_filter_string"];
3002
3003 for filter in filters {
3004 let filter_lower = filter.to_lowercase();
3005 let start = Instant::now();
3006
3007 let filtered: Vec<_> = items
3009 .iter()
3010 .filter(|item| {
3011 if filter_lower.is_empty() {
3012 true
3013 } else {
3014 item.name.to_lowercase().contains(&filter_lower)
3015 || item.description.to_lowercase().contains(&filter_lower)
3016 }
3017 })
3018 .collect();
3019
3020 let elapsed = start.elapsed();
3021
3022 assert!(
3023 elapsed < timeout,
3024 "Filter '{}' took {:?} (timeout: {:?}) - HANG DETECTED with {} items, {} results",
3025 filter, elapsed, timeout, items.len(), filtered.len()
3026 );
3027 }
3028 }
3029
3030 #[test]
3032 fn test_filter_scales_linearly() {
3033 use jugar_probar::tui_load::DataGenerator;
3034 use std::time::Instant;
3035
3036 let sizes = [100, 500, 1000, 2000, 5000];
3037 let mut times_us = Vec::new();
3038
3039 for size in sizes {
3040 let items = DataGenerator::new(size).generate();
3041 let filter_lower = "sys".to_lowercase();
3042
3043 let start = Instant::now();
3044 for _ in 0..10 {
3046 let _: Vec<_> = items
3047 .iter()
3048 .filter(|item| {
3049 item.name.to_lowercase().contains(&filter_lower)
3050 || item.description.to_lowercase().contains(&filter_lower)
3051 })
3052 .collect();
3053 }
3054 let elapsed = start.elapsed().as_micros() as u64;
3055 times_us.push((size, elapsed));
3056 }
3057
3058 let time_1k = times_us.iter().find(|(s, _)| *s == 1000).map(|(_, t)| *t).unwrap_or(1);
3061 let time_5k = times_us.iter().find(|(s, _)| *s == 5000).map(|(_, t)| *t).unwrap_or(1);
3062
3063 let ratio = time_5k as f64 / time_1k as f64;
3064
3065 assert!(
3067 ratio < 8.0,
3068 "Filter time scaled {}x from 1K to 5K items (expected ~5x). \
3069 Times: {:?}. May indicate O(n²) complexity.",
3070 ratio, times_us
3071 );
3072 }
3073
3074 #[test]
3076 fn test_filter_stress_with_probar() {
3077 use jugar_probar::tui_load::TuiLoadTest;
3078
3079 let load_test = TuiLoadTest::new()
3080 .with_item_count(5000) .with_timeout_ms(2000) .with_frames_per_filter(3);
3083
3084 let result = load_test.run_filter_stress(|items, filter| {
3086 let filter_lower = filter.to_lowercase();
3087 items
3088 .iter()
3089 .filter(|item| {
3090 if filter_lower.is_empty() {
3091 true
3092 } else {
3093 contains_ignore_case(&item.name, &filter_lower)
3095 || contains_ignore_case(&item.description, &filter_lower)
3096 }
3097 })
3098 .cloned()
3099 .collect()
3100 });
3101
3102 assert!(result.is_ok(), "TUI filter stress test detected hang: {:?}", result.err());
3104
3105 let results = result.expect("result should be ok");
3107 assert!(!results.is_empty(), "Should have run at least one filter");
3108
3109 for (filter, metrics) in &results {
3111 let avg = metrics.avg_frame_ms();
3112 assert!(
3113 avg < 500.0,
3114 "Filter '{}' took {:.1}ms avg - too slow for responsive UI",
3115 filter, avg
3116 );
3117 }
3118 }
3119
3120 #[test]
3131 fn test_integration_load_real_app_no_hang() {
3132 use jugar_probar::tui_load::{ComponentTimings, IntegrationLoadTest};
3133
3134 let test = IntegrationLoadTest::new()
3136 .with_frame_budget_ms(500.0) .with_timeout_ms(5000) .with_frame_count(3) .with_component_budget("container_analyzer", 200.0) .with_component_budget("disk_analyzer", 200.0)
3142 .with_component_budget("network_analyzer", 200.0)
3143 .with_component_budget("sensor_analyzer", 100.0);
3144
3145 let first_frame = std::sync::atomic::AtomicBool::new(true);
3147 let app = std::sync::Mutex::new(None::<App>);
3148
3149 let result = test.run(|| {
3150 let mut timings = ComponentTimings::new();
3151
3152 let mut guard = app.lock().expect("lock");
3154 let app = guard.get_or_insert_with(|| {
3155 App::new(false, false) });
3158
3159 let start = Instant::now();
3161 app.container_analyzer.collect();
3162 timings.record("container_analyzer", start.elapsed().as_secs_f64() * 1000.0);
3163
3164 let start = Instant::now();
3165 app.disk_io_analyzer.collect();
3166 timings.record("disk_analyzer", start.elapsed().as_secs_f64() * 1000.0);
3167
3168 let start = Instant::now();
3169 app.network_stats.collect();
3170 timings.record("network_analyzer", start.elapsed().as_secs_f64() * 1000.0);
3171
3172 let start = Instant::now();
3173 app.sensor_health.collect();
3174 timings.record("sensor_analyzer", start.elapsed().as_secs_f64() * 1000.0);
3175
3176 if first_frame.swap(false, std::sync::atomic::Ordering::SeqCst) {
3178 return ComponentTimings::new();
3180 }
3181
3182 timings
3183 });
3184
3185 assert!(
3188 result.is_ok(),
3189 "Integration load test failed! This catches real collector hangs: {:?}",
3190 result.err()
3191 );
3192
3193 let metrics = result.expect("test passed");
3194 assert!(
3195 metrics.p95_frame_ms() < 1000.0,
3196 "Frame time p95 {:.1}ms is too slow",
3197 metrics.p95_frame_ms()
3198 );
3199 }
3200
3201 #[test]
3220 fn test_per_core_history_exists() {
3221 let app = App::new_mock();
3222 assert!(!app.per_core_history.is_empty(), "per_core_history must exist");
3224 assert_eq!(app.per_core_history.len(), app.per_core_percent.len(),
3225 "per_core_history should have one entry per core");
3226 }
3227
3228 #[test]
3230 fn test_per_core_history_capacity() {
3231 let app = App::new_mock();
3232 for (i, history) in app.per_core_history.iter().enumerate() {
3234 assert!(history.capacity() >= 60,
3235 "Core {} history should have capacity for 60 samples, got {}",
3236 i, history.capacity());
3237 }
3238 }
3239
3240 #[test]
3242 fn test_per_core_history_values_valid() {
3243 let app = App::new_mock();
3244 for (i, history) in app.per_core_history.iter().enumerate() {
3245 for (j, &value) in history.iter().enumerate() {
3246 assert!(value >= 0.0 && value <= 100.0,
3247 "Core {} history[{}] = {} is invalid (must be 0-100)",
3248 i, j, value);
3249 }
3250 }
3251 }
3252
3253 #[test]
3255 fn test_cpu_state_breakdown_exists() {
3256 let app = App::new_mock();
3257 assert!(!app.per_core_state.is_empty(), "per_core_state must exist");
3259 assert_eq!(app.per_core_state.len(), app.per_core_percent.len(),
3260 "per_core_state should have one entry per core");
3261 }
3262
3263 #[test]
3265 fn test_cpu_state_breakdown_components() {
3266 let app = App::new_mock();
3267 for (i, state) in app.per_core_state.iter().enumerate() {
3268 let total = state.user + state.system + state.iowait + state.idle;
3270 assert!((total - 100.0).abs() < 1.0,
3271 "Core {} state breakdown should sum to ~100%, got {:.1}%",
3272 i, total);
3273 }
3274 }
3275
3276 #[test]
3278 fn test_freq_history_exists() {
3279 let app = App::new_mock();
3280 assert!(app.freq_history.capacity() >= 60,
3282 "freq_history should have capacity for 60 samples");
3283 }
3284
3285 #[test]
3287 fn test_top_process_per_core_exists() {
3288 let app = App::new_mock();
3289 assert!(!app.top_process_per_core.is_empty(),
3291 "top_process_per_core must exist");
3292 }
3293
3294 #[test]
3296 fn test_thermal_throttling_state_exists() {
3297 let app = App::new_mock();
3298 assert!(app.thermal_throttle_active.is_some() || !app.thermal_throttle_active.is_some(),
3300 "thermal_throttle_active field must exist (can be None)");
3301 }
3302
3303 #[test]
3305 fn test_per_core_history_update_performance() {
3306 use std::time::Instant;
3307
3308 let mut app = App::new_mock();
3309 let core_count = 48; app.per_core_history = vec![Vec::with_capacity(60); core_count];
3313 app.per_core_percent = vec![50.0; core_count];
3314
3315 let start = Instant::now();
3317 for _ in 0..1000 {
3318 for (i, history) in app.per_core_history.iter_mut().enumerate() {
3320 let value = app.per_core_percent.get(i).copied().unwrap_or(0.0);
3321 if history.len() >= 60 {
3322 history.remove(0);
3323 }
3324 history.push(value);
3325 }
3326 }
3327 let elapsed = start.elapsed();
3328
3329 assert!(elapsed.as_millis() < 100,
3331 "1000 per-core history updates took {:?} (should be < 100ms)",
3332 elapsed);
3333 }
3334
3335 #[test]
3337 fn test_cpu_core_state_struct() {
3338 let state = CpuCoreState {
3339 user: 25.0,
3340 system: 10.0,
3341 iowait: 5.0,
3342 idle: 60.0,
3343 };
3344 assert_eq!(state.user, 25.0);
3345 assert_eq!(state.system, 10.0);
3346 assert_eq!(state.iowait, 5.0);
3347 assert_eq!(state.idle, 60.0);
3348 assert!((state.total_busy() - 40.0).abs() < 0.1);
3349 }
3350
3351 #[test]
3370 fn test_mem_pressure_history_exists() {
3371 let app = App::new_mock();
3372 assert!(app.mem_pressure_history.capacity() >= 60,
3373 "mem_pressure_history should have capacity for 60 samples");
3374 }
3375
3376 #[test]
3378 fn test_mem_reclaim_rate_exists() {
3379 let app = App::new_mock();
3380 assert!(app.mem_reclaim_rate >= 0.0, "mem_reclaim_rate should be >= 0");
3382 }
3383
3384 #[test]
3386 fn test_top_mem_consumers_exists() {
3387 let app = App::new_mock();
3388 assert!(!app.top_mem_consumers.is_empty(),
3389 "top_mem_consumers should have at least one entry");
3390 }
3391
3392 #[test]
3394 fn test_mem_breakdown_struct() {
3395 let breakdown = MemoryBreakdown {
3396 used_bytes: 8 * 1024 * 1024 * 1024,
3397 cached_bytes: 4 * 1024 * 1024 * 1024,
3398 buffers_bytes: 512 * 1024 * 1024,
3399 free_bytes: 3 * 1024 * 1024 * 1024,
3400 };
3401 assert_eq!(breakdown.used_bytes, 8 * 1024 * 1024 * 1024);
3402 assert_eq!(breakdown.cached_bytes, 4 * 1024 * 1024 * 1024);
3403 }
3404
3405 #[test]
3407 fn test_swap_trend_exists() {
3408 let app = App::new_mock();
3409 match app.swap_trend {
3411 SwapTrend::Rising | SwapTrend::Falling | SwapTrend::Stable => (),
3412 }
3413 }
3414
3415 #[test]
3428 fn test_disk_latency_history_exists() {
3429 let app = App::new_mock();
3430 assert!(app.disk_latency_history.capacity() >= 60,
3431 "disk_latency_history should have capacity for 60 samples");
3432 }
3433
3434 #[test]
3436 fn test_disk_iops_breakdown_exists() {
3437 let app = App::new_mock();
3438 assert!(app.disk_read_iops >= 0.0, "disk_read_iops should be >= 0");
3439 assert!(app.disk_write_iops >= 0.0, "disk_write_iops should be >= 0");
3440 }
3441
3442 #[test]
3444 fn test_disk_queue_depth_exists() {
3445 let app = App::new_mock();
3446 assert!(app.disk_queue_depth >= 0.0, "disk_queue_depth should be >= 0");
3447 }
3448
3449 #[test]
3451 fn test_disk_health_struct() {
3452 let health = DiskHealthStatus {
3453 device: "sda".to_string(),
3454 status: DiskHealth::Good,
3455 temperature: Some(35.0),
3456 reallocated_sectors: 0,
3457 };
3458 assert_eq!(health.device, "sda");
3459 assert_eq!(health.status, DiskHealth::Good);
3460 }
3461
3462 #[test]
3464 fn test_disk_health_status_exists() {
3465 let app = App::new_mock();
3466 assert!(app.disk_health.is_empty() || !app.disk_health.is_empty(),
3468 "disk_health field must exist");
3469 }
3470
3471 #[test]
3484 fn test_net_interface_history_exists() {
3485 let app = App::new_mock();
3486 assert!(app.net_rx_history.capacity() >= 8,
3487 "net_rx_history should have capacity for samples");
3488 assert!(app.net_tx_history.capacity() >= 8,
3489 "net_tx_history should have capacity for samples");
3490 }
3491
3492 #[test]
3494 fn test_net_error_counts_exists() {
3495 let app = App::new_mock();
3496 assert!(app.net_errors >= 0, "net_errors should be >= 0");
3497 assert!(app.net_drops >= 0, "net_drops should be >= 0");
3498 }
3499
3500 #[test]
3502 fn test_net_connection_states_exists() {
3503 let app = App::new_mock();
3504 assert!(app.net_established >= 0, "net_established should be >= 0");
3505 assert!(app.net_listening >= 0, "net_listening should be >= 0");
3506 }
3507
3508 #[test]
3521 fn test_process_tree_data_exists() {
3522 let app = App::new_mock();
3523 assert!(!app.show_tree || app.show_tree, "show_tree field must exist");
3525 }
3526
3527 #[test]
3529 fn test_process_extra_fields_exist() {
3530 let app = App::new_mock();
3531 let _ = &app.process_extra;
3534 assert!(true, "process_extra analyzer must exist");
3535 }
3536}