From 36f59143b97a1fcc90c93fc6c4e70a5fd1d97689 Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Tue, 5 Nov 2024 12:48:07 -0600 Subject: [PATCH 01/12] Some updates --- src/app.rs | 14 ++-- src/components/performance.rs | 123 ++++++++++++++++------------------ src/components/process.rs | 2 +- src/components/system.rs | 2 +- src/config.rs | 21 +++++- src/main.rs | 12 ++-- 6 files changed, 91 insertions(+), 83 deletions(-) diff --git a/src/app.rs b/src/app.rs index 9066779..43892b8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -33,7 +33,7 @@ impl App { Self { system: SystemComponent::new(config.key_config.clone()), process: ProcessComponent::new(config.key_config.clone()), - performance: PerformanceComponent::new(config.key_config.clone(), 10), + performance: PerformanceComponent::new(config.clone(), 10), tab: TabComponent::new(config.key_config.clone()), help: HelpComponent::new(config.key_config.clone()), error: ErrorComponent::new(config.key_config.clone()), @@ -95,12 +95,12 @@ impl App { return Ok(EventState::Consumed) } // Process termination code - //else if key.code == self.config.key_config.terminate { - // if let Some(pid) = self.process.selected_pid() { - // self.system.terminate_process(pid)?; - // return Ok(EventState::Consumed) - // } - //} + else if key.code == self.config.key_config.terminate { + if let Some(pid) = self.process.selected_pid() { + self.system.terminate_process(pid)?; + return Ok(EventState::Consumed) + } + } } Tab::Performance => { diff --git a/src/components/performance.rs b/src/components/performance.rs index 4d96089..35c2056 100644 --- a/src/components/performance.rs +++ b/src/components/performance.rs @@ -9,7 +9,7 @@ use performance_queue::{CpuItem, MemoryItem, PerformanceQueue}; use super::EventState; use super::DrawableComponent; use super::vertical_tabs::VerticalTab; -use crate::config::KeyConfig; +use crate::config::Config; use crate::components::Component; use crate::components::vertical_tabs::VerticalTabComponent; @@ -18,16 +18,16 @@ pub struct PerformanceComponent { cpu_info: PerformanceQueue, memory_info: PerformanceQueue, vertical_tabs: VerticalTabComponent, - key_config: KeyConfig, + config: Config, } impl PerformanceComponent { - pub fn new(key_config: KeyConfig, max_size: usize) -> Self { + pub fn new(config: Config, max_size: usize) -> Self { Self { cpu_info: PerformanceQueue::new(max_size), memory_info: PerformanceQueue::new(max_size), vertical_tabs: VerticalTabComponent::default(), - key_config: key_config, + config, } } @@ -38,83 +38,30 @@ impl PerformanceComponent { } fn draw_memory_graph(&self, f: &mut Frame, area: Rect) -> io::Result<()> { - // TODO: make sure there is something to draw... - let refresh_rate = 5; + let refresh_rate = (self.config.refresh_rate() / 1000) as usize; // Converting ms to s. let data_points = self.memory_info.performance_items .iter() .enumerate() - .map(|(i, item)| { - ((i*refresh_rate) as f64, item.used_memory_gb() as f64) - }) + .map(|(i, item)| ((i * refresh_rate) as f64, item.used_memory_gb() as f64)) .collect::>(); - let data_set = vec![ - Dataset::default() - .marker(Marker::Dot) - .graph_type(GraphType::Line) - .style(Style::default().cyan()) - .data(&data_points) - ]; - - let x_axis = Axis::default() - .title("Time (s)") - .style(Style::default().white()) - .bounds([0.0, ((self.memory_info.max_size() - 1) * refresh_rate) as f64]) - .labels(vec![0.to_string().into(), ((self.memory_info.max_size() - 1) * refresh_rate).to_string().into()]); - - let y_axis = Axis::default() - .title("Used Memory (GB)") - .style(Style::default().white()) - .bounds([0.0, self.memory_info.back().unwrap().total_memory_gb() as f64]) - .labels(vec![0.to_string().into(), self.memory_info.back().unwrap().total_memory_gb().to_string().into()]); - - let chart = Chart::new(data_set) - .block(Block::default()) - .x_axis(x_axis) - .y_axis(y_axis); - - f.render_widget(chart, area); - + let y_axis_title = String::from("Used Memory (GB)"); + let y_bounds = [0.0, self.memory_info.back().unwrap().total_memory_gb() as f64]; + draw_graph(f, area, refresh_rate, self.memory_info.max_size(), data_points, y_axis_title, y_bounds)?; Ok(()) } fn draw_cpu_graph(&self, f: &mut Frame, area: Rect) -> io::Result<()> { - //TODO - let refresh_rate = 5; + let refresh_rate = (self.config.refresh_rate() / 1000) as usize; // Converting ms to s. let data_points = self.cpu_info.performance_items .iter() .enumerate() - .map(|(i, item)| { - ((i*refresh_rate) as f64, item.global_usage() as f64) - }) + .map(|(i, item)| ((i * refresh_rate) as f64, item.global_usage() as f64)) .collect::>(); - let data_set = vec![ - Dataset::default() - .marker(Marker::Dot) - .graph_type(GraphType::Line) - .style(Style::default().cyan()) - .data(&data_points) - ]; - - let x_axis = Axis::default() - .title("Time (s)") - .style(Style::default().white()) - .bounds([0.0, ((self.cpu_info.max_size() - 1) * refresh_rate) as f64]) - .labels(vec![0.to_string().into(), ((self.cpu_info.max_size() - 1) * refresh_rate).to_string().into()]); - - let y_axis = Axis::default() - .title("Global CPU Usage (%)") - .style(Style::default().white()) - .bounds([0.0, 100.0]) - .labels(vec![0.to_string().into(), 100.to_string().into()]); - - let chart = Chart::new(data_set) - .block(Block::default()) - .x_axis(x_axis) - .y_axis(y_axis); - - f.render_widget(chart, area); + let y_axis_title = String::from("Global CPU Usage (%)"); + let y_bounds = [0.0, 100.0]; + draw_graph(f, area, refresh_rate, self.cpu_info.max_size(), data_points, y_axis_title, y_bounds)?; Ok(()) } @@ -236,4 +183,46 @@ impl DrawableComponent for PerformanceComponent { Ok(()) } +} + +fn draw_graph( + f: &mut Frame, + area: Rect, + refresh_rate: usize, + max_size: usize, + data_points: Vec<(f64, f64)>, + y_axis_title: String, + y_bounds: [f64; 2], +) -> io::Result<()> { + let data_set = vec![ + Dataset::default() + .marker(Marker::Dot) + .graph_type(GraphType::Line) + .style(Style::default().cyan()) + .data(&data_points) + ]; + + let x_axis = Axis::default() + .title("Time (s)") + .style(Style::default().white()) + .bounds([0.0, ((max_size - 1) * refresh_rate as usize) as f64]) + .labels(vec![ + 0.to_string().into(), + ((max_size - 1) * refresh_rate as usize).to_string().into(), + ]); + + let y_axis = Axis::default() + .title(y_axis_title) + .style(Style::default().white()) + .bounds(y_bounds) + .labels(vec![y_bounds[0].to_string().into(), y_bounds[1].to_string().into()]); + + let chart = Chart::new(data_set) + .block(Block::default()) + .x_axis(x_axis) + .y_axis(y_axis); + + f.render_widget(chart, area); + + Ok(()) } \ No newline at end of file diff --git a/src/components/process.rs b/src/components/process.rs index a45945b..5197b54 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -107,7 +107,7 @@ impl ProcessComponent { let out_of_focus_style = Style::default().fg(Color::DarkGray); // Setting the header. - let header = ["", "Pid", "Name", "Cpu Usage (%)", "Memory Usage (Bytes)"] + let header = ["", "Pid", "Name", "CPU Usage (%)", "Memory Usage (Bytes)"] .into_iter() .map(Cell::from) .collect::() diff --git a/src/components/system.rs b/src/components/system.rs index 11ad127..f62641c 100644 --- a/src/components/system.rs +++ b/src/components/system.rs @@ -1,6 +1,6 @@ use std::io; use crossterm::event::KeyEvent; -use sysinfo::{System, Networks, Pid}; +use sysinfo::{System, Pid}; use process_list::ProcessListItem; use performance_queue::{CpuItem, MemoryItem}; use super::{Component, EventState}; diff --git a/src/config.rs b/src/config.rs index a22095d..a7ea891 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,19 +1,34 @@ use crossterm::event::KeyCode; +use serde::{Deserialize, Serialize}; -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] pub struct Config { pub key_config: KeyConfig, + refresh_rate: u64, + tick_rate: u64, } impl Default for Config { fn default() -> Self { Self { key_config: KeyConfig::default(), + refresh_rate: 5000, + tick_rate: 250, } } } -#[derive(Clone)] +impl Config { + pub fn refresh_rate(&self) -> u64 { + return self.refresh_rate.clone() + } + + pub fn tick_rate(&self) -> u64 { + return self.tick_rate.clone() + } +} + +#[derive(Clone, Serialize, Deserialize)] pub struct KeyConfig { pub move_up: KeyCode, pub move_top: KeyCode, @@ -48,7 +63,7 @@ impl Default for KeyConfig { enter: KeyCode::Enter, tab: KeyCode::Tab, filter: KeyCode::Char('/'), - terminate: KeyCode::Delete, + terminate: KeyCode::Char('T'), tab_right: KeyCode::Char('d'), tab_left: KeyCode::Char('a'), open_help: KeyCode::Char('?'), diff --git a/src/main.rs b/src/main.rs index d0c2415..d3024b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,14 +26,18 @@ async fn main() -> Result<(), Box> { setup_terminal()?; let backend = CrosstermBackend::new(io::stdout()); let mut terminal = Terminal::new(backend)?; - - // event handler setup - // argument 1: tick_rate , argument 2: system refresh_rate - let events = Events::new(250, 5000); // app creation and initialization let config = config::Config::default(); + let tick_rate = config.tick_rate(); + let refresh_rate = config.refresh_rate(); + // event handler setup + // argument 1: tick_rate , argument 2: system refresh_rate + let events = Events::new(tick_rate, refresh_rate); + let mut app = App::new(config); + + app.refresh().await?; // clear terminal From f26fb05e6357d2b56341ae7ab328399d57840fcf Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Tue, 25 Feb 2025 15:30:40 -0600 Subject: [PATCH 02/12] updates --- src/app.rs | 4 ++-- src/components/performance.rs | 6 +++--- src/components/tab.rs | 24 ++++++++++++------------ src/components/vertical_tabs.rs | 22 +++++++++++----------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/app.rs b/src/app.rs index 43892b8..3397c06 100644 --- a/src/app.rs +++ b/src/app.rs @@ -109,7 +109,7 @@ impl App { } } - Tab::Users => {} + //Tab::Users => {} } if self.tab.event(key)?.is_consumed() { @@ -171,7 +171,7 @@ impl App { self.performance.draw(f, chunks[0], false)?; } - Tab::Users => {} + //Tab::Users => {} } // Drawing the HelpComponent as a pop up. See /components/help.rs. diff --git a/src/components/performance.rs b/src/components/performance.rs index 35c2056..70484fa 100644 --- a/src/components/performance.rs +++ b/src/components/performance.rs @@ -176,9 +176,9 @@ impl DrawableComponent for PerformanceComponent { self.draw_memory_graph(f, vertical_chunks[1])?; self.draw_memory_item(f, horizontal_chunks[1])?; } - else if matches!(self.vertical_tabs.selected_vert_tab, VerticalTab::Network) { + //else if matches!(self.vertical_tabs.selected_vert_tab, VerticalTab::Network) { //self.draw_network_graph(f, vertical_chunks[1])?; - } + //} self.vertical_tabs.draw(f, horizontal_chunks[0], false)?; Ok(()) @@ -207,8 +207,8 @@ fn draw_graph( .style(Style::default().white()) .bounds([0.0, ((max_size - 1) * refresh_rate as usize) as f64]) .labels(vec![ - 0.to_string().into(), ((max_size - 1) * refresh_rate as usize).to_string().into(), + 0.to_string().into(), ]); let y_axis = Axis::default() diff --git a/src/components/tab.rs b/src/components/tab.rs index a5116ab..dbd0a05 100644 --- a/src/components/tab.rs +++ b/src/components/tab.rs @@ -17,7 +17,7 @@ enum MoveTabDirection { pub enum Tab { Process, Performance, - Users, + //Users, } pub struct TabComponent { @@ -45,7 +45,7 @@ impl TabComponent { vec![ String::from("Process"), String::from("Performance"), - String::from("Users"), + //String::from("Users"), ] } @@ -56,25 +56,25 @@ impl TabComponent { self.selected_tab = Tab::Performance; } else { - self.selected_tab = Tab::Users; + self.selected_tab = Tab::Performance; } } Tab::Performance => { - if direction == MoveTabDirection::Right { - self.selected_tab = Tab::Users; - } - else { - self.selected_tab = Tab::Process; - } - } - Tab::Users => { if direction == MoveTabDirection::Right { self.selected_tab = Tab::Process; } else { - self.selected_tab = Tab::Performance; + self.selected_tab = Tab::Process; } } + //Tab::Users => { + // if direction == MoveTabDirection::Right { + // self.selected_tab = Tab::Process; + // } + // else { + // self.selected_tab = Tab::Performance; + // } + //} } } } diff --git a/src/components/vertical_tabs.rs b/src/components/vertical_tabs.rs index 5371711..89cf0b6 100644 --- a/src/components/vertical_tabs.rs +++ b/src/components/vertical_tabs.rs @@ -17,7 +17,7 @@ pub enum MoveTabDirection { pub enum VerticalTab { Cpu, Memory, - Network, + //Network, } impl Default for VerticalTab { @@ -49,7 +49,7 @@ impl VerticalTabComponent { vec![ String::from("CPU"), String::from("Memory"), - String::from("Network"), + //String::from("Network"), ] } @@ -57,7 +57,7 @@ impl VerticalTabComponent { match self.selected_vert_tab { VerticalTab::Cpu => { if direction == MoveTabDirection::Up { - self.selected_vert_tab = VerticalTab::Network; + self.selected_vert_tab = VerticalTab::Memory; } else { self.selected_vert_tab = VerticalTab::Memory; @@ -67,18 +67,18 @@ impl VerticalTabComponent { if direction == MoveTabDirection::Up { self.selected_vert_tab = VerticalTab::Cpu; } - else { - self.selected_vert_tab = VerticalTab::Network; - } - } - VerticalTab::Network => { - if direction == MoveTabDirection::Up { - self.selected_vert_tab = VerticalTab::Memory; - } else { self.selected_vert_tab = VerticalTab::Cpu; } } + //VerticalTab::Network => { + // if direction == MoveTabDirection::Up { + // self.selected_vert_tab = VerticalTab::Memory; + // } + // else { + // self.selected_vert_tab = VerticalTab::Cpu; + // } + //} } } } From 829771328131175941a868915366417f4091a4d1 Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Tue, 1 Apr 2025 00:00:02 -0500 Subject: [PATCH 03/12] some cleanup on process-list --- process-list/src/process_list.rs | 10 +++--- process-list/src/process_list_item.rs | 8 ++--- process-list/src/process_list_items.rs | 42 +++++++++----------------- src/components/process.rs | 4 +-- src/config.rs | 12 +++++--- 5 files changed, 34 insertions(+), 42 deletions(-) diff --git a/process-list/src/process_list.rs b/process-list/src/process_list.rs index 6f3fee0..343383d 100644 --- a/process-list/src/process_list.rs +++ b/process-list/src/process_list.rs @@ -59,13 +59,13 @@ impl ProcessList { // filter_text: String -- text to filter processes by name // outputs: // new ProcessList - pub fn filter(&self, filter_text: String) -> Self { + pub fn filter(&self, filter_text: &String) -> Self { let new_self = Self { - items: self.items.filter(filter_text.clone()), + items: self.items.filter(filter_text), sort: ListSortOrder::default(), follow_selection: false, selection: - if self.items.filter(filter_text.clone()).items_len() > 0 { + if self.items.filter(filter_text).items_len() > 0 { Some(0) } else { @@ -313,13 +313,13 @@ mod test { // Filter constructor case 1. let filter_string = String::from("c"); - let filter_instance = instance.filter(filter_string); + let filter_instance = instance.filter(&filter_string); assert!(filter_instance.empty()); assert_eq!(filter_instance.selection(), None); // Filter constructor case 2. let filter_string = String::from("b"); - let filter_instance = instance.filter(filter_string); + let filter_instance = instance.filter(&filter_string); assert!(!filter_instance.empty()); assert_eq!(filter_instance.selection(), Some(0)); } diff --git a/process-list/src/process_list_item.rs b/process-list/src/process_list_item.rs index 67493fd..6015c8a 100644 --- a/process-list/src/process_list_item.rs +++ b/process-list/src/process_list_item.rs @@ -18,10 +18,10 @@ impl ProcessListItem { } } - // This is a boolean function to determine if the name contained by the instance of ProcessListItem, &self, - // matches the parameter filter_text. + // filter by name or pid pub fn is_match(&self, filter_text: &str) -> bool { - self.name.contains(filter_text) + self.name.contains(filter_text) || + self.pid.to_string().contains(filter_text) } // This function gets the pid of a ProcessListItem instance. @@ -51,7 +51,7 @@ impl PartialEq for ProcessListItem { // This is a boolean function to determine if the ProcessListItem instance &self // is equal to the parameter other. The comparison is done by pid. fn eq(&self, other: &Self) -> bool { - return self.pid().eq(&other.pid()) + return self.pid.eq(&other.pid) } } diff --git a/process-list/src/process_list_items.rs b/process-list/src/process_list_items.rs index 1211a99..9360311 100644 --- a/process-list/src/process_list_items.rs +++ b/process-list/src/process_list_items.rs @@ -10,45 +10,33 @@ pub struct ProcessListItems { } impl ProcessListItems { - // This function constructs a `new` instance of ProcessListItems and initializes field - // list_items by passing the parameter list to the instance function create_items(list). + // creator pub fn new(list: &Vec) -> Self { Self { list_items: Self::create_items(list), } } - // This function populates a new vector of type ProcessListItem by cloning each item - // contained in the list parameter then pushing the cloned item onto the new vector. - // The new vector is returned. + // creator helper fn create_items(list: &Vec) -> Vec { - let list_len = list.len(); - let mut items = Vec::with_capacity(list_len); - for e in list { - let item = e.clone(); - items.push(item); - } - return items; + list.iter().cloned().collect() } - // This function constructs a new ProcessListItems instance by filtering items - // from the current ProcessListItems instance. Item's are filtered using the - // ProcessListItem instance function is_match(&filter_text: &str). - pub fn filter(&self, filter_text: String) -> Self { + // creator for filtered list, call on existing list + pub fn filter(&self, filter_text: &String) -> Self { Self { - list_items: self.list_items - .iter() - .filter(|item| { - item.is_match(&filter_text) - }) - .map(|item| { - let item = item.clone(); - item - }) - .collect::>(), + list_items: self.create_filtered_items(filter_text) } } + fn create_filtered_items(&self, filter_text: &String) -> Vec { + self.list_items + .iter() + .filter(|item| {item.is_match(filter_text)}) + .cloned() + .collect() + } + // This function updates the ProcessListItems instance field list_items by adding new items, updating fields of // existing items, and removing old items given the parameters new_list and sort. Item's can be sorted by a // ListSortOrder, the sort of a list determines the items insert position. @@ -233,7 +221,7 @@ mod test { let items = vec![item_0, item_1]; let instance = ProcessListItems::new(&items); - let filtered_instance = instance.filter(String::from("a")); + let filtered_instance = instance.filter(&String::from("a")); assert_eq!(filtered_instance.items_len(), 1); assert_eq!(filtered_instance.get_item(0), Some(&clone_0)); assert_eq!(filtered_instance.get_item(1), None); diff --git a/src/components/process.rs b/src/components/process.rs index 5197b54..23878ad 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -52,7 +52,7 @@ impl ProcessComponent { // We first filter the new processes by the filter, let processes = ProcessListItems::new(new_processes); let filter_text = self.filter.input_str(); - let filtered_processes = processes.filter(filter_text); + let filtered_processes = processes.filter(&filter_text); // then we update the filtered list with the new filtered processes. filtered_list.update(&filtered_processes.list_items)?; } @@ -207,7 +207,7 @@ impl Component for ProcessComponent { None } else { - Some(self.list.filter(self.filter.input_str())) + Some(self.list.filter(&self.filter.input_str())) }; } diff --git a/src/config.rs b/src/config.rs index a7ea891..29d64ce 100644 --- a/src/config.rs +++ b/src/config.rs @@ -56,16 +56,20 @@ pub struct KeyConfig { impl Default for KeyConfig { fn default() -> Self { Self { - move_up: KeyCode::Char('w'), + move_up: KeyCode::Up, + //move_up: KeyCode::Char('w'), move_top: KeyCode::Char('W'), - move_down: KeyCode::Char('s'), + move_down: KeyCode::Down, + //move_down: KeyCode::Char('s'), move_bottom: KeyCode::Char('S'), enter: KeyCode::Enter, tab: KeyCode::Tab, filter: KeyCode::Char('/'), terminate: KeyCode::Char('T'), - tab_right: KeyCode::Char('d'), - tab_left: KeyCode::Char('a'), + tab_right: KeyCode::Right, + //tab_right: KeyCode::Char('d'), + tab_left: KeyCode::Left, + //tab_left: KeyCode::Char('a'), open_help: KeyCode::Char('?'), exit_popup: KeyCode::Esc, sort_name_inc: KeyCode::Char('n'), From 36f3050fe07e6b0e98cb18c6d6663635325e8349 Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Tue, 1 Apr 2025 21:06:38 -0500 Subject: [PATCH 04/12] more cleanup --- process-list/src/process_list.rs | 71 +++++------- process-list/src/process_list_item.rs | 9 +- process-list/src/process_list_items.rs | 143 +++++-------------------- src/components/process.rs | 10 +- 4 files changed, 64 insertions(+), 169 deletions(-) diff --git a/process-list/src/process_list.rs b/process-list/src/process_list.rs index 343383d..275a251 100644 --- a/process-list/src/process_list.rs +++ b/process-list/src/process_list.rs @@ -40,11 +40,7 @@ pub struct ProcessList { } impl ProcessList { - // new Constructor - // inputs: - // list: &Vec -- Reference to a Vector of ProcessListItem - // outputs: - // new ProcessList + // constructor pub fn new(list: &Vec) -> Self { Self { items: ProcessListItems::new(list), @@ -54,11 +50,7 @@ impl ProcessList { } } - // pub fn filter - // inputs: - // filter_text: String -- text to filter processes by name - // outputs: - // new ProcessList + // constructor for filtered list pub fn filter(&self, filter_text: &String) -> Self { let new_self = Self { items: self.items.filter(filter_text), @@ -75,53 +67,48 @@ impl ProcessList { new_self } - // This function is called whenever there is a refresh event. The function is responsible for - // updating the instance items with the parameter new_list. - pub fn update(&mut self, new_list: &Vec) -> io::Result<()> { - // Get the selected item, selected_item = Some(item) || None. + // update process list with new list + pub fn update(&mut self, new_list: &Vec) { + // get the selected item, either some(item) or None let selected_item: Option<&ProcessListItem> = self.items.get_item(self.selection.unwrap_or_default()); - // Get the selected item's pid, pid = Some(pid) || None. + // get the selected item's pid, either some(pid) or None let pid: Option = selected_item.map(|item| item.pid()); - // Update items with new list. - self.items.update_items(new_list, &self.sort)?; + // update items with new list. + self.items.update_items(new_list); + + // re-sort + self.items.sort_items(&self.sort); - // If pid is some then set self.selection = pid, else self.selection = None. IE: If the item being followed - // is removed from the list on self.items.update_items(), then follow_selection is set to None. + // if list is empty, set selection to None and return + if self.items.items_len() == 0 { + self.selection = None; + return + } + + // if following, then set selection to selected item's new index if self.follow_selection { self.selection = pid.and_then(|p| self.items.get_idx(p)); } else { - // since it is the case that the process list might - // change in size on update, we need to check if the - // selection is still in range of the list. If it is not, - // then set self.selection to the max_idx. if let Some(selection) = self.selection { let max_idx = self.items.items_len().saturating_sub(1); - // If the are no items in the list after the update, then set selection to None. - if self.items.items_len() == 0 { - self.selection = None - } - // Else if the length of items shrinks in size after the update and the selection is - // now greater than the max_idx, set selection to max_idx. - else if selection > max_idx { + + if selection > max_idx { self.selection = Some(max_idx) } } } - // If selection is None prior to self.update being called or if selection is set to None because the followed item - // was removed from the list, then we need to check if the list is non-empty and set selection to Some(0). - if self.items.items_len() > 0 && self.selection.is_none() { - self.selection = Some(0); + // if selection is none at this point, set to 0 + if self.selection.is_none() { + self.selection = Some(0) } - - Ok(()) } // This function is called when there is a `ListSortOrder` key event. The items length should never change here. - pub fn sort(&mut self, sort: ListSortOrder) -> io::Result<()> { + pub fn sort(&mut self, sort: &ListSortOrder) -> io::Result<()> { // Get the selected item, selected_item = Some(item) || None. let selected_item: Option<&ProcessListItem> = self.items.get_item(self.selection.unwrap_or_default()); @@ -129,8 +116,7 @@ impl ProcessList { let pid: Option = selected_item.map(|item| item.pid()); // Sort items. - self.sort = sort.clone(); - self.items.sort_items(&sort)?; + self.items.sort_items(sort); // If follow selection, then set self.selection to the new index of the selected item's pid. if self.follow_selection { @@ -142,14 +128,13 @@ impl ProcessList { // This function is responsible for changing the follow_selection field. If follow_selection is true, // then set to false, else set to true. - pub fn change_follow_selection(&mut self) -> io::Result<()> { + pub fn change_follow_selection(&mut self) { if self.follow_selection { self.follow_selection = false; } else { self.follow_selection = true; } - Ok(()) } // pub fn move_selection -- change self.selected_item given a direction @@ -394,7 +379,7 @@ mod test { assert!(instance.sort == ListSortOrder::CpuUsageDec); assert!(!instance.follow()); assert_eq!(instance.selection(), Some(0)); - let _ = instance.sort(ListSortOrder::CpuUsageInc); + let _ = instance.sort(&ListSortOrder::CpuUsageInc); assert_eq!(instance.selection(), Some(0)); @@ -406,7 +391,7 @@ mod test { let _ = instance.change_follow_selection(); assert!(instance.follow()); assert_eq!(instance.selection(), Some(0)); - let _ = instance.sort(ListSortOrder::CpuUsageInc); + let _ = instance.sort(&ListSortOrder::CpuUsageInc); assert_eq!(instance.selection(), Some(1)); } diff --git a/process-list/src/process_list_item.rs b/process-list/src/process_list_item.rs index 6015c8a..1668997 100644 --- a/process-list/src/process_list_item.rs +++ b/process-list/src/process_list_item.rs @@ -1,4 +1,3 @@ -// This structure contains pertinent information to a Process. #[derive(Default, Clone, Debug)] pub struct ProcessListItem { pid: u32, @@ -8,7 +7,7 @@ pub struct ProcessListItem { } impl ProcessListItem { - // This function constructs a new instance of ProcessListItem with the parameters. + // constructor pub fn new(pid: u32, name: String, cpu_usage: f32, memory_usage: u64) -> Self { Self { pid, @@ -45,11 +44,9 @@ impl ProcessListItem { } } -// Here, I am implementing the trait Partial Equality for an instance of ProcessListItem. -// This is done so that ProcessListItems in a Vector can be iterated over and compared. +// PartialEq is needed for comparison, e.g., calling contains impl PartialEq for ProcessListItem { - // This is a boolean function to determine if the ProcessListItem instance &self - // is equal to the parameter other. The comparison is done by pid. + // comparing by pid fn eq(&self, other: &Self) -> bool { return self.pid.eq(&other.pid) } diff --git a/process-list/src/process_list_items.rs b/process-list/src/process_list_items.rs index 9360311..46eabc6 100644 --- a/process-list/src/process_list_items.rs +++ b/process-list/src/process_list_items.rs @@ -1,34 +1,34 @@ -use std::io; +use std::collections::HashMap; use crate::process_list::ListSortOrder; use crate::process_list_item::ProcessListItem; use crate::list_items_iter::ListItemsIterator; -// This structure contains a vector of type ProcessListItem. #[derive(Default, Clone)] pub struct ProcessListItems { pub list_items: Vec, } impl ProcessListItems { - // creator + // constructor pub fn new(list: &Vec) -> Self { Self { list_items: Self::create_items(list), } } - // creator helper + // constructor helper fn create_items(list: &Vec) -> Vec { list.iter().cloned().collect() } - // creator for filtered list, call on existing list + // constructor for filtered list pub fn filter(&self, filter_text: &String) -> Self { Self { list_items: self.create_filtered_items(filter_text) } } + // filtered constructor helper fn create_filtered_items(&self, filter_text: &String) -> Vec { self.list_items .iter() @@ -37,82 +37,30 @@ impl ProcessListItems { .collect() } - // This function updates the ProcessListItems instance field list_items by adding new items, updating fields of - // existing items, and removing old items given the parameters new_list and sort. Item's can be sorted by a - // ListSortOrder, the sort of a list determines the items insert position. - pub fn update_items(&mut self, new_list: &Vec, sort: &ListSortOrder) -> io::Result<()> { - for e in new_list { - // 1. If the new list contains an entry not in the instance list, then add entry to instance list. - if !self.list_items.contains(e) { - // 1.1 Get the index to insert the item `e`. - let idx = self.insert_item_idx(e, sort); - // 1.2 Clone the item from the new list and insert in instance list. - let item = e.clone(); - self.list_items.insert(idx, item); + // updates existing items, removes old items, and adds new items + // sort order is ruined by this function, call sort after use + pub fn update_items(&mut self, new_list: &Vec) { + let mut updated_list: Vec = Vec::new(); + let new_map: HashMap<_,_> = new_list.iter().map(|item| (item.pid(),item)).collect(); + + for item in &mut self.list_items { + if let Some(&updated_item) = new_map.get(&item.pid()) { + *item = updated_item.clone(); + updated_list.push(item.clone()); } - // 2. If the new list contains updated information for a process in the instance list, then update the instance list item. - else if let Some(instance_item) = - self.list_items.iter_mut().find(|item| item == &e) { *instance_item = e.clone(); } } - // 3. If the instance list contains an entry not in the new list, then remove entry from instance list. - self.list_items.retain(|item| new_list.contains(item)); - - // The instance list might become unsorted when updating items if sorting by usage (step 2 above). - // Sort the list if the list is being sorted by usage. - if *sort == ListSortOrder::CpuUsageInc || *sort == ListSortOrder::CpuUsageDec || *sort == ListSortOrder::MemoryUsageInc || *sort == ListSortOrder::MemoryUsageDec { - self.sort_items(sort)?; - } - Ok(()) - } - // This function determines the parameter item's insert index position in the instance list given a ListSortOrder. - fn insert_item_idx(&self, item: &ProcessListItem, sort: &ListSortOrder) -> usize { - match sort { - ListSortOrder::PidInc => { - self.list_items - .binary_search_by(|probe| probe.pid().cmp(&item.pid())) - .unwrap_or_else(|index| index) - } - ListSortOrder::PidDec => { - self.list_items - .binary_search_by(|probe| probe.pid().cmp(&item.pid()).reverse()) - .unwrap_or_else(|index| index) - } - ListSortOrder::NameInc => { - self.list_items - .binary_search_by(|probe| probe.name().cmp(&item.name())) - .unwrap_or_else(|index| index) - } - ListSortOrder::NameDec => { - self.list_items - .binary_search_by(|probe| probe.name().cmp(&item.name()).reverse()) - .unwrap_or_else(|index| index) - } - ListSortOrder::CpuUsageInc => { - self.list_items - .binary_search_by(|probe| probe.cpu_usage().partial_cmp(&item.cpu_usage()).unwrap_or(std::cmp::Ordering::Equal)) - .unwrap_or_else(|index| index) - } - ListSortOrder::CpuUsageDec => { - self.list_items - .binary_search_by(|probe| item.cpu_usage().partial_cmp(&probe.cpu_usage()).unwrap_or(std::cmp::Ordering::Equal)) - .unwrap_or_else(|index| index) - } - ListSortOrder::MemoryUsageInc => { - self.list_items - .binary_search_by(|probe| probe.memory_usage().cmp(&item.memory_usage())) - .unwrap_or_else(|index| index) - } - ListSortOrder::MemoryUsageDec => { - self.list_items - .binary_search_by(|probe| item.memory_usage().cmp(&probe.memory_usage())) - .unwrap_or_else(|index| index) + for new_item in new_list { + if !updated_list.contains(new_item) { + updated_list.push(new_item.clone()) } } + + self.list_items = updated_list; } - // This function sorts the instance list by parameter sort. - pub fn sort_items(&mut self, sort: &ListSortOrder) -> io::Result<()> { + // sort by list sort order + pub fn sort_items(&mut self, sort: &ListSortOrder) { match sort { ListSortOrder::PidInc => { self.list_items.sort_by(|a, b| a.pid().cmp(&b.pid())); @@ -139,11 +87,10 @@ impl ProcessListItems { self.list_items.sort_by(|a, b| b.memory_usage().cmp(&a.memory_usage())); } } - Ok(()) } // GETTERS - // This function gets the reference to an item given an index into the instance list. + // gets the reference to an item given an index pub fn get_item(&self, idx: usize) -> Option<&ProcessListItem> { let list_len = self.items_len(); let max_idx = list_len.saturating_sub(1); @@ -154,7 +101,7 @@ impl ProcessListItems { item } - // This function gets the index of an item in the instance list given the item's pid. + // gets the index of an item given the item's pid. pub fn get_idx(&self, pid: u32) -> Option { if let Some(idx) = self.list_items .iter() @@ -167,12 +114,12 @@ impl ProcessListItems { } } - // This function gets the length of the instance list. + // gets the length of the instance list. pub fn items_len(&self) -> usize { self.list_items.len() } - // This function returns a ListItemIterator instance given a start position and max number of iterations. + // returns a ListItemIterator instance given a start position and max number of iterations. pub const fn iterate(&self, start: usize, max_amount: usize) -> ListItemsIterator<'_> { ListItemsIterator::new(self, start, max_amount) } @@ -229,41 +176,6 @@ mod test { assert_eq!(filtered_instance.get_idx(2), None); } - #[test] - fn test_insert_item_idx() { - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2); - let items = vec![item_0, item_1]; - let mut instance = ProcessListItems::new(&items); - let item_to_insert = ProcessListItem::new(3, String::from("c"), 1.5, 0); - - // The item's must be sorted by the argument provided to insert_item_idx(sort) to produce accurate results. - let _ = instance.sort_items(&ListSortOrder::NameDec); - let idx = instance.insert_item_idx(&item_to_insert, &ListSortOrder::NameDec); - assert_eq!(idx, 0); - let _ = instance.sort_items(&ListSortOrder::NameInc); - let idx = instance.insert_item_idx(&item_to_insert, &ListSortOrder::NameInc); - assert_eq!(idx, 2); - let _ = instance.sort_items(&ListSortOrder::PidDec); - let idx = instance.insert_item_idx(&item_to_insert, &ListSortOrder::PidDec); - assert_eq!(idx, 0); - let _ = instance.sort_items(&ListSortOrder::PidInc); - let idx = instance.insert_item_idx(&item_to_insert, &ListSortOrder::PidInc); - assert_eq!(idx, 2); - let _ = instance.sort_items(&ListSortOrder::CpuUsageDec); - let idx = instance.insert_item_idx(&item_to_insert, &ListSortOrder::CpuUsageDec); - assert_eq!(idx, 1); - let _ = instance.sort_items(&ListSortOrder::CpuUsageInc); - let idx = instance.insert_item_idx(&item_to_insert, &ListSortOrder::CpuUsageInc); - assert_eq!(idx, 1); - let _ = instance.sort_items(&ListSortOrder::MemoryUsageDec); - let idx = instance.insert_item_idx(&item_to_insert, &ListSortOrder::MemoryUsageDec); - assert_eq!(idx, 2); - let _ = instance.sort_items(&ListSortOrder::MemoryUsageInc); - let idx = instance.insert_item_idx(&item_to_insert, &ListSortOrder::MemoryUsageInc); - assert_eq!(idx, 0) - } - #[test] fn test_update_items() { let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1); @@ -279,7 +191,8 @@ mod test { let _ = instance.sort_items(&ListSortOrder::CpuUsageInc); assert_eq!(instance.get_idx(1), Some(0)); assert_eq!(instance.get_idx(2), Some(1)); - let _ = instance.update_items(&new_items, &ListSortOrder::CpuUsageInc); + let _ = instance.update_items(&new_items); + let _ = instance.sort_items(&ListSortOrder::CpuUsageInc); // Pid 2 is not in new_items so it should be removed from the instance list. assert_eq!(instance.get_idx(2), None); // Pid 3 cpu usage is 3.0 so it should be first in the instance list. diff --git a/src/components/process.rs b/src/components/process.rs index 23878ad..a8aa86e 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -47,14 +47,14 @@ impl ProcessComponent { // will always be an unfiltered list, it is updated without any conditions. // The filtered list is only updated if there is some filtered list. pub fn update(&mut self, new_processes: &Vec) -> io::Result<()> { - self.list.update(new_processes)?; + self.list.update(new_processes); if let Some(filtered_list) = self.filtered_list.as_mut() { // We first filter the new processes by the filter, let processes = ProcessListItems::new(new_processes); let filter_text = self.filter.input_str(); let filtered_processes = processes.filter(&filter_text); // then we update the filtered list with the new filtered processes. - filtered_list.update(&filtered_processes.list_items)?; + filtered_list.update(&filtered_processes.list_items); } Ok(()) } @@ -245,10 +245,10 @@ impl Component for ProcessComponent { // Check if the key is to change the follow_selection value. else if key.code == self.key_config.follow_selection { if let Some(filtered_list) = self.filtered_list.as_mut() { - filtered_list.change_follow_selection()?; + filtered_list.change_follow_selection(); } else { - self.list.change_follow_selection()?; + self.list.change_follow_selection(); } return Ok(EventState::Consumed) @@ -291,7 +291,7 @@ fn list_nav(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> bo // Else return false. fn list_sort(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> io::Result { if let Some(sort) = common_sort(key, key_config) { - list.sort(sort)?; + list.sort(&sort)?; Ok(true) } else { From e95368a54da4fb4ae351db00961920675545e7ae Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Wed, 2 Apr 2025 01:33:52 -0500 Subject: [PATCH 05/12] more cleanup on process-list --- process-list/src/process_list.rs | 169 ++++++++++++------------------- src/components/process.rs | 5 +- 2 files changed, 69 insertions(+), 105 deletions(-) diff --git a/process-list/src/process_list.rs b/process-list/src/process_list.rs index 275a251..a700a4f 100644 --- a/process-list/src/process_list.rs +++ b/process-list/src/process_list.rs @@ -1,4 +1,3 @@ -use std::io; use crate::process_list_items::ProcessListItems; use crate::process_list_item::ProcessListItem; use crate::list_iter::ListIterator; @@ -92,6 +91,7 @@ impl ProcessList { self.selection = pid.and_then(|p| self.items.get_idx(p)); } else { + // not following, keep the selection where it is, ensuring it is still in bounds if let Some(selection) = self.selection { let max_idx = self.items.items_len().saturating_sub(1); @@ -107,135 +107,92 @@ impl ProcessList { } } - // This function is called when there is a `ListSortOrder` key event. The items length should never change here. - pub fn sort(&mut self, sort: &ListSortOrder) -> io::Result<()> { - // Get the selected item, selected_item = Some(item) || None. + // sorts process list by list sort order + pub fn sort(&mut self, sort: &ListSortOrder) { + // get the selected item, selected_item = Some(item) || None. let selected_item: Option<&ProcessListItem> = self.items.get_item(self.selection.unwrap_or_default()); - // Get the selected item's pid, pid = Some(pid) || None. + // get the selected item's pid, pid = Some(pid) || None. let pid: Option = selected_item.map(|item| item.pid()); - // Sort items. + // sort items self.items.sort_items(sort); - // If follow selection, then set self.selection to the new index of the selected item's pid. + // if follow selection, then set self.selection to the new index of the selected item's pid. if self.follow_selection { self.selection = pid.and_then(|p| self.items.get_idx(p)); } - - Ok(()) - } - - // This function is responsible for changing the follow_selection field. If follow_selection is true, - // then set to false, else set to true. - pub fn change_follow_selection(&mut self) { - if self.follow_selection { - self.follow_selection = false; - } - else { - self.follow_selection = true; - } } - // pub fn move_selection -- change self.selected_item given a direction - // inputs: - // dir: MoveSelection - // outputs: - // If selection was moved, then True, else False. - pub fn move_selection(&mut self, dir: MoveSelection) -> bool { - self.selection.map_or(false, |selection| { - let new_index = match dir { + // move selection based on MoveSelection variant + pub fn move_selection(&mut self, dir: MoveSelection) { + if let Some(selection) = self.selection() { + let new_idx = match dir { MoveSelection::Down => self.selection_down(selection, 1), MoveSelection::MultipleDown => self.selection_down(selection, 10), MoveSelection::Up => self.selection_up(selection, 1), MoveSelection::MultipleUp => self.selection_up(selection, 10), MoveSelection::End => self.selection_end(selection), - MoveSelection::Top => self.selection_start(selection), + MoveSelection::Top => self.selection_start(selection), }; - - // Changed_index is true if index was moved. - let changed_index = new_index.map(|i| i != selection).unwrap_or_default(); - - if changed_index { - self.selection = new_index; - } - - // "if changed index is true then new_index should always be some" - changed_index || new_index.is_some() - }) + self.selection = new_idx; + } } - // fn selection_down -- move selection down - // inputs: - // current_index: usize, lines: usize -- how many lines to move down from current_index - // outputs: - // if the selection was moved, then Some(index), else none - fn selection_down(&self, current_index: usize, lines: usize) -> Option { - let mut new_index = current_index; - let items_max = self.items.items_len().saturating_sub(1); + // calculates and returns new index after moving current down by lines + fn selection_down(&self, current_idx: usize, lines: usize) -> Option { + let mut new_idx = current_idx; + let max_idx = self.items.items_len().saturating_sub(1); 'a: for _ in 0..lines { - if new_index >= items_max { + if new_idx >= max_idx { break 'a; } - new_index = new_index.saturating_add(1); + new_idx = new_idx.saturating_add(1); } - if new_index == current_index { - None - } - else { - Some(new_index) - } + Some(new_idx) } - // fn selection_up -- move selection up - // inputs: - // current_index: usize, lines: usize -- how many lines to move up from current_index - // outputs: - // if the selection was moved, then Some(new_index), else None. - fn selection_up(&self, current_index: usize, lines: usize) -> Option { - let mut new_index = current_index; - // labeling loop `a` to break out of `a` from within nested loop + // calculates and returns new index after moving current index up by lines + fn selection_up(&self, current_idx: usize, lines: usize) -> Option { + let mut new_idx = current_idx; + let min_idx = 0; + 'a: for _ in 0..lines { - if new_index == 0 { + if new_idx == min_idx { break 'a; } - new_index = new_index.saturating_sub(1); + new_idx = new_idx.saturating_sub(1); } - if new_index == current_index { None } - else { Some(new_index) } + Some(new_idx) } - // fn selection_end -- move selection to last item in list - // inputs: - // current_index: usize - // outputs: - // If selection was moved, then Some(new_index), else None. - fn selection_end(&self, current_index: usize) -> Option { - let items_max = self.items.items_len().saturating_sub(1); - let new_index = items_max; + // calculates and returns max index + fn selection_end(&self, _current_idx: usize) -> Option { + let max_idx = self.items.items_len().saturating_sub(1); - if new_index == current_index { None } - else { Some(new_index) } + Some(max_idx) } - // fn selection_start -- move selection to first item in list - // inputs: - // current_index: usize - // outputs: - // If selection was moved, then Some(0), else None. - fn selection_start(&self, current_index: usize) -> Option { - if current_index == 0 { None } - else { Some(0) } + // returns min index + fn selection_start(&self, _current_idx: usize) -> Option { + let min_idx = 0; + + Some(min_idx) } - // Iterator - pub fn iterate(&self, start_index: usize, max_amount: usize) -> ListIterator<'_> { - let start = start_index; - ListIterator::new(self.items.iterate(start, max_amount), self.selection) + //todo: resume cleanup starting from here + // toggle follow selection + pub fn change_follow_selection(&mut self) { + if self.follow_selection { + self.follow_selection = false; + } + else { + self.follow_selection = true; + } } // BOOLS @@ -274,6 +231,12 @@ impl ProcessList { } None } + + // iterator + pub fn iterate(&self, start_index: usize, max_amount: usize) -> ListIterator<'_> { + let start = start_index; + ListIterator::new(self.items.iterate(start, max_amount), self.selection) + } } #[cfg(test)] @@ -398,7 +361,7 @@ mod test { #[test] fn test_selection() { let mut empty_instance = ProcessList::default(); - assert_eq!(empty_instance.move_selection(MoveSelection::Down), false); + empty_instance.move_selection(MoveSelection::Down); assert_eq!(empty_instance.selection(), None); let item_0 = ProcessListItem::new(1, String::from("a"), 2.0, 2); @@ -406,28 +369,28 @@ mod test { let items = vec![item_0, item_1]; let mut instance = ProcessList::new(&items); assert_eq!(instance.selection(), Some(0)); - assert_eq!(instance.move_selection(MoveSelection::Down), true); - assert_eq!(instance.move_selection(MoveSelection::Down), false); + instance.move_selection(MoveSelection::Down); + instance.move_selection(MoveSelection::Down); assert_eq!(instance.selection(), Some(1)); - assert_eq!(instance.move_selection(MoveSelection::Up), true); - assert_eq!(instance.move_selection(MoveSelection::Up), false); + instance.move_selection(MoveSelection::Up); + instance.move_selection(MoveSelection::Up); assert_eq!(instance.selection(), Some(0)); - assert_eq!(instance.move_selection(MoveSelection::End), true); - assert_eq!(instance.move_selection(MoveSelection::End), false); + instance.move_selection(MoveSelection::End); + instance.move_selection(MoveSelection::End); assert_eq!(instance.selection(), Some(1)); - assert_eq!(instance.move_selection(MoveSelection::Top), true); - assert_eq!(instance.move_selection(MoveSelection::Top), false); + instance.move_selection(MoveSelection::Top); + instance.move_selection(MoveSelection::Top); assert_eq!(instance.selection(), Some(0)); - assert_eq!(instance.move_selection(MoveSelection::MultipleDown), true); - assert_eq!(instance.move_selection(MoveSelection::MultipleDown), false); + instance.move_selection(MoveSelection::MultipleDown); + instance.move_selection(MoveSelection::MultipleDown); assert_eq!(instance.selection(), Some(1)); - assert_eq!(instance.move_selection(MoveSelection::MultipleUp), true); - assert_eq!(instance.move_selection(MoveSelection::MultipleUp), false); + instance.move_selection(MoveSelection::MultipleUp); + instance.move_selection(MoveSelection::MultipleUp); assert_eq!(instance.selection(), Some(0)); } } \ No newline at end of file diff --git a/src/components/process.rs b/src/components/process.rs index a8aa86e..586ddbd 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -279,7 +279,8 @@ impl Component for ProcessComponent { // Else return false. fn list_nav(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> bool { if let Some(move_dir) = common_nav(key, key_config) { - list.move_selection(move_dir) + list.move_selection(move_dir); + true } else { false @@ -291,7 +292,7 @@ fn list_nav(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> bo // Else return false. fn list_sort(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> io::Result { if let Some(sort) = common_sort(key, key_config) { - list.sort(&sort)?; + list.sort(&sort); Ok(true) } else { From 6f1e0f871411f1933a55d328dad3a08e99b04311 Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Wed, 2 Apr 2025 03:11:02 -0500 Subject: [PATCH 06/12] cleanup, making more functional --- process-list/src/process_list.rs | 69 ++++++++++++++------------ process-list/src/process_list_items.rs | 38 +++----------- src/app.rs | 2 +- src/components/process.rs | 25 ++++++---- 4 files changed, 59 insertions(+), 75 deletions(-) diff --git a/process-list/src/process_list.rs b/process-list/src/process_list.rs index a700a4f..cce989b 100644 --- a/process-list/src/process_list.rs +++ b/process-list/src/process_list.rs @@ -45,7 +45,12 @@ impl ProcessList { items: ProcessListItems::new(list), sort: ListSortOrder::default(), // CpuUsageDec follow_selection: false, - selection: if list.is_empty() { None } else { Some(0) }, + selection: if list.is_empty() { + None + } + else { + Some(0) + }, } } @@ -55,14 +60,13 @@ impl ProcessList { items: self.items.filter(filter_text), sort: ListSortOrder::default(), follow_selection: false, - selection: - if self.items.filter(filter_text).items_len() > 0 { - Some(0) - } - else { - None - }, - }; + selection: if self.items.filter(filter_text).items_len() > 0 { + Some(0) + } + else { + None + }, + }; new_self } @@ -184,7 +188,6 @@ impl ProcessList { Some(min_idx) } - //todo: resume cleanup starting from here // toggle follow selection pub fn change_follow_selection(&mut self) { if self.follow_selection { @@ -195,39 +198,41 @@ impl ProcessList { } } - // BOOLS - pub fn empty(&self) -> bool { + pub fn is_empty(&self) -> bool { self.items.items_len() == 0 } - pub fn follow(&self) -> bool { + pub fn is_follow_selection(&self) -> bool { self.follow_selection } - // GETTERS pub fn len(&self) -> usize { self.items.items_len() } - // This function gets the selection index. + // gets selection index pub fn selection(&self) -> Option { self.selection } - // This function gets an optional reference to the selected process item. + // gets reference to selected process list item pub fn selected_item(&self) -> Option<&ProcessListItem> { - let selected_item: Option<&ProcessListItem> = self.items.get_item(self.selection.unwrap_or_default()); - selected_item + if let Some(selection) = self.selection { + let selected_item = self.items.get_item(selection); + return selected_item + } + None } - // This function returns the Pid of the selected item, returns None if item cannot - // be retrieved or selection is None. + // gets pid of selected process list item pub fn selected_pid(&self) -> Option { if let Some(selection) = self.selection { if let Some(item) = self.items.get_item(selection) { return Some(item.pid()) } - else { return None } + else { + return None + } } None } @@ -248,7 +253,7 @@ mod test { fn test_constructors() { // Default constructor. let empty_instance = ProcessList::default(); - assert!(empty_instance.empty()); + assert!(empty_instance.is_empty()); assert_eq!(empty_instance.selection(), None); // New constructor. @@ -256,19 +261,19 @@ mod test { let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2); let items = vec![item_0, item_1]; let instance = ProcessList::new(&items); - assert!(!instance.empty()); + assert!(!instance.is_empty()); assert_eq!(instance.selection(), Some(0)); // Filter constructor case 1. let filter_string = String::from("c"); let filter_instance = instance.filter(&filter_string); - assert!(filter_instance.empty()); + assert!(filter_instance.is_empty()); assert_eq!(filter_instance.selection(), None); // Filter constructor case 2. let filter_string = String::from("b"); let filter_instance = instance.filter(&filter_string); - assert!(!filter_instance.empty()); + assert!(!filter_instance.is_empty()); assert_eq!(filter_instance.selection(), Some(0)); } @@ -281,7 +286,7 @@ mod test { let mut instance = ProcessList::new(&items); let empty_items = vec![]; let _ = instance.update(&empty_items); - assert!(instance.empty()); + assert!(instance.is_empty()); assert!(instance.selection().is_none()); // Update with non-empty list of items. @@ -292,7 +297,7 @@ mod test { let item_2 = ProcessListItem::new(3, String::from("c"), 3.0, 3); let new_items = vec![item_2]; let _ = instance.update(&new_items); - assert!(!instance.empty()); + assert!(!instance.is_empty()); assert_eq!(instance.selection(), Some(0)); // Update with empty list of items and follow_selection set to true. @@ -303,7 +308,7 @@ mod test { let _ = instance.change_follow_selection(); let empty_items = vec![]; let _ = instance.update(&empty_items); - assert!(instance.empty()); + assert!(instance.is_empty()); assert!(instance.selection().is_none()); // Update with non-empty list of items and follow_selection set to true case 1. @@ -315,7 +320,7 @@ mod test { let item_2 = ProcessListItem::new(3, String::from("c"), 3.0, 3); let new_items = vec![item_2]; let _ = instance.update(&new_items); - assert!(!instance.empty()); + assert!(!instance.is_empty()); assert_eq!(instance.selection(), Some(0)); // Update with non-empty list of items and follow_selection set to true case 2. @@ -328,7 +333,7 @@ mod test { let item_3 = ProcessListItem::new(3, String::from("c"), 3.0, 3); let new_items = vec![item_2, item_3]; let _ = instance.update(&new_items); - assert!(!instance.empty()); + assert!(!instance.is_empty()); assert_eq!(instance.selection(), Some(0)); } @@ -340,7 +345,7 @@ mod test { let items = vec![item_1, item_0]; let mut instance = ProcessList::new(&items); assert!(instance.sort == ListSortOrder::CpuUsageDec); - assert!(!instance.follow()); + assert!(!instance.is_follow_selection()); assert_eq!(instance.selection(), Some(0)); let _ = instance.sort(&ListSortOrder::CpuUsageInc); assert_eq!(instance.selection(), Some(0)); @@ -352,7 +357,7 @@ mod test { let mut instance = ProcessList::new(&items); assert!(instance.sort == ListSortOrder::CpuUsageDec); let _ = instance.change_follow_selection(); - assert!(instance.follow()); + assert!(instance.is_follow_selection()); assert_eq!(instance.selection(), Some(0)); let _ = instance.sort(&ListSortOrder::CpuUsageInc); assert_eq!(instance.selection(), Some(1)); diff --git a/process-list/src/process_list_items.rs b/process-list/src/process_list_items.rs index 46eabc6..183c2b5 100644 --- a/process-list/src/process_list_items.rs +++ b/process-list/src/process_list_items.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use crate::process_list::ListSortOrder; use crate::process_list_item::ProcessListItem; use crate::list_items_iter::ListItemsIterator; @@ -32,31 +31,15 @@ impl ProcessListItems { fn create_filtered_items(&self, filter_text: &String) -> Vec { self.list_items .iter() - .filter(|item| {item.is_match(filter_text)}) + .filter(|item| { item.is_match(filter_text) }) .cloned() .collect() } - // updates existing items, removes old items, and adds new items - // sort order is ruined by this function, call sort after use + // simply replace old list with new list + // this function ruins sort order--call sort after using pub fn update_items(&mut self, new_list: &Vec) { - let mut updated_list: Vec = Vec::new(); - let new_map: HashMap<_,_> = new_list.iter().map(|item| (item.pid(),item)).collect(); - - for item in &mut self.list_items { - if let Some(&updated_item) = new_map.get(&item.pid()) { - *item = updated_item.clone(); - updated_list.push(item.clone()); - } - } - - for new_item in new_list { - if !updated_list.contains(new_item) { - updated_list.push(new_item.clone()) - } - } - - self.list_items = updated_list; + self.list_items = new_list.iter().cloned().collect(); } // sort by list sort order @@ -89,16 +72,9 @@ impl ProcessListItems { } } - // GETTERS // gets the reference to an item given an index pub fn get_item(&self, idx: usize) -> Option<&ProcessListItem> { - let list_len = self.items_len(); - let max_idx = list_len.saturating_sub(1); - if list_len == 0 || max_idx < idx { - return None - } - let item = self.list_items.get(idx); - item + self.list_items.get(idx) } // gets the index of an item given the item's pid. @@ -109,9 +85,7 @@ impl ProcessListItems { { return Some(idx); } - else { - return None; - } + None } // gets the length of the instance list. diff --git a/src/app.rs b/src/app.rs index 3397c06..41bf015 100644 --- a/src/app.rs +++ b/src/app.rs @@ -138,7 +138,7 @@ impl App { fn update_process(&mut self) -> io::Result<()> { //let new_processes = self.system.get_process_list(); let new_processes = self.system.get_processes(); - self.process.update(&new_processes)?; + self.process.update(&new_processes); Ok(()) } diff --git a/src/components/process.rs b/src/components/process.rs index 586ddbd..d80ece6 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -1,3 +1,8 @@ +// todo: +// 1. clean up code relating to process-list backend +// 2. clean up code relating to FilterComponent +// 3. work on/improve UI + use std::io; use crossterm::event::KeyEvent; use ratatui::{ @@ -12,9 +17,7 @@ use super::{filter::FilterComponent, Component, DrawableComponent, EventState}; use super::utils::vertical_scroll::VerticalScroll; use crate::config::KeyConfig; -// The ProcessComponent can be navigated to focus on -// either a ProcessList or -// FilterComponent. +// focus of process component, this can be on either a ProcessList or FilterComponent #[derive(PartialEq)] pub enum Focus { Filter, @@ -46,21 +49,23 @@ impl ProcessComponent { // This function is used to update the process lists. Presumeably there will // will always be an unfiltered list, it is updated without any conditions. // The filtered list is only updated if there is some filtered list. - pub fn update(&mut self, new_processes: &Vec) -> io::Result<()> { + + // update process list + pub fn update(&mut self, new_processes: &Vec) { + // it is assumed new_processes will never be empty, if it is, then + // check system component and sysinfo crate backend self.list.update(new_processes); + if let Some(filtered_list) = self.filtered_list.as_mut() { - // We first filter the new processes by the filter, + // filter new processes let processes = ProcessListItems::new(new_processes); let filter_text = self.filter.input_str(); let filtered_processes = processes.filter(&filter_text); - // then we update the filtered list with the new filtered processes. filtered_list.update(&filtered_processes.list_items); } - Ok(()) } - // This function can be used to communicate the selected item's pid to the application. - // Note: The function will always return None if the focus is on the filter. + // gets the selected process pid pub fn selected_pid(&self) -> Option { if matches!(self.focus, Focus::List) { if let Some(list) = self.filtered_list.as_ref() { @@ -98,7 +103,7 @@ impl ProcessComponent { ); // Getting the boolean list.follow(); The follow_flag is used to differentiate between a selected item being followed(underlined) and not. - let follow_flag = list.follow(); + let follow_flag = list.is_follow_selection(); // Different styles used to visually differentiate between components and focus. let header_style = Style::default().fg(Color::Black).bg(Color::Gray); let select_style = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD); From 86bd497d82dd394398a2ea949043c6e8458914c5 Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Wed, 2 Apr 2025 22:01:09 -0500 Subject: [PATCH 07/12] some work on ui & themes --- src/app.rs | 26 ++-- src/components/error.rs | 10 +- src/components/help.rs | 16 +-- src/components/mod.rs | 2 - src/components/process.rs | 217 +++++++++++----------------------- src/components/system.rs | 8 +- src/components/tab.rs | 14 +-- src/config.rs | 81 ++++++++++++- src/main.rs | 1 + src/ui/mod.rs | 1 + src/ui/process_list_widget.rs | 109 +++++++++++++++++ 11 files changed, 300 insertions(+), 185 deletions(-) create mode 100644 src/ui/mod.rs create mode 100644 src/ui/process_list_widget.rs diff --git a/src/app.rs b/src/app.rs index 41bf015..3e38e98 100644 --- a/src/app.rs +++ b/src/app.rs @@ -31,12 +31,12 @@ impl App { // New constructor. pub fn new(config: Config) -> Self { Self { - system: SystemComponent::new(config.key_config.clone()), - process: ProcessComponent::new(config.key_config.clone()), + system: SystemComponent::new(config.clone()), + process: ProcessComponent::new(config.clone()), performance: PerformanceComponent::new(config.clone(), 10), - tab: TabComponent::new(config.key_config.clone()), - help: HelpComponent::new(config.key_config.clone()), - error: ErrorComponent::new(config.key_config.clone()), + tab: TabComponent::new(config.clone()), + help: HelpComponent::new(config.clone()), + error: ErrorComponent::new(config.clone()), config: config.clone(), } } @@ -44,6 +44,12 @@ impl App { pub async fn event(&mut self, key: KeyEvent) -> io::Result { self.update_commands(); + if key.code == self.config.key_config.toggle_themes { + self.config.theme_config.toggle_themes(); + self.process.config.theme_config.toggle_themes(); + return Ok(EventState::Consumed) + } + if self.components_event(key).await?.is_consumed() { return Ok(EventState::Consumed); } @@ -128,18 +134,18 @@ impl App { // Refresh system structure. self.system.refresh_all().await?; // Update process component. - self.update_process()?; + self.update_process(); // Update performance component. self.update_performance()?; Ok(()) } - fn update_process(&mut self) -> io::Result<()> { - //let new_processes = self.system.get_process_list(); + // return result of process update + fn update_process(&mut self) -> bool { let new_processes = self.system.get_processes(); - self.process.update(&new_processes); - Ok(()) + let res = self.process.update(&new_processes); + res } fn update_performance(&mut self) -> io::Result<()> { diff --git a/src/components/error.rs b/src/components/error.rs index 4a8e34e..517e9ec 100644 --- a/src/components/error.rs +++ b/src/components/error.rs @@ -5,22 +5,22 @@ use ratatui::{ prelude::*, widgets::*, }; -use crate::config::KeyConfig; +use crate::config::Config; use crate::components::Component; use super::{DrawableComponent, EventState}; pub struct ErrorComponent { pub error: String, visible: bool, - key_config: KeyConfig, + config: Config, } impl ErrorComponent { - pub fn new(key_config: KeyConfig) -> Self { + pub fn new(config: Config) -> Self { Self { error: String::new(), visible: false, - key_config: key_config, + config: config, } } } @@ -45,7 +45,7 @@ impl ErrorComponent { impl Component for ErrorComponent { fn event(&mut self, key: KeyEvent) -> io::Result { if self.visible { - if key.code == self.key_config.exit_popup { + if key.code == self.config.key_config.exit_popup { self.error = String::new(); self.hide()?; return Ok(EventState::Consumed); diff --git a/src/components/help.rs b/src/components/help.rs index 32a8314..5fae1bc 100644 --- a/src/components/help.rs +++ b/src/components/help.rs @@ -6,7 +6,7 @@ use ratatui::{ prelude::*, widgets::*, }; -use crate::config::KeyConfig; +use crate::config::Config; use super::command::CommandInfo; use super::EventState; use super::DrawableComponent; @@ -16,16 +16,16 @@ pub struct HelpComponent { cmds: Vec, visible: bool, selection: u16, - key_config: KeyConfig, + config: Config, } impl HelpComponent { - pub const fn new(key_config: KeyConfig) -> Self { + pub const fn new(config: Config) -> Self { Self { cmds: vec![], visible: false, selection: 0, - key_config, + config: config, } } @@ -104,20 +104,20 @@ impl HelpComponent { impl Component for HelpComponent { fn event(&mut self, key: KeyEvent) -> io::Result { if self.visible { - if key.code == self.key_config.exit_popup { + if key.code == self.config.key_config.exit_popup { self.hide(); return Ok(EventState::Consumed); } - else if key.code == self.key_config.move_down { + else if key.code == self.config.key_config.move_down { self.scroll_selection(true); return Ok(EventState::Consumed); } - else if key.code == self.key_config.move_up { + else if key.code == self.config.key_config.move_up { self.scroll_selection(false); return Ok(EventState::Consumed); } } - else if key.code == self.key_config.open_help { + else if key.code == self.config.key_config.open_help { self.show()?; return Ok(EventState::Consumed); } diff --git a/src/components/mod.rs b/src/components/mod.rs index bc84ddb..2059ace 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,10 +1,8 @@ use std::io; use crossterm::event::KeyEvent; use ratatui::prelude::*; - use process_list::{ListSortOrder, MoveSelection}; use super::config::KeyConfig; - pub mod system; pub mod filter; pub mod help; diff --git a/src/components/process.rs b/src/components/process.rs index d80ece6..7a1e87d 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -8,17 +8,17 @@ use crossterm::event::KeyEvent; use ratatui::{ Frame, prelude::*, - widgets::{block::*, *}, }; -use process_list::{ProcessList, ProcessListItems, ProcessListItem}; +use process_list::{ProcessList, ProcessListItem, ProcessListItems}; use super::{common_nav, common_sort}; use super::{filter::FilterComponent, Component, DrawableComponent, EventState}; use super::utils::vertical_scroll::VerticalScroll; -use crate::config::KeyConfig; +use crate::config::Config; +use crate::{config::KeyConfig, ui::process_list_widget::ProcessListWidget}; // focus of process component, this can be on either a ProcessList or FilterComponent -#[derive(PartialEq)] +#[derive(PartialEq, Clone)] pub enum Focus { Filter, List, @@ -30,31 +30,29 @@ pub struct ProcessComponent { filter: FilterComponent, filtered_list: Option, scroll: VerticalScroll, - key_config: KeyConfig, + pub config: Config, } impl ProcessComponent { - // New constructor. - pub fn new(key_config: KeyConfig) -> Self { + // constructor + pub fn new(config: Config) -> Self { Self { focus: Focus::List, list: ProcessList::default(), filter: FilterComponent::default(), filtered_list: None, scroll: VerticalScroll::new(), - key_config: key_config.clone(), + config: config, } } - // This function is used to update the process lists. Presumeably there will - // will always be an unfiltered list, it is updated without any conditions. - // The filtered list is only updated if there is some filtered list. - - // update process list - pub fn update(&mut self, new_processes: &Vec) { - // it is assumed new_processes will never be empty, if it is, then - // check system component and sysinfo crate backend - self.list.update(new_processes); + // update process list, return true if new processes is non empty + pub fn update(&mut self, new_processes: &Vec) -> bool { + if new_processes.is_empty() { + return false + } + + self.list.update(new_processes); if let Some(filtered_list) = self.filtered_list.as_mut() { // filter new processes @@ -63,13 +61,14 @@ impl ProcessComponent { let filtered_processes = processes.filter(&filter_text); filtered_list.update(&filtered_processes.list_items); } + true } - // gets the selected process pid + // gets the selected process pid, returns Some(pid) or None pub fn selected_pid(&self) -> Option { if matches!(self.focus, Focus::List) { - if let Some(list) = self.filtered_list.as_ref() { - return list.selected_pid() + if let Some(filtered_list) = self.filtered_list.as_ref() { + return filtered_list.selected_pid() } else { return self.list.selected_pid() @@ -77,129 +76,13 @@ impl ProcessComponent { } None } - - fn draw_process_list(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> io::Result<()> { - // Setting the list height to the height of the vertical chunk for the process list; We are subtracting - // three from the height to account for the area that will be taken up by the border around the list. - let visual_list_height = (area.height.saturating_sub(3)) as usize; - - // Getting the list to display; If there is some filtered list display it, else display the unfiltered list. - let list = if let Some(list) = self.filtered_list.as_ref() { - list - } - else { - &self.list - }; - - // Updating the scroll struct which calculates the position at the top of the displayed list. - list.selection().map_or_else( - { || - self.scroll.reset() - }, |selection| { - self.scroll.update( - selection,list.len(), visual_list_height - ); - }, - ); - - // Getting the boolean list.follow(); The follow_flag is used to differentiate between a selected item being followed(underlined) and not. - let follow_flag = list.is_follow_selection(); - // Different styles used to visually differentiate between components and focus. - let header_style = Style::default().fg(Color::Black).bg(Color::Gray); - let select_style = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD); - let select_follow_style = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED); - let default_style = Style::default().fg(Color::White); - let out_of_focus_style = Style::default().fg(Color::DarkGray); - - // Setting the header. - let header = ["", "Pid", "Name", "CPU Usage (%)", "Memory Usage (Bytes)"] - .into_iter() - .map(Cell::from) - .collect::() - .style( - if matches!(self.focus, Focus::List) { - header_style - } - else { - out_of_focus_style - } - ) - .height(1); - - // Setting the rows to display; The iterate function iterates starting from the value self.scroll.get_top() and a list_height number of times. - // We don't iterate over the entire list everytime we draw the list, instead we only iterate over the portion that is being displayed. - // See process_structs::list_items_iter::next for the implementation. - let rows = list - .iterate(self.scroll.get_top(), visual_list_height) - .map(|(item, selected)| { - let style = - if matches!(self.focus, Focus::List) && selected && follow_flag { - select_follow_style - } - else if matches!(self.focus, Focus::List) && selected && !follow_flag { - select_style - } - else if matches!(self.focus, Focus::List) { - default_style - } - else { - out_of_focus_style - }; - - let cells: Vec = vec![ - if style == select_style || style == select_follow_style { - Cell::from(String::from("->")) - } - else { - Cell::from(String::from("")) - }, - Cell::from(item.pid().to_string()), - Cell::from(item.name().to_string()), - Cell::from(item.cpu_usage().to_string()), - Cell::from(item.memory_usage().to_string()), - ]; - Row::new(cells).style(style) - }) - .collect::>(); - - // Setting the width constraints. - let widths = - vec![ - Constraint::Length(2), - Constraint::Length(10), - Constraint::Length(50), - Constraint::Length(20), - Constraint::Length(20), - ]; - - // Setting block information. - let block_title: &str = "Process List"; - let block_style = - if matches!(self.focus, Focus::List) { - Style::default().fg(Color::White) - } - else { - Style::default().fg(Color::DarkGray) - }; - - // Setting the table. - let table = Table::new(rows, widths) - .header(header) - .block(Block::default().borders(Borders::ALL).title(block_title)) - .style(block_style); - - // Render. - f.render_widget(table, area); - - Ok(()) - } } impl Component for ProcessComponent { // Handle key events for ProcessComponent. fn event(&mut self, key: KeyEvent) -> io::Result { // If they key event is filter and the ProcessComponent Focus is on the List, then move the focus to Filter and return. - if key.code == self.key_config.filter && self.focus == Focus::List { + if key.code == self.config.key_config.filter && self.focus == Focus::List { self.focus = Focus::Filter; return Ok(EventState::Consumed) } @@ -217,7 +100,7 @@ impl Component for ProcessComponent { } // If the key event is enter and the focus is on the Filter, then change the focus to List and return. - if key.code == self.key_config.enter && matches!(self.focus, Focus::Filter) { + if key.code == self.config.key_config.enter && matches!(self.focus, Focus::Filter) { self.focus = Focus::List; return Ok(EventState::Consumed) } @@ -242,13 +125,13 @@ impl Component for ProcessComponent { &mut self.list }, key, - &self.key_config + &self.config.key_config ) { return Ok(EventState::Consumed); } // Check if the key is to change the follow_selection value. - else if key.code == self.key_config.follow_selection { + else if key.code == self.config.key_config.follow_selection { if let Some(filtered_list) = self.filtered_list.as_mut() { filtered_list.change_follow_selection(); } @@ -269,7 +152,7 @@ impl Component for ProcessComponent { &mut self.list }, key, - &self.key_config + &self.config.key_config )? { return Ok(EventState::Consumed); } @@ -279,6 +162,7 @@ impl Component for ProcessComponent { } } + // Function calls common_nav, common_nav checks if key can be consumed, if so, // Some(MoveSelection) is returned and list.move_selection(MoveSelection) is called. // Else return false. @@ -307,7 +191,7 @@ fn list_sort(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> i impl DrawableComponent for ProcessComponent { fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> io::Result<()> { - // Splitting the parameter area into two vertical chunks. + // splitting the parameter area into two vertical chunks let vertical_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -316,7 +200,7 @@ impl DrawableComponent for ProcessComponent { ].as_ref()) .split(area); - // Splitting the vertical chunk for the TabComponent and FilterComponent into two horizontal chunks. + // splitting the vertical chunk for the TabComponent and FilterComponent into two horizontal chunks let horizontal_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ @@ -325,19 +209,58 @@ impl DrawableComponent for ProcessComponent { ].as_ref()) .split(vertical_chunks[0]); - // Drawing the filter component. + // calculate list + let visible_list_height = vertical_chunks[1].height.saturating_sub(3) as usize; + + // determine list to display + let list = if let Some(filtered_list) = self.filtered_list.as_ref() { + filtered_list + } + else { + &self.list + }; + + // updating the scroll struct which calculates the position at the top of the displayed list. + list.selection().map_or_else( + { || + self.scroll.reset() + }, |selection| { + self.scroll.update( + selection,list.len(), visible_list_height + ); + }, + ); + + let list_iterator = list.iterate(self.scroll.get_top(), visible_list_height); + + let process_list_widget = ProcessListWidget { + visible_items: list_iterator, + focus: self.focus.clone(), + follow_selection: list.is_follow_selection(), + theme_config: self.config.theme_config.clone(), + }; + self.filter.draw(f, horizontal_chunks[1], matches!(self.focus, Focus::Filter))?; - // Draw process list. - self.draw_process_list(f, vertical_chunks[1], matches!(self.focus, Focus::List))?; + process_list_widget.draw(f, vertical_chunks[1], matches!(self.focus, Focus::List)); + - // Draw scrollbar. + + + + + // Draw process list + //self.draw_process_list(f, vertical_chunks[1], matches!(self.focus, Focus::List))?; + + // Draw scrollbar self.scroll.draw(f, vertical_chunks[1], false)?; Ok(()) } } + + #[cfg(test)] mod test { // TODO: write tests diff --git a/src/components/system.rs b/src/components/system.rs index f62641c..a54b88b 100644 --- a/src/components/system.rs +++ b/src/components/system.rs @@ -3,8 +3,8 @@ use crossterm::event::KeyEvent; use sysinfo::{System, Pid}; use process_list::ProcessListItem; use performance_queue::{CpuItem, MemoryItem}; +use crate::config::Config; use super::{Component, EventState}; -use super::KeyConfig; // See here for refreshing system: https://crates.io/crates/sysinfo#:~:text=use%20sysinfo%3A%3ASystem,(sysinfo%3A%3AMINIMUM_CPU_UPDATE_INTERVAL)%3B%0A%7D // note: sysinfo::MINIMUM_CPU_UPDATE_INTERVAL = 200 ms @@ -12,15 +12,15 @@ use super::KeyConfig; pub struct SystemComponent { system: System, //network: Networks, - _key_config: KeyConfig + _config: Config } impl SystemComponent { - pub fn new(key_config: KeyConfig) -> Self { + pub fn new(config: Config) -> Self { Self { system: System::new_all(), //network: Networks::new_with_refreshed_list(), - _key_config: key_config, + _config: config } } diff --git a/src/components/tab.rs b/src/components/tab.rs index dbd0a05..3ea0345 100644 --- a/src/components/tab.rs +++ b/src/components/tab.rs @@ -5,7 +5,7 @@ use ratatui::{ text::Span, }; use super::{DrawableComponent, Component, EventState}; -use crate::config::KeyConfig; +use crate::config::Config; #[derive(Clone, PartialEq)] enum MoveTabDirection { @@ -22,22 +22,22 @@ pub enum Tab { pub struct TabComponent { pub selected_tab: Tab, - key_config: KeyConfig, + config: Config, } impl TabComponent { // default constructor - pub fn new(key_config: KeyConfig) -> Self { + pub fn new(config: Config) -> Self { Self { selected_tab: Tab::Process, - key_config: key_config, + config: config, } } // set internal TabComponent State to default pub fn reset(&mut self) { self.selected_tab = Tab::Process; - self.key_config = KeyConfig::default(); + self.config = Config::default(); } // String representation of Tab variants used in self.draw() @@ -81,11 +81,11 @@ impl TabComponent { impl Component for TabComponent { fn event(&mut self, key: crossterm::event::KeyEvent) -> std::io::Result { - if key.code == self.key_config.tab_right { + if key.code == self.config.key_config.tab_right { self.update_selected_tab(MoveTabDirection::Right); return Ok(EventState::Consumed); } - else if key.code == self.key_config.tab_left { + else if key.code == self.config.key_config.tab_left { self.update_selected_tab(MoveTabDirection::Left); return Ok(EventState::Consumed); } diff --git a/src/config.rs b/src/config.rs index 29d64ce..3372af6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,10 @@ use crossterm::event::KeyCode; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize,Serialize}; -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone,Serialize,Deserialize)] pub struct Config { pub key_config: KeyConfig, + pub theme_config: ThemeConfig, refresh_rate: u64, tick_rate: u64, } @@ -12,6 +13,7 @@ impl Default for Config { fn default() -> Self { Self { key_config: KeyConfig::default(), + theme_config: ThemeConfig::default(), refresh_rate: 5000, tick_rate: 250, } @@ -51,6 +53,7 @@ pub struct KeyConfig { pub sort_memory_usage_inc: KeyCode, pub sort_memory_usage_dec: KeyCode, pub follow_selection: KeyCode, + pub toggle_themes: KeyCode, } impl Default for KeyConfig { @@ -81,6 +84,80 @@ impl Default for KeyConfig { sort_memory_usage_inc: KeyCode::Char('m'), sort_memory_usage_dec: KeyCode::Char('M'), follow_selection: KeyCode::Char('f'), + toggle_themes: KeyCode::Char('t'), } } +} + +#[derive(Clone,PartialEq,Serialize,Deserialize)] +pub enum ThemeVariant { + Dark, + Light, +} + +use ratatui::prelude::{Style,Color,Modifier}; +#[derive(Clone,PartialEq,Serialize,Deserialize)] +pub struct ThemeConfig { + pub theme_variant: ThemeVariant, + pub list_header: Style, + pub item_style: Style, //default + pub item_select: Style, + pub item_select_follow: Style, + pub component_out_of_focus: Style, +} + +impl ThemeConfig { + pub fn dark_theme() -> Self { + Self { + theme_variant: ThemeVariant::Dark, + list_header: Style::default().fg(Color::Black).bg(Color::Gray), + item_style: Style::default().fg(Color::White), + item_select: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD), + item_select_follow: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED), + component_out_of_focus: Style::default().fg(Color::DarkGray), + } + } + + pub fn light_theme() -> Self { + Self { + theme_variant: ThemeVariant::Light, + list_header: Style::default().fg(Color::White).bg(Color::Black), + item_style: Style::default().fg(Color::Black), + item_select: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD), + item_select_follow: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED), + component_out_of_focus: Style::default().fg(Color::DarkGray), + } + } + + fn set_dark_theme(&mut self) { + self.theme_variant = ThemeVariant::Dark; + self.list_header = Style::default().fg(Color::Black).bg(Color::Gray); + self.item_style = Style::default().fg(Color::White); + self.item_select = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD); + self.item_select_follow = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED); + self.component_out_of_focus = Style::default().fg(Color::DarkGray); + } + + fn set_light_theme(&mut self) { + self.theme_variant = ThemeVariant::Light; + self.list_header = Style::default().fg(Color::White).bg(Color::Black); + self.item_style = Style::default().fg(Color::Black); + self.item_select = Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD); + self.item_select_follow = Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED); + self.component_out_of_focus = Style::default().fg(Color::DarkGray); + } + + pub fn toggle_themes(&mut self) { + if self.theme_variant == ThemeVariant::Dark { + self.set_light_theme(); + return + } + self.set_dark_theme(); + } +} + +impl Default for ThemeConfig { + fn default() -> Self { + ThemeConfig::dark_theme() + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d3024b8..7c851e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ use crate::app::App; pub mod app; pub mod config; pub mod components; +pub mod ui; pub mod events; #[tokio::main] diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..e10a1b3 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1 @@ +pub mod process_list_widget; \ No newline at end of file diff --git a/src/ui/process_list_widget.rs b/src/ui/process_list_widget.rs new file mode 100644 index 0000000..4e40d30 --- /dev/null +++ b/src/ui/process_list_widget.rs @@ -0,0 +1,109 @@ +//todo: determine what is needed to render process list widget +// 1. chunk of list +use process_list::ListIterator; +use crate::{components::process::Focus, config::ThemeConfig}; +use ratatui::{ + Frame, + prelude::*, + widgets::{block::*, *}, +}; + +pub struct ProcessListWidget<'a> { + pub visible_items: ListIterator<'a>, + pub focus: Focus, + pub follow_selection: bool, + pub theme_config: ThemeConfig, +} + +impl<'a> ProcessListWidget<'a> { + pub fn draw(self, f: &mut Frame, area: Rect, _focus: bool) { + let follow_flag = self.follow_selection; + let header_style = self.theme_config.list_header; + //let header_style = Style::default().fg(Color::Black).bg(Color::Gray); + let select_style = self.theme_config.item_select; + //let select_style = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD); + let select_follow_style = self.theme_config.item_select_follow; + //let select_follow_style = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED); + let default_style = self.theme_config.item_style; + //let default_style = Style::default().fg(Color::White); + let out_of_focus_style = self.theme_config.component_out_of_focus; + //let out_of_focus_style = Style::default().fg(Color::DarkGray); + + // setting header + let header = ["", "Pid", "Name", "CPU Usage (%)", "Memory Usage (Bytes)"] + .into_iter() + .map(Cell::from) + .collect::() + .style( + if matches!(self.focus, Focus::List) { + header_style + } + else { + out_of_focus_style + } + ) + .height(1); + + // setting rows + let rows = self.visible_items + .map(|(item, selected)| { + let style = + if matches!(self.focus, Focus::List) && selected && follow_flag { + select_follow_style + } + else if matches!(self.focus, Focus::List) && selected && !follow_flag { + select_style + } + else if matches!(self.focus, Focus::List) { + default_style + } + else { + out_of_focus_style + }; + + let cells: Vec = vec![ + if style == select_style || style == select_follow_style { + Cell::from(String::from("->")) + } + else { + Cell::from(String::from("")) + }, + Cell::from(item.pid().to_string()), + Cell::from(item.name().to_string()), + Cell::from(item.cpu_usage().to_string()), + Cell::from(item.memory_usage().to_string()), + ]; + Row::new(cells).style(style) + }) + .collect::>(); + + // Setting the width constraints. + let widths = + vec![ + Constraint::Length(2), + Constraint::Length(10), + Constraint::Length(50), + Constraint::Length(20), + Constraint::Length(20), + ]; + + // Setting block information. + let block_title: &str = "Process List"; + let block_style = + if matches!(self.focus, Focus::List) { + Style::default().fg(Color::White) + } + else { + Style::default().fg(Color::DarkGray) + }; + + // Setting the table. + let table = Table::new(rows, widths) + .header(header) + .block(Block::default().borders(Borders::ALL).title(block_title)) + .style(block_style); + + // Render. + f.render_widget(table, area); + } +} \ No newline at end of file From e33115bb874f4c0d34c25a1259b5e5b3df1a5830 Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Thu, 3 Apr 2025 14:39:15 -0500 Subject: [PATCH 08/12] todo:fix return types, impl statefulwidget for process --- Cargo.lock | 153 +----------------- Cargo.toml | 2 +- src/app.rs | 116 +++++++------ src/components/error.rs | 12 +- src/components/filter.rs | 19 ++- src/components/help.rs | 10 +- src/components/mod.rs | 6 +- src/components/performance.rs | 20 +-- src/components/process.rs | 32 +--- src/components/system.rs | 15 +- src/components/tab.rs | 7 +- src/components/utils/vertical_scroll.rs | 3 +- src/components/vertical_tabs.rs | 5 +- src/config.rs | 37 ++--- src/main.rs | 13 +- src/ui/mod.rs | 2 +- ...cess_list_widget.rs => process_list_ui.rs} | 26 ++- 17 files changed, 168 insertions(+), 310 deletions(-) rename src/ui/{process_list_widget.rs => process_list_ui.rs} (72%) diff --git a/Cargo.lock b/Cargo.lock index 23cb033..3156491 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,21 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "addr2line" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "ahash" version = "0.8.11" @@ -36,25 +21,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] -name = "autocfg" -version = "1.3.0" +name = "anyhow" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] -name = "backtrace" -version = "0.3.73" +name = "autocfg" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bitflags" @@ -65,12 +41,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bytes" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" - [[package]] name = "cassowary" version = "0.3.0" @@ -86,12 +56,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "cc" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" - [[package]] name = "cfg-if" version = "1.0.0" @@ -182,12 +146,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" -[[package]] -name = "gimli" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" - [[package]] name = "hashbrown" version = "0.14.5" @@ -204,12 +162,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "itertools" version = "0.10.5" @@ -271,15 +223,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - [[package]] name = "mio" version = "0.8.11" @@ -289,7 +232,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -301,25 +244,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "object" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.19.0" @@ -359,12 +283,6 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" name = "performance-queue" version = "0.1.0" -[[package]] -name = "pin-project-lite" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" - [[package]] name = "proc-macro2" version = "1.0.86" @@ -378,6 +296,7 @@ dependencies = [ name = "process-display" version = "0.1.0" dependencies = [ + "anyhow", "crossterm", "itertools 0.10.5", "performance-queue", @@ -385,7 +304,6 @@ dependencies = [ "ratatui", "serde", "sysinfo", - "tokio", ] [[package]] @@ -454,12 +372,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustversion" version = "1.0.17" @@ -534,16 +446,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "stability" version = "0.2.0" @@ -607,36 +509,6 @@ dependencies = [ "windows", ] -[[package]] -name = "tokio" -version = "1.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "num_cpus", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-macros" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "unicode-ident" version = "1.0.12" @@ -761,15 +633,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.5", -] - [[package]] name = "windows-targets" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index b322a17..6e20fb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ sysinfo = "0.31.2" crossterm = { version = "0.27.0", features = ["serde", "event-stream"] } ratatui = { version = "0.26.0", features = ["serde", "macros"] } serde = { version = "1.0.188", features = ["derive"] } -tokio = { version = "1.11.0", features = ["full"] } +anyhow = "1.0.97" itertools = "0.10.0" process-list = {path = "./process-list", version = "0.1.0"} performance-queue = {path = "./performance-queue", version = "0.1.0"} \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 3e38e98..267ade6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,11 @@ -use std::io::{self}; +use anyhow::{Ok, Result}; use crossterm::event::KeyEvent; use ratatui::prelude::*; -use crate::components::performance::PerformanceComponent; use crate::config::Config; use crate::components::{ tab::TabComponent, process::ProcessComponent, + performance::PerformanceComponent, system::SystemComponent, error::ErrorComponent, Component, @@ -28,7 +28,7 @@ pub struct App { } impl App { - // New constructor. + // constructor. pub fn new(config: Config) -> Self { Self { system: SystemComponent::new(config.clone()), @@ -41,19 +41,52 @@ impl App { } } - pub async fn event(&mut self, key: KeyEvent) -> io::Result { + // call after constructor + pub fn init(&mut self) -> Result<()> { + self.system.refresh_all()?; + self.update_process(); + self.update_performance()?; self.update_commands(); + Ok(()) + } + + // refresh system and dependencies + pub fn refresh(&mut self) -> Result<()>{ + self.system.refresh_all()?; + self.update_process(); + self.update_performance()?; + + Ok(()) + } + + // return result of process update + fn update_process(&mut self) -> bool { + let new_processes = self.system.get_processes(); + let res = self.process.update(&new_processes); + + res + } + + // fix return type + fn update_performance(&mut self) -> Result<()> { + let new_cpu_info = self.system.get_cpu_info(); + let new_memory_info = self.system.get_memory_info(); + self.performance.update(&new_cpu_info, &new_memory_info)?; + + Ok(()) + } + + // top level key event processor + pub fn event(&mut self, key: KeyEvent) -> Result { if key.code == self.config.key_config.toggle_themes { - self.config.theme_config.toggle_themes(); - self.process.config.theme_config.toggle_themes(); + self.update_component_themes(); + return Ok(EventState::Consumed) } - - if self.components_event(key).await?.is_consumed() { + else if self.components_event(key)?.is_consumed() { return Ok(EventState::Consumed); } - else if self.move_focus(key)?.is_consumed() { return Ok(EventState::Consumed); } @@ -61,12 +94,22 @@ impl App { Ok(EventState::NotConsumed) } - // This function populates the HelpComponent with CommandInfo. + // toggle color scheme + fn update_component_themes(&mut self) { + self.config.theme_config.toggle_themes(); + self.process.config.theme_config.toggle_themes(); + self.performance.config.theme_config.toggle_themes(); + self.help.config.theme_config.toggle_themes(); + self.system._config.theme_config.toggle_themes(); + self.tab.config.theme_config.toggle_themes(); + } + + // update help dialogue--commands fn update_commands(&mut self) { self.help.set_commands(self.commands()); } - // This function populates and returns a vector with CommandInfo. + // set commands fn commands(&self) -> Vec { let res = vec![ CommandInfo::new(command::help(&self.config.key_config)), @@ -82,11 +125,12 @@ impl App { CommandInfo::new(command::filter_submit(&self.config.key_config)), CommandInfo::new(command::terminate_process(&self.config.key_config)), ]; + res } - // Async function to process component's event. - async fn components_event(&mut self, key: KeyEvent) -> io::Result { + // component key event processor + fn components_event(&mut self, key: KeyEvent) -> Result { if self.error.event(key)?.is_consumed() { return Ok(EventState::Consumed) } @@ -97,24 +141,24 @@ impl App { match self.tab.selected_tab { Tab::Process => { + // see if key event is processed by process component if self.process.event(key)?.is_consumed() { return Ok(EventState::Consumed) } - // Process termination code + // terminate system process else if key.code == self.config.key_config.terminate { if let Some(pid) = self.process.selected_pid() { self.system.terminate_process(pid)?; + return Ok(EventState::Consumed) } } } - Tab::Performance => { if self.performance.event(key)?.is_consumed() { return Ok(EventState::Consumed) } } - //Tab::Users => {} } @@ -125,38 +169,13 @@ impl App { Ok(EventState::NotConsumed) } - fn move_focus(&mut self, _key: KeyEvent) -> io::Result { + // not being used, implement if there is a need for focus control outside of tab + fn move_focus(&mut self, _key: KeyEvent) -> Result { return Ok(EventState::NotConsumed); } - // Async function to refresh the system structure and update dependent components. - pub async fn refresh(&mut self) -> io::Result<()> { - // Refresh system structure. - self.system.refresh_all().await?; - // Update process component. - self.update_process(); - // Update performance component. - self.update_performance()?; - - Ok(()) - } - - // return result of process update - fn update_process(&mut self) -> bool { - let new_processes = self.system.get_processes(); - let res = self.process.update(&new_processes); - res - } - - fn update_performance(&mut self) -> io::Result<()> { - let new_cpu_info = self.system.get_cpu_info(); - let new_memory_info = self.system.get_memory_info(); - self.performance.update(&new_cpu_info, &new_memory_info)?; - Ok(()) - } - - // App draw. - pub fn draw(&mut self, f: &mut Frame) -> io::Result<()> { + // draw the app + pub fn draw(&mut self, f: &mut Frame) -> Result<()> { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -164,23 +183,24 @@ impl App { ]) .split(f.size()); + // if error always draw--error component state determines if anything is drawn self.error.draw(f, chunks[0], false)?; + // always draw tab--identifies tab state self.tab.draw(f, chunks[0], false)?; + // only draw selected tab match self.tab.selected_tab { Tab::Process => { self.process.draw(f, chunks[0], false)?; } - Tab::Performance => { self.performance.draw(f, chunks[0], false)?; } - //Tab::Users => {} } - // Drawing the HelpComponent as a pop up. See /components/help.rs. + // if help--help component state determines if anything is drawn self.help.draw(f, Rect::default(), false)?; return Ok(()) diff --git a/src/components/error.rs b/src/components/error.rs index 517e9ec..40bb97a 100644 --- a/src/components/error.rs +++ b/src/components/error.rs @@ -1,4 +1,4 @@ -use std::io; +use anyhow::Result; use crossterm::event::KeyEvent; use ratatui::{ Frame, @@ -26,24 +26,24 @@ impl ErrorComponent { } impl ErrorComponent { - pub fn set(&mut self, error: String) -> io::Result<()> { + pub fn set(&mut self, error: String) -> Result<()> { self.error = error; self.show() } - fn hide(&mut self) -> io::Result<()> { + fn hide(&mut self) -> Result<()> { self.visible = false; Ok(()) } - fn show(&mut self) -> io::Result<()> { + fn show(&mut self) -> Result<()> { self.visible = true; Ok(()) } } impl Component for ErrorComponent { - fn event(&mut self, key: KeyEvent) -> io::Result { + fn event(&mut self, key: KeyEvent) -> Result { if self.visible { if key.code == self.config.key_config.exit_popup { self.error = String::new(); @@ -57,7 +57,7 @@ impl Component for ErrorComponent { } impl DrawableComponent for ErrorComponent { - fn draw(&mut self, f: &mut Frame, _area: Rect, _focused: bool) -> io::Result<()> { + fn draw(&mut self, f: &mut Frame, _area: Rect, _focused: bool) -> Result<()> { if self.visible { let width = 60; let height = 10; diff --git a/src/components/filter.rs b/src/components/filter.rs index 85853b6..edab2e4 100644 --- a/src/components/filter.rs +++ b/src/components/filter.rs @@ -1,18 +1,27 @@ -use std::io; +use anyhow::Result; use crossterm::event::{KeyEvent, KeyCode}; use ratatui::{ Frame, prelude::*, widgets::{block::*, *}, }; +use crate::config::Config; use super::{EventState, DrawableComponent, Component}; #[derive(Default)] pub struct FilterComponent { input_str: String, + pub config: Config, } impl FilterComponent { + pub fn new(config: Config) -> Self { + Self { + input_str: String::new(), + config: config, + } + } + pub fn reset(&mut self) { self.input_str.clear(); } @@ -27,7 +36,7 @@ impl FilterComponent { } impl Component for FilterComponent { - fn event(&mut self, key: KeyEvent) -> io::Result { + fn event(&mut self, key: KeyEvent) -> Result { match key.code { KeyCode::Char(c) => { self.input_str.push(c); @@ -43,15 +52,15 @@ impl Component for FilterComponent { } impl DrawableComponent for FilterComponent { - fn draw(&mut self, f: &mut Frame, area: ratatui::prelude::Rect, focused: bool) -> io::Result<()> { + fn draw(&mut self, f: &mut Frame, area: ratatui::prelude::Rect, focused: bool) -> Result<()> { let title: &str = "Filter"; let style: Style = if focused { - Style::default().fg(Color::White) + self.config.theme_config.component_in_focus } else { - Style::default().fg(Color::DarkGray) + self.config.theme_config.component_out_of_focus }; let filter_text: &str = self.input_str.as_str(); diff --git a/src/components/help.rs b/src/components/help.rs index 5fae1bc..31d2049 100644 --- a/src/components/help.rs +++ b/src/components/help.rs @@ -1,4 +1,4 @@ -use std::io; +use anyhow::Result; use itertools::Itertools; use crossterm::event::KeyEvent; use ratatui::{ @@ -16,7 +16,7 @@ pub struct HelpComponent { cmds: Vec, visible: bool, selection: u16, - config: Config, + pub config: Config, } impl HelpComponent { @@ -93,7 +93,7 @@ impl HelpComponent { self.visible = false; } - fn show(&mut self) -> io::Result<()> { + fn show(&mut self) -> Result<()> { self.visible = true; Ok(()) @@ -102,7 +102,7 @@ impl HelpComponent { } impl Component for HelpComponent { - fn event(&mut self, key: KeyEvent) -> io::Result { + fn event(&mut self, key: KeyEvent) -> Result { if self.visible { if key.code == self.config.key_config.exit_popup { self.hide(); @@ -126,7 +126,7 @@ impl Component for HelpComponent { } impl DrawableComponent for HelpComponent { - fn draw(&mut self, f: &mut Frame, _area: Rect, _focused: bool) -> io::Result<()> { + fn draw(&mut self, f: &mut Frame, _area: Rect, _focused: bool) -> Result<()> { if self.visible { const SIZE: (u16, u16) = (65, 24); let scroll_threshold = SIZE.1 / 3; diff --git a/src/components/mod.rs b/src/components/mod.rs index 2059ace..f46dd72 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,4 +1,4 @@ -use std::io; +use anyhow::Result; use crossterm::event::KeyEvent; use ratatui::prelude::*; use process_list::{ListSortOrder, MoveSelection}; @@ -15,11 +15,11 @@ pub mod command; pub mod vertical_tabs; pub trait DrawableComponent { - fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> io::Result<()>; + fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()>; } pub trait Component { - fn event(&mut self, key: KeyEvent) -> io::Result; + fn event(&mut self, key: KeyEvent) -> Result; } #[derive(PartialEq)] diff --git a/src/components/performance.rs b/src/components/performance.rs index 70484fa..0bb8037 100644 --- a/src/components/performance.rs +++ b/src/components/performance.rs @@ -1,4 +1,4 @@ -use std::io; +use anyhow::Result; use crossterm::event::KeyEvent; use ratatui::widgets::Dataset; use ratatui::{ @@ -18,7 +18,7 @@ pub struct PerformanceComponent { cpu_info: PerformanceQueue, memory_info: PerformanceQueue, vertical_tabs: VerticalTabComponent, - config: Config, + pub config: Config, } impl PerformanceComponent { @@ -31,13 +31,13 @@ impl PerformanceComponent { } } - pub fn update(&mut self, cpu_item: &CpuItem, memory_item: &MemoryItem) -> io::Result<()> { + pub fn update(&mut self, cpu_item: &CpuItem, memory_item: &MemoryItem) -> Result<()> { self.cpu_info.add_item(cpu_item)?; self.memory_info.add_item(memory_item)?; Ok(()) } - fn draw_memory_graph(&self, f: &mut Frame, area: Rect) -> io::Result<()> { + fn draw_memory_graph(&self, f: &mut Frame, area: Rect) -> Result<()> { let refresh_rate = (self.config.refresh_rate() / 1000) as usize; // Converting ms to s. let data_points = self.memory_info.performance_items .iter() @@ -51,7 +51,7 @@ impl PerformanceComponent { Ok(()) } - fn draw_cpu_graph(&self, f: &mut Frame, area: Rect) -> io::Result<()> { + fn draw_cpu_graph(&self, f: &mut Frame, area: Rect) -> Result<()> { let refresh_rate = (self.config.refresh_rate() / 1000) as usize; // Converting ms to s. let data_points = self.cpu_info.performance_items .iter() @@ -65,7 +65,7 @@ impl PerformanceComponent { Ok(()) } - fn draw_cpu_item(&self, f: &mut Frame, area: Rect) -> io::Result<()> { + fn draw_cpu_item(&self, f: &mut Frame, area: Rect) -> Result<()> { //TODO if let Some(item) = self.cpu_info.back() { let info = vec![ @@ -102,7 +102,7 @@ impl PerformanceComponent { Ok(()) } - fn draw_memory_item(&self, f: &mut Frame, area: Rect) -> io::Result<()> { + fn draw_memory_item(&self, f: &mut Frame, area: Rect) -> Result<()> { if let Some(item) = self.memory_info.back() { let info = vec![ Line::from(vec![ @@ -142,7 +142,7 @@ impl PerformanceComponent { } impl Component for PerformanceComponent { - fn event(&mut self, key: KeyEvent) -> io::Result { + fn event(&mut self, key: KeyEvent) -> Result { //todo self.vertical_tabs.event(key)?; Ok(EventState::NotConsumed) @@ -150,7 +150,7 @@ impl Component for PerformanceComponent { } impl DrawableComponent for PerformanceComponent { - fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> io::Result<()> { + fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> Result<()> { let vertical_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -193,7 +193,7 @@ fn draw_graph( data_points: Vec<(f64, f64)>, y_axis_title: String, y_bounds: [f64; 2], -) -> io::Result<()> { +) -> Result<()> { let data_set = vec![ Dataset::default() .marker(Marker::Dot) diff --git a/src/components/process.rs b/src/components/process.rs index 7a1e87d..0e12fd5 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -1,9 +1,4 @@ -// todo: -// 1. clean up code relating to process-list backend -// 2. clean up code relating to FilterComponent -// 3. work on/improve UI - -use std::io; +use anyhow::{Ok, Result}; use crossterm::event::KeyEvent; use ratatui::{ Frame, @@ -15,7 +10,7 @@ use super::{common_nav, common_sort}; use super::{filter::FilterComponent, Component, DrawableComponent, EventState}; use super::utils::vertical_scroll::VerticalScroll; use crate::config::Config; -use crate::{config::KeyConfig, ui::process_list_widget::ProcessListWidget}; +use crate::{config::KeyConfig, ui::process_list_ui::ProcessListUI}; // focus of process component, this can be on either a ProcessList or FilterComponent #[derive(PartialEq, Clone)] @@ -39,7 +34,7 @@ impl ProcessComponent { Self { focus: Focus::List, list: ProcessList::default(), - filter: FilterComponent::default(), + filter: FilterComponent::new(config.clone()), filtered_list: None, scroll: VerticalScroll::new(), config: config, @@ -80,7 +75,7 @@ impl ProcessComponent { impl Component for ProcessComponent { // Handle key events for ProcessComponent. - fn event(&mut self, key: KeyEvent) -> io::Result { + fn event(&mut self, key: KeyEvent) -> Result { // If they key event is filter and the ProcessComponent Focus is on the List, then move the focus to Filter and return. if key.code == self.config.key_config.filter && self.focus == Focus::List { self.focus = Focus::Filter; @@ -179,7 +174,7 @@ fn list_nav(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> bo // Function calls common_sort, common_sort checks if key can be consumed, if so, // Some(ListSortOrder) is returned and list.sort(ListSortOrder) is called. // Else return false. -fn list_sort(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> io::Result { +fn list_sort(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> Result { if let Some(sort) = common_sort(key, key_config) { list.sort(&sort); Ok(true) @@ -190,7 +185,7 @@ fn list_sort(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> i } impl DrawableComponent for ProcessComponent { - fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> io::Result<()> { + fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> Result<()> { // splitting the parameter area into two vertical chunks let vertical_chunks = Layout::default() .direction(Direction::Vertical) @@ -233,9 +228,9 @@ impl DrawableComponent for ProcessComponent { let list_iterator = list.iterate(self.scroll.get_top(), visible_list_height); - let process_list_widget = ProcessListWidget { + // re-creating process list ui on every draw event--this can be optimized--add UI state to process component + let process_list_widget = ProcessListUI { visible_items: list_iterator, - focus: self.focus.clone(), follow_selection: list.is_follow_selection(), theme_config: self.config.theme_config.clone(), }; @@ -244,23 +239,12 @@ impl DrawableComponent for ProcessComponent { process_list_widget.draw(f, vertical_chunks[1], matches!(self.focus, Focus::List)); - - - - - - // Draw process list - //self.draw_process_list(f, vertical_chunks[1], matches!(self.focus, Focus::List))?; - - // Draw scrollbar self.scroll.draw(f, vertical_chunks[1], false)?; Ok(()) } } - - #[cfg(test)] mod test { // TODO: write tests diff --git a/src/components/system.rs b/src/components/system.rs index a54b88b..3caf905 100644 --- a/src/components/system.rs +++ b/src/components/system.rs @@ -1,4 +1,4 @@ -use std::io; +use anyhow::Result; use crossterm::event::KeyEvent; use sysinfo::{System, Pid}; use process_list::ProcessListItem; @@ -12,7 +12,7 @@ use super::{Component, EventState}; pub struct SystemComponent { system: System, //network: Networks, - _config: Config + pub _config: Config } impl SystemComponent { @@ -24,10 +24,9 @@ impl SystemComponent { } } - pub async fn refresh_all(&mut self) -> io::Result { - self.system.refresh_all(); // 1. refresh system - //self.network.refresh_list(); // 2. refresh network interfaces list - //self.network.refresh(); // 3. refresh network interfaces' content + pub fn refresh_all(&mut self) -> Result { + self.system.refresh_all(); + Ok(EventState::Consumed) } @@ -78,7 +77,7 @@ impl SystemComponent { } } - pub fn terminate_process(&mut self, pid: u32) -> io::Result { + pub fn terminate_process(&mut self, pid: u32) -> Result { if let Some(process) = self.system.process(Pid::from_u32(pid)) { process.kill(); } @@ -87,7 +86,7 @@ impl SystemComponent { } impl Component for SystemComponent { - fn event(&mut self, _key: KeyEvent) -> io::Result { + fn event(&mut self, _key: KeyEvent) -> Result { Ok(EventState::NotConsumed) } } \ No newline at end of file diff --git a/src/components/tab.rs b/src/components/tab.rs index 3ea0345..9be5eb3 100644 --- a/src/components/tab.rs +++ b/src/components/tab.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use ratatui::{ Frame, prelude::*, @@ -22,7 +23,7 @@ pub enum Tab { pub struct TabComponent { pub selected_tab: Tab, - config: Config, + pub config: Config, } impl TabComponent { @@ -80,7 +81,7 @@ impl TabComponent { } impl Component for TabComponent { - fn event(&mut self, key: crossterm::event::KeyEvent) -> std::io::Result { + fn event(&mut self, key: crossterm::event::KeyEvent) -> Result { if key.code == self.config.key_config.tab_right { self.update_selected_tab(MoveTabDirection::Right); return Ok(EventState::Consumed); @@ -94,7 +95,7 @@ impl Component for TabComponent { } impl DrawableComponent for TabComponent { - fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> std::io::Result<()> { + fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> Result<()> { let vertical_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ diff --git a/src/components/utils/vertical_scroll.rs b/src/components/utils/vertical_scroll.rs index c70e67c..9304e5b 100644 --- a/src/components/utils/vertical_scroll.rs +++ b/src/components/utils/vertical_scroll.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use std::cell::Cell; use ratatui::{ Frame, @@ -55,7 +56,7 @@ const fn calc_scroll_top( } impl DrawableComponent for VerticalScroll { - fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> std::io::Result<()> { + fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> Result<()> { draw_scrollbar( f, area, diff --git a/src/components/vertical_tabs.rs b/src/components/vertical_tabs.rs index 89cf0b6..eb6723a 100644 --- a/src/components/vertical_tabs.rs +++ b/src/components/vertical_tabs.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use ratatui::{ Frame, prelude::*, @@ -84,7 +85,7 @@ impl VerticalTabComponent { } impl Component for VerticalTabComponent { - fn event(&mut self, key: crossterm::event::KeyEvent) -> std::io::Result { + fn event(&mut self, key: crossterm::event::KeyEvent) -> Result { if key.code == self.key_config.move_up { self.update_selected_tab(MoveTabDirection::Up); return Ok(EventState::Consumed); @@ -98,7 +99,7 @@ impl Component for VerticalTabComponent { } impl DrawableComponent for VerticalTabComponent { - fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> std::io::Result<()> { + fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> Result<()> { let selected_tab = self.selected_vert_tab.clone() as usize; let selected_style = Style::default().fg(Color::White).add_modifier(Modifier::BOLD); let default_style = Style::default().fg(Color::DarkGray); diff --git a/src/config.rs b/src/config.rs index 3372af6..4e6437b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -103,32 +103,11 @@ pub struct ThemeConfig { pub item_style: Style, //default pub item_select: Style, pub item_select_follow: Style, - pub component_out_of_focus: Style, + pub component_out_of_focus: Style, + pub component_in_focus: Style, } impl ThemeConfig { - pub fn dark_theme() -> Self { - Self { - theme_variant: ThemeVariant::Dark, - list_header: Style::default().fg(Color::Black).bg(Color::Gray), - item_style: Style::default().fg(Color::White), - item_select: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD), - item_select_follow: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED), - component_out_of_focus: Style::default().fg(Color::DarkGray), - } - } - - pub fn light_theme() -> Self { - Self { - theme_variant: ThemeVariant::Light, - list_header: Style::default().fg(Color::White).bg(Color::Black), - item_style: Style::default().fg(Color::Black), - item_select: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD), - item_select_follow: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED), - component_out_of_focus: Style::default().fg(Color::DarkGray), - } - } - fn set_dark_theme(&mut self) { self.theme_variant = ThemeVariant::Dark; self.list_header = Style::default().fg(Color::Black).bg(Color::Gray); @@ -136,6 +115,7 @@ impl ThemeConfig { self.item_select = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD); self.item_select_follow = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED); self.component_out_of_focus = Style::default().fg(Color::DarkGray); + self.component_in_focus = Style::default().fg(Color::White); } fn set_light_theme(&mut self) { @@ -145,6 +125,7 @@ impl ThemeConfig { self.item_select = Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD); self.item_select_follow = Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED); self.component_out_of_focus = Style::default().fg(Color::DarkGray); + self.component_in_focus = Style::default().fg(Color::White); } pub fn toggle_themes(&mut self) { @@ -158,6 +139,14 @@ impl ThemeConfig { impl Default for ThemeConfig { fn default() -> Self { - ThemeConfig::dark_theme() + Self { + theme_variant: ThemeVariant::Dark, + list_header: Style::default().fg(Color::Black).bg(Color::Gray), + item_style: Style::default().fg(Color::White), + item_select: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD), + item_select_follow: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED), + component_out_of_focus: Style::default().fg(Color::DarkGray), + component_in_focus: Style::default().fg(Color::White), + } } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 7c851e2..bab5717 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ +use anyhow::Result; use std::io; -use std::error::Error; use crossterm::ExecutableCommand; use crossterm::{ execute, @@ -20,8 +20,7 @@ pub mod components; pub mod ui; pub mod events; -#[tokio::main] -async fn main() -> Result<(), Box> { +fn main() -> Result<()> { // terminal setup setup_terminal()?; @@ -39,7 +38,7 @@ async fn main() -> Result<(), Box> { let mut app = App::new(config); - app.refresh().await?; + app.init()?; // clear terminal terminal.clear()?; @@ -59,7 +58,7 @@ async fn main() -> Result<(), Box> { // process next event match events.next()? { - Event::Input(key) => match app.event(key).await { + Event::Input(key) => match app.event(key) { Ok(state) => { if !state.is_consumed() && key.code == app.config.key_config.exit_popup { break; @@ -69,7 +68,7 @@ async fn main() -> Result<(), Box> { app.error.set(err.to_string())?; } } - Event::Refresh => match app.refresh().await { + Event::Refresh => match app.refresh() { Ok(_state) => {} Err(err) => { app.error.set(err.to_string())?; @@ -91,7 +90,7 @@ async fn main() -> Result<(), Box> { Ok(()) } -fn setup_terminal() -> Result<(), Box> { +fn setup_terminal() -> Result<()> { enable_raw_mode()?; io::stdout().execute(EnterAlternateScreen)?; Ok(()) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e10a1b3..c225c1c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1 +1 @@ -pub mod process_list_widget; \ No newline at end of file +pub mod process_list_ui; \ No newline at end of file diff --git a/src/ui/process_list_widget.rs b/src/ui/process_list_ui.rs similarity index 72% rename from src/ui/process_list_widget.rs rename to src/ui/process_list_ui.rs index 4e40d30..5818fa8 100644 --- a/src/ui/process_list_widget.rs +++ b/src/ui/process_list_ui.rs @@ -1,33 +1,25 @@ -//todo: determine what is needed to render process list widget -// 1. chunk of list use process_list::ListIterator; -use crate::{components::process::Focus, config::ThemeConfig}; +use crate::config::ThemeConfig; use ratatui::{ Frame, prelude::*, widgets::{block::*, *}, }; -pub struct ProcessListWidget<'a> { +pub struct ProcessListUI<'a> { pub visible_items: ListIterator<'a>, - pub focus: Focus, pub follow_selection: bool, pub theme_config: ThemeConfig, } -impl<'a> ProcessListWidget<'a> { - pub fn draw(self, f: &mut Frame, area: Rect, _focus: bool) { +impl<'a> ProcessListUI<'a> { + pub fn draw(self, f: &mut Frame, area: Rect, focus: bool) { let follow_flag = self.follow_selection; let header_style = self.theme_config.list_header; - //let header_style = Style::default().fg(Color::Black).bg(Color::Gray); let select_style = self.theme_config.item_select; - //let select_style = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD); let select_follow_style = self.theme_config.item_select_follow; - //let select_follow_style = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED); let default_style = self.theme_config.item_style; - //let default_style = Style::default().fg(Color::White); let out_of_focus_style = self.theme_config.component_out_of_focus; - //let out_of_focus_style = Style::default().fg(Color::DarkGray); // setting header let header = ["", "Pid", "Name", "CPU Usage (%)", "Memory Usage (Bytes)"] @@ -35,7 +27,7 @@ impl<'a> ProcessListWidget<'a> { .map(Cell::from) .collect::() .style( - if matches!(self.focus, Focus::List) { + if focus { header_style } else { @@ -48,13 +40,13 @@ impl<'a> ProcessListWidget<'a> { let rows = self.visible_items .map(|(item, selected)| { let style = - if matches!(self.focus, Focus::List) && selected && follow_flag { + if focus && selected && follow_flag { select_follow_style } - else if matches!(self.focus, Focus::List) && selected && !follow_flag { + else if focus && selected && !follow_flag { select_style } - else if matches!(self.focus, Focus::List) { + else if focus { default_style } else { @@ -90,7 +82,7 @@ impl<'a> ProcessListWidget<'a> { // Setting block information. let block_title: &str = "Process List"; let block_style = - if matches!(self.focus, Focus::List) { + if focus { Style::default().fg(Color::White) } else { From c614387c39a89894bd9cf35313b85595ac889b1a Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Thu, 3 Apr 2025 18:23:30 -0500 Subject: [PATCH 09/12] some ideas for expandable process info & updating sysinfo version --- Cargo.lock | 71 ++++++--------------------- Cargo.toml | 2 +- process-list/src/lib.rs | 4 +- process-list/src/process_item_info.rs | 44 +++++++++++++++++ process-list/src/process_list.rs | 3 ++ src/components/system.rs | 20 +++++++- 6 files changed, 84 insertions(+), 60 deletions(-) create mode 100644 process-list/src/process_item_info.rs diff --git a/Cargo.lock b/Cargo.lock index 3156491..8f26494 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,37 +76,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "core-foundation-sys" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" - -[[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - [[package]] name = "crossterm" version = "0.27.0" @@ -188,9 +157,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "lock_api" @@ -244,6 +213,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +dependencies = [ + "bitflags", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -343,26 +321,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "redox_syscall" version = "0.5.2" @@ -497,15 +455,14 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.31.2" +version = "0.34.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4115055da5f572fff541dd0c4e61b0262977f453cc9fe04be83aba25a89bdab" +checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2" dependencies = [ - "core-foundation-sys", "libc", "memchr", "ntapi", - "rayon", + "objc2-core-foundation", "windows", ] diff --git a/Cargo.toml b/Cargo.toml index 6e20fb7..862704f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ authours = ["rhasler1 "] [dependencies] -sysinfo = "0.31.2" +sysinfo = "0.34.2" crossterm = { version = "0.27.0", features = ["serde", "event-stream"] } ratatui = { version = "0.26.0", features = ["serde", "macros"] } serde = { version = "1.0.188", features = ["derive"] } diff --git a/process-list/src/lib.rs b/process-list/src/lib.rs index babf514..dd9a2c6 100644 --- a/process-list/src/lib.rs +++ b/process-list/src/lib.rs @@ -3,9 +3,11 @@ mod list_iter; mod process_list_item; mod process_list_items; mod process_list; +mod process_item_info; pub use list_items_iter::ListItemsIterator; pub use list_iter::ListIterator; pub use process_list::{ProcessList, ListSortOrder, MoveSelection}; pub use process_list_items::ProcessListItems; -pub use process_list_item::ProcessListItem; \ No newline at end of file +pub use process_list_item::ProcessListItem; +pub use process_item_info::ProcessItemInfo; \ No newline at end of file diff --git a/process-list/src/process_item_info.rs b/process-list/src/process_item_info.rs new file mode 100644 index 0000000..4330de3 --- /dev/null +++ b/process-list/src/process_item_info.rs @@ -0,0 +1,44 @@ +#[derive(Clone)] +pub struct ProcessItemInfo { + start_time: u64, + run_time: u64, + accumulated_cpu_time: u64, + status: String, +} + +impl ProcessItemInfo { + pub fn new( + start_time: u64, + run_time: u64, + accumulated_cpu_time: u64, + status: String, + ) -> Self { + Self { + start_time, run_time, accumulated_cpu_time, status + } + } + + pub fn update( + &mut self, + start_time: u64, + run_time: u64, + accumulated_cpu_time: u64, + status: String, + ) { + self.start_time = start_time; + self.run_time = run_time; + self.accumulated_cpu_time = accumulated_cpu_time; + self.status = status; + } + + pub fn get_info(&self) -> Vec { + let info: Vec = [ + self.start_time.to_string(), + self.run_time.to_string(), + self.accumulated_cpu_time.to_string(), + self.status.clone(), + ].to_vec(); + + info + } +} \ No newline at end of file diff --git a/process-list/src/process_list.rs b/process-list/src/process_list.rs index cce989b..1fef2c9 100644 --- a/process-list/src/process_list.rs +++ b/process-list/src/process_list.rs @@ -122,6 +122,9 @@ impl ProcessList { // sort items self.items.sort_items(sort); + // set sort field + self.sort = sort.clone(); + // if follow selection, then set self.selection to the new index of the selected item's pid. if self.follow_selection { self.selection = pid.and_then(|p| self.items.get_idx(p)); diff --git a/src/components/system.rs b/src/components/system.rs index 3caf905..d8ff34b 100644 --- a/src/components/system.rs +++ b/src/components/system.rs @@ -5,6 +5,7 @@ use process_list::ProcessListItem; use performance_queue::{CpuItem, MemoryItem}; use crate::config::Config; use super::{Component, EventState}; +use process_list::ProcessItemInfo; // See here for refreshing system: https://crates.io/crates/sysinfo#:~:text=use%20sysinfo%3A%3ASystem,(sysinfo%3A%3AMINIMUM_CPU_UPDATE_INTERVAL)%3B%0A%7D // note: sysinfo::MINIMUM_CPU_UPDATE_INTERVAL = 200 ms @@ -43,7 +44,7 @@ impl SystemComponent { } let global_cpu_usage = self.system.global_cpu_usage(); let brand_cpu = String::from(brand_cpu); - let num_cores = self.system.physical_core_count(); + let num_cores = sysinfo::System::physical_core_count(); let item = CpuItem::new(global_cpu_usage, num_cores, cpu_frequency, brand_cpu); item } @@ -83,6 +84,23 @@ impl SystemComponent { } Ok(true) } + + pub fn detailed_process_info(&mut self, pid: u32) -> Option { + if let Some(process) = self.system.process(Pid::from_u32(pid)) { + let start_time = process.start_time(); + let run_time = process.run_time(); + let accumulated_cpu_time = process.accumulated_cpu_time(); + let status = process.status().to_string(); + // should be a better way to do this lol + //todo: let path = process.exe(); + //todo: let env_vars = process.environ(); + + let item_info = ProcessItemInfo::new(start_time, run_time, accumulated_cpu_time, status); + + return Some(item_info) + } + None + } } impl Component for SystemComponent { From 96877353036db2cf634affe2828e1d21ea2053be Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Fri, 18 Apr 2025 02:18:43 -0500 Subject: [PATCH 10/12] beginning work on ui --- performance-queue/src/bounded_queue.rs | 12 +- performance-queue/src/cpu_item.rs | 39 ++++-- process-list/src/process_list.rs | 44 +++---- process-list/src/process_list_item.rs | 74 ++++++++++-- process-list/src/process_list_items.rs | 22 ++-- src/app.rs | 142 +++++++++++++++------- src/components/command.rs | 10 ++ src/components/cpu.rs | 160 +++++++++++++++++++++++++ src/components/mod.rs | 1 + src/components/performance.rs | 14 +-- src/components/process.rs | 61 +++++----- src/components/system.rs | 118 ++++++++++-------- src/components/tab.rs | 55 ++++----- src/config.rs | 12 +- src/ui/process_list_ui.rs | 59 ++++----- 15 files changed, 576 insertions(+), 247 deletions(-) create mode 100644 src/components/cpu.rs diff --git a/performance-queue/src/bounded_queue.rs b/performance-queue/src/bounded_queue.rs index 4d2cc52..8e7b5b0 100644 --- a/performance-queue/src/bounded_queue.rs +++ b/performance-queue/src/bounded_queue.rs @@ -1,5 +1,6 @@ use std::{collections::VecDeque, io}; +#[derive(Default)] pub struct PerformanceQueue { pub performance_items: VecDeque, max_size: usize, @@ -14,7 +15,7 @@ impl PerformanceQueue { } } - pub fn add_item(&mut self, item: &T) -> io::Result<()> { + pub fn add_item(&mut self, item: &T) { if self.performance_items.len() < self.max_size { let item = item.clone(); self.performance_items.push_back(item); @@ -31,7 +32,6 @@ impl PerformanceQueue { let item = item.clone(); self.performance_items.push_back(item); } - Ok(()) } pub fn front(&self) -> Option<&T> { @@ -45,6 +45,10 @@ impl PerformanceQueue { pub fn max_size(&self) -> usize { self.max_size.clone() } + + pub fn iter(&self) -> std::collections::vec_deque::Iter<'_, T> { + self.performance_items.iter() + } } #[cfg(test)] @@ -52,7 +56,7 @@ mod test { use super::PerformanceQueue; use crate::CpuItem; - #[test] + /*#[test] fn test_bounded_queue() { let mut instance: PerformanceQueue = PerformanceQueue::new(2); assert_eq!(instance.max_size(), 2); @@ -68,5 +72,5 @@ mod test { let _ = instance.add_item(&cpu_item_3); assert_eq!(instance.back().unwrap().global_usage(), 15.7); assert_eq!(instance.front().unwrap().global_usage(), 13.2); - } + }*/ } \ No newline at end of file diff --git a/performance-queue/src/cpu_item.rs b/performance-queue/src/cpu_item.rs index 08945e8..1fa5430 100644 --- a/performance-queue/src/cpu_item.rs +++ b/performance-queue/src/cpu_item.rs @@ -1,31 +1,47 @@ #[derive(Clone, Default, Debug)] pub struct CpuItem { - global_usage: f32, - num_cores: Option, + id: usize, + usage: f32, frequency: u64, + name: String, brand: String, + vendor_id: String, } impl CpuItem { - pub fn new(global_usage: f32, num_cores: Option, frequency: u64, brand: String) -> Self { + pub fn new( + id: usize, + usage: f32, + frequency: u64, + name: String, + brand: String, + vendor_id: String, + ) -> Self { Self { - global_usage, - num_cores, + id, + usage, frequency, + name, brand, + vendor_id, } } + + pub fn usage(&self) -> f32 { + self.usage + } + + pub fn id(&self) -> usize { + self.id + } pub fn global_usage(&self) -> f32 { - self.global_usage.clone() + self.usage } - pub fn num_cores(&self) -> Option { - self.num_cores.clone() - } pub fn frequency(&self) -> u64 { - self.frequency.clone() + self.frequency } pub fn brand(&self) -> String { @@ -36,7 +52,7 @@ impl CpuItem { #[cfg(test)] mod test { use super::CpuItem; - +/* #[test] fn test_default() { let instance = CpuItem::default(); @@ -54,4 +70,5 @@ mod test { assert_eq!(instance.frequency(), 4056); assert_eq!(instance.brand(), String::from("Apple")); } + */ } \ No newline at end of file diff --git a/process-list/src/process_list.rs b/process-list/src/process_list.rs index 1fef2c9..092a6ff 100644 --- a/process-list/src/process_list.rs +++ b/process-list/src/process_list.rs @@ -260,8 +260,8 @@ mod test { assert_eq!(empty_instance.selection(), None); // New constructor. - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2); + let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test")); + let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test")); let items = vec![item_0, item_1]; let instance = ProcessList::new(&items); assert!(!instance.is_empty()); @@ -283,8 +283,8 @@ mod test { #[test] fn test_update() { // Update with empty list of items. - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2); + let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test")); + let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test")); let items = vec![item_0, item_1]; let mut instance = ProcessList::new(&items); let empty_items = vec![]; @@ -293,19 +293,19 @@ mod test { assert!(instance.selection().is_none()); // Update with non-empty list of items. - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2); + let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test")); + let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test")); let items = vec![item_0, item_1]; let mut instance = ProcessList::new(&items); - let item_2 = ProcessListItem::new(3, String::from("c"), 3.0, 3); + let item_2 = ProcessListItem::new(3, String::from("c"), 3.0, 3, 0, 10, 10, String::from("test")); let new_items = vec![item_2]; let _ = instance.update(&new_items); assert!(!instance.is_empty()); assert_eq!(instance.selection(), Some(0)); // Update with empty list of items and follow_selection set to true. - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2); + let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test")); + let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test")); let items = vec![item_0, item_1]; let mut instance = ProcessList::new(&items); let _ = instance.change_follow_selection(); @@ -315,25 +315,25 @@ mod test { assert!(instance.selection().is_none()); // Update with non-empty list of items and follow_selection set to true case 1. - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2); + let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test")); + let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test")); let items = vec![item_0, item_1]; let mut instance = ProcessList::new(&items); let _ = instance.change_follow_selection(); - let item_2 = ProcessListItem::new(3, String::from("c"), 3.0, 3); + let item_2 = ProcessListItem::new(3, String::from("c"), 3.0, 3, 0, 10, 10, String::from("test")); let new_items = vec![item_2]; let _ = instance.update(&new_items); assert!(!instance.is_empty()); assert_eq!(instance.selection(), Some(0)); // Update with non-empty list of items and follow_selection set to true case 2. - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2); + let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test")); + let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test")); let items = vec![item_0, item_1]; let mut instance = ProcessList::new(&items); let _ = instance.change_follow_selection(); - let item_2 = ProcessListItem::new(2, String::from("b"), 2.0, 2); - let item_3 = ProcessListItem::new(3, String::from("c"), 3.0, 3); + let item_2 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test")); + let item_3 = ProcessListItem::new(3, String::from("c"), 3.0, 3, 0, 10, 10, String::from("test")); let new_items = vec![item_2, item_3]; let _ = instance.update(&new_items); assert!(!instance.is_empty()); @@ -343,8 +343,8 @@ mod test { #[test] fn test_sort() { // Test sort when follow_selection = false. - let item_0 = ProcessListItem::new(1, String::from("a"), 2.0, 2); - let item_1 = ProcessListItem::new(2, String::from("b"), 1.0, 1); + let item_0 = ProcessListItem::new(1, String::from("a"), 2.0, 2, 0, 10, 10, String::from("test")); + let item_1 = ProcessListItem::new(2, String::from("b"), 1.0, 1, 0, 10, 10, String::from("test")); let items = vec![item_1, item_0]; let mut instance = ProcessList::new(&items); assert!(instance.sort == ListSortOrder::CpuUsageDec); @@ -354,8 +354,8 @@ mod test { assert_eq!(instance.selection(), Some(0)); - let item_0 = ProcessListItem::new(1, String::from("a"), 2.0, 2); - let item_1 = ProcessListItem::new(2, String::from("b"), 1.0, 1); + let item_0 = ProcessListItem::new(1, String::from("a"), 2.0, 2, 0, 10, 10, String::from("test")); + let item_1 = ProcessListItem::new(2, String::from("b"), 1.0, 1, 0, 10, 10, String::from("test")); let items = vec![item_0, item_1]; let mut instance = ProcessList::new(&items); assert!(instance.sort == ListSortOrder::CpuUsageDec); @@ -372,8 +372,8 @@ mod test { empty_instance.move_selection(MoveSelection::Down); assert_eq!(empty_instance.selection(), None); - let item_0 = ProcessListItem::new(1, String::from("a"), 2.0, 2); - let item_1 = ProcessListItem::new(2, String::from("b"), 1.0, 1); + let item_0 = ProcessListItem::new(1, String::from("a"), 2.0, 2, 0, 10, 10, String::from("test")); + let item_1 = ProcessListItem::new(2, String::from("b"), 1.0, 1, 0, 10, 10, String::from("test")); let items = vec![item_0, item_1]; let mut instance = ProcessList::new(&items); assert_eq!(instance.selection(), Some(0)); diff --git a/process-list/src/process_list_item.rs b/process-list/src/process_list_item.rs index 1668997..69d4568 100644 --- a/process-list/src/process_list_item.rs +++ b/process-list/src/process_list_item.rs @@ -4,43 +4,72 @@ pub struct ProcessListItem { name: String, cpu_usage: f32, memory_usage: u64, + start_time: u64, + run_time: u64, + accumulated_cpu_time: u64, + status: String, } impl ProcessListItem { // constructor - pub fn new(pid: u32, name: String, cpu_usage: f32, memory_usage: u64) -> Self { + pub fn new( + pid: u32, + name: String, + cpu_usage: f32, + memory_usage: u64, + start_time: u64, + run_time: u64, + accumulated_cpu_time: u64, + status: String, + ) -> Self { Self { pid, name, cpu_usage, memory_usage, + start_time, + run_time, + accumulated_cpu_time, + status, } } - // filter by name or pid + // match by name or pid pub fn is_match(&self, filter_text: &str) -> bool { self.name.contains(filter_text) || self.pid.to_string().contains(filter_text) } - // This function gets the pid of a ProcessListItem instance. pub fn pid(&self) -> u32 { - self.pid.clone() + self.pid } - // This function gets the name of a ProcessListItem instance. pub fn name(&self) -> String { self.name.clone() } - // This function gets the cpu usage of a ProcessListItem instance. pub fn cpu_usage(&self) -> f32 { - self.cpu_usage.clone() + self.cpu_usage } - // This function gets the memory usage of a ProcessListItem instance. pub fn memory_usage(&self) -> u64 { - self.memory_usage.clone() + self.memory_usage + } + + pub fn start_time(&self) -> u64 { + self.start_time + } + + pub fn run_time(&self) -> u64 { + self.run_time + } + + pub fn accumulated_cpu_time(&self) -> u64 { + self.accumulated_cpu_time + } + + pub fn status(&self) -> String { + self.status.clone() } } @@ -63,31 +92,50 @@ pub mod test { assert!(String::is_empty(&instance.name)); assert_eq!(instance.cpu_usage, 0.0); assert_eq!(instance.memory_usage, 0); + assert_eq!(instance.start_time, 0); + assert_eq!(instance.run_time, 0); + assert_eq!(instance.accumulated_cpu_time, 0); + assert!(String::is_empty(&instance.status)); - let instance = ProcessListItem::new(1, String::from("a"), 1.0, 1); + let instance = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test")); assert_eq!(instance.pid, 1); assert_eq!(instance.name, String::from("a")); assert_eq!(instance.cpu_usage, 1.0); assert_eq!(instance.memory_usage, 1); + assert_eq!(instance.start_time, 0); + assert_eq!(instance.run_time, 10); + assert_eq!(instance.accumulated_cpu_time, 10); + assert_eq!(instance.status, String::from("test")); + } #[test] fn test_instance_functions() { let instance_0 = ProcessListItem::default(); - let instance_1 = ProcessListItem::new(1, String::from("a"), 1.0, 1); + let instance_1 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test")); assert_eq!(instance_0.pid(), instance_0.pid); assert_eq!(instance_0.name(), instance_0.name); assert_eq!(instance_0.cpu_usage(), instance_0.cpu_usage); assert_eq!(instance_0.memory_usage(), instance_0.memory_usage); + assert_eq!(instance_0.start_time(), instance_0.start_time); + assert_eq!(instance_0.run_time(), instance_0.run_time); + assert_eq!(instance_0.accumulated_cpu_time(), instance_0.accumulated_cpu_time); + assert_eq!(instance_0.status(), instance_0.status); assert_eq!(instance_0.is_match(""), true); - assert_ne!(instance_0.is_match("a"), true); + assert_eq!(instance_0.is_match("a"), false); + assert_eq!(instance_0.is_match(&instance_0.pid.to_string()), true); assert_eq!(instance_1.pid(), instance_1.pid); assert_eq!(instance_1.name(), instance_1.name); assert_eq!(instance_1.cpu_usage(), instance_1.cpu_usage); assert_eq!(instance_1.memory_usage(), instance_1.memory_usage); + assert_eq!(instance_0.start_time(), instance_0.start_time); + assert_eq!(instance_0.run_time(), instance_0.run_time); + assert_eq!(instance_0.accumulated_cpu_time(), instance_0.accumulated_cpu_time); + assert_eq!(instance_0.status(), instance_0.status); assert_eq!(instance_1.is_match("a"), true); - assert_ne!(instance_1.is_match("aa"), true); + assert_eq!(instance_1.is_match("aa"), false); + assert_eq!(instance_1.is_match(&instance_1.pid.to_string()), true); } } \ No newline at end of file diff --git a/process-list/src/process_list_items.rs b/process-list/src/process_list_items.rs index 183c2b5..2b98745 100644 --- a/process-list/src/process_list_items.rs +++ b/process-list/src/process_list_items.rs @@ -116,9 +116,9 @@ mod test { #[test] fn test_new() { - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1); + let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test")); let clone_0 = item_0.clone(); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2); + let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test")); let clone_1 = item_1.clone(); let items = vec![item_0, item_1]; let instance = ProcessListItems::new(&items); @@ -135,9 +135,9 @@ mod test { #[test] fn test_filter() { - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1); + let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test")); let clone_0 = item_0.clone(); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2); + let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test")); let _clone_1 = item_1.clone(); let items = vec![item_0, item_1]; let instance = ProcessListItems::new(&items); @@ -152,14 +152,14 @@ mod test { #[test] fn test_update_items() { - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2); + let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test")); + let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test")); let items = vec![item_0, item_1]; let mut instance = ProcessListItems::new(&items); // Note: ProcessListItem's are compared by Pid. - let item_2 = ProcessListItem::new(1, String::from("a"), 7.0, 1337); - let item_3 = ProcessListItem::new(3, String::from("c"), 3.0, 3); + let item_2 = ProcessListItem::new(1, String::from("a"), 7.0, 1337, 0, 10, 10, String::from("test")); + let item_3 = ProcessListItem::new(3, String::from("c"), 3.0, 3, 0, 10, 10, String::from("test")); let new_items = vec![item_2, item_3]; let _ = instance.sort_items(&ListSortOrder::CpuUsageInc); @@ -177,9 +177,9 @@ mod test { #[test] fn test_sort_items() { - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2); - let item_3 = ProcessListItem::new(3, String::from("c"), 3.0, 3); + let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test")); + let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test")); + let item_3 = ProcessListItem::new(3, String::from("c"), 3.0, 3, 0, 10, 10, String::from("test")); let items = vec![item_0, item_1, item_3]; let mut instance = ProcessListItems::new(&items); diff --git a/src/app.rs b/src/app.rs index 267ade6..5d4e0e7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,11 @@ use anyhow::{Ok, Result}; use crossterm::event::KeyEvent; use ratatui::prelude::*; +use crate::components::cpu::CPUComponent; use crate::config::Config; use crate::components::{ tab::TabComponent, process::ProcessComponent, - performance::PerformanceComponent, system::SystemComponent, error::ErrorComponent, Component, @@ -17,10 +17,20 @@ use crate::components::{ command::CommandInfo, }; +pub enum MainFocus { + CPU, + Memory, + Network, + Temperature, + Process, +} + pub struct App { + focus: MainFocus, + expand: bool, system: SystemComponent, process: ProcessComponent, - performance: PerformanceComponent, + cpu: CPUComponent, tab: TabComponent, help: HelpComponent, pub error: ErrorComponent, @@ -31,9 +41,11 @@ impl App { // constructor. pub fn new(config: Config) -> Self { Self { + focus: MainFocus::Process, + expand: false, system: SystemComponent::new(config.clone()), process: ProcessComponent::new(config.clone()), - performance: PerformanceComponent::new(config.clone(), 10), + cpu: CPUComponent::default(), tab: TabComponent::new(config.clone()), help: HelpComponent::new(config.clone()), error: ErrorComponent::new(config.clone()), @@ -45,7 +57,8 @@ impl App { pub fn init(&mut self) -> Result<()> { self.system.refresh_all()?; self.update_process(); - self.update_performance()?; + self.update_cpu(); + //self.update_performance()?; self.update_commands(); Ok(()) @@ -55,11 +68,18 @@ impl App { pub fn refresh(&mut self) -> Result<()>{ self.system.refresh_all()?; self.update_process(); - self.update_performance()?; + self.update_cpu(); + //self.update_performance()?; Ok(()) } + fn update_cpu(&mut self) -> bool { + let new_cpus = self.system.get_cpus(); + self.cpu.update(&new_cpus); + true + } + // return result of process update fn update_process(&mut self) -> bool { let new_processes = self.system.get_processes(); @@ -69,19 +89,18 @@ impl App { } // fix return type - fn update_performance(&mut self) -> Result<()> { - let new_cpu_info = self.system.get_cpu_info(); - let new_memory_info = self.system.get_memory_info(); - self.performance.update(&new_cpu_info, &new_memory_info)?; + //fn update_performance(&mut self) -> Result<()> { + // let new_cpu_info = self.system.get_cpu_info(); + // let new_memory_info = self.system.get_memory_info(); + // self.performance.update(&new_cpu_info, &new_memory_info)?; - Ok(()) - } + // Ok(()) + //} // top level key event processor pub fn event(&mut self, key: KeyEvent) -> Result { if key.code == self.config.key_config.toggle_themes { self.update_component_themes(); - return Ok(EventState::Consumed) } else if self.components_event(key)?.is_consumed() { @@ -98,7 +117,7 @@ impl App { fn update_component_themes(&mut self) { self.config.theme_config.toggle_themes(); self.process.config.theme_config.toggle_themes(); - self.performance.config.theme_config.toggle_themes(); + //self.performance.config.theme_config.toggle_themes(); self.help.config.theme_config.toggle_themes(); self.system._config.theme_config.toggle_themes(); self.tab.config.theme_config.toggle_themes(); @@ -129,6 +148,8 @@ impl App { res } + /* */ + // component key event processor fn components_event(&mut self, key: KeyEvent) -> Result { if self.error.event(key)?.is_consumed() { @@ -139,27 +160,20 @@ impl App { return Ok(EventState::Consumed) } - match self.tab.selected_tab { - Tab::Process => { - // see if key event is processed by process component - if self.process.event(key)?.is_consumed() { + match self.focus { + MainFocus::CPU => { + if self.cpu.event(key)?.is_consumed() { return Ok(EventState::Consumed) } - // terminate system process - else if key.code == self.config.key_config.terminate { - if let Some(pid) = self.process.selected_pid() { - self.system.terminate_process(pid)?; - - return Ok(EventState::Consumed) - } - } } - Tab::Performance => { - if self.performance.event(key)?.is_consumed() { + MainFocus::Memory => {} + MainFocus::Network => {} + MainFocus::Temperature => {} + MainFocus::Process => { + if self.process.event(key)?.is_consumed() { return Ok(EventState::Consumed) } } - //Tab::Users => {} } if self.tab.event(key)?.is_consumed() { @@ -169,9 +183,33 @@ impl App { Ok(EventState::NotConsumed) } - // not being used, implement if there is a need for focus control outside of tab - fn move_focus(&mut self, _key: KeyEvent) -> Result { - return Ok(EventState::NotConsumed); + + /* + Control with Tab + CPU -> Memory -> Network -> Temperature -> Process -> ... + */ + fn move_focus(&mut self, key: KeyEvent) -> Result { + if key.code == self.config.key_config.tab { + match self.focus { + MainFocus::CPU => { + self.focus = MainFocus::Memory + } + MainFocus::Memory => { + self.focus = MainFocus::Network + } + MainFocus::Network => { + self.focus = MainFocus::Temperature + } + MainFocus::Temperature => { + self.focus = MainFocus::Process + } + MainFocus::Process => { + self.focus = MainFocus::CPU + } + } + return Ok(EventState::Consumed) + } + Ok(EventState::NotConsumed) } // draw the app @@ -185,19 +223,39 @@ impl App { // if error always draw--error component state determines if anything is drawn self.error.draw(f, chunks[0], false)?; - - // always draw tab--identifies tab state - self.tab.draw(f, chunks[0], false)?; - // only draw selected tab - match self.tab.selected_tab { - Tab::Process => { - self.process.draw(f, chunks[0], false)?; - } - Tab::Performance => { - self.performance.draw(f, chunks[0], false)?; + + if self.expand { + // split screen to draw only focused component + } + else { + // draw all components + // split screen + let vertical_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ].as_ref()) + .split(chunks[0]); + + let mut horizontal_chunks = Vec::new(); + for chunk in vertical_chunks.iter() { + let horizontal_chunk = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(60), + Constraint::Fill(1), + ]) + .split(*chunk); + + horizontal_chunks.push(horizontal_chunk); } - //Tab::Users => {} + + self.process.draw(f, horizontal_chunks[3][1], false)?; + self.cpu.draw(f, vertical_chunks[0], false)?; } // if help--help component state determines if anything is drawn diff --git a/src/components/command.rs b/src/components/command.rs index 6c88b94..2e37400 100644 --- a/src/components/command.rs +++ b/src/components/command.rs @@ -153,4 +153,14 @@ pub fn follow_selection(key: &KeyConfig) -> CommandText { ), CMD_GROUP_GENERAL ) +} + +pub fn more_process_info(key: &KeyConfig) -> CommandText { + CommandText::new( + format!( + "Get more process information [{:?}]", + key.process_info, + ), + CMD_GROUP_GENERAL + ) } \ No newline at end of file diff --git a/src/components/cpu.rs b/src/components/cpu.rs new file mode 100644 index 0000000..fd7da30 --- /dev/null +++ b/src/components/cpu.rs @@ -0,0 +1,160 @@ +use std::collections::BTreeMap; +use std::{collections::HashMap, hash::Hash}; + +use crossterm::style::style; +use ratatui::prelude::*; +use ratatui::style::palette::material::YELLOW; +use ratatui::widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, List, ListItem, ListState}; + +use anyhow::Ok; +use performance_queue::{PerformanceQueue, CpuItem}; +use crate::config::Config; + +use super::{Component, DrawableComponent}; + +#[derive(Default)] +pub struct CPUComponent { + cpus: BTreeMap>, + selection: usize, + config: Config, +} + +impl CPUComponent { + pub fn update(&mut self, cpus: &Vec) { + for cpu in cpus { + let id = cpu.id(); + + let perf_q = self.cpus.entry(id).or_insert_with(|| { + PerformanceQueue::new(self.config.events_per_min() as usize) + }); + + perf_q.add_item(cpu); + } + } +} + +impl Component for CPUComponent { + fn event(&mut self, key: crossterm::event::KeyEvent) -> anyhow::Result { + if key.code == self.config.key_config.move_down { + if self.selection < self.cpus.len() { // this works b/c we prepend ALL to the drawn list + self.selection = self.selection.saturating_add(1); + } + return Ok(super::EventState::Consumed); + } + if key.code == self.config.key_config.move_up { + self.selection = self.selection.saturating_sub(1); + return Ok(super::EventState::Consumed); + } + + Ok(super::EventState::NotConsumed) + } +} + +/* +if selection = 0 display all +if selection = 1 display global +if selection = 2 display cpu 0 +... + +*/ + +impl DrawableComponent for CPUComponent { + fn draw(&mut self, f: &mut ratatui::Frame, area: ratatui::prelude::Rect, _focused: bool) -> anyhow::Result<()> { + let horizontal_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(90), + Constraint::Fill(1), + ]).split(area); + + let mut all_data: Vec> = Vec::new(); + let mut datasets: Vec = Vec::new(); // holds references to data to be drawn + + if self.selection == 0 { + // display all + for (_id, queue) in self.cpus.iter() { + let data: Vec<(f64, f64)> = queue + .iter() + .enumerate() + .map(|(i, item)| (i as f64, item.usage() as f64)) + .collect(); + + all_data.push(data); + } + } + else { + // display selection-1 : accounting for All + if let Some(queue) = self.cpus.get(&self.selection.saturating_sub(1)) { + let data: Vec<(f64, f64)> = queue + .iter() + .enumerate() + .map(|(i, item)| (i as f64, item.usage() as f64)) + .collect(); + + all_data.push(data); + } + } + + for data in all_data.iter() { + datasets.push( + Dataset::default() + .name(format!("CPU")) + .data(data) + .graph_type(GraphType::Line) + .marker(symbols::Marker::Braille), + ); + } + + let chart = Chart::new(datasets) + .block(Block::default().borders(Borders::ALL).title("CPU Usage")) + .x_axis( + Axis::default() + .title("Time") + .bounds([0.0, self.config.events_per_min() as f64]) + .labels(vec![Span::raw("0"), Span::raw("now")]), + ) + .y_axis( + Axis::default() + .title("%") + .bounds([0.0, 100.0]) + .labels(vec![ + Span::raw("0"), + Span::raw("50"), + Span::raw("100"), + ]), + ); + + f.render_widget(chart, horizontal_chunks[0]); + + // cpu list + let mut names: Vec = self.cpus + .iter() + .map(|(key, _queue)| { + let title = if *key == 0 { + format!("Global") + } + else { + format!("CPU {}", key.saturating_sub(1).to_string()) + }; + ListItem::new(title) + }) + .collect(); + + names.insert(0, ListItem::new(String::from("All"))); + + let mut list_state = ListState::default(); + list_state.select(Some(self.selection)); + + + let cpu_list = List::new(names) + .scroll_padding(horizontal_chunks[1].height as usize / 2) + .block(Block::default().title("CPU List").borders(Borders::ALL)) + .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) + ); + + f.render_stateful_widget(cpu_list, horizontal_chunks[1], &mut list_state); + + + Ok(()) + } +} \ No newline at end of file diff --git a/src/components/mod.rs b/src/components/mod.rs index f46dd72..71e8b06 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -13,6 +13,7 @@ pub mod tab; pub mod utils; pub mod command; pub mod vertical_tabs; +pub mod cpu; pub trait DrawableComponent { fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()>; diff --git a/src/components/performance.rs b/src/components/performance.rs index 0bb8037..0498e3f 100644 --- a/src/components/performance.rs +++ b/src/components/performance.rs @@ -32,8 +32,8 @@ impl PerformanceComponent { } pub fn update(&mut self, cpu_item: &CpuItem, memory_item: &MemoryItem) -> Result<()> { - self.cpu_info.add_item(cpu_item)?; - self.memory_info.add_item(memory_item)?; + self.cpu_info.add_item(cpu_item); + self.memory_info.add_item(memory_item); Ok(()) } @@ -80,11 +80,11 @@ impl PerformanceComponent { Span::raw(item.brand().to_string()), ]) .style(Color::White), - Line::from(vec![ - Span::raw("Number of Cores: "), - Span::raw(item.num_cores().unwrap_or_default().to_string()), - ]) - .style(Color::White), + //Line::from(vec![ + // Span::raw("Number of Cores: "), + // Span::raw(item.num_cores().unwrap_or_default().to_string()), + //]) + //.style(Color::White), Line::from(vec![ Span::raw("Frequency: "), Span::raw(item.frequency().to_string()), diff --git a/src/components/process.rs b/src/components/process.rs index 0e12fd5..e90025e 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -10,7 +10,8 @@ use super::{common_nav, common_sort}; use super::{filter::FilterComponent, Component, DrawableComponent, EventState}; use super::utils::vertical_scroll::VerticalScroll; use crate::config::Config; -use crate::{config::KeyConfig, ui::process_list_ui::ProcessListUI}; +use crate::config::KeyConfig; +use crate::ui::process_list_ui::process_list_ui::draw_process_list; // focus of process component, this can be on either a ProcessList or FilterComponent #[derive(PartialEq, Clone)] @@ -186,26 +187,16 @@ fn list_sort(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> R impl DrawableComponent for ProcessComponent { fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> Result<()> { - // splitting the parameter area into two vertical chunks - let vertical_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // vertical chunk for TabComponent and FilterComponent : vertical_chunks[0] - Constraint::Min(1), // vertical chunk for the ProcessList : vertical_chunks[1] - ].as_ref()) - .split(area); - - // splitting the vertical chunk for the TabComponent and FilterComponent into two horizontal chunks + // split for filter let horizontal_chunks = Layout::default() - .direction(Direction::Horizontal) + .direction(Direction::Vertical) .constraints([ - Constraint::Percentage(50), // horizontal chunk for TabComponent : horizontal_chunks[0] - Constraint::Percentage(50), // horizontal chunk for FilterComponent : horizontal_chunks[1] - ].as_ref()) - .split(vertical_chunks[0]); + Constraint::Percentage(70), + Constraint::Length(3), + ]).split(area); - // calculate list - let visible_list_height = vertical_chunks[1].height.saturating_sub(3) as usize; + // calculate visible list height + let visible_list_height = area.height.saturating_sub(3) as usize; // determine list to display let list = if let Some(filtered_list) = self.filtered_list.as_ref() { @@ -215,7 +206,7 @@ impl DrawableComponent for ProcessComponent { &self.list }; - // updating the scroll struct which calculates the position at the top of the displayed list. + // updating the scroll struct--calculates the position at the top of the displayed list list.selection().map_or_else( { || self.scroll.reset() @@ -226,20 +217,28 @@ impl DrawableComponent for ProcessComponent { }, ); - let list_iterator = list.iterate(self.scroll.get_top(), visible_list_height); + let visible_items = list.iterate(self.scroll.get_top(), visible_list_height); - // re-creating process list ui on every draw event--this can be optimized--add UI state to process component - let process_list_widget = ProcessListUI { - visible_items: list_iterator, - follow_selection: list.is_follow_selection(), - theme_config: self.config.theme_config.clone(), - }; - - self.filter.draw(f, horizontal_chunks[1], matches!(self.focus, Focus::Filter))?; - - process_list_widget.draw(f, vertical_chunks[1], matches!(self.focus, Focus::List)); + draw_process_list( + f, + horizontal_chunks[0], + visible_items, + self.list.is_follow_selection(), + matches!(self.focus, Focus::List), + self.config.theme_config.clone() + ); - self.scroll.draw(f, vertical_chunks[1], false)?; + self.scroll.draw( + f, + horizontal_chunks[0], + false + )?; + + self.filter.draw( + f, + horizontal_chunks[1], + matches!(self.focus, Focus::Filter) + )?; Ok(()) } diff --git a/src/components/system.rs b/src/components/system.rs index d8ff34b..903b68e 100644 --- a/src/components/system.rs +++ b/src/components/system.rs @@ -1,6 +1,8 @@ +use std::vec; + use anyhow::Result; use crossterm::event::KeyEvent; -use sysinfo::{System, Pid}; +use sysinfo::{Cpu, Pid, System}; use process_list::ProcessListItem; use performance_queue::{CpuItem, MemoryItem}; use crate::config::Config; @@ -12,7 +14,6 @@ use process_list::ProcessItemInfo; pub struct SystemComponent { system: System, - //network: Networks, pub _config: Config } @@ -20,7 +21,6 @@ impl SystemComponent { pub fn new(config: Config) -> Self { Self { system: System::new_all(), - //network: Networks::new_with_refreshed_list(), _config: config } } @@ -31,51 +31,73 @@ impl SystemComponent { Ok(EventState::Consumed) } - //pub fn get_network_info(&self) -> NetworkItem { + pub fn get_cpus(&self) -> Vec { + let mut cpus: Vec = Vec::new(); - //} + // dummy item for global cpu usage + cpus.push(CpuItem::new( + 0, + self.system.global_cpu_usage(), + 0, String::from("Global"), + String::from(""), + String::from("") + )); - pub fn get_cpu_info(&self) -> CpuItem { - let mut brand_cpu: &str = ""; - let mut cpu_frequency: u64 = 0; - for cpu in self.system.cpus() { - brand_cpu = cpu.brand(); - cpu_frequency = cpu.frequency(); + for (id, cpu) in self.system.cpus().iter().enumerate() { + let cpu_item = CpuItem::new( + id + 1, // id=0 reserved for global cpu usage + cpu.cpu_usage(), + cpu.frequency(), + String::from(cpu.name()), + String::from(cpu.brand()), + String::from(cpu.vendor_id()), + ); + + cpus.push(cpu_item); } - let global_cpu_usage = self.system.global_cpu_usage(); - let brand_cpu = String::from(brand_cpu); - let num_cores = sysinfo::System::physical_core_count(); - let item = CpuItem::new(global_cpu_usage, num_cores, cpu_frequency, brand_cpu); - item + + cpus } - pub fn get_memory_info(&self) -> MemoryItem { - let total_memory = self.system.total_memory(); - let used_memory = self.system.used_memory(); - let free_memory = self.system.free_memory(); - let available_memory = self.system.available_memory(); - let memory_info = MemoryItem::new(total_memory, used_memory, free_memory, available_memory); - memory_info + pub fn get_global_cpu_info(&self) -> f32 { + self.system.global_cpu_usage() } pub fn get_processes(&self) -> Vec { let mut processes: Vec = Vec::new(); + for (pid, process) in self.system.processes() { - let name = self.get_process_name(*pid); - let cpu_usage = process.cpu_usage(); + let name = if let Some(name) = process.name().to_str() { + String::from(name) + } + else { + String::from("No name") + }; + let cpu_usage = if let Some(core_count) = sysinfo::System::physical_core_count() { + process.cpu_usage() / core_count as f32 // normalizing process cpu usage by the number of cores + } + else { + process.cpu_usage() + }; let memory_usage = process.memory(); - let item = ProcessListItem::new(pid.as_u32(), name, cpu_usage, memory_usage); + let start_time = process.start_time(); + let run_time = process.run_time(); + let accumulated_cpu_time = process.accumulated_cpu_time(); + let status = process.status().to_string(); + let item = ProcessListItem::new( + pid.as_u32(), + name, cpu_usage, + memory_usage, + start_time, + run_time, + accumulated_cpu_time, + status + ); + processes.push(item); } - return processes; - } - fn get_process_name(&self, pid: Pid) -> String { - match self.system.process(pid) { - //Some(p) => return String::from(p.name()), - Some(p) => return String::from(p.name().to_str().unwrap_or_default()), - None => return String::from("No Process given pid"), - } + processes } pub fn terminate_process(&mut self, pid: u32) -> Result { @@ -85,22 +107,20 @@ impl SystemComponent { Ok(true) } - pub fn detailed_process_info(&mut self, pid: u32) -> Option { - if let Some(process) = self.system.process(Pid::from_u32(pid)) { - let start_time = process.start_time(); - let run_time = process.run_time(); - let accumulated_cpu_time = process.accumulated_cpu_time(); - let status = process.status().to_string(); - // should be a better way to do this lol - //todo: let path = process.exe(); - //todo: let env_vars = process.environ(); - - let item_info = ProcessItemInfo::new(start_time, run_time, accumulated_cpu_time, status); - - return Some(item_info) - } - None + /* + pub fn get_memory_info(&self) -> MemoryItem { + let total_memory = self.system.total_memory(); + let used_memory = self.system.used_memory(); + let free_memory = self.system.free_memory(); + let available_memory = self.system.available_memory(); + let memory_info = MemoryItem::new(total_memory, used_memory, free_memory, available_memory); + memory_info } +*/ + + //pub fn get_network_info(&self) -> NetworkItem { + + //} } impl Component for SystemComponent { diff --git a/src/components/tab.rs b/src/components/tab.rs index 9be5eb3..2dba861 100644 --- a/src/components/tab.rs +++ b/src/components/tab.rs @@ -17,7 +17,9 @@ enum MoveTabDirection { #[derive(Clone)] pub enum Tab { Process, - Performance, + CPU, + Memory, + Disk, //Users, } @@ -45,7 +47,9 @@ impl TabComponent { fn names(&self) -> Vec { vec![ String::from("Process"), - String::from("Performance"), + String::from("CPU"), + String::from("Memory"), + String::from("Disk"), //String::from("Users"), ] } @@ -54,28 +58,36 @@ impl TabComponent { match self.selected_tab { Tab::Process => { if direction == MoveTabDirection::Right { - self.selected_tab = Tab::Performance; + self.selected_tab = Tab::CPU; } else { - self.selected_tab = Tab::Performance; + self.selected_tab = Tab::Disk; } } - Tab::Performance => { + Tab::CPU => { if direction == MoveTabDirection::Right { + self.selected_tab = Tab::Memory; + } + else { self.selected_tab = Tab::Process; } + } + Tab::Memory => { + if direction == MoveTabDirection::Right { + self.selected_tab = Tab::Disk; + } else { + self.selected_tab = Tab::CPU; + } + } + Tab::Disk => { + if direction == MoveTabDirection::Right { self.selected_tab = Tab::Process; } + else { + self.selected_tab = Tab::Memory; + } } - //Tab::Users => { - // if direction == MoveTabDirection::Right { - // self.selected_tab = Tab::Process; - // } - // else { - // self.selected_tab = Tab::Performance; - // } - //} } } } @@ -100,18 +112,11 @@ impl DrawableComponent for TabComponent { .direction(Direction::Vertical) .constraints([ Constraint::Length(3), // filter and tab chunk - Constraint::Min(1) // list chunk + Constraint::Min(1), // list chunk + Constraint::Length(3), // filter chunk ].as_ref()) .split(area); - let horizontal_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(50), // space for tab - Constraint::Percentage(50), // space for filter - ].as_ref()) - .split(vertical_chunks[0]); - let names: Vec = self.names(); let titles: Vec = names .iter() @@ -124,20 +129,16 @@ impl DrawableComponent for TabComponent { ) ) .collect(); - let selected_tab = self.selected_tab.clone() as usize; - let selected_tab_style = Style::default().fg(Color::White).add_modifier(Modifier::BOLD); - let other_tab_style = Style::default().fg(Color::DarkGray); - let tabs: Tabs = Tabs::new(titles) .block(Block::default().borders(Borders::ALL)) .select(selected_tab) .style(other_tab_style) .highlight_style(selected_tab_style); - f.render_widget(tabs, horizontal_chunks[0]); + f.render_widget(tabs, vertical_chunks[0]); return Ok(()) } diff --git a/src/config.rs b/src/config.rs index 4e6437b..3be5b13 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ pub struct Config { pub key_config: KeyConfig, pub theme_config: ThemeConfig, refresh_rate: u64, + events_per_min: u64, tick_rate: u64, } @@ -15,6 +16,7 @@ impl Default for Config { key_config: KeyConfig::default(), theme_config: ThemeConfig::default(), refresh_rate: 5000, + events_per_min: 60000 / 5000, tick_rate: 250, } } @@ -22,11 +24,15 @@ impl Default for Config { impl Config { pub fn refresh_rate(&self) -> u64 { - return self.refresh_rate.clone() + self.refresh_rate } pub fn tick_rate(&self) -> u64 { - return self.tick_rate.clone() + self.tick_rate + } + + pub fn events_per_min(&self) -> u64 { + self.events_per_min } } @@ -54,6 +60,7 @@ pub struct KeyConfig { pub sort_memory_usage_dec: KeyCode, pub follow_selection: KeyCode, pub toggle_themes: KeyCode, + pub process_info: KeyCode, } impl Default for KeyConfig { @@ -85,6 +92,7 @@ impl Default for KeyConfig { sort_memory_usage_dec: KeyCode::Char('M'), follow_selection: KeyCode::Char('f'), toggle_themes: KeyCode::Char('t'), + process_info: KeyCode::Enter, } } } diff --git a/src/ui/process_list_ui.rs b/src/ui/process_list_ui.rs index 5818fa8..ed31e87 100644 --- a/src/ui/process_list_ui.rs +++ b/src/ui/process_list_ui.rs @@ -1,25 +1,27 @@ -use process_list::ListIterator; -use crate::config::ThemeConfig; -use ratatui::{ - Frame, - prelude::*, - widgets::{block::*, *}, -}; +pub mod process_list_ui { + use ratatui::{ + Frame, + prelude::*, + widgets::{block::*, *}, + }; + use process_list::ListIterator; + use crate::config::ThemeConfig; -pub struct ProcessListUI<'a> { - pub visible_items: ListIterator<'a>, - pub follow_selection: bool, - pub theme_config: ThemeConfig, -} - -impl<'a> ProcessListUI<'a> { - pub fn draw(self, f: &mut Frame, area: Rect, focus: bool) { - let follow_flag = self.follow_selection; - let header_style = self.theme_config.list_header; - let select_style = self.theme_config.item_select; - let select_follow_style = self.theme_config.item_select_follow; - let default_style = self.theme_config.item_style; - let out_of_focus_style = self.theme_config.component_out_of_focus; + pub fn draw_process_list<'a>( + f: &mut Frame, + area: Rect, + visible_items: ListIterator<'a>, + follow_selection: bool, + focus: bool, + theme_config: ThemeConfig, + ) { + let follow_flag = follow_selection; + let header_style = theme_config.list_header; + let select_style = theme_config.item_select; + let select_follow_style = theme_config.item_select_follow; + let default_style = theme_config.item_style; + let out_of_focus_style = theme_config.component_out_of_focus; + let in_focus_style = theme_config.component_in_focus; // setting header let header = ["", "Pid", "Name", "CPU Usage (%)", "Memory Usage (Bytes)"] @@ -37,7 +39,7 @@ impl<'a> ProcessListUI<'a> { .height(1); // setting rows - let rows = self.visible_items + let rows = visible_items .map(|(item, selected)| { let style = if focus && selected && follow_flag { @@ -69,7 +71,7 @@ impl<'a> ProcessListUI<'a> { }) .collect::>(); - // Setting the width constraints. + // setting the width constraints. let widths = vec![ Constraint::Length(2), @@ -79,23 +81,24 @@ impl<'a> ProcessListUI<'a> { Constraint::Length(20), ]; - // Setting block information. + // setting block information let block_title: &str = "Process List"; let block_style = if focus { - Style::default().fg(Color::White) + in_focus_style } else { - Style::default().fg(Color::DarkGray) + out_of_focus_style }; - // Setting the table. + // setting the table let table = Table::new(rows, widths) .header(header) .block(Block::default().borders(Borders::ALL).title(block_title)) .style(block_style); - // Render. + // render f.render_widget(table, area); } + } \ No newline at end of file From ac1d1af6f17ec527fc5149967ed1df9f66238174 Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Fri, 18 Apr 2025 17:16:49 -0500 Subject: [PATCH 11/12] updates --- performance-queue/src/bounded_queue.rs | 18 +- src/app.rs | 49 +++-- src/components/cpu.rs | 237 +++++++++++++++++------- src/components/performance.rs | 4 +- src/components/process.rs | 33 +++- src/components/utils/vertical_scroll.rs | 14 +- src/config.rs | 14 +- src/ui/process_list_ui.rs | 2 +- 8 files changed, 262 insertions(+), 109 deletions(-) diff --git a/performance-queue/src/bounded_queue.rs b/performance-queue/src/bounded_queue.rs index 8e7b5b0..220f9a9 100644 --- a/performance-queue/src/bounded_queue.rs +++ b/performance-queue/src/bounded_queue.rs @@ -3,30 +3,30 @@ use std::{collections::VecDeque, io}; #[derive(Default)] pub struct PerformanceQueue { pub performance_items: VecDeque, - max_size: usize, + capacity: usize, } // Clone trait is required for T to clone elements when adding items. impl PerformanceQueue { - pub fn new(max_size: usize) -> Self { + pub fn new(capacity: usize) -> Self { Self { - performance_items: VecDeque::with_capacity(max_size), - max_size, + performance_items: VecDeque::with_capacity(capacity), + capacity, } } pub fn add_item(&mut self, item: &T) { - if self.performance_items.len() < self.max_size { + if self.performance_items.len() < self.capacity { let item = item.clone(); self.performance_items.push_back(item); } - else if self.performance_items.len() == self.max_size { + else if self.performance_items.len() == self.capacity { self.performance_items.pop_front(); let item = item.clone(); self.performance_items.push_back(item); } else { - while self.performance_items.len() >= self.max_size { + while self.performance_items.len() >= self.capacity { self.performance_items.pop_front(); } let item = item.clone(); @@ -42,8 +42,8 @@ impl PerformanceQueue { return self.performance_items.back() } - pub fn max_size(&self) -> usize { - self.max_size.clone() + pub fn capacity(&self) -> usize { + self.capacity } pub fn iter(&self) -> std::collections::vec_deque::Iter<'_, T> { diff --git a/src/app.rs b/src/app.rs index 5d4e0e7..b989367 100644 --- a/src/app.rs +++ b/src/app.rs @@ -97,31 +97,44 @@ impl App { // Ok(()) //} + fn toggle_expand(&mut self) { + if self.expand == true { + self.expand = false; + } + else { + self.expand = true; + } + } + // top level key event processor pub fn event(&mut self, key: KeyEvent) -> Result { - if key.code == self.config.key_config.toggle_themes { - self.update_component_themes(); - return Ok(EventState::Consumed) - } - else if self.components_event(key)?.is_consumed() { + //if key.code == self.config.key_config.toggle_themes { + // self.update_component_themes(); + // return Ok(EventState::Consumed) + //} + if self.components_event(key)?.is_consumed() { return Ok(EventState::Consumed); } else if self.move_focus(key)?.is_consumed() { return Ok(EventState::Consumed); } + else if key.code == self.config.key_config.expand { + self.toggle_expand(); + return Ok(EventState::Consumed); + } Ok(EventState::NotConsumed) } // toggle color scheme - fn update_component_themes(&mut self) { - self.config.theme_config.toggle_themes(); - self.process.config.theme_config.toggle_themes(); - //self.performance.config.theme_config.toggle_themes(); - self.help.config.theme_config.toggle_themes(); - self.system._config.theme_config.toggle_themes(); - self.tab.config.theme_config.toggle_themes(); - } + //fn update_component_themes(&mut self) { + // self.config.theme_config.toggle_themes(); + // self.process.config.theme_config.toggle_themes(); + // //self.performance.config.theme_config.toggle_themes(); + // self.help.config.theme_config.toggle_themes(); + // self.system._config.theme_config.toggle_themes(); + // self.tab.config.theme_config.toggle_themes(); + //} // update help dialogue--commands fn update_commands(&mut self) { @@ -227,6 +240,12 @@ impl App { if self.expand { // split screen to draw only focused component + if matches!(self.focus, MainFocus::Process) { + self.process.draw(f, chunks[0], true)?; + } + if matches!(self.focus, MainFocus::CPU) { + self.cpu.draw(f, chunks[0], true)?; + } } else { // draw all components @@ -254,8 +273,8 @@ impl App { horizontal_chunks.push(horizontal_chunk); } - self.process.draw(f, horizontal_chunks[3][1], false)?; - self.cpu.draw(f, vertical_chunks[0], false)?; + self.process.draw(f, horizontal_chunks[3][1], matches!(self.focus, MainFocus::Process))?; + self.cpu.draw(f, vertical_chunks[0], matches!(self.focus, MainFocus::CPU))?; } // if help--help component state determines if anything is drawn diff --git a/src/components/cpu.rs b/src/components/cpu.rs index fd7da30..89630cd 100644 --- a/src/components/cpu.rs +++ b/src/components/cpu.rs @@ -1,11 +1,10 @@ use std::collections::BTreeMap; -use std::{collections::HashMap, hash::Hash}; -use crossterm::style::style; use ratatui::prelude::*; -use ratatui::style::palette::material::YELLOW; use ratatui::widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, List, ListItem, ListState}; +use std::str::FromStr; + use anyhow::Ok; use performance_queue::{PerformanceQueue, CpuItem}; use crate::config::Config; @@ -15,10 +14,63 @@ use super::{Component, DrawableComponent}; #[derive(Default)] pub struct CPUComponent { cpus: BTreeMap>, - selection: usize, + ui_selection: usize, config: Config, } +#[derive(PartialEq, Clone, Copy)] +pub enum ColorWheel { + Red, + Blue, + Cyan, + Green, + LightGreen, + Magenta, +} + +impl Default for ColorWheel { + fn default() -> Self { + ColorWheel::Red + } +} + +impl ColorWheel { + const ALL: [ColorWheel; 6] = [ + ColorWheel::Red, + ColorWheel::Blue, + ColorWheel::Cyan, + ColorWheel::Green, + ColorWheel::LightGreen, + ColorWheel::Magenta, + ]; + + pub fn as_str(&self) -> &'static str { + match self { + ColorWheel::Red => "red", + ColorWheel::Blue => "blue", + ColorWheel::Cyan => "cyan", + ColorWheel::Green => "green", + ColorWheel::LightGreen => "lightgreen", + ColorWheel::Magenta => "magenta", + } + } + + pub fn rotate(&mut self) { + if let Some(idx) = Self::ALL.iter().position(|c| c == self) { + let next_idx = (idx + 1) % Self::ALL.len(); + *self = Self::ALL[next_idx]; + } + } + + pub fn from_index(index: usize) -> Self { + Self::ALL[index % Self::ALL.len()] + } + + pub fn reset(&mut self) { + *self = Self::default(); + } +} + impl CPUComponent { pub fn update(&mut self, cpus: &Vec) { for cpu in cpus { @@ -36,13 +88,13 @@ impl CPUComponent { impl Component for CPUComponent { fn event(&mut self, key: crossterm::event::KeyEvent) -> anyhow::Result { if key.code == self.config.key_config.move_down { - if self.selection < self.cpus.len() { // this works b/c we prepend ALL to the drawn list - self.selection = self.selection.saturating_add(1); + if self.ui_selection < self.cpus.len() { // this works b/c we prepend ALL to the drawn list + self.ui_selection = self.ui_selection.saturating_add(1); } return Ok(super::EventState::Consumed); } if key.code == self.config.key_config.move_up { - self.selection = self.selection.saturating_sub(1); + self.ui_selection = self.ui_selection.saturating_sub(1); return Ok(super::EventState::Consumed); } @@ -50,83 +102,91 @@ impl Component for CPUComponent { } } -/* -if selection = 0 display all -if selection = 1 display global -if selection = 2 display cpu 0 -... - -*/ - impl DrawableComponent for CPUComponent { - fn draw(&mut self, f: &mut ratatui::Frame, area: ratatui::prelude::Rect, _focused: bool) -> anyhow::Result<()> { + fn draw(&mut self, f: &mut ratatui::Frame, area: ratatui::prelude::Rect, focused: bool) -> anyhow::Result<()> { + // split screen let horizontal_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ - Constraint::Percentage(90), - Constraint::Fill(1), + Constraint::Percentage(90), // chart + Constraint::Fill(1), // list ]).split(area); - let mut all_data: Vec> = Vec::new(); - let mut datasets: Vec = Vec::new(); // holds references to data to be drawn + // containers + let mut all_data: Vec<(u32, Vec<(f64, f64)>)> = Vec::new(); // collect all data ensuring references live long enough to be drawn by `datasets` + let mut datasets: Vec = Vec::new(); // holds references to data to be drawn + + // get max index of a queue (they are all the same) + let perf_q_max_idx = self.cpus + .get(&0) + .map(|q| q.capacity().saturating_sub(1)) + .unwrap_or(0); + + // The UICPUList will look like: + // All ui_selection=0 cpu_selection=None + // Global Usage ui_selection=1 cpu_selection=0 + // CPU 0 ui_selection=2 cpu_selection=1 + // CPU 1 ui_selection=3 cpu_selection=2 + // ... ... + // This means len(UICPUList) = 1 + len(cpus) - if self.selection == 0 { + // set cpu selection + let cpu_selection = if self.ui_selection == 0 { + None + } + else { + Some(self.ui_selection.saturating_sub(1)) + }; + + + // iterate over cpus + if self.ui_selection == 0 { // display all - for (_id, queue) in self.cpus.iter() { - let data: Vec<(f64, f64)> = queue - .iter() - .enumerate() - .map(|(i, item)| (i as f64, item.usage() as f64)) - .collect(); - - all_data.push(data); - } + for (id, queue) in self.cpus + .iter() + { + let data: Vec<(f64, f64)> = queue + .iter() + .rev() + .enumerate() + .map(|(i, item)| ((perf_q_max_idx - i) as f64, item.usage() as f64)) + .collect(); + + all_data.push((*id as u32, data)); + } } else { - // display selection-1 : accounting for All - if let Some(queue) = self.cpus.get(&self.selection.saturating_sub(1)) { + let id = cpu_selection.unwrap(); // unwrap should be safe here + + if let Some(queue) = self.cpus.get(&id) { let data: Vec<(f64, f64)> = queue .iter() + .rev() .enumerate() - .map(|(i, item)| (i as f64, item.usage() as f64)) + .map(|(i, item)| ((perf_q_max_idx - i) as f64, item.usage() as f64)) .collect(); - all_data.push(data); + all_data.push((id as u32, data)); } } - for data in all_data.iter() { + // TODO: colors should match between these two + // datasets can be 1..len(cpus) + // names is always len(cpus) + 1(All) + + + // populate datasets for drawing chart + for (id, data) in all_data.iter() { datasets.push( Dataset::default() - .name(format!("CPU")) .data(data) .graph_type(GraphType::Line) - .marker(symbols::Marker::Braille), + .marker(symbols::Marker::Braille) + .style(Color::from_str(ColorWheel::from_index(*id as usize).as_str())?) ); } - let chart = Chart::new(datasets) - .block(Block::default().borders(Borders::ALL).title("CPU Usage")) - .x_axis( - Axis::default() - .title("Time") - .bounds([0.0, self.config.events_per_min() as f64]) - .labels(vec![Span::raw("0"), Span::raw("now")]), - ) - .y_axis( - Axis::default() - .title("%") - .bounds([0.0, 100.0]) - .labels(vec![ - Span::raw("0"), - Span::raw("50"), - Span::raw("100"), - ]), - ); - - f.render_widget(chart, horizontal_chunks[0]); - - // cpu list + // populate names for UIList to draw cpu list let mut names: Vec = self.cpus .iter() .map(|(key, _queue)| { @@ -136,24 +196,67 @@ impl DrawableComponent for CPUComponent { else { format!("CPU {}", key.saturating_sub(1).to_string()) }; - ListItem::new(title) + ListItem::new(title).style(Color::from_str(ColorWheel::from_index(*key).as_str()).unwrap()) }) .collect(); + // insert All option into UI list names.insert(0, ListItem::new(String::from("All"))); - let mut list_state = ListState::default(); - list_state.select(Some(self.selection)); + // render chart + let chart = Chart::new(datasets) + .block( + { + if !focused { + Block::default().borders(Borders::ALL).title(" CPU % ").style(Color::DarkGray) + } + else { + Block::default().borders(Borders::ALL).title(" CPU % ").style(Color::LightGreen) + } + } + ) + .x_axis( + Axis::default() + .bounds([0.0, self.config.events_per_min().saturating_sub(1) as f64]) + .labels(vec![Span::raw(format!("-{}s",self.config.min_as_s())), Span::raw("now")]) + .labels_alignment(Alignment::Right), + ) + .y_axis( + Axis::default() + .bounds([0.0, 100.0]) + .labels(vec![ + Span::raw("0"), + Span::raw("50"), + Span::raw("100"), + ]) + .labels_alignment(Alignment::Right), + ); + + f.render_widget(chart, horizontal_chunks[0]); + // render cpu list + let mut list_state = ListState::default(); + list_state.select(Some(self.ui_selection)); let cpu_list = List::new(names) .scroll_padding(horizontal_chunks[1].height as usize / 2) - .block(Block::default().title("CPU List").borders(Borders::ALL)) - .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) - ); - - f.render_stateful_widget(cpu_list, horizontal_chunks[1], &mut list_state); + .block( + { + if !focused { + Block::default().borders(Borders::ALL).style(Color::DarkGray) + } + else { + Block::default().borders(Borders::ALL).style(Color::LightGreen) + } + } + ) + .highlight_style( + Style::default() + .bg(Color::LightBlue) + .add_modifier(Modifier::BOLD) + ); + f.render_stateful_widget(cpu_list, horizontal_chunks[1], &mut list_state); Ok(()) } diff --git a/src/components/performance.rs b/src/components/performance.rs index 0498e3f..f87b008 100644 --- a/src/components/performance.rs +++ b/src/components/performance.rs @@ -47,7 +47,7 @@ impl PerformanceComponent { let y_axis_title = String::from("Used Memory (GB)"); let y_bounds = [0.0, self.memory_info.back().unwrap().total_memory_gb() as f64]; - draw_graph(f, area, refresh_rate, self.memory_info.max_size(), data_points, y_axis_title, y_bounds)?; + draw_graph(f, area, refresh_rate, self.memory_info.capacity(), data_points, y_axis_title, y_bounds)?; Ok(()) } @@ -61,7 +61,7 @@ impl PerformanceComponent { let y_axis_title = String::from("Global CPU Usage (%)"); let y_bounds = [0.0, 100.0]; - draw_graph(f, area, refresh_rate, self.cpu_info.max_size(), data_points, y_axis_title, y_bounds)?; + draw_graph(f, area, refresh_rate, self.cpu_info.capacity(), data_points, y_axis_title, y_bounds)?; Ok(()) } diff --git a/src/components/process.rs b/src/components/process.rs index e90025e..e11b4c9 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -186,17 +186,17 @@ fn list_sort(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> R } impl DrawableComponent for ProcessComponent { - fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> Result<()> { + fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { // split for filter let horizontal_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Percentage(70), + Constraint::Fill(1), Constraint::Length(3), ]).split(area); // calculate visible list height - let visible_list_height = area.height.saturating_sub(3) as usize; + let visible_list_height = horizontal_chunks[0].height.saturating_sub(3) as usize; // determine list to display let list = if let Some(filtered_list) = self.filtered_list.as_ref() { @@ -223,21 +223,36 @@ impl DrawableComponent for ProcessComponent { f, horizontal_chunks[0], visible_items, - self.list.is_follow_selection(), - matches!(self.focus, Focus::List), + self.list.is_follow_selection(), + if focused { + matches!(self.focus, Focus::List) + } + else { + false + }, self.config.theme_config.clone() ); self.scroll.draw( f, - horizontal_chunks[0], - false + horizontal_chunks[0], + if focused { + matches!(self.focus, Focus::List) + } + else { + false + }, )?; self.filter.draw( f, - horizontal_chunks[1], - matches!(self.focus, Focus::Filter) + horizontal_chunks[1], + if focused { + matches!(self.focus, Focus::Filter) + } + else { + false + }, )?; Ok(()) diff --git a/src/components/utils/vertical_scroll.rs b/src/components/utils/vertical_scroll.rs index 9304e5b..7e8db2a 100644 --- a/src/components/utils/vertical_scroll.rs +++ b/src/components/utils/vertical_scroll.rs @@ -56,22 +56,30 @@ const fn calc_scroll_top( } impl DrawableComponent for VerticalScroll { - fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> Result<()> { + fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { draw_scrollbar( f, area, self.top.get(), self.count.get(), + focused, ); Ok(()) } } -fn draw_scrollbar(f: &mut Frame, area: Rect, top: usize, count: usize) { +fn draw_scrollbar(f: &mut Frame, area: Rect, top: usize, count: usize, focused: bool) { let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) .begin_symbol(Some("↑")) .end_symbol(Some("↓")) - .style(Color::White); + .style({ + if focused { + Color::LightGreen + } + else { + Color::DarkGray + } + }); let mut scrollbar_state = ScrollbarState::new(count).position(top); f.render_stateful_widget(scrollbar, diff --git a/src/config.rs b/src/config.rs index 3be5b13..d8f62ee 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ pub struct Config { pub key_config: KeyConfig, pub theme_config: ThemeConfig, refresh_rate: u64, + min_as_s: u64, events_per_min: u64, tick_rate: u64, } @@ -16,6 +17,7 @@ impl Default for Config { key_config: KeyConfig::default(), theme_config: ThemeConfig::default(), refresh_rate: 5000, + min_as_s: 60000/ 1000, events_per_min: 60000 / 5000, tick_rate: 250, } @@ -31,6 +33,10 @@ impl Config { self.tick_rate } + pub fn min_as_s(&self) -> u64 { + self.min_as_s + } + pub fn events_per_min(&self) -> u64 { self.events_per_min } @@ -61,6 +67,7 @@ pub struct KeyConfig { pub follow_selection: KeyCode, pub toggle_themes: KeyCode, pub process_info: KeyCode, + pub expand: KeyCode, } impl Default for KeyConfig { @@ -93,6 +100,7 @@ impl Default for KeyConfig { follow_selection: KeyCode::Char('f'), toggle_themes: KeyCode::Char('t'), process_info: KeyCode::Enter, + expand: KeyCode::Char('e'), } } } @@ -123,7 +131,7 @@ impl ThemeConfig { self.item_select = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD); self.item_select_follow = Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED); self.component_out_of_focus = Style::default().fg(Color::DarkGray); - self.component_in_focus = Style::default().fg(Color::White); + self.component_in_focus = Style::default().fg(Color::LightGreen); } fn set_light_theme(&mut self) { @@ -133,7 +141,7 @@ impl ThemeConfig { self.item_select = Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD); self.item_select_follow = Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED); self.component_out_of_focus = Style::default().fg(Color::DarkGray); - self.component_in_focus = Style::default().fg(Color::White); + self.component_in_focus = Style::default().fg(Color::LightGreen); } pub fn toggle_themes(&mut self) { @@ -154,7 +162,7 @@ impl Default for ThemeConfig { item_select: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD), item_select_follow: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED), component_out_of_focus: Style::default().fg(Color::DarkGray), - component_in_focus: Style::default().fg(Color::White), + component_in_focus: Style::default().fg(Color::LightGreen), } } } \ No newline at end of file diff --git a/src/ui/process_list_ui.rs b/src/ui/process_list_ui.rs index ed31e87..47bbe68 100644 --- a/src/ui/process_list_ui.rs +++ b/src/ui/process_list_ui.rs @@ -30,7 +30,7 @@ pub mod process_list_ui { .collect::() .style( if focus { - header_style + in_focus_style } else { out_of_focus_style From e4d340e1749dfbad93ed8575cac82d785f0be324 Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Sat, 19 Apr 2025 16:52:41 -0500 Subject: [PATCH 12/12] adding system ui --- src/app.rs | 77 ++++++++-- src/components/mod.rs | 3 +- src/components/process.rs | 10 +- src/components/system_component.rs | 141 ++++++++++++++++++ .../{system.rs => system_wrapper.rs} | 44 ++++-- src/components/utils/vertical_scroll.rs | 32 ++-- src/ui/process_list_ui.rs | 16 +- 7 files changed, 280 insertions(+), 43 deletions(-) create mode 100644 src/components/system_component.rs rename src/components/{system.rs => system_wrapper.rs} (79%) diff --git a/src/app.rs b/src/app.rs index b989367..95865d7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,12 +6,12 @@ use crate::config::Config; use crate::components::{ tab::TabComponent, process::ProcessComponent, - system::SystemComponent, + system_wrapper::SystemWrapper, + system_component::SystemComponent, error::ErrorComponent, Component, EventState, DrawableComponent, - tab::Tab, help::HelpComponent, command, command::CommandInfo, @@ -19,6 +19,7 @@ use crate::components::{ pub enum MainFocus { CPU, + System, Memory, Network, Temperature, @@ -28,7 +29,8 @@ pub enum MainFocus { pub struct App { focus: MainFocus, expand: bool, - system: SystemComponent, + system_wrapper: SystemWrapper, + system_component: SystemComponent, process: ProcessComponent, cpu: CPUComponent, tab: TabComponent, @@ -43,7 +45,8 @@ impl App { Self { focus: MainFocus::Process, expand: false, - system: SystemComponent::new(config.clone()), + system_wrapper: SystemWrapper::new(config.clone()), + system_component: SystemComponent::default(), process: ProcessComponent::new(config.clone()), cpu: CPUComponent::default(), tab: TabComponent::new(config.clone()), @@ -55,7 +58,8 @@ impl App { // call after constructor pub fn init(&mut self) -> Result<()> { - self.system.refresh_all()?; + self.system_wrapper.refresh_all()?; + self.init_system_component(); self.update_process(); self.update_cpu(); //self.update_performance()?; @@ -64,9 +68,14 @@ impl App { Ok(()) } + fn init_system_component(&mut self) { + let vec: Vec = SystemWrapper::get_static_sysinfo(); + self.system_component.init(vec); // transfer ownership + } + // refresh system and dependencies pub fn refresh(&mut self) -> Result<()>{ - self.system.refresh_all()?; + self.system_wrapper.refresh_all()?; self.update_process(); self.update_cpu(); //self.update_performance()?; @@ -75,14 +84,14 @@ impl App { } fn update_cpu(&mut self) -> bool { - let new_cpus = self.system.get_cpus(); + let new_cpus = self.system_wrapper.get_cpus(); self.cpu.update(&new_cpus); true } // return result of process update fn update_process(&mut self) -> bool { - let new_processes = self.system.get_processes(); + let new_processes = self.system_wrapper.get_processes(); let res = self.process.update(&new_processes); res @@ -179,6 +188,11 @@ impl App { return Ok(EventState::Consumed) } } + MainFocus::System => { + if self.system_component.event(key)?.is_consumed() { + return Ok(EventState::Consumed) + } + } MainFocus::Memory => {} MainFocus::Network => {} MainFocus::Temperature => {} @@ -217,6 +231,9 @@ impl App { self.focus = MainFocus::Process } MainFocus::Process => { + self.focus = MainFocus::System + } + MainFocus::System => { self.focus = MainFocus::CPU } } @@ -241,10 +258,27 @@ impl App { if self.expand { // split screen to draw only focused component if matches!(self.focus, MainFocus::Process) { - self.process.draw(f, chunks[0], true)?; + self.process.draw( + f, + chunks[0], + true, + )?; } + if matches!(self.focus, MainFocus::CPU) { - self.cpu.draw(f, chunks[0], true)?; + self.cpu.draw( + f, + chunks[0], + true, + )?; + } + + if matches!(self.focus, MainFocus::System) { + self.system_component.draw( + f, + chunks[0], + true, + )?; } } else { @@ -265,16 +299,31 @@ impl App { let horizontal_chunk = Layout::default() .direction(Direction::Horizontal) .constraints([ - Constraint::Percentage(60), - Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Percentage(80), ]) .split(*chunk); horizontal_chunks.push(horizontal_chunk); } - self.process.draw(f, horizontal_chunks[3][1], matches!(self.focus, MainFocus::Process))?; - self.cpu.draw(f, vertical_chunks[0], matches!(self.focus, MainFocus::CPU))?; + self.process.draw( + f, + horizontal_chunks[3][1], + matches!(self.focus, MainFocus::Process) + )?; + + self.system_component.draw( + f, + horizontal_chunks[3][0], + matches!(self.focus, MainFocus::System) + )?; + + self.cpu.draw( + f, + vertical_chunks[0], + matches!(self.focus, MainFocus::CPU) + )?; } // if help--help component state determines if anything is drawn diff --git a/src/components/mod.rs b/src/components/mod.rs index 71e8b06..dd90483 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -3,7 +3,7 @@ use crossterm::event::KeyEvent; use ratatui::prelude::*; use process_list::{ListSortOrder, MoveSelection}; use super::config::KeyConfig; -pub mod system; +pub mod system_wrapper; pub mod filter; pub mod help; pub mod error; @@ -14,6 +14,7 @@ pub mod utils; pub mod command; pub mod vertical_tabs; pub mod cpu; +pub mod system_component; pub trait DrawableComponent { fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()>; diff --git a/src/components/process.rs b/src/components/process.rs index e11b4c9..42ba8fc 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -212,12 +212,18 @@ impl DrawableComponent for ProcessComponent { self.scroll.reset() }, |selection| { self.scroll.update( - selection,list.len(), visible_list_height + selection, + list.len(), + visible_list_height, ); }, ); - let visible_items = list.iterate(self.scroll.get_top(), visible_list_height); + let visible_items = list + .iterate( + self.scroll.get_top(), + visible_list_height, + ); draw_process_list( f, diff --git a/src/components/system_component.rs b/src/components/system_component.rs new file mode 100644 index 0000000..8333e4b --- /dev/null +++ b/src/components/system_component.rs @@ -0,0 +1,141 @@ +use anyhow::Ok; +use ratatui::{style::{Color, Modifier, Style}, widgets::{Block, Borders, List, ListItem, ListState}}; + +use crate::config::{self, Config}; + +use super::{Component, DrawableComponent}; + +#[derive(Default)] +pub struct SystemComponentInner { + kernel_version: String, + host_name: String, + cpu_architecture: String, + physical_core_count: String, + os_version: String, + system_uptime: String, +} + +impl SystemComponentInner { + pub const FIELD_NAMES: [&str; 6] = [ + "Kernel Version", + "Host Name", + "CPU Architecture", + "Physical Cores", + "OS Version", + "System Uptime", + ]; + + pub fn as_vec(&self) -> Vec<&str> { // read only + let vec: Vec<&str> = vec![ + &self.kernel_version, + &self.host_name, + &self.cpu_architecture, + &self.physical_core_count, + &self.os_version, + &self.system_uptime, + ]; + + vec + } + + pub fn update( + &mut self, + system_uptime: String, + ) { + self.system_uptime = system_uptime; + } +} + +#[derive(Default)] +pub struct SystemComponent { + system_inner: SystemComponentInner, + config: Config, + selection_inner: Option, +} + +impl SystemComponent { + pub fn init( + &mut self, + vec: Vec, + ) { + let mut iter = vec.into_iter(); + + self.system_inner.kernel_version = iter.next().unwrap(); + self.system_inner.host_name = iter.next().unwrap(); + self.system_inner.cpu_architecture = iter.next().unwrap(); + self.system_inner.physical_core_count = iter.next().unwrap(); + self.system_inner.os_version = iter.next().unwrap(); + self.system_inner.system_uptime = iter.next().unwrap(); + self.selection_inner = Some(0); + } + + pub fn update( + &mut self, + system_uptime: String, + ) { + self.system_inner.system_uptime = system_uptime; + } +} + +impl Component for SystemComponent { + fn event(&mut self, key: crossterm::event::KeyEvent) -> anyhow::Result { + if key.code == self.config.key_config.move_down { + if let Some(s) = self.selection_inner { + if s < SystemComponentInner::FIELD_NAMES.len() - 1 { + self.selection_inner = Some(s.saturating_add(1)); + } + return Ok(super::EventState::Consumed) + } + } + if key.code == self.config.key_config.move_up { + if let Some(s) = self.selection_inner { + self.selection_inner = Some(s.saturating_sub(1)); + return Ok(super::EventState::Consumed) + } + } + Ok(super::EventState::NotConsumed) + } +} + +impl DrawableComponent for SystemComponent { + fn draw(&mut self, + f: &mut ratatui::Frame, + area: ratatui::prelude::Rect, + focused: bool + ) -> anyhow::Result<()> { + let items: Vec = SystemComponentInner::FIELD_NAMES + .iter() + .zip(self.system_inner.as_vec().iter()) + .map(|(label, item)| ListItem::new(format!("{label}: {item}")).style(Color::White)) + .collect(); + + let mut list_state = ListState::default(); + list_state.select(self.selection_inner); + + let list = List::new(items) + .scroll_padding(area.height as usize / 2) + .block( + { + if !focused { + Block::default() + .title(" System Info ") + .borders(Borders::ALL) + .style(Color::DarkGray) + } + else { + Block::default() + .title(" System Info ") + .borders(Borders::ALL) + .style(Color::LightGreen) + } + } + ) + .highlight_style( + Style::default().bg(Color::LightBlue).add_modifier(Modifier::BOLD) + ); + + f.render_stateful_widget(list, area, &mut list_state); + + Ok(()) + } +} diff --git a/src/components/system.rs b/src/components/system_wrapper.rs similarity index 79% rename from src/components/system.rs rename to src/components/system_wrapper.rs index 903b68e..957d253 100644 --- a/src/components/system.rs +++ b/src/components/system_wrapper.rs @@ -1,5 +1,5 @@ use std::vec; - +use sysinfo::*; use anyhow::Result; use crossterm::event::KeyEvent; use sysinfo::{Cpu, Pid, System}; @@ -12,12 +12,12 @@ use process_list::ProcessItemInfo; // See here for refreshing system: https://crates.io/crates/sysinfo#:~:text=use%20sysinfo%3A%3ASystem,(sysinfo%3A%3AMINIMUM_CPU_UPDATE_INTERVAL)%3B%0A%7D // note: sysinfo::MINIMUM_CPU_UPDATE_INTERVAL = 200 ms -pub struct SystemComponent { +pub struct SystemWrapper { system: System, pub _config: Config } -impl SystemComponent { +impl SystemWrapper { pub fn new(config: Config) -> Self { Self { system: System::new_all(), @@ -59,6 +59,38 @@ impl SystemComponent { cpus } + pub fn get_static_sysinfo() -> Vec { + let vec: Vec = vec![ + sysinfo::System::kernel_long_version(), + if let Some(name) = sysinfo::System::host_name() { + name + } + else { + String::from("None") + }, + sysinfo::System::cpu_arch(), + if let Some(count) = sysinfo::System::physical_core_count() { + format!("{count}") + } + else { + String::from("None") + }, + if let Some(version) = sysinfo::System::long_os_version() { + version + } + else { + String::from("None") + }, + format!("{}", sysinfo::System::uptime()) + ]; + + vec + } + + pub fn update_sys_info() -> String { + format!("{}", sysinfo::System::uptime()) + } + pub fn get_global_cpu_info(&self) -> f32 { self.system.global_cpu_usage() } @@ -121,10 +153,4 @@ impl SystemComponent { //pub fn get_network_info(&self) -> NetworkItem { //} -} - -impl Component for SystemComponent { - fn event(&mut self, _key: KeyEvent) -> Result { - Ok(EventState::NotConsumed) - } } \ No newline at end of file diff --git a/src/components/utils/vertical_scroll.rs b/src/components/utils/vertical_scroll.rs index 7e8db2a..68a5bba 100644 --- a/src/components/utils/vertical_scroll.rs +++ b/src/components/utils/vertical_scroll.rs @@ -28,13 +28,19 @@ impl VerticalScroll { self.top.set(0); } - pub fn update(&self, selection: usize, selection_count: usize, visual_height: usize) -> usize { - let new_top = calc_scroll_top(self.get_top(), visual_height, selection); - self.count.set(selection_count); - self.top.set(new_top); - new_top + pub fn update( + &self, + selection: usize, + selection_len: usize, + visual_height: usize) + -> usize { + let new_top = calc_scroll_top(self.get_top(), visual_height, selection); + self.count.set(selection_len); + self.top.set(new_top); + + new_top + } } -} const fn calc_scroll_top( current_top: usize, @@ -44,14 +50,18 @@ const fn calc_scroll_top( if visual_height == 0 { return 0; } - if current_top + visual_height <= selection { - return selection; + + let padding = visual_height / 2; + let min_top = selection.saturating_sub(padding); + + if selection < current_top + padding { + min_top } - else if current_top > selection { - return selection; + else if selection >= current_top + visual_height - padding { + min_top } else { - return current_top; + current_top } } diff --git a/src/ui/process_list_ui.rs b/src/ui/process_list_ui.rs index 47bbe68..f12d0dd 100644 --- a/src/ui/process_list_ui.rs +++ b/src/ui/process_list_ui.rs @@ -24,7 +24,7 @@ pub mod process_list_ui { let in_focus_style = theme_config.component_in_focus; // setting header - let header = ["", "Pid", "Name", "CPU Usage (%)", "Memory Usage (Bytes)"] + let header = ["", "Pid", "Name", "CPU (%)", "Memory (B)", "Runtime (s)", "Status"] .into_iter() .map(Cell::from) .collect::() @@ -66,6 +66,8 @@ pub mod process_list_ui { Cell::from(item.name().to_string()), Cell::from(item.cpu_usage().to_string()), Cell::from(item.memory_usage().to_string()), + Cell::from(item.run_time().to_string()), + Cell::from(item.status()), ]; Row::new(cells).style(style) }) @@ -75,14 +77,16 @@ pub mod process_list_ui { let widths = vec![ Constraint::Length(2), - Constraint::Length(10), - Constraint::Length(50), - Constraint::Length(20), - Constraint::Length(20), + Constraint::Length(10), // pid + Constraint::Length(50), // name + Constraint::Length(20), // cpu usage + Constraint::Length(20), // memory usage + Constraint::Length(20), // run time + Constraint::Length(20), // status ]; // setting block information - let block_title: &str = "Process List"; + let block_title: &str = " Process List "; let block_style = if focus { in_focus_style