Skip to main content

procx/
tui.rs

1use anyhow::Result;
2use crossterm::{
3    event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
4    terminal::{disable_raw_mode, enable_raw_mode},
5};
6use ratatui::{prelude::*, TerminalOptions};
7use std::io;
8
9mod rendering;
10
11use crate::{
12    processes::{FilterOptions, ProcessManager, ProcessSearchResults},
13    settings::AppSettings,
14};
15
16use self::rendering::Tui;
17
18struct App {
19    process_manager: ProcessManager,
20    search_results: ProcessSearchResults,
21    filter_options: FilterOptions,
22    tui: Tui,
23}
24
25impl App {
26    fn new(search_criteria: String, app_settings: AppSettings) -> Result<App> {
27        let mut app = App {
28            process_manager: ProcessManager::new()?,
29            search_results: ProcessSearchResults::empty(),
30            filter_options: app_settings.filter_opions,
31            tui: Tui::new(search_criteria),
32        };
33        app.search_for_processess();
34        Ok(app)
35    }
36
37    fn enter_char(&mut self, new_char: char) {
38        self.tui.enter_char(new_char);
39        self.search_for_processess();
40    }
41
42    fn search_for_processess(&mut self) {
43        self.tui.reset_error_message();
44        self.process_manager.refresh();
45        self.search_results = self
46            .process_manager
47            .find_processes(self.tui.search_input_text(), self.filter_options);
48        self.tui
49            .update_process_table_number_of_items(self.search_results.len());
50    }
51
52    fn delete_char(&mut self) {
53        self.tui.delete_char();
54        self.search_for_processess();
55    }
56
57    fn kill_selected_process(&mut self) {
58        self.tui.reset_error_message();
59        let prc_index = self.tui.get_selected_row_index();
60        if let Some(prc) = self.search_results.nth(prc_index) {
61            let pid = prc.pid;
62            if self.process_manager.kill_process(pid) {
63                self.search_for_processess();
64                //NOTE: cache refresh takes time and process may reappear in list!
65                self.search_results.remove(pid);
66                //TODO: this must be here because details will show 1/0 when removed!
67                // seems like this can only be fixed by autorefresh!
68                self.tui
69                    .update_process_table_number_of_items(self.search_results.len());
70            } else {
71                self.tui
72                    .set_error_message("Failed to kill process, check permissions");
73            }
74        }
75    }
76}
77
78pub fn start_app(search_criteria: String, app_settings: AppSettings) -> Result<()> {
79    // setup terminal
80    enable_raw_mode()?;
81    let backend = CrosstermBackend::new(io::stdout());
82    let viewport = app_settings.viewport.clone();
83    let mut terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
84
85    // create app and run it
86    let app = App::new(search_criteria, app_settings)?;
87    let res = run_app(&mut terminal, app);
88
89    // restore terminal
90    disable_raw_mode()?;
91    terminal.clear()?;
92
93    //FIXME: add error handling, for exaple some error page should be shown
94    if let Err(err) = res {
95        println!("{err:?}");
96    }
97
98    Ok(())
99}
100
101fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
102    loop {
103        terminal.draw(|f| app.tui.render_ui(&app.search_results, f))?;
104
105        if let Event::Key(key) = event::read()? {
106            if key.kind == KeyEventKind::Press {
107                use KeyCode::*;
108                match key.code {
109                    Esc => return Ok(()),
110                    Up if key.modifiers.contains(KeyModifiers::CONTROL) => {
111                        app.tui.select_first_row()
112                    }
113                    Down if key.modifiers.contains(KeyModifiers::CONTROL) => {
114                        app.tui.select_last_row()
115                    }
116                    Up | BackTab => app.tui.select_previous_row(1),
117                    Tab | Down => app.tui.select_next_row(1),
118                    Char('j') if key.modifiers.contains(KeyModifiers::CONTROL) => {
119                        app.tui.select_next_row(1);
120                    }
121                    Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => {
122                        app.tui.select_previous_row(1);
123                    }
124                    PageUp => app.tui.select_previous_row(10),
125                    PageDown => app.tui.select_next_row(10),
126                    Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
127                        return Ok(());
128                    }
129                    Char('x') if key.modifiers.contains(KeyModifiers::CONTROL) => {
130                        app.kill_selected_process()
131                    }
132                    Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
133                        app.search_for_processess()
134                    }
135                    Char('f') if key.modifiers.contains(KeyModifiers::CONTROL) => {
136                        app.tui.process_details_down(&mut terminal.get_frame())
137                    }
138                    Char('b') if key.modifiers.contains(KeyModifiers::CONTROL) => {
139                        app.tui.process_details_up()
140                    }
141                    Char(to_insert) => app.enter_char(to_insert),
142                    Backspace => app.delete_char(),
143                    _ => app.tui.handle_input(key),
144                }
145            }
146        }
147    }
148}