1use compact_str::CompactString;
13use presentar_core::{
14 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event, Key,
15 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
16};
17use std::any::Any;
18use std::collections::HashMap;
19use std::time::Duration;
20
21use super::micro_heat_bar::HeatScheme;
22use super::selection::RowHighlight;
23
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
26pub enum ProcessDisplayState {
27 #[default]
28 Running,
29 Sleeping,
30 Idle,
31 Zombie,
32 Stopped,
33 Unknown,
34}
35
36impl ProcessDisplayState {
37 #[must_use]
39 pub fn render(self) -> (char, Color) {
40 match self {
41 Self::Running => ('R', Color::new(0.2, 0.9, 0.2, 1.0)),
42 Self::Sleeping => ('S', Color::new(0.6, 0.6, 0.6, 1.0)),
43 Self::Idle => ('I', Color::new(0.5, 0.5, 0.5, 1.0)),
44 Self::Zombie => ('Z', Color::new(0.9, 0.2, 0.2, 1.0)),
45 Self::Stopped => ('T', Color::new(0.9, 0.7, 0.1, 1.0)),
46 Self::Unknown => ('?', Color::new(0.4, 0.4, 0.4, 1.0)),
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
53pub enum ProcessSortColumn {
54 Pid,
55 Name,
56 #[default]
57 Cpu,
58 Mem,
59 Threads,
60 Priority,
61 User,
62 Time,
63}
64
65impl ProcessSortColumn {
66 #[must_use]
68 pub fn header(self) -> &'static str {
69 match self {
70 Self::Pid => "PID",
71 Self::Name => "COMMAND",
72 Self::Cpu => "CPU%",
73 Self::Mem => "MEM%",
74 Self::Threads => "THR",
75 Self::Priority => "PRI",
76 Self::User => "USER",
77 Self::Time => "TIME+",
78 }
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct ProcessRow {
85 pub pid: u32,
87 pub name: CompactString,
89 pub cpu_percent: f32,
91 pub cpu_history: Vec<f32>,
93 pub mem_percent: f32,
95 pub state: ProcessDisplayState,
97 pub threads: u32,
99 pub priority: i32,
101 pub user: CompactString,
103 pub cpu_time_secs: u64,
105}
106
107impl Default for ProcessRow {
108 fn default() -> Self {
109 Self {
110 pid: 0,
111 name: CompactString::new(""),
112 cpu_percent: 0.0,
113 cpu_history: Vec::new(),
114 mem_percent: 0.0,
115 state: ProcessDisplayState::default(),
116 threads: 1,
117 priority: 0,
118 user: CompactString::new(""),
119 cpu_time_secs: 0,
120 }
121 }
122}
123
124#[derive(Debug, Clone)]
126pub struct ProcessDataFrame {
127 rows: Vec<ProcessRow>,
129 cpu_histories: HashMap<u32, Vec<f32>>,
131 history_len: usize,
133 sort_column: ProcessSortColumn,
135 sort_desc: bool,
137 selected_row: Option<usize>,
139 scroll_offset: usize,
141 show_header: bool,
143 col_widths: ProcessColumnWidths,
145 accent_color: Color,
147 bounds: Rect,
149}
150
151#[derive(Debug, Clone, Copy)]
153pub struct ProcessColumnWidths {
154 pub pid: usize,
155 pub name: usize,
156 pub cpu: usize,
157 pub sparkline: usize,
158 pub mem: usize,
159 pub state: usize,
160 pub threads: usize,
161 pub priority: usize,
162 pub user: usize,
163 pub time: usize,
164}
165
166impl Default for ProcessColumnWidths {
167 fn default() -> Self {
168 Self {
169 pid: 7,
170 name: 20,
171 cpu: 6,
172 sparkline: 12,
173 mem: 6,
174 state: 3,
175 threads: 4,
176 priority: 4,
177 user: 10,
178 time: 10,
179 }
180 }
181}
182
183impl Default for ProcessDataFrame {
184 fn default() -> Self {
185 Self::new()
186 }
187}
188
189impl ProcessDataFrame {
190 #[must_use]
192 pub fn new() -> Self {
193 Self {
194 rows: Vec::new(),
195 cpu_histories: HashMap::new(),
196 history_len: 60, sort_column: ProcessSortColumn::Cpu,
198 sort_desc: true,
199 selected_row: None,
200 scroll_offset: 0,
201 show_header: true,
202 col_widths: ProcessColumnWidths::default(),
203 accent_color: Color::new(0.4, 0.7, 1.0, 1.0),
204 bounds: Rect::default(),
205 }
206 }
207
208 pub fn update_processes(&mut self, processes: Vec<ProcessRow>) {
210 for proc in &processes {
212 let history = self.cpu_histories.entry(proc.pid).or_default();
213 history.push(proc.cpu_percent);
214 if history.len() > self.history_len {
215 history.remove(0);
216 }
217 }
218
219 let live_pids: std::collections::HashSet<u32> = processes.iter().map(|p| p.pid).collect();
221 self.cpu_histories.retain(|pid, _| live_pids.contains(pid));
222
223 self.rows = processes
225 .into_iter()
226 .map(|mut proc| {
227 if let Some(history) = self.cpu_histories.get(&proc.pid) {
228 proc.cpu_history = history.clone();
229 }
230 proc
231 })
232 .collect();
233
234 self.sort_rows();
235 }
236
237 #[must_use]
239 pub fn with_sort(mut self, column: ProcessSortColumn, descending: bool) -> Self {
240 self.sort_column = column;
241 self.sort_desc = descending;
242 self
243 }
244
245 #[must_use]
247 pub fn with_accent_color(mut self, color: Color) -> Self {
248 self.accent_color = color;
249 self
250 }
251
252 #[must_use]
254 pub fn with_column_widths(mut self, widths: ProcessColumnWidths) -> Self {
255 self.col_widths = widths;
256 self
257 }
258
259 #[must_use]
261 pub fn with_history_len(mut self, len: usize) -> Self {
262 self.history_len = len;
263 self
264 }
265
266 #[must_use]
268 pub fn selected_pid(&self) -> Option<u32> {
269 self.selected_row
270 .and_then(|idx| self.rows.get(idx).map(|r| r.pid))
271 }
272
273 pub fn scroll_up(&mut self) {
275 if let Some(sel) = self.selected_row {
276 if sel > 0 {
277 self.selected_row = Some(sel - 1);
278 if sel - 1 < self.scroll_offset {
279 self.scroll_offset = sel - 1;
280 }
281 }
282 } else if !self.rows.is_empty() {
283 self.selected_row = Some(0);
284 }
285 }
286
287 pub fn scroll_down(&mut self) {
289 let max_visible = self.visible_rows();
290 if let Some(sel) = self.selected_row {
291 if sel < self.rows.len().saturating_sub(1) {
292 self.selected_row = Some(sel + 1);
293 if sel + 1 >= self.scroll_offset + max_visible {
294 self.scroll_offset = (sel + 2).saturating_sub(max_visible);
295 }
296 }
297 } else if !self.rows.is_empty() {
298 self.selected_row = Some(0);
299 }
300 }
301
302 pub fn cycle_sort(&mut self) {
304 self.sort_column = match self.sort_column {
305 ProcessSortColumn::Cpu => ProcessSortColumn::Mem,
306 ProcessSortColumn::Mem => ProcessSortColumn::Pid,
307 ProcessSortColumn::Pid => ProcessSortColumn::Name,
308 ProcessSortColumn::Name => ProcessSortColumn::Threads,
309 ProcessSortColumn::Threads => ProcessSortColumn::Time,
310 ProcessSortColumn::Time => ProcessSortColumn::Cpu,
311 _ => ProcessSortColumn::Cpu,
312 };
313 self.sort_rows();
314 }
315
316 pub fn toggle_sort_direction(&mut self) {
318 self.sort_desc = !self.sort_desc;
319 self.sort_rows();
320 }
321
322 fn sort_rows(&mut self) {
323 let desc = self.sort_desc;
324 match self.sort_column {
325 ProcessSortColumn::Pid => {
326 self.rows.sort_by(|a, b| {
327 if desc {
328 b.pid.cmp(&a.pid)
329 } else {
330 a.pid.cmp(&b.pid)
331 }
332 });
333 }
334 ProcessSortColumn::Name => {
335 self.rows.sort_by(|a, b| {
336 if desc {
337 b.name.cmp(&a.name)
338 } else {
339 a.name.cmp(&b.name)
340 }
341 });
342 }
343 ProcessSortColumn::Cpu => {
344 self.rows.sort_by(|a, b| {
345 if desc {
346 b.cpu_percent
347 .partial_cmp(&a.cpu_percent)
348 .unwrap_or(std::cmp::Ordering::Equal)
349 } else {
350 a.cpu_percent
351 .partial_cmp(&b.cpu_percent)
352 .unwrap_or(std::cmp::Ordering::Equal)
353 }
354 });
355 }
356 ProcessSortColumn::Mem => {
357 self.rows.sort_by(|a, b| {
358 if desc {
359 b.mem_percent
360 .partial_cmp(&a.mem_percent)
361 .unwrap_or(std::cmp::Ordering::Equal)
362 } else {
363 a.mem_percent
364 .partial_cmp(&b.mem_percent)
365 .unwrap_or(std::cmp::Ordering::Equal)
366 }
367 });
368 }
369 ProcessSortColumn::Threads => {
370 self.rows.sort_by(|a, b| {
371 if desc {
372 b.threads.cmp(&a.threads)
373 } else {
374 a.threads.cmp(&b.threads)
375 }
376 });
377 }
378 ProcessSortColumn::Priority => {
379 self.rows.sort_by(|a, b| {
380 if desc {
381 b.priority.cmp(&a.priority)
382 } else {
383 a.priority.cmp(&b.priority)
384 }
385 });
386 }
387 ProcessSortColumn::User => {
388 self.rows.sort_by(|a, b| {
389 if desc {
390 b.user.cmp(&a.user)
391 } else {
392 a.user.cmp(&b.user)
393 }
394 });
395 }
396 ProcessSortColumn::Time => {
397 self.rows.sort_by(|a, b| {
398 if desc {
399 b.cpu_time_secs.cmp(&a.cpu_time_secs)
400 } else {
401 a.cpu_time_secs.cmp(&b.cpu_time_secs)
402 }
403 });
404 }
405 }
406 }
407
408 fn visible_rows(&self) -> usize {
409 let header_offset = if self.show_header { 2 } else { 0 };
410 (self.bounds.height as usize).saturating_sub(header_offset)
411 }
412
413 fn render_sparkline(values: &[f32], width: usize) -> String {
414 const BARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
415
416 if values.is_empty() {
417 return "─".repeat(width);
418 }
419
420 let sample_width = width.min(values.len());
422 let step = values.len().saturating_sub(1) / sample_width.max(1);
423
424 (0..sample_width)
425 .map(|i| {
426 let idx = (i * step.max(1)).min(values.len().saturating_sub(1));
427 let v = values[idx].clamp(0.0, 100.0);
428 let norm = ((v / 100.0) * 7.0).round() as usize;
429 BARS[norm.min(7)]
430 })
431 .collect()
432 }
433
434 fn render_microbar(value: f32, width: usize) -> String {
435 let pct = (value / 100.0).clamp(0.0, 1.0);
436 let filled = ((width as f32) * pct).round() as usize;
437 let empty = width.saturating_sub(filled);
438 format!("{}{}", "█".repeat(filled), "░".repeat(empty))
439 }
440
441 fn format_time(secs: u64) -> String {
442 let hours = secs / 3600;
443 let mins = (secs % 3600) / 60;
444 let secs = secs % 60;
445 if hours > 99 {
446 format!("{hours}h")
447 } else if hours > 0 {
448 format!("{hours}:{mins:02}:{secs:02}")
449 } else {
450 format!("{mins}:{secs:02}")
451 }
452 }
453}
454
455impl Brick for ProcessDataFrame {
456 fn brick_name(&self) -> &'static str {
457 "process_dataframe"
458 }
459
460 fn assertions(&self) -> &[BrickAssertion] {
461 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
462 ASSERTIONS
463 }
464
465 fn budget(&self) -> BrickBudget {
466 BrickBudget::uniform(16)
467 }
468
469 fn verify(&self) -> BrickVerification {
470 BrickVerification {
471 passed: self.assertions().to_vec(),
472 failed: vec![],
473 verification_time: Duration::from_micros(10),
474 }
475 }
476
477 fn to_html(&self) -> String {
478 format!(
479 r#"<table class="process-dataframe"><thead><tr>{}</tr></thead></table>"#,
480 ["PID", "COMMAND", "CPU%", "MEM%", "THR", "USER"]
481 .iter()
482 .map(|h| format!("<th>{h}</th>"))
483 .collect::<String>()
484 )
485 }
486
487 fn to_css(&self) -> String {
488 ".process-dataframe { sort: cpu; }".to_string()
489 }
490}
491
492impl Widget for ProcessDataFrame {
493 fn type_id(&self) -> TypeId {
494 TypeId::of::<Self>()
495 }
496
497 fn measure(&self, constraints: Constraints) -> Size {
498 let header_height = if self.show_header { 2.0 } else { 0.0 };
499 let row_height = self.rows.len().min(30) as f32;
500 let height = (header_height + row_height).min(constraints.max_height);
501 constraints.constrain(Size::new(constraints.max_width, height))
502 }
503
504 fn layout(&mut self, bounds: Rect) -> LayoutResult {
505 self.bounds = bounds;
506 LayoutResult {
507 size: Size::new(bounds.width, bounds.height),
508 }
509 }
510
511 fn paint(&self, canvas: &mut dyn Canvas) {
512 if self.bounds.width < 40.0 || self.bounds.height < 3.0 {
513 return;
514 }
515
516 let mut y = self.bounds.y;
517 let x_start = self.bounds.x;
518 let w = &self.col_widths;
519
520 let col_positions = [
522 0usize, w.pid, w.pid + w.name, w.pid + w.name + w.cpu, w.pid + w.name + w.cpu + w.sparkline, w.pid + w.name + w.cpu + w.sparkline + w.mem, w.pid + w.name + w.cpu + w.sparkline + w.mem + w.state, w.pid + w.name + w.cpu + w.sparkline + w.mem + w.state + w.threads, w.pid + w.name + w.cpu + w.sparkline + w.mem + w.state + w.threads + w.user, ];
532
533 let header_style = TextStyle {
534 color: self.accent_color,
535 ..Default::default()
536 };
537
538 let dim_style = TextStyle {
539 color: Color::new(0.5, 0.5, 0.5, 1.0),
540 ..Default::default()
541 };
542
543 if self.show_header {
545 let headers = [
546 (ProcessSortColumn::Pid, w.pid),
547 (ProcessSortColumn::Name, w.name),
548 (ProcessSortColumn::Cpu, w.cpu + w.sparkline),
549 (ProcessSortColumn::Mem, w.mem),
550 (ProcessSortColumn::Threads, w.state + w.threads),
551 (ProcessSortColumn::User, w.user),
552 (ProcessSortColumn::Time, w.time),
553 ];
554
555 let mut hx = x_start;
556 for (col, width) in headers {
557 let is_sorted = col == self.sort_column;
558 let arrow = if is_sorted {
559 if self.sort_desc {
560 "▼"
561 } else {
562 "▲"
563 }
564 } else {
565 ""
566 };
567 let text = format!("{}{}", col.header(), arrow);
568 let text = if text.len() > width {
569 text[..width].to_string()
570 } else {
571 text
572 };
573
574 let style = if is_sorted { &header_style } else { &dim_style };
575 canvas.draw_text(&text, Point::new(hx, y), style);
576 hx += width as f32 + 1.0;
577 }
578
579 y += 1.0;
580
581 let sep = "─".repeat((self.bounds.width as usize).min(120));
583 canvas.draw_text(&sep, Point::new(x_start, y), &dim_style);
584 y += 1.0;
585 }
586
587 let visible_rows = self.visible_rows();
589 let end_idx = (self.scroll_offset + visible_rows).min(self.rows.len());
590
591 for (rel_idx, row) in self.rows[self.scroll_offset..end_idx].iter().enumerate() {
592 let abs_idx = self.scroll_offset + rel_idx;
593 let is_selected = self.selected_row == Some(abs_idx);
594
595 let row_bounds = Rect::new(x_start, y, self.bounds.width, 1.0);
598 let row_highlight = RowHighlight::new(row_bounds, is_selected);
599 row_highlight.paint(canvas);
600
601 let text_style = row_highlight.text_style();
603
604 let pid_str = format!("{:>width$}", row.pid, width = w.pid);
606 canvas.draw_text(
607 &pid_str,
608 Point::new(x_start + col_positions[0] as f32, y),
609 &text_style,
610 );
611
612 let name = if row.name.len() > w.name {
614 format!("{}…", &row.name[..w.name - 1])
615 } else {
616 format!("{:<width$}", row.name, width = w.name)
617 };
618 canvas.draw_text(
619 &name,
620 Point::new(x_start + col_positions[1] as f32, y),
621 &text_style,
622 );
623
624 let cpu_color = HeatScheme::Thermal.color_for_percent(row.cpu_percent as f64);
627 let cpu_str = format!("{:>5.1}", row.cpu_percent);
628 canvas.draw_text(
629 &cpu_str,
630 Point::new(x_start + col_positions[2] as f32, y),
631 &TextStyle {
632 color: cpu_color,
633 ..Default::default()
634 },
635 );
636
637 let sparkline = Self::render_sparkline(&row.cpu_history, w.sparkline);
639 canvas.draw_text(
640 &sparkline,
641 Point::new(x_start + col_positions[3] as f32, y),
642 &TextStyle {
643 color: self.accent_color,
644 ..Default::default()
645 },
646 );
647
648 let mem_bar = Self::render_microbar(row.mem_percent, 4);
650 let mem_str = format!("{:>4.1}", row.mem_percent);
651 canvas.draw_text(
652 &format!("{mem_bar}{mem_str}"),
653 Point::new(x_start + col_positions[4] as f32, y),
654 &TextStyle {
655 color: Color::new(0.7, 0.5, 0.9, 1.0),
656 ..Default::default()
657 },
658 );
659
660 let (state_ch, state_color) = row.state.render();
662 canvas.draw_text(
663 &state_ch.to_string(),
664 Point::new(x_start + col_positions[5] as f32, y),
665 &TextStyle {
666 color: state_color,
667 ..Default::default()
668 },
669 );
670
671 let thr_str = format!("{:>3}", row.threads);
673 canvas.draw_text(
674 &thr_str,
675 Point::new(x_start + col_positions[6] as f32 + 1.0, y),
676 &text_style,
677 );
678
679 let user = if row.user.len() > w.user {
681 format!("{}…", &row.user[..w.user - 1])
682 } else {
683 format!("{:<width$}", row.user, width = w.user)
684 };
685 canvas.draw_text(
686 &user,
687 Point::new(x_start + col_positions[7] as f32, y),
688 &text_style,
689 );
690
691 let time_str = Self::format_time(row.cpu_time_secs);
693 canvas.draw_text(
694 &time_str,
695 Point::new(x_start + col_positions[8] as f32, y),
696 &text_style,
697 );
698
699 y += 1.0;
700 }
701
702 if self.rows.len() > visible_rows {
704 let scroll_pct = self.scroll_offset as f32 / (self.rows.len() - visible_rows) as f32;
705 let indicator_y = self.bounds.y + 2.0 + (scroll_pct * (visible_rows - 1) as f32);
706 canvas.draw_text(
707 "│",
708 Point::new(self.bounds.x + self.bounds.width - 1.0, indicator_y),
709 &dim_style,
710 );
711 }
712 }
713
714 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
715 match event {
716 Event::KeyDown {
717 key: Key::Up | Key::K,
718 ..
719 } => {
720 self.scroll_up();
721 None
722 }
723 Event::KeyDown {
724 key: Key::Down | Key::J,
725 ..
726 } => {
727 self.scroll_down();
728 None
729 }
730 Event::KeyDown { key: Key::S, .. } => {
731 self.cycle_sort();
732 None
733 }
734 Event::KeyDown { key: Key::R, .. } => {
735 self.toggle_sort_direction();
736 None
737 }
738 _ => None,
739 }
740 }
741
742 fn children(&self) -> &[Box<dyn Widget>] {
743 &[]
744 }
745
746 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
747 &mut []
748 }
749}
750
751#[cfg(test)]
752#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
753mod tests {
754 use super::*;
755 use crate::direct::{CellBuffer, DirectTerminalCanvas};
756
757 #[test]
759 fn test_display_state_running() {
760 let (ch, color) = ProcessDisplayState::Running.render();
761 assert_eq!(ch, 'R');
762 assert!(color.g > 0.8, "Running should be green");
763 }
764
765 #[test]
766 fn test_display_state_sleeping() {
767 let (ch, color) = ProcessDisplayState::Sleeping.render();
768 assert_eq!(ch, 'S');
769 assert!(color.r > 0.5 && color.g > 0.5, "Sleeping should be gray");
770 }
771
772 #[test]
773 fn test_display_state_idle() {
774 let (ch, color) = ProcessDisplayState::Idle.render();
775 assert_eq!(ch, 'I');
776 assert!(color.r > 0.4, "Idle should be grayish");
777 }
778
779 #[test]
780 fn test_display_state_zombie() {
781 let (ch, color) = ProcessDisplayState::Zombie.render();
782 assert_eq!(ch, 'Z');
783 assert!(color.r > 0.8 && color.g < 0.3, "Zombie should be red");
784 }
785
786 #[test]
787 fn test_display_state_stopped() {
788 let (ch, color) = ProcessDisplayState::Stopped.render();
789 assert_eq!(ch, 'T');
790 assert!(
791 color.r > 0.8 && color.g > 0.6,
792 "Stopped should be yellow/orange"
793 );
794 }
795
796 #[test]
797 fn test_display_state_unknown() {
798 let (ch, color) = ProcessDisplayState::Unknown.render();
799 assert_eq!(ch, '?');
800 assert!(color.r < 0.5, "Unknown should be dim");
801 }
802
803 #[test]
804 fn test_display_state_default() {
805 assert_eq!(ProcessDisplayState::default(), ProcessDisplayState::Running);
806 }
807
808 #[test]
810 fn test_sort_column_headers() {
811 assert_eq!(ProcessSortColumn::Pid.header(), "PID");
812 assert_eq!(ProcessSortColumn::Name.header(), "COMMAND");
813 assert_eq!(ProcessSortColumn::Cpu.header(), "CPU%");
814 assert_eq!(ProcessSortColumn::Mem.header(), "MEM%");
815 assert_eq!(ProcessSortColumn::Threads.header(), "THR");
816 assert_eq!(ProcessSortColumn::Priority.header(), "PRI");
817 assert_eq!(ProcessSortColumn::User.header(), "USER");
818 assert_eq!(ProcessSortColumn::Time.header(), "TIME+");
819 }
820
821 #[test]
822 fn test_sort_column_default() {
823 assert_eq!(ProcessSortColumn::default(), ProcessSortColumn::Cpu);
824 }
825
826 #[test]
828 fn test_process_row_default() {
829 let row = ProcessRow::default();
830 assert_eq!(row.pid, 0);
831 assert!(row.name.is_empty());
832 assert_eq!(row.cpu_percent, 0.0);
833 assert!(row.cpu_history.is_empty());
834 assert_eq!(row.mem_percent, 0.0);
835 assert_eq!(row.state, ProcessDisplayState::Running);
836 assert_eq!(row.threads, 1);
837 assert_eq!(row.priority, 0);
838 assert!(row.user.is_empty());
839 assert_eq!(row.cpu_time_secs, 0);
840 }
841
842 #[test]
844 fn test_column_widths_default() {
845 let w = ProcessColumnWidths::default();
846 assert_eq!(w.pid, 7);
847 assert_eq!(w.name, 20);
848 assert_eq!(w.cpu, 6);
849 assert_eq!(w.sparkline, 12);
850 assert_eq!(w.mem, 6);
851 assert_eq!(w.state, 3);
852 assert_eq!(w.threads, 4);
853 assert_eq!(w.priority, 4);
854 assert_eq!(w.user, 10);
855 assert_eq!(w.time, 10);
856 }
857
858 #[test]
860 fn test_process_dataframe_creation() {
861 let df = ProcessDataFrame::new()
862 .with_sort(ProcessSortColumn::Mem, true)
863 .with_history_len(30);
864
865 assert_eq!(df.sort_column, ProcessSortColumn::Mem);
866 assert!(df.sort_desc);
867 assert_eq!(df.history_len, 30);
868 }
869
870 #[test]
871 fn test_process_dataframe_default() {
872 let df = ProcessDataFrame::default();
873 assert_eq!(df.sort_column, ProcessSortColumn::Cpu);
874 assert!(df.sort_desc);
875 assert_eq!(df.history_len, 60);
876 }
877
878 #[test]
879 fn test_with_accent_color() {
880 let color = Color::new(1.0, 0.0, 0.0, 1.0);
881 let df = ProcessDataFrame::new().with_accent_color(color);
882 assert_eq!(df.accent_color.r, 1.0);
883 }
884
885 #[test]
886 fn test_with_column_widths() {
887 let widths = ProcessColumnWidths {
888 pid: 10,
889 name: 30,
890 ..Default::default()
891 };
892 let df = ProcessDataFrame::new().with_column_widths(widths);
893 assert_eq!(df.col_widths.pid, 10);
894 assert_eq!(df.col_widths.name, 30);
895 }
896
897 #[test]
899 fn test_process_row_update() {
900 let mut df = ProcessDataFrame::new();
901
902 let rows = vec![
903 ProcessRow {
904 pid: 1,
905 name: "systemd".into(),
906 cpu_percent: 0.5,
907 mem_percent: 1.2,
908 state: ProcessDisplayState::Running,
909 threads: 1,
910 priority: 0,
911 user: "root".into(),
912 cpu_time_secs: 3600,
913 ..Default::default()
914 },
915 ProcessRow {
916 pid: 100,
917 name: "firefox".into(),
918 cpu_percent: 25.0,
919 mem_percent: 15.0,
920 state: ProcessDisplayState::Sleeping,
921 threads: 120,
922 priority: 20,
923 user: "noah".into(),
924 cpu_time_secs: 7200,
925 ..Default::default()
926 },
927 ];
928
929 df.update_processes(rows);
930
931 assert_eq!(df.rows[0].pid, 100); assert_eq!(df.rows[1].pid, 1);
934 }
935
936 #[test]
937 fn test_update_clears_dead_processes() {
938 let mut df = ProcessDataFrame::new();
939
940 df.update_processes(vec![
942 ProcessRow {
943 pid: 1,
944 cpu_percent: 10.0,
945 ..Default::default()
946 },
947 ProcessRow {
948 pid: 2,
949 cpu_percent: 20.0,
950 ..Default::default()
951 },
952 ]);
953 assert_eq!(df.cpu_histories.len(), 2);
954
955 df.update_processes(vec![ProcessRow {
957 pid: 1,
958 cpu_percent: 15.0,
959 ..Default::default()
960 }]);
961 assert_eq!(df.cpu_histories.len(), 1);
962 assert!(df.cpu_histories.contains_key(&1));
963 assert!(!df.cpu_histories.contains_key(&2));
964 }
965
966 #[test]
967 fn test_history_length_limit() {
968 let mut df = ProcessDataFrame::new().with_history_len(3);
969
970 for i in 0..5 {
971 df.update_processes(vec![ProcessRow {
972 pid: 1,
973 cpu_percent: i as f32,
974 ..Default::default()
975 }]);
976 }
977
978 let history = df.cpu_histories.get(&1).unwrap();
979 assert_eq!(history.len(), 3);
980 assert_eq!(history, &vec![2.0, 3.0, 4.0]);
981 }
982
983 #[test]
984 fn test_selected_pid() {
985 let mut df = ProcessDataFrame::new();
986 assert!(df.selected_pid().is_none());
987
988 df.update_processes(vec![ProcessRow {
989 pid: 42,
990 cpu_percent: 10.0,
991 ..Default::default()
992 }]);
993 df.selected_row = Some(0);
994 assert_eq!(df.selected_pid(), Some(42));
995 }
996
997 #[test]
998 fn test_selected_pid_out_of_range() {
999 let mut df = ProcessDataFrame::new();
1000 df.selected_row = Some(999);
1001 assert!(df.selected_pid().is_none());
1002 }
1003
1004 #[test]
1006 fn test_sort_by_pid() {
1007 let mut df = ProcessDataFrame::new().with_sort(ProcessSortColumn::Pid, false);
1008 df.update_processes(vec![
1009 ProcessRow {
1010 pid: 100,
1011 ..Default::default()
1012 },
1013 ProcessRow {
1014 pid: 1,
1015 ..Default::default()
1016 },
1017 ProcessRow {
1018 pid: 50,
1019 ..Default::default()
1020 },
1021 ]);
1022 assert_eq!(df.rows[0].pid, 1);
1023 assert_eq!(df.rows[1].pid, 50);
1024 assert_eq!(df.rows[2].pid, 100);
1025 }
1026
1027 #[test]
1028 fn test_sort_by_pid_desc() {
1029 let mut df = ProcessDataFrame::new().with_sort(ProcessSortColumn::Pid, true);
1030 df.update_processes(vec![
1031 ProcessRow {
1032 pid: 100,
1033 ..Default::default()
1034 },
1035 ProcessRow {
1036 pid: 1,
1037 ..Default::default()
1038 },
1039 ProcessRow {
1040 pid: 50,
1041 ..Default::default()
1042 },
1043 ]);
1044 assert_eq!(df.rows[0].pid, 100);
1045 assert_eq!(df.rows[1].pid, 50);
1046 assert_eq!(df.rows[2].pid, 1);
1047 }
1048
1049 #[test]
1050 fn test_sort_by_name() {
1051 let mut df = ProcessDataFrame::new().with_sort(ProcessSortColumn::Name, false);
1052 df.update_processes(vec![
1053 ProcessRow {
1054 pid: 1,
1055 name: "zsh".into(),
1056 ..Default::default()
1057 },
1058 ProcessRow {
1059 pid: 2,
1060 name: "bash".into(),
1061 ..Default::default()
1062 },
1063 ProcessRow {
1064 pid: 3,
1065 name: "fish".into(),
1066 ..Default::default()
1067 },
1068 ]);
1069 assert_eq!(df.rows[0].name.as_str(), "bash");
1070 assert_eq!(df.rows[1].name.as_str(), "fish");
1071 assert_eq!(df.rows[2].name.as_str(), "zsh");
1072 }
1073
1074 #[test]
1075 fn test_sort_by_mem() {
1076 let mut df = ProcessDataFrame::new().with_sort(ProcessSortColumn::Mem, true);
1077 df.update_processes(vec![
1078 ProcessRow {
1079 pid: 1,
1080 mem_percent: 10.0,
1081 ..Default::default()
1082 },
1083 ProcessRow {
1084 pid: 2,
1085 mem_percent: 50.0,
1086 ..Default::default()
1087 },
1088 ProcessRow {
1089 pid: 3,
1090 mem_percent: 25.0,
1091 ..Default::default()
1092 },
1093 ]);
1094 assert_eq!(df.rows[0].pid, 2);
1095 assert_eq!(df.rows[1].pid, 3);
1096 assert_eq!(df.rows[2].pid, 1);
1097 }
1098
1099 #[test]
1100 fn test_sort_by_threads() {
1101 let mut df = ProcessDataFrame::new().with_sort(ProcessSortColumn::Threads, true);
1102 df.update_processes(vec![
1103 ProcessRow {
1104 pid: 1,
1105 threads: 1,
1106 ..Default::default()
1107 },
1108 ProcessRow {
1109 pid: 2,
1110 threads: 100,
1111 ..Default::default()
1112 },
1113 ProcessRow {
1114 pid: 3,
1115 threads: 10,
1116 ..Default::default()
1117 },
1118 ]);
1119 assert_eq!(df.rows[0].pid, 2);
1120 assert_eq!(df.rows[1].pid, 3);
1121 assert_eq!(df.rows[2].pid, 1);
1122 }
1123
1124 #[test]
1125 fn test_sort_by_priority() {
1126 let mut df = ProcessDataFrame::new().with_sort(ProcessSortColumn::Priority, false);
1127 df.update_processes(vec![
1128 ProcessRow {
1129 pid: 1,
1130 priority: 20,
1131 ..Default::default()
1132 },
1133 ProcessRow {
1134 pid: 2,
1135 priority: -10,
1136 ..Default::default()
1137 },
1138 ProcessRow {
1139 pid: 3,
1140 priority: 0,
1141 ..Default::default()
1142 },
1143 ]);
1144 assert_eq!(df.rows[0].pid, 2);
1145 assert_eq!(df.rows[1].pid, 3);
1146 assert_eq!(df.rows[2].pid, 1);
1147 }
1148
1149 #[test]
1150 fn test_sort_by_user() {
1151 let mut df = ProcessDataFrame::new().with_sort(ProcessSortColumn::User, false);
1152 df.update_processes(vec![
1153 ProcessRow {
1154 pid: 1,
1155 user: "root".into(),
1156 ..Default::default()
1157 },
1158 ProcessRow {
1159 pid: 2,
1160 user: "alice".into(),
1161 ..Default::default()
1162 },
1163 ProcessRow {
1164 pid: 3,
1165 user: "bob".into(),
1166 ..Default::default()
1167 },
1168 ]);
1169 assert_eq!(df.rows[0].user.as_str(), "alice");
1170 assert_eq!(df.rows[1].user.as_str(), "bob");
1171 assert_eq!(df.rows[2].user.as_str(), "root");
1172 }
1173
1174 #[test]
1175 fn test_sort_by_time() {
1176 let mut df = ProcessDataFrame::new().with_sort(ProcessSortColumn::Time, true);
1177 df.update_processes(vec![
1178 ProcessRow {
1179 pid: 1,
1180 cpu_time_secs: 100,
1181 ..Default::default()
1182 },
1183 ProcessRow {
1184 pid: 2,
1185 cpu_time_secs: 10000,
1186 ..Default::default()
1187 },
1188 ProcessRow {
1189 pid: 3,
1190 cpu_time_secs: 1000,
1191 ..Default::default()
1192 },
1193 ]);
1194 assert_eq!(df.rows[0].pid, 2);
1195 assert_eq!(df.rows[1].pid, 3);
1196 assert_eq!(df.rows[2].pid, 1);
1197 }
1198
1199 #[test]
1200 fn test_cycle_sort() {
1201 let mut df = ProcessDataFrame::new();
1202 assert_eq!(df.sort_column, ProcessSortColumn::Cpu);
1203
1204 df.cycle_sort();
1205 assert_eq!(df.sort_column, ProcessSortColumn::Mem);
1206
1207 df.cycle_sort();
1208 assert_eq!(df.sort_column, ProcessSortColumn::Pid);
1209
1210 df.cycle_sort();
1211 assert_eq!(df.sort_column, ProcessSortColumn::Name);
1212
1213 df.cycle_sort();
1214 assert_eq!(df.sort_column, ProcessSortColumn::Threads);
1215
1216 df.cycle_sort();
1217 assert_eq!(df.sort_column, ProcessSortColumn::Time);
1218
1219 df.cycle_sort();
1220 assert_eq!(df.sort_column, ProcessSortColumn::Cpu);
1221 }
1222
1223 #[test]
1224 fn test_cycle_sort_from_priority() {
1225 let mut df = ProcessDataFrame::new().with_sort(ProcessSortColumn::Priority, true);
1226 df.cycle_sort();
1227 assert_eq!(df.sort_column, ProcessSortColumn::Cpu);
1228 }
1229
1230 #[test]
1231 fn test_toggle_sort_direction() {
1232 let mut df = ProcessDataFrame::new();
1233 assert!(df.sort_desc);
1234
1235 df.toggle_sort_direction();
1236 assert!(!df.sort_desc);
1237
1238 df.toggle_sort_direction();
1239 assert!(df.sort_desc);
1240 }
1241
1242 #[test]
1244 fn test_sparkline_rendering() {
1245 let values = vec![10.0, 20.0, 50.0, 80.0, 100.0];
1246 let sparkline = ProcessDataFrame::render_sparkline(&values, 5);
1247 assert_eq!(sparkline.chars().count(), 5);
1248 }
1249
1250 #[test]
1251 fn test_sparkline_empty() {
1252 let sparkline = ProcessDataFrame::render_sparkline(&[], 5);
1253 assert_eq!(sparkline, "─────");
1254 }
1255
1256 #[test]
1257 fn test_sparkline_single_value() {
1258 let sparkline = ProcessDataFrame::render_sparkline(&[50.0], 3);
1260 assert_eq!(sparkline.chars().count(), 1);
1261 }
1262
1263 #[test]
1264 fn test_microbar_rendering() {
1265 let bar = ProcessDataFrame::render_microbar(50.0, 10);
1266 assert_eq!(bar.chars().count(), 10);
1267 assert!(bar.contains('█'));
1268 assert!(bar.contains('░'));
1269 }
1270
1271 #[test]
1272 fn test_microbar_zero() {
1273 let bar = ProcessDataFrame::render_microbar(0.0, 5);
1274 assert_eq!(bar, "░░░░░");
1275 }
1276
1277 #[test]
1278 fn test_microbar_full() {
1279 let bar = ProcessDataFrame::render_microbar(100.0, 5);
1280 assert_eq!(bar, "█████");
1281 }
1282
1283 #[test]
1284 fn test_microbar_clamped() {
1285 let bar_over = ProcessDataFrame::render_microbar(150.0, 5);
1286 assert_eq!(bar_over, "█████");
1287
1288 let bar_under = ProcessDataFrame::render_microbar(-10.0, 5);
1289 assert_eq!(bar_under, "░░░░░");
1290 }
1291
1292 #[test]
1293 fn test_format_time() {
1294 assert_eq!(ProcessDataFrame::format_time(59), "0:59");
1295 assert_eq!(ProcessDataFrame::format_time(3661), "1:01:01");
1296 assert_eq!(ProcessDataFrame::format_time(360000), "100h");
1297 }
1298
1299 #[test]
1300 fn test_format_time_zero() {
1301 assert_eq!(ProcessDataFrame::format_time(0), "0:00");
1302 }
1303
1304 #[test]
1305 fn test_format_time_exact_hour() {
1306 assert_eq!(ProcessDataFrame::format_time(3600), "1:00:00");
1307 }
1308
1309 #[test]
1310 fn test_format_time_99_hours() {
1311 assert_eq!(ProcessDataFrame::format_time(99 * 3600), "99:00:00");
1312 }
1313
1314 #[test]
1316 fn test_scroll() {
1317 let mut df = ProcessDataFrame::new();
1318 df.bounds = Rect::new(0.0, 0.0, 100.0, 10.0);
1319
1320 let rows: Vec<ProcessRow> = (0..20)
1321 .map(|i| ProcessRow {
1322 pid: i,
1323 name: format!("proc{i}").into(),
1324 cpu_percent: i as f32,
1325 ..Default::default()
1326 })
1327 .collect();
1328
1329 df.update_processes(rows);
1330
1331 df.scroll_down();
1332 assert_eq!(df.selected_row, Some(0));
1333
1334 df.scroll_down();
1335 assert_eq!(df.selected_row, Some(1));
1336
1337 df.scroll_up();
1338 assert_eq!(df.selected_row, Some(0));
1339 }
1340
1341 #[test]
1342 fn test_scroll_up_at_top() {
1343 let mut df = ProcessDataFrame::new();
1344 df.update_processes(vec![ProcessRow {
1345 pid: 1,
1346 ..Default::default()
1347 }]);
1348 df.selected_row = Some(0);
1349
1350 df.scroll_up();
1351 assert_eq!(df.selected_row, Some(0));
1352 }
1353
1354 #[test]
1355 fn test_scroll_down_at_bottom() {
1356 let mut df = ProcessDataFrame::new();
1357 df.bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
1358 df.update_processes(vec![ProcessRow {
1359 pid: 1,
1360 ..Default::default()
1361 }]);
1362 df.selected_row = Some(0);
1363
1364 df.scroll_down();
1365 assert_eq!(df.selected_row, Some(0));
1366 }
1367
1368 #[test]
1369 fn test_scroll_empty() {
1370 let mut df = ProcessDataFrame::new();
1371 df.scroll_up();
1372 assert!(df.selected_row.is_none());
1373
1374 df.scroll_down();
1375 assert!(df.selected_row.is_none());
1376 }
1377
1378 #[test]
1379 fn test_scroll_triggers_offset_adjustment() {
1380 let mut df = ProcessDataFrame::new();
1381 df.bounds = Rect::new(0.0, 0.0, 100.0, 5.0); df.show_header = false;
1383
1384 let rows: Vec<ProcessRow> = (0..20)
1385 .map(|i| ProcessRow {
1386 pid: i,
1387 cpu_percent: (20 - i) as f32,
1388 ..Default::default()
1389 })
1390 .collect();
1391 df.update_processes(rows);
1392
1393 for _ in 0..10 {
1395 df.scroll_down();
1396 }
1397 assert!(df.scroll_offset > 0);
1398 }
1399
1400 #[test]
1402 fn test_visible_rows_with_header() {
1403 let mut df = ProcessDataFrame::new();
1404 df.bounds = Rect::new(0.0, 0.0, 100.0, 10.0);
1405 df.show_header = true;
1406 assert_eq!(df.visible_rows(), 8); }
1408
1409 #[test]
1410 fn test_visible_rows_without_header() {
1411 let mut df = ProcessDataFrame::new();
1412 df.bounds = Rect::new(0.0, 0.0, 100.0, 10.0);
1413 df.show_header = false;
1414 assert_eq!(df.visible_rows(), 10);
1415 }
1416
1417 #[test]
1419 fn test_brick_name() {
1420 let df = ProcessDataFrame::new();
1421 assert_eq!(df.brick_name(), "process_dataframe");
1422 }
1423
1424 #[test]
1425 fn test_brick_assertions() {
1426 let df = ProcessDataFrame::new();
1427 let assertions = df.assertions();
1428 assert!(!assertions.is_empty());
1429 }
1430
1431 #[test]
1432 fn test_brick_budget() {
1433 let df = ProcessDataFrame::new();
1434 let budget = df.budget();
1435 assert!(budget.total_ms > 0);
1436 }
1437
1438 #[test]
1439 fn test_brick_verify() {
1440 let df = ProcessDataFrame::new();
1441 let verification = df.verify();
1442 assert!(verification.failed.is_empty());
1443 }
1444
1445 #[test]
1446 fn test_to_html() {
1447 let df = ProcessDataFrame::new();
1448 let html = df.to_html();
1449 assert!(html.contains("process-dataframe"));
1450 assert!(html.contains("PID"));
1451 assert!(html.contains("COMMAND"));
1452 }
1453
1454 #[test]
1455 fn test_to_css() {
1456 let df = ProcessDataFrame::new();
1457 let css = df.to_css();
1458 assert!(css.contains("process-dataframe"));
1459 assert!(css.contains("sort"));
1460 }
1461
1462 #[test]
1464 fn test_type_id() {
1465 let df = ProcessDataFrame::new();
1466 let id = Widget::type_id(&df);
1467 assert_eq!(id, TypeId::of::<ProcessDataFrame>());
1468 }
1469
1470 #[test]
1471 fn test_measure() {
1472 let df = ProcessDataFrame::new();
1473 let constraints = Constraints::tight(Size::new(100.0, 50.0));
1474 let size = df.measure(constraints);
1475 assert!(size.width <= 100.0);
1476 assert!(size.height <= 50.0);
1477 }
1478
1479 #[test]
1480 fn test_measure_with_rows() {
1481 let mut df = ProcessDataFrame::new();
1482 df.update_processes(vec![
1483 ProcessRow {
1484 pid: 1,
1485 ..Default::default()
1486 },
1487 ProcessRow {
1488 pid: 2,
1489 ..Default::default()
1490 },
1491 ]);
1492 let constraints = Constraints::tight(Size::new(100.0, 50.0));
1493 let size = df.measure(constraints);
1494 assert!(size.height >= 3.0); }
1496
1497 #[test]
1498 fn test_layout() {
1499 let mut df = ProcessDataFrame::new();
1500 let bounds = Rect::new(10.0, 20.0, 200.0, 100.0);
1501 let result = df.layout(bounds);
1502 assert_eq!(result.size.width, 200.0);
1503 assert_eq!(result.size.height, 100.0);
1504 assert_eq!(df.bounds, bounds);
1505 }
1506
1507 #[test]
1508 fn test_paint_too_small() {
1509 let mut df = ProcessDataFrame::new();
1510 df.bounds = Rect::new(0.0, 0.0, 30.0, 2.0); let mut buffer = CellBuffer::new(30, 2);
1513 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1514 df.paint(&mut canvas);
1515 }
1517
1518 #[test]
1519 fn test_paint_with_data() {
1520 let mut df = ProcessDataFrame::new();
1521 df.bounds = Rect::new(0.0, 0.0, 100.0, 20.0);
1522 df.update_processes(vec![ProcessRow {
1523 pid: 1,
1524 name: "test".into(),
1525 cpu_percent: 50.0,
1526 mem_percent: 25.0,
1527 state: ProcessDisplayState::Running,
1528 threads: 4,
1529 user: "root".into(),
1530 cpu_time_secs: 3600,
1531 ..Default::default()
1532 }]);
1533
1534 let mut buffer = CellBuffer::new(100, 20);
1535 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1536 df.paint(&mut canvas);
1537 }
1539
1540 #[test]
1541 fn test_paint_with_scroll_indicator() {
1542 let mut df = ProcessDataFrame::new();
1543 df.bounds = Rect::new(0.0, 0.0, 100.0, 5.0);
1544 df.show_header = false;
1545
1546 let rows: Vec<ProcessRow> = (0..20)
1547 .map(|i| ProcessRow {
1548 pid: i,
1549 cpu_percent: i as f32,
1550 ..Default::default()
1551 })
1552 .collect();
1553 df.update_processes(rows);
1554
1555 let mut buffer = CellBuffer::new(100, 5);
1556 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1557 df.paint(&mut canvas);
1558 }
1559
1560 #[test]
1561 fn test_paint_with_selection() {
1562 let mut df = ProcessDataFrame::new();
1563 df.bounds = Rect::new(0.0, 0.0, 100.0, 10.0);
1564 df.update_processes(vec![
1565 ProcessRow {
1566 pid: 1,
1567 cpu_percent: 50.0,
1568 ..Default::default()
1569 },
1570 ProcessRow {
1571 pid: 2,
1572 cpu_percent: 25.0,
1573 ..Default::default()
1574 },
1575 ]);
1576 df.selected_row = Some(0);
1577
1578 let mut buffer = CellBuffer::new(100, 10);
1579 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1580 df.paint(&mut canvas);
1581 }
1582
1583 #[test]
1585 fn test_event_key_up() {
1586 let mut df = ProcessDataFrame::new();
1587 df.update_processes(vec![
1588 ProcessRow {
1589 pid: 1,
1590 ..Default::default()
1591 },
1592 ProcessRow {
1593 pid: 2,
1594 ..Default::default()
1595 },
1596 ]);
1597 df.selected_row = Some(1);
1598
1599 let result = df.event(&Event::key_down(Key::Up));
1600 assert!(result.is_none());
1601 assert_eq!(df.selected_row, Some(0));
1602 }
1603
1604 #[test]
1605 fn test_event_key_k() {
1606 let mut df = ProcessDataFrame::new();
1607 df.update_processes(vec![
1608 ProcessRow {
1609 pid: 1,
1610 ..Default::default()
1611 },
1612 ProcessRow {
1613 pid: 2,
1614 ..Default::default()
1615 },
1616 ]);
1617 df.selected_row = Some(1);
1618
1619 let result = df.event(&Event::key_down(Key::K));
1620 assert!(result.is_none());
1621 assert_eq!(df.selected_row, Some(0));
1622 }
1623
1624 #[test]
1625 fn test_event_key_down() {
1626 let mut df = ProcessDataFrame::new();
1627 df.bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
1628 df.update_processes(vec![
1629 ProcessRow {
1630 pid: 1,
1631 ..Default::default()
1632 },
1633 ProcessRow {
1634 pid: 2,
1635 ..Default::default()
1636 },
1637 ]);
1638
1639 let result = df.event(&Event::key_down(Key::Down));
1640 assert!(result.is_none());
1641 assert_eq!(df.selected_row, Some(0));
1642 }
1643
1644 #[test]
1645 fn test_event_key_j() {
1646 let mut df = ProcessDataFrame::new();
1647 df.bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
1648 df.update_processes(vec![
1649 ProcessRow {
1650 pid: 1,
1651 ..Default::default()
1652 },
1653 ProcessRow {
1654 pid: 2,
1655 ..Default::default()
1656 },
1657 ]);
1658
1659 let result = df.event(&Event::key_down(Key::J));
1660 assert!(result.is_none());
1661 assert_eq!(df.selected_row, Some(0));
1662 }
1663
1664 #[test]
1665 fn test_event_key_s_cycles_sort() {
1666 let mut df = ProcessDataFrame::new();
1667 assert_eq!(df.sort_column, ProcessSortColumn::Cpu);
1668
1669 df.event(&Event::key_down(Key::S));
1670 assert_eq!(df.sort_column, ProcessSortColumn::Mem);
1671 }
1672
1673 #[test]
1674 fn test_event_key_r_toggles_direction() {
1675 let mut df = ProcessDataFrame::new();
1676 assert!(df.sort_desc);
1677
1678 df.event(&Event::key_down(Key::R));
1679 assert!(!df.sort_desc);
1680 }
1681
1682 #[test]
1683 fn test_event_unhandled() {
1684 let mut df = ProcessDataFrame::new();
1685 let result = df.event(&Event::key_down(Key::Escape));
1686 assert!(result.is_none());
1687 }
1688
1689 #[test]
1690 fn test_children_empty() {
1691 let df = ProcessDataFrame::new();
1692 assert!(df.children().is_empty());
1693 }
1694
1695 #[test]
1696 fn test_children_mut_empty() {
1697 let mut df = ProcessDataFrame::new();
1698 assert!(df.children_mut().is_empty());
1699 }
1700}