gardesk/gartop / 1f8a15f

Browse files

Add process detail view (Enter to open, Esc to close)

Authored by espadonne
SHA
1f8a15f014ed8d0304f718ad5e44bac620e68e6f
Parents
77bbe2d
Tree
a4b4430

1 changed file

StatusFile+-
M gartop/src/gui/app.rs 229 1
gartop/src/gui/app.rsmodified
@@ -60,6 +60,8 @@ pub struct App {
6060
     jump_time: Option<Instant>,
6161
     /// Kill confirmation: (pid, signal, process_name)
6262
     kill_confirm: Option<(i32, i32, String)>,
63
+    /// Process detail view: PID of process being viewed
64
+    detail_pid: Option<i32>,
6365
     status: Option<StatusInfo>,
6466
     cpu_stats: Option<CpuStats>,
6567
     memory_stats: Option<MemoryStats>,
@@ -153,6 +155,7 @@ impl App {
153155
             jump_pattern: String::new(),
154156
             jump_time: None,
155157
             kill_confirm: None,
158
+            detail_pid: None,
156159
             status: None,
157160
             cpu_stats: None,
158161
             memory_stats: None,
@@ -371,6 +374,13 @@ impl App {
371374
             self.render_tab_content(content_y, content_height)?;
372375
         }
373376
 
377
+        // Process detail view (rendered on top of content)
378
+        if let Some(pid) = self.detail_pid {
379
+            if let Some(process) = self.processes.iter().find(|p| p.pid == pid) {
380
+                self.render_detail_view(process)?;
381
+            }
382
+        }
383
+
374384
         // Help overlay (rendered on top)
375385
         if self.show_help {
376386
             self.render_help_overlay()?;
@@ -462,11 +472,11 @@ impl App {
462472
             ("\u{2191} / \u{2193}", "Navigate list"),
463473
             ("Home / End", "First / last"),
464474
             ("PgUp/PgDn", "Jump 10 rows"),
475
+            ("Enter", "Process detail"),
465476
             ("", ""),
466477
             ("Alt+f", "Freeze list"),
467478
             ("/", "Search filter"),
468479
             ("a-z", "Fuzzy jump"),
469
-            ("Backspace", "Delete jump char"),
470480
             ("", ""),
471481
             ("K", "Kill (SIGTERM)"),
472482
             ("X", "Kill (SIGKILL)"),
@@ -545,6 +555,187 @@ impl App {
545555
         Ok(())
546556
     }
547557
 
558
+    /// Render process detail view overlay.
559
+    fn render_detail_view(&self, process: &ProcessInfo) -> Result<()> {
560
+        // Full-screen backdrop
561
+        let backdrop = Rect::new(0, 0, self.width, self.height);
562
+        self.renderer.fill_rect(backdrop, gartk_core::Color::new(0.0, 0.0, 0.0, 0.85))?;
563
+
564
+        // Detail panel (slightly smaller than full screen)
565
+        let margin = 24u32;
566
+        let panel_rect = Rect::new(
567
+            margin as i32,
568
+            margin as i32,
569
+            self.width - margin * 2,
570
+            self.height - margin * 2,
571
+        );
572
+        self.renderer.fill_rounded_rect(panel_rect, 8.0, self.theme.panel_bg)?;
573
+
574
+        let x = margin as f64 + 20.0;
575
+        let right_col = (self.width / 2) as f64 + 20.0;
576
+        let mut y = margin as f64 + 24.0;
577
+
578
+        // Title
579
+        let title_style = TextStyle {
580
+            font_family: "monospace".to_string(),
581
+            font_size: 14.0,
582
+            color: self.theme.text,
583
+            ..Default::default()
584
+        };
585
+        let title = format!("Process: {} (PID {})", process.name, process.pid);
586
+        self.renderer.text(&title, x, y, &title_style)?;
587
+
588
+        // Back hint
589
+        let hint_style = TextStyle {
590
+            font_family: "monospace".to_string(),
591
+            font_size: 10.0,
592
+            color: self.theme.text_secondary,
593
+            ..Default::default()
594
+        };
595
+        self.renderer.text("[Esc] Back", (self.width - margin - 80) as f64, y, &hint_style)?;
596
+
597
+        // Separator
598
+        y += 20.0;
599
+        self.renderer.line(
600
+            x, y,
601
+            (self.width - margin * 2) as f64 + x - 20.0, y,
602
+            self.theme.border, 1.0
603
+        )?;
604
+        y += 16.0;
605
+
606
+        // Info labels and values
607
+        let label_style = TextStyle {
608
+            font_family: "monospace".to_string(),
609
+            font_size: 11.0,
610
+            color: self.theme.text_secondary,
611
+            ..Default::default()
612
+        };
613
+        let value_style = TextStyle {
614
+            font_family: "monospace".to_string(),
615
+            font_size: 11.0,
616
+            color: self.theme.text,
617
+            ..Default::default()
618
+        };
619
+
620
+        // Left column
621
+        let row_height = 20.0;
622
+
623
+        // State
624
+        self.renderer.text("State:", x, y, &label_style)?;
625
+        let state_color = match process.state.as_str() {
626
+            "R" => self.theme.cpu_color,     // Running
627
+            "S" => self.theme.memory_color,  // Sleeping
628
+            "Z" => self.theme.swap_color,    // Zombie
629
+            "T" => self.theme.disk_color,    // Stopped
630
+            _ => self.theme.text,
631
+        };
632
+        let state_style = TextStyle { color: state_color, ..value_style.clone() };
633
+        let state_name = match process.state.as_str() {
634
+            "R" => "Running",
635
+            "S" => "Sleeping",
636
+            "D" => "Disk Sleep",
637
+            "Z" => "Zombie",
638
+            "T" => "Stopped",
639
+            "t" => "Tracing",
640
+            "X" => "Dead",
641
+            _ => &process.state,
642
+        };
643
+        self.renderer.text(state_name, x + 80.0, y, &state_style)?;
644
+        y += row_height;
645
+
646
+        // User
647
+        self.renderer.text("User:", x, y, &label_style)?;
648
+        self.renderer.text(&process.user, x + 80.0, y, &value_style)?;
649
+        y += row_height;
650
+
651
+        // CPU
652
+        self.renderer.text("CPU:", x, y, &label_style)?;
653
+        let cpu_style = TextStyle { color: self.theme.cpu_color, ..value_style.clone() };
654
+        self.renderer.text(&format!("{:.1}%", process.cpu_percent), x + 80.0, y, &cpu_style)?;
655
+        y += row_height;
656
+
657
+        // Memory
658
+        self.renderer.text("Memory:", x, y, &label_style)?;
659
+        let mem_style = TextStyle { color: self.theme.memory_color, ..value_style.clone() };
660
+        self.renderer.text(
661
+            &format!("{} ({:.1}%)", format_bytes(process.rss), process.memory_percent),
662
+            x + 80.0, y, &mem_style
663
+        )?;
664
+        y += row_height;
665
+
666
+        // Virtual
667
+        self.renderer.text("Virtual:", x, y, &label_style)?;
668
+        self.renderer.text(&format_bytes(process.vsize), x + 80.0, y, &value_style)?;
669
+
670
+        // Right column - reset y
671
+        y = margin as f64 + 24.0 + 20.0 + 16.0;
672
+
673
+        // Disk I/O
674
+        self.renderer.text("Disk Read:", right_col, y, &label_style)?;
675
+        let disk_style = TextStyle { color: self.theme.disk_color, ..value_style.clone() };
676
+        self.renderer.text(&format_rate(process.io_read_rate), right_col + 90.0, y, &disk_style)?;
677
+        y += row_height;
678
+
679
+        self.renderer.text("Disk Write:", right_col, y, &label_style)?;
680
+        self.renderer.text(&format_rate(process.io_write_rate), right_col + 90.0, y, &disk_style)?;
681
+        y += row_height;
682
+
683
+        // Network
684
+        self.renderer.text("Net Conn:", right_col, y, &label_style)?;
685
+        let net_style = TextStyle { color: self.theme.network_color, ..value_style.clone() };
686
+        self.renderer.text(
687
+            &format!("{} ({} TCP, {} UDP)", process.net_connections, process.net_tcp, process.net_udp),
688
+            right_col + 90.0, y, &net_style
689
+        )?;
690
+        y += row_height;
691
+
692
+        self.renderer.text("Net Rate:", right_col, y, &label_style)?;
693
+        self.renderer.text(
694
+            &format!("↓{} ↑{}", format_rate(process.net_rx_rate), format_rate(process.net_tx_rate)),
695
+            right_col + 90.0, y, &net_style
696
+        )?;
697
+
698
+        // Command line section
699
+        y = margin as f64 + 24.0 + 20.0 + 16.0 + row_height * 6.0;
700
+        self.renderer.text("Command:", x, y, &label_style)?;
701
+        y += row_height;
702
+
703
+        // Command line (truncate if too long)
704
+        let cmd_style = TextStyle {
705
+            font_family: "monospace".to_string(),
706
+            font_size: 10.0,
707
+            color: self.theme.text,
708
+            ..Default::default()
709
+        };
710
+        let max_cmd_len = ((self.width - margin * 2 - 40) / 6) as usize; // Approximate char width
711
+        let cmdline = if process.cmdline.len() > max_cmd_len {
712
+            format!("{}...", &process.cmdline[..max_cmd_len - 3])
713
+        } else if process.cmdline.is_empty() {
714
+            format!("[{}]", process.name)
715
+        } else {
716
+            process.cmdline.clone()
717
+        };
718
+        self.renderer.text(&cmdline, x, y, &cmd_style)?;
719
+
720
+        // Actions footer
721
+        let footer_y = (self.height - margin - 40) as f64;
722
+        self.renderer.line(
723
+            x, footer_y - 8.0,
724
+            (self.width - margin * 2) as f64 + x - 20.0, footer_y - 8.0,
725
+            self.theme.border, 1.0
726
+        )?;
727
+
728
+        let action_style = TextStyle {
729
+            font_family: "monospace".to_string(),
730
+            font_size: 10.0,
731
+            color: self.theme.cpu_color,
732
+            ..Default::default()
733
+        };
734
+        self.renderer.text("[K] Kill (SIGTERM)   [X] Kill (SIGKILL)", x, footer_y, &action_style)?;
735
+
736
+        Ok(())
737
+    }
738
+
548739
     /// Render the content for the active tab.
549740
     fn render_tab_content(&self, content_y: i32, _content_height: u32) -> Result<()> {
550741
         let graph_width = self.width - (CONTENT_PADDING * 2);
@@ -1166,6 +1357,36 @@ impl App {
11661357
                             }
11671358
                             _ => {}
11681359
                         }
1360
+                    }
1361
+                    // Detail view mode
1362
+                    else if self.detail_pid.is_some() {
1363
+                        match key_event.key {
1364
+                            Key::Escape => {
1365
+                                self.detail_pid = None;
1366
+                                ev_loop.request_redraw();
1367
+                            }
1368
+                            Key::Char('K') => {
1369
+                                if let Some(pid) = self.detail_pid {
1370
+                                    let name = self.processes.iter()
1371
+                                        .find(|p| p.pid == pid)
1372
+                                        .map(|p| p.name.clone())
1373
+                                        .unwrap_or_else(|| format!("PID {}", pid));
1374
+                                    self.kill_confirm = Some((pid, 15, name));
1375
+                                    ev_loop.request_redraw();
1376
+                                }
1377
+                            }
1378
+                            Key::Char('X') => {
1379
+                                if let Some(pid) = self.detail_pid {
1380
+                                    let name = self.processes.iter()
1381
+                                        .find(|p| p.pid == pid)
1382
+                                        .map(|p| p.name.clone())
1383
+                                        .unwrap_or_else(|| format!("PID {}", pid));
1384
+                                    self.kill_confirm = Some((pid, 9, name));
1385
+                                    ev_loop.request_redraw();
1386
+                                }
1387
+                            }
1388
+                            _ => {}
1389
+                        }
11691390
                     } else {
11701391
                         // Normal mode key handling
11711392
                         match key_event.key {
@@ -1285,6 +1506,13 @@ impl App {
12851506
                                 }
12861507
                                 ev_loop.request_redraw();
12871508
                             }
1509
+                            // Enter - open process detail view
1510
+                            Key::Return => {
1511
+                                if let Some(pid) = self.process_list.selected_pid() {
1512
+                                    self.detail_pid = Some(pid);
1513
+                                    ev_loop.request_redraw();
1514
+                                }
1515
+                            }
12881516
                             // Kill selected process (K = SIGTERM, X = SIGKILL) - with confirmation
12891517
                             Key::Char('K') => {
12901518
                                 if let Some(pid) = self.process_list.selected_pid() {