From 2c06809095de961300755a5d899921dcd916a932 Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Fri, 11 Jul 2025 13:13:46 -0500 Subject: [PATCH 1/5] working on structure --- src/app.rs | 1 - src/components/cpu.rs | 108 +++---- src/components/mod.rs | 7 +- src/components/process.rs | 45 +-- src/components/sysinfo_wrapper.rs | 6 +- src/components/temp.rs | 2 +- src/components/utils/mod.rs | 13 +- src/components/utils/selection.rs | 78 +++++ src/main.rs | 1 - .../bounded_queue.rs | 0 src/models/{b_queue => bounded_queue}/mod.rs | 0 src/models/mod.rs | 4 +- src/models/p_list/list_items_iter.rs | 51 ---- src/models/p_list/list_iter.rs | 26 -- src/models/p_list/mod.rs | 5 - src/models/p_list/process_list_items.rs | 241 ---------------- src/models/process_list/mod.rs | 37 +++ .../process_item.rs} | 68 ++++- src/models/process_list/process_item_iter.rs | 37 +++ .../{p_list => process_list}/process_list.rs | 267 +++++++++--------- 20 files changed, 441 insertions(+), 556 deletions(-) create mode 100644 src/components/utils/selection.rs rename src/models/{b_queue => bounded_queue}/bounded_queue.rs (100%) rename src/models/{b_queue => bounded_queue}/mod.rs (100%) delete mode 100644 src/models/p_list/list_items_iter.rs delete mode 100644 src/models/p_list/list_iter.rs delete mode 100644 src/models/p_list/mod.rs delete mode 100644 src/models/p_list/process_list_items.rs create mode 100644 src/models/process_list/mod.rs rename src/models/{p_list/process_list_item.rs => process_list/process_item.rs} (68%) create mode 100644 src/models/process_list/process_item_iter.rs rename src/models/{p_list => process_list}/process_list.rs (58%) diff --git a/src/app.rs b/src/app.rs index 9ca01a1..1769739 100644 --- a/src/app.rs +++ b/src/app.rs @@ -164,7 +164,6 @@ impl App { self.error.draw(f, chunks[0], false)?; - if self.expand { if matches!(self.focus, MainFocus::Process) { self.process.draw( diff --git a/src/components/cpu.rs b/src/components/cpu.rs index f226586..c93ea0b 100644 --- a/src/components/cpu.rs +++ b/src/components/cpu.rs @@ -1,10 +1,15 @@ use std::collections::BTreeMap; +use ratatui::Frame; use ratatui::prelude::*; use ratatui::widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, List, ListItem, ListState}; use std::str::FromStr; -use anyhow::Ok; +use anyhow::{Ok, Result}; +use crossterm::event::KeyEvent; +use super::EventState; +use crate::components::common_nav; use crate::components::sysinfo_wrapper::SysInfoWrapper; -use crate::models::b_queue::bounded_queue::BoundedQueue; +use crate::components::utils::selection::SelectionState; +use crate::models::bounded_queue::bounded_queue::BoundedQueue; use crate::models::items::cpu_item::CpuItem; use crate::config::Config; use super::{Component, DrawableComponent}; @@ -62,17 +67,16 @@ impl ColorWheel { } } -#[derive(Default)] pub struct CPUComponent { cpus: BTreeMap>, - ui_selection: usize, + selection_state: SelectionState, + selection_offset: usize, config: Config, } impl CPUComponent { pub fn new(config: Config, sysinfo: &SysInfoWrapper) -> Self { let mut cpus: BTreeMap> = BTreeMap::new(); - let ui_selection: usize = 0; for cpu in sysinfo.get_cpus() { let id = cpu.id(); @@ -85,9 +89,16 @@ impl CPUComponent { perf_q.add_item(cpu); } + let selection_state = if cpus.len() > 0 { SelectionState::new(Some(0)) } else { SelectionState::new(None) }; + // this is the offset between the SelectionState Selection (ui selection) & cpu_selection (backend) + // this offset is present because an option to display ALL cpus is present in the ui list that is + // not present in the CPU list + let selection_offset = 1; + Self { cpus, - ui_selection, + selection_state, + selection_offset, config, } } @@ -108,16 +119,11 @@ 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.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.ui_selection = self.ui_selection.saturating_sub(1); - return Ok(super::EventState::Consumed); + fn event(&mut self, key: KeyEvent) -> Result { + if let Some(dir) = common_nav(key, &self.config.key_config) { + self.selection_state.move_selection(dir, self.cpus.len() + self.selection_offset); + + return Ok(EventState::Consumed) } Ok(super::EventState::NotConsumed) @@ -125,7 +131,8 @@ impl Component for CPUComponent { } impl DrawableComponent for CPUComponent { - fn draw(&mut self, f: &mut ratatui::Frame, area: ratatui::prelude::Rect, focused: bool) -> anyhow::Result<()> { + // draw function has some magic numbers relating to render position: TODO-research a fix/better approach + fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { // split screen let horizontal_chunks = Layout::default() .direction(Direction::Horizontal) @@ -153,42 +160,43 @@ impl DrawableComponent for CPUComponent { // This means len(UICPUList) = 1 + len(cpus) // set cpu selection - let cpu_selection = if self.ui_selection == 0 { - None + let cpu_selection = if let Some(selection) = self.selection_state.selection { + if selection == 0 { None } + else {Some(selection.saturating_sub(self.selection_offset))} + } - else { - Some(self.ui_selection.saturating_sub(1)) - }; - + else { None }; // iterate over cpus - if self.ui_selection == 0 { - // display all - for (id, queue) in self.cpus - .iter() - { + if let Some(selection) = self.selection_state.selection { + if selection == 0 { + // display all + 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 { + 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)| ((perf_q_max_idx - i) as f64, item.usage() as f64)) .collect(); - - all_data.push((*id as u32, data)); - } - } - else { - 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)| ((perf_q_max_idx - i) as f64, item.usage() as f64)) - .collect(); - all_data.push((id as u32, data)); + all_data.push((id as u32, data)); + } } } @@ -231,14 +239,19 @@ impl DrawableComponent for CPUComponent { .block( { if !focused { - Block::default().borders(Borders::ALL).title(" CPU % ").style(self.config.theme_config.style_border_not_focused) + Block::default() + .borders(Borders::ALL) + .title(" CPU % ") + .style(self.config.theme_config.style_border_not_focused) } else { - Block::default().borders(Borders::ALL).title(" CPU % ").style(self.config.theme_config.style_border_focused) + Block::default() + .borders(Borders::ALL) + .title(" CPU % ") + .style(self.config.theme_config.style_border_focused) } } ) - .x_axis( Axis::default() .bounds([0.0, self.config.events_per_min().saturating_sub(1) as f64]) @@ -260,8 +273,7 @@ impl DrawableComponent for CPUComponent { // render cpu list let mut list_state = ListState::default(); - - list_state.select(Some(self.ui_selection)); + list_state.select(self.selection_state.selection); let cpu_list = List::new(names) .scroll_padding(horizontal_chunks[1].height as usize / 2) diff --git a/src/components/mod.rs b/src/components/mod.rs index 601746a..bef2b6f 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,8 +1,8 @@ use anyhow::Result; use crossterm::event::KeyEvent; use ratatui::prelude::*; -use crate::models::p_list::process_list::{ListSortOrder, MoveSelection}; use super::config::KeyConfig; +use crate::components::utils::MoveSelection; pub mod sysinfo_wrapper; pub mod filter; pub mod help; @@ -51,8 +51,9 @@ pub fn common_nav(key: KeyEvent, key_config: &KeyConfig) -> Option Option { -pub fn common_sort(key: KeyEvent, key_config: &KeyConfig) -> Option { if key.code == key_config.sort_cpu_usage_dec { Some(ListSortOrder::CpuUsageDec) } @@ -80,4 +81,4 @@ pub fn common_sort(key: KeyEvent, key_config: &KeyConfig) -> Option bool { self.filtered_list .as_ref() - .and_then(|f| f.selected_pid()) - .or_else(|| self.list.selected_pid()) + .and_then(|f| f.selection_pid()) + .or_else(|| self.list.selection_pid()) .map(|pid| { sysinfo.terminate_process(pid); true @@ -143,7 +146,8 @@ fn list_nav(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> bo } fn list_sort(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> Result { - if let Some(sort) = common_sort(key, key_config) { + // sorting is not common between componenets + if let Some(sort) = map_key_to_process_sort(key, key_config) { list.sort(&sort); Ok(true) @@ -232,36 +236,37 @@ impl DrawableComponent for ProcessComponent { } use ratatui::widgets::{block::*, *}; -use crate::models::p_list::list_iter::ListIterator; +//use crate::models::process_list::list_iter::ListIterator; use crate::config::ThemeConfig; -fn draw_process_list( +fn draw_process_list<'a>( f: &mut Frame, area: Rect, - visible_items: ListIterator<'_>, + visible_items: ProcessItemIterator, follow_selection: bool, focus: bool, theme_config: ThemeConfig, - sort_order: &ListSortOrder, -) { + sort_order: &ProcessItemSortOrder, +) +{ let follow_flag = follow_selection; // setting header let header = ["", - if matches!(sort_order, ListSortOrder::PidInc) { "PID ▲" } - else if matches!(sort_order, ListSortOrder::PidDec) { "PID ▼" } + if matches!(sort_order, ProcessItemSortOrder::PidInc) { "PID ▲" } + else if matches!(sort_order, ProcessItemSortOrder::PidDec) { "PID ▼" } else { "PID" }, - if matches!(sort_order, ListSortOrder::NameInc) { "Name ▲" } - else if matches!(sort_order, ListSortOrder::NameDec) { "Name ▼" } + if matches!(sort_order, ProcessItemSortOrder::NameInc) { "Name ▲" } + else if matches!(sort_order, ProcessItemSortOrder::NameDec) { "Name ▼" } else { "Name" }, - if matches!(sort_order, ListSortOrder::CpuUsageInc) { "CPU (%) ▲" } - else if matches!(sort_order, ListSortOrder::CpuUsageDec) { "CPU (%) ▼" } + if matches!(sort_order, ProcessItemSortOrder::CpuUsageInc) { "CPU (%) ▲" } + else if matches!(sort_order, ProcessItemSortOrder::CpuUsageDec) { "CPU (%) ▼" } else { "CPU (%)" }, - if matches!(sort_order, ListSortOrder::MemoryUsageInc) { "Memory (MB) ▲" } - else if matches!(sort_order, ListSortOrder::MemoryUsageDec) { "Memory (MB) ▼" } + if matches!(sort_order, ProcessItemSortOrder::MemoryUsageInc) { "Memory (MB) ▲" } + else if matches!(sort_order, ProcessItemSortOrder::MemoryUsageDec) { "Memory (MB) ▼" } else { "Memory (MB)" }, "Run (hh:mm:ss)", diff --git a/src/components/sysinfo_wrapper.rs b/src/components/sysinfo_wrapper.rs index 7f49503..ed996c9 100644 --- a/src/components/sysinfo_wrapper.rs +++ b/src/components/sysinfo_wrapper.rs @@ -1,5 +1,5 @@ use sysinfo::{Pid, System, Components}; -use crate::models::p_list::process_list_item::ProcessListItem; +use crate::models::process_list::process_item::ProcessItem; use crate::models::items::{memory_item::MemoryItem, temp_item::TempItem, cpu_item::CpuItem}; use crate::config::Config; @@ -47,7 +47,7 @@ impl SysInfoWrapper { cpus } - pub fn get_processes(&self, processes: &mut Vec) { + pub fn get_processes(&self, processes: &mut Vec) { processes.clear(); for (pid, process) in self.system.processes() { @@ -86,7 +86,7 @@ impl SysInfoWrapper { String::from("Permission Denied") }; - let item = ProcessListItem::new( + let item = ProcessItem::new( pid.as_u32(), name, cpu_usage, diff --git a/src/components/temp.rs b/src/components/temp.rs index ba49e3d..4b356ef 100644 --- a/src/components/temp.rs +++ b/src/components/temp.rs @@ -38,7 +38,7 @@ impl TempComponent { impl Component for TempComponent { fn event(&mut self, key: KeyEvent) -> Result { - let temps_max_idx = self.temps.len() - 1; + let temps_max_idx = self.temps.len().saturating_sub(1); if key.code == self.config.key_config.move_down { if self.ui_selection < temps_max_idx { diff --git a/src/components/utils/mod.rs b/src/components/utils/mod.rs index d838e6b..7d232ca 100644 --- a/src/components/utils/mod.rs +++ b/src/components/utils/mod.rs @@ -1 +1,12 @@ -pub mod vertical_scroll; \ No newline at end of file +pub mod vertical_scroll; +pub mod selection; + +#[derive(Copy, Clone)] +pub enum MoveSelection { + Up, + Down, + MultipleUp, + MultipleDown, + Top, + Bottom, +} \ No newline at end of file diff --git a/src/components/utils/selection.rs b/src/components/utils/selection.rs new file mode 100644 index 0000000..21a9b8a --- /dev/null +++ b/src/components/utils/selection.rs @@ -0,0 +1,78 @@ +use crate::components::MoveSelection; + +pub struct SelectionState { + pub selection: Option, + pub follow_selection: bool, +} + +impl SelectionState { + pub fn new(idx: Option) -> Self { + Self { + selection: idx, + follow_selection: false, + } + } + + pub fn set_selection(&mut self, idx: Option) { + self.selection = idx; + } + + pub fn set_follow(&mut self, follow: bool) { + self.follow_selection = follow; + } + + pub fn move_selection(&mut self, dir: MoveSelection, len: usize) { + if let Some(selection) = self.selection { + let new_idx = match dir { + MoveSelection::Down => self.selection_down(selection, 1, len), + MoveSelection::MultipleDown => self.selection_down(selection, 10, len), + MoveSelection::Up => self.selection_up(selection, 1), + MoveSelection::MultipleUp => self.selection_up(selection, 10), + MoveSelection::Bottom => self.selection_bottom(selection, len), + MoveSelection::Top => self.selection_top(selection), + }; + + self.selection = new_idx; + } + } + + fn selection_down(&self, current_idx: usize, lines: usize, len: usize) -> Option { + let mut new_idx = current_idx; + let max_idx = len.saturating_sub(1); + + 'a: for _ in 0..lines { + if new_idx >= max_idx { + break 'a; + } + new_idx = new_idx.saturating_add(1); + } + + Some(new_idx) + } + + 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_idx == min_idx { + break 'a; + } + new_idx = new_idx.saturating_sub(1); + } + + Some(new_idx) + } + + fn selection_bottom(&self, _current_idx: usize, len: usize) -> Option { + let max_idx = len.saturating_sub(1); + + Some(max_idx) + } + + fn selection_top(&self, _current_idx: usize) -> Option { + let min_idx = 0; + + Some(min_idx) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 6edd703..0084187 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,6 @@ pub mod components; pub mod events; pub mod models; -// If the program's view of the World is incorrect, crash the program, don't hide false beliefs. fn main() -> Result<()> { enable_raw_mode()?; io::stdout().execute(EnterAlternateScreen)?; diff --git a/src/models/b_queue/bounded_queue.rs b/src/models/bounded_queue/bounded_queue.rs similarity index 100% rename from src/models/b_queue/bounded_queue.rs rename to src/models/bounded_queue/bounded_queue.rs diff --git a/src/models/b_queue/mod.rs b/src/models/bounded_queue/mod.rs similarity index 100% rename from src/models/b_queue/mod.rs rename to src/models/bounded_queue/mod.rs diff --git a/src/models/mod.rs b/src/models/mod.rs index b039fa3..f494c30 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,3 @@ -pub mod p_list; +pub mod process_list; pub mod items; -pub mod b_queue; \ No newline at end of file +pub mod bounded_queue; \ No newline at end of file diff --git a/src/models/p_list/list_items_iter.rs b/src/models/p_list/list_items_iter.rs deleted file mode 100644 index a236f3b..0000000 --- a/src/models/p_list/list_items_iter.rs +++ /dev/null @@ -1,51 +0,0 @@ -use super::process_list_item::ProcessListItem; -use super::process_list_items::ProcessListItems; - -pub struct ListItemsIterator<'a> { - list: &'a ProcessListItems, - index: usize, - increments: Option, - max_amount: usize, -} - -impl <'a> ListItemsIterator<'a> { - pub const fn new(list: &'a ProcessListItems, start: usize, max_amount: usize) -> Self { - Self { - list, - index: start, - increments: None, - max_amount, - } - } -} - -impl<'a> Iterator for ListItemsIterator<'a> { - type Item = (usize, &'a ProcessListItem); - - // required function for Iterator - fn next(&mut self) -> Option { - if self.increments.unwrap_or_default() < self.max_amount { - let items = &self.list.list_items; - let init = self.increments.is_none(); - - if let Some(i) = self.increments.as_mut() { - *i += 1; - } - else { - self.increments = Some(0); - }; - - if !init { - self.index += 1; - } - - if self.index >= self.list.list_items.len() { - return None - } - - return Some((self.index, &items[self.index])); - } - - None - } -} \ No newline at end of file diff --git a/src/models/p_list/list_iter.rs b/src/models/p_list/list_iter.rs deleted file mode 100644 index bca7cef..0000000 --- a/src/models/p_list/list_iter.rs +++ /dev/null @@ -1,26 +0,0 @@ -use super::list_items_iter::ListItemsIterator; -use super::process_list_item::ProcessListItem; - -pub struct ListIterator<'a> { - item_iter: ListItemsIterator<'a>, - selection: Option, -} - -impl<'a> ListIterator<'a> { - pub const fn new(item_iter: ListItemsIterator<'a>, selection: Option) -> Self { - Self { - item_iter, - selection, - } - } -} - -impl<'a> Iterator for ListIterator<'a> { - type Item = (&'a ProcessListItem, bool); - - fn next(&mut self) -> Option { - self.item_iter - .next() - .map(|(index, item)| (item, self.selection.map(|i| i == index).unwrap_or_default())) - } -} \ No newline at end of file diff --git a/src/models/p_list/mod.rs b/src/models/p_list/mod.rs deleted file mode 100644 index b33826a..0000000 --- a/src/models/p_list/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod process_list; -pub mod process_list_items; -pub mod process_list_item; -pub mod list_items_iter; -pub mod list_iter; \ No newline at end of file diff --git a/src/models/p_list/process_list_items.rs b/src/models/p_list/process_list_items.rs deleted file mode 100644 index 18aa3bd..0000000 --- a/src/models/p_list/process_list_items.rs +++ /dev/null @@ -1,241 +0,0 @@ -use crate::components::sysinfo_wrapper::SysInfoWrapper; -use super::process_list::ListSortOrder; -use super::process_list_item::ProcessListItem; -use super::list_items_iter::ListItemsIterator; - -#[derive(Default, Clone)] -pub struct ProcessListItems { - pub list_items: Vec, -} - -impl ProcessListItems { - pub fn new(sysinfo: &SysInfoWrapper) -> Self { - let mut processes = Vec::new(); - - sysinfo.get_processes(&mut processes); - - Self { - list_items: processes, - } - } - - pub fn filter(&self, filter_text: &str) -> Self { - let list_items = self.list_items - .iter() - .filter(|item| { - item.name().contains(filter_text) || - item.pid().to_string().contains(filter_text) - }) - .cloned() - .collect(); - - Self { - list_items - } - } - - pub fn update(&mut self, sysinfo: &SysInfoWrapper, filter_text: &str) { - sysinfo.get_processes(&mut self.list_items); - - if !filter_text.is_empty() { - self.list_items.retain(|item| { - item.name().contains(filter_text) || - item.pid().to_string().contains(filter_text) - }); - } - } - - pub fn sort_items(&mut self, sort: &ListSortOrder) { - match sort { - ListSortOrder::PidInc => { - self.list_items.sort_by_key(|a| a.pid()); - } - ListSortOrder::PidDec => { - self.list_items.sort_by_key(|b| std::cmp::Reverse (b.pid())); - } - ListSortOrder::NameInc => { - self.list_items.sort_by_key(|a| a.name().to_string()); - } - ListSortOrder::NameDec => { - self.list_items.sort_by_key(|b| std::cmp::Reverse (b.name().to_string())); - } - ListSortOrder::CpuUsageInc => { - self.list_items.sort_by(|a, b| a.cpu_usage().partial_cmp(&b.cpu_usage()).unwrap_or(std::cmp::Ordering::Equal)); - } - ListSortOrder::CpuUsageDec => { - self.list_items.sort_by(|a, b| b.cpu_usage().partial_cmp(&a.cpu_usage()).unwrap_or(std::cmp::Ordering::Equal)); - } - ListSortOrder::MemoryUsageInc => { - self.list_items.sort_by_key(|a| a.memory_usage()); - } - ListSortOrder::MemoryUsageDec => { - self.list_items.sort_by_key(|b| std::cmp::Reverse (b.memory_usage())); - } - } - } - - pub fn get_item(&self, idx: usize) -> Option<&ProcessListItem> { - self.list_items.get(idx) - } - - pub fn get_idx(&self, pid: u32) -> Option { - if let Some(idx) = self.list_items - .iter() - .position(|item| item.pid() == pid) - { - return Some(idx); - } - None - } - - pub fn len(&self) -> usize { - self.list_items.len() - } - - pub const fn iterate(&self, start: usize, max_amount: usize) -> ListItemsIterator<'_> { - ListItemsIterator::new(self, start, max_amount) - } -} - - -/* -// TODO: come up with new unit testing strategy -#[cfg(test)] -mod test { - use std::vec; - use crate::components::sysinfo_wrapper::{self, SysInfoWrapper}; - use crate::config::{self, Config}; - use crate::models::process_list::ListSortOrder; - use crate::models::process_list_item::ProcessListItem; - use crate::models::process_list_items::ProcessListItems; - - #[test] - fn test_default() { - let instance = ProcessListItems::default(); - assert_eq!(instance.len(), 0); - assert_eq!(instance.get_idx(4), None); - assert_eq!(instance.get_item(0), None); - } - - #[test] - fn test_new() { - let config = Config::default(); - let sysinfo_wrapper = SysInfoWrapper::new(config); - sysinfo_wrapper.refresh_all(); - - /*let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); - let clone_0 = item_0.clone(); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test"), String::from("test")); - let clone_1 = item_1.clone(); - let items = vec![item_0, item_1]; - let instance = ProcessListItems::new(items);*/ - - let pl_instance = ProcessListItems::new(&sysinfo_wrapper); - - assert_eq!(pl_instance.len(), 2); - assert_eq!(pl_instance.get_idx(1), Some(0)); - assert_eq!(pl_instance.get_idx(2), Some(1)); - assert_eq!(pl_instance.get_idx(3), None); - - assert_eq!(pl_instance.get_item(0), Some(&clone_0)); - assert_eq!(pl_instance.get_item(1), Some(&clone_1)); - assert_eq!(pl_instance.get_item(2), None); - } - - #[test] - fn test_filter() { - let config = Config::new - let system_wrapper = SysInfoWrapper::new(config) - - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); - let clone_0 = item_0.clone(); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test"), String::from("test")); - let _clone_1 = item_1.clone(); - let items = vec![item_0, item_1]; - let instance = ProcessListItems::new(items); - - let filtered_instance = instance.filter(&String::from("a")); - assert_eq!(filtered_instance.len(), 1); - assert_eq!(filtered_instance.get_item(0), Some(&clone_0)); - assert_eq!(filtered_instance.get_item(1), None); - assert_eq!(filtered_instance.get_idx(1), Some(0)); - assert_eq!(filtered_instance.get_idx(2), None); - } - - #[test] - fn test_update_items() { - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test"), 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, 0, 10, 10, String::from("test"), String::from("test")); - let item_3 = ProcessListItem::new(3, String::from("c"), 3.0, 3, 0, 10, 10, String::from("test"), String::from("test")); - let new_items = vec![item_2, item_3]; - - 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(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. - assert_eq!(instance.get_idx(3), Some(0)); - // Pid 1 cpu usage is updated to 7.0 so it should be last in the instance list. - assert_eq!(instance.get_idx(1), Some(1)); - } - - #[test] - fn test_sort_items() { - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test"), String::from("test")); - let item_3 = ProcessListItem::new(3, String::from("c"), 3.0, 3, 0, 10, 10, String::from("test"), String::from("test")); - let items = vec![item_0, item_1, item_3]; - let mut instance = ProcessListItems::new(items); - - assert_eq!(instance.get_idx(1), Some(0)); - assert_eq!(instance.get_idx(2), Some(1)); - assert_eq!(instance.get_idx(3), Some(2)); - let _ = instance.sort_items(&ListSortOrder::CpuUsageInc); - assert_eq!(instance.get_idx(1), Some(0)); - assert_eq!(instance.get_idx(2), Some(1)); - assert_eq!(instance.get_idx(3), Some(2)); - - let _ = instance.sort_items(&ListSortOrder::CpuUsageDec); - assert_eq!(instance.get_idx(1), Some(2)); - assert_eq!(instance.get_idx(2), Some(1)); - assert_eq!(instance.get_idx(3), Some(0)); - - let _ = instance.sort_items(&ListSortOrder::NameInc); - assert_eq!(instance.get_idx(1), Some(0)); - assert_eq!(instance.get_idx(2), Some(1)); - assert_eq!(instance.get_idx(3), Some(2)); - - let _ = instance.sort_items(&ListSortOrder::NameDec); - assert_eq!(instance.get_idx(1), Some(2)); - assert_eq!(instance.get_idx(2), Some(1)); - assert_eq!(instance.get_idx(3), Some(0)); - - let _ = instance.sort_items(&ListSortOrder::PidInc); - assert_eq!(instance.get_idx(1), Some(0)); - assert_eq!(instance.get_idx(2), Some(1)); - assert_eq!(instance.get_idx(3), Some(2)); - - let _ = instance.sort_items(&ListSortOrder::PidDec); - assert_eq!(instance.get_idx(1), Some(2)); - assert_eq!(instance.get_idx(2), Some(1)); - assert_eq!(instance.get_idx(3), Some(0)); - - let _ = instance.sort_items(&ListSortOrder::MemoryUsageInc); - assert_eq!(instance.get_idx(1), Some(0)); - assert_eq!(instance.get_idx(2), Some(1)); - assert_eq!(instance.get_idx(3), Some(2)); - - let _ = instance.sort_items(&ListSortOrder::MemoryUsageDec); - assert_eq!(instance.get_idx(1), Some(2)); - assert_eq!(instance.get_idx(2), Some(1)); - assert_eq!(instance.get_idx(3), Some(0)); - } -}*/ \ No newline at end of file diff --git a/src/models/process_list/mod.rs b/src/models/process_list/mod.rs new file mode 100644 index 0000000..f59234f --- /dev/null +++ b/src/models/process_list/mod.rs @@ -0,0 +1,37 @@ +pub mod process_list; +pub mod process_item; +pub mod process_item_iter; + +// possible set of values (known at compile time) +// Variants correspond to static comparator functions +// found at ../items/process_list_item.rs +#[derive(PartialEq, Clone)] +pub enum ProcessItemSortOrder { + PidInc, + PidDec, + NameInc, + NameDec, + CpuUsageInc, + CpuUsageDec, + MemoryUsageInc, + MemoryUsageDec, +} + +// This function maps user's dynamic key press to static sorting strategy. +// Supported mappings are explicitly stated in function match statement. +// Returns Some(ProcessItemSortOrder::Variant) or None. +use crossterm::event::KeyEvent; +use crate::config::KeyConfig; +pub fn map_key_to_process_sort(key: KeyEvent, key_config: &KeyConfig) -> Option { + match key.code { + code if code == key_config.sort_cpu_usage_dec => Some(ProcessItemSortOrder::CpuUsageDec), + code if code == key_config.sort_cpu_usage_inc => Some(ProcessItemSortOrder::CpuUsageInc), + code if code == key_config.sort_memory_usage_dec => Some(ProcessItemSortOrder::MemoryUsageDec), + code if code == key_config.sort_memory_usage_inc => Some(ProcessItemSortOrder::MemoryUsageInc), + code if code == key_config.sort_pid_dec => Some(ProcessItemSortOrder::PidDec), + code if code == key_config.sort_pid_inc => Some(ProcessItemSortOrder::PidInc), + code if code == key_config.sort_name_dec => Some(ProcessItemSortOrder::NameDec), + code if code == key_config.sort_name_inc => Some(ProcessItemSortOrder::NameInc), + _ => None, + } +} \ No newline at end of file diff --git a/src/models/p_list/process_list_item.rs b/src/models/process_list/process_item.rs similarity index 68% rename from src/models/p_list/process_list_item.rs rename to src/models/process_list/process_item.rs index 00ae816..cb72dd1 100644 --- a/src/models/p_list/process_list_item.rs +++ b/src/models/process_list/process_item.rs @@ -1,5 +1,5 @@ -#[derive(Default, Clone, Debug)] -pub struct ProcessListItem { +#[derive(Default, Clone)] +pub struct ProcessItem { pid: u32, name: String, cpu_usage: f32, @@ -11,7 +11,7 @@ pub struct ProcessListItem { path: String, } -impl ProcessListItem { +impl ProcessItem { pub fn new( pid: u32, name: String, @@ -36,12 +36,7 @@ impl ProcessListItem { } } - // 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) - //} - + // GETTERS pub fn pid(&self) -> u32 { self.pid } @@ -87,10 +82,53 @@ impl ProcessListItem { pub fn path(&self) -> &str { &self.path } + + // STATIC COMPARATORS + pub fn cmp_pid_inc(a: &Self, b: &Self) -> std::cmp::Ordering { + a.pid.cmp(&b.pid) + } + + pub fn cmp_pid_dec(a: &Self, b: &Self) -> std::cmp::Ordering { + b.pid.cmp(&a.pid) + } + + pub fn cmp_name_inc(a: &Self, b: &Self) -> std::cmp::Ordering { + a.name.cmp(&b.name) + } + + pub fn cmp_name_dec(a: &Self, b: &Self) -> std::cmp::Ordering { + b.name.cmp(&a.name) + } + + pub fn cmp_cpu_inc(a: &Self, b: &Self) -> std::cmp::Ordering { + // ordering cannot always be determined with f32 + // in case where f32 comparison returns None this + // function will return Equal + a.cpu_usage + .partial_cmp(&b.cpu_usage) + .unwrap_or(std::cmp::Ordering::Equal) + } + + pub fn cmp_cpu_dec(a: &Self, b: &Self) -> std::cmp::Ordering { + // ordering cannot always be determined with f32 + // in case where f32 comparison returns None this + // function will return Equal + b.cpu_usage + .partial_cmp(&a.cpu_usage) + .unwrap_or(std::cmp::Ordering::Equal) + } + + pub fn cmp_mem_inc(a: &Self, b: &Self) -> std::cmp::Ordering { + a.memory_usage.cmp(&b.memory_usage) + } + + pub fn cmp_mem_dec(a: &Self, b: &Self) -> std::cmp::Ordering { + b.memory_usage.cmp(&a.memory_usage) + } } // PartialEq is needed for comparison, e.g., calling contains -impl PartialEq for ProcessListItem { +impl PartialEq for ProcessItem { fn eq(&self, other: &Self) -> bool { self.pid.eq(&other.pid) } @@ -98,11 +136,11 @@ impl PartialEq for ProcessListItem { #[cfg(test)] pub mod test { - use super::ProcessListItem; + use super::ProcessItem; #[test] fn test_constructors() { - let instance = ProcessListItem::default(); + let instance = ProcessItem::default(); assert_eq!(instance.pid, 0); assert!(String::is_empty(&instance.name)); assert_eq!(instance.cpu_usage, 0.0); @@ -112,7 +150,7 @@ pub mod test { assert_eq!(instance.accumulated_cpu_time, 0); assert!(String::is_empty(&instance.status)); - let instance = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); + let instance = ProcessItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); assert_eq!(instance.pid, 1); assert_eq!(instance.name, String::from("a")); assert_eq!(instance.cpu_usage, 1.0); @@ -126,8 +164,8 @@ pub mod test { #[test] fn test_instance_functions() { - let instance_0 = ProcessListItem::default(); - let instance_1 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); + let instance_0 = ProcessItem::default(); + let instance_1 = ProcessItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); assert_eq!(instance_0.pid(), instance_0.pid); assert_eq!(instance_0.name(), instance_0.name); diff --git a/src/models/process_list/process_item_iter.rs b/src/models/process_list/process_item_iter.rs new file mode 100644 index 0000000..33cf907 --- /dev/null +++ b/src/models/process_list/process_item_iter.rs @@ -0,0 +1,37 @@ +use super::process_item::ProcessItem; + +pub struct ProcessItemIterator<'a> { + list: &'a Vec, + selection: Option, + start_idx: usize, + end_idx: usize, +} + +impl<'a> ProcessItemIterator<'a> { + pub fn new(list: &'a Vec, selection: Option, start_idx: usize, max_iter: usize) -> Self { + let end_idx = usize::min(start_idx + max_iter, list.len()); + Self { + list, + selection, + start_idx, + end_idx, + } + } +} + +impl<'a> Iterator for ProcessItemIterator<'a> { + type Item = (&'a ProcessItem, bool); + + fn next(&mut self) -> Option { + if self.start_idx >= self.end_idx { + return None; + } + + let item = &self.list[self.start_idx]; + let is_selected = Some(self.start_idx) == self.selection; + + self.start_idx += 1; + + Some((item, is_selected)) + } +} \ No newline at end of file diff --git a/src/models/p_list/process_list.rs b/src/models/process_list/process_list.rs similarity index 58% rename from src/models/p_list/process_list.rs rename to src/models/process_list/process_list.rs index 77c5c82..eeb944e 100644 --- a/src/models/p_list/process_list.rs +++ b/src/models/process_list/process_list.rs @@ -1,198 +1,190 @@ -use super::process_list_items::ProcessListItems; -use super::process_list_item::ProcessListItem; -use super::list_iter::ListIterator; +use super::process_item::ProcessItem; +use super::ProcessItemSortOrder; +use crate::components::utils::selection::SelectionState; +use crate::components::utils::MoveSelection; use crate::components::sysinfo_wrapper::SysInfoWrapper; +use crate::models::process_list::process_item_iter::ProcessItemIterator; -#[derive(PartialEq, Clone, Default)] -pub enum ListSortOrder { - PidInc, - PidDec, - NameInc, - NameDec, - CpuUsageInc, - #[default] CpuUsageDec, - MemoryUsageInc, - MemoryUsageDec, -} - -#[derive(Copy, Clone)] -pub enum MoveSelection { - Up, - Down, - MultipleUp, - MultipleDown, - Top, - Bottom, -} - -#[derive(Default)] +// A ProcessList can be constructed as an "unfiltered list" with the new(&SysInfoWrapper) constructor +// or as a "filtered list" with filter(&str) constructor. +// Additionally, the filter(&str) constructor uses an existing instance of a ProcessList, +// thus the only constructor that interacts with the sysinfo backend is new(&SysInfoWrapper). +// If filter = None, then the List is Unfiltered, if Some() then the List if Filtered. pub struct ProcessList { - items: ProcessListItems, - sort: ListSortOrder, - follow_selection: bool, - pub selection: Option, + processes: Vec, + sort: ProcessItemSortOrder, + selection_state: SelectionState, + filter: Option, } impl ProcessList { + // constructor pub fn new(sysinfo: &SysInfoWrapper) -> Self { + let mut processes: Vec = Vec::new(); + // sysinfo.get_processes(&mut vec) populates argument Vec with system processes. See /components/sysinfo_wrapper.rs for implementation details. + sysinfo.get_processes(&mut processes); + + // setting defaults explicitly + let sort: ProcessItemSortOrder = ProcessItemSortOrder::CpuUsageDec; + let selection_state: SelectionState = if processes.len() > 0 { SelectionState::new(Some(0)) } else { SelectionState::new(None) }; + let filter: Option = None; + Self { - items: ProcessListItems::new(sysinfo), - sort: ListSortOrder::default(), - follow_selection: false, - selection: Some(0), + processes, + sort, + selection_state, + filter, } } + // filter constructor pub fn filter(&self, filter_text: &str) -> Self { - let items = self.items.filter(filter_text); - let len = items.len(); + // filtering by process name--case insensitive + let processes: Vec = self.processes + .iter() + .filter(|item| { + item.name().to_lowercase().contains(&filter_text.to_lowercase()) + }) + .cloned() + .collect(); + + // setting defaults explicitly + let sort: ProcessItemSortOrder = ProcessItemSortOrder::CpuUsageDec; + let selection_state: SelectionState = if processes.len() > 0 { SelectionState::new(Some(0)) } else { SelectionState::new(None) }; + let filter: Option = Some(String::from(filter_text)); Self { - items, - sort: ListSortOrder::default(), - follow_selection: false, - selection: if len > 0 { - Some(0) - } - else { - None - }, + processes, + sort, + selection_state, + filter, } } - pub fn update(&mut self, sysinfo: &SysInfoWrapper, filter_text: &str) { - let selected_item: Option<&ProcessListItem> = self.items.get_item(self.selection.unwrap_or_default()); - let pid: Option = selected_item.map(|item| item.pid()); + pub fn update(&mut self, sysinfo: &SysInfoWrapper) { + // storing reference to selected item and deep copy of it's PID before updating processes + let selection_item: Option<&ProcessItem> = self.processes.get(self.selection_state.selection.unwrap_or_default()); + let selection_pid: Option = selection_item.map(|item| item.pid()); - self.items.update(sysinfo, filter_text); - - self.items.sort_items(&self.sort); + // get new processes + sysinfo.get_processes(&mut self.processes); + // filter if this is a "filtered list" + if let Some(filter) = &self.filter { + self.processes.retain(|item| { + item.name().to_lowercase().contains(&filter.to_lowercase()) + }); + } - if self.items.len() == 0 { - self.selection = None; + // return if update resulted in no processes + if self.processes.len() == 0 { + self.selection_state.set_selection(None); + self.selection_state.set_follow(false); return } - if self.follow_selection { - self.selection = pid.and_then(|p| self.items.get_idx(p)); + // sort order is lost when getting new processes + self.sort(&self.sort.clone()); + + // set selection after update + let selection = if self.selection_state.follow_selection { + selection_pid.and_then(|p| { + self.processes + .iter() + .position(|item| item.pid() == p) + }) } else { - if let Some(selection) = self.selection { - let max_idx = self.items.len().saturating_sub(1); + if let Some(selection) = self.selection_state.selection { + // check upper bound (lowerbound is effectively checked when checking for NO processes) + let max_idx = self.processes.len().saturating_sub(1); if selection > max_idx { - self.selection = Some(max_idx) + Some(max_idx) + } + else { + Some(selection) } } - } + else { + None + } + }; - if self.selection.is_none() { - self.selection = Some(0) - } + self.selection_state.set_selection(selection); } - pub fn sort(&mut self, sort: &ListSortOrder) { - let selected_item: Option<&ProcessListItem> = self.items.get_item(self.selection.unwrap_or_default()); - - let pid: Option = selected_item.map(|item| item.pid()); - - self.items.sort_items(sort); + pub fn sort(&mut self, sort: &ProcessItemSortOrder) { + let selection_item: Option<&ProcessItem> = self.processes.get(self.selection_state.selection.unwrap_or_default()); + let selection_pid: Option = selection_item.map(|item| item.pid()); + + // mapping variants to corresponding static comparator functions (see /process_item.rs) + match sort { + ProcessItemSortOrder::PidInc => self.processes.sort_by(ProcessItem::cmp_pid_inc), + ProcessItemSortOrder::PidDec => self.processes.sort_by(ProcessItem::cmp_pid_dec), + ProcessItemSortOrder::NameInc => self.processes.sort_by(ProcessItem::cmp_name_inc), + ProcessItemSortOrder::NameDec => self.processes.sort_by(ProcessItem::cmp_name_dec), + ProcessItemSortOrder::CpuUsageInc => self.processes.sort_by(ProcessItem::cmp_cpu_inc), + ProcessItemSortOrder::CpuUsageDec => self.processes.sort_by(ProcessItem::cmp_cpu_dec), + ProcessItemSortOrder::MemoryUsageInc => self.processes.sort_by(ProcessItem::cmp_mem_inc), + ProcessItemSortOrder::MemoryUsageDec => self.processes.sort_by(ProcessItem::cmp_mem_dec), + } + // assign field to new sort variant self.sort = sort.clone(); - if self.follow_selection { - self.selection = pid.and_then(|p| self.items.get_idx(p)); + // update selection if following + if self.selection_state.follow_selection { + self.selection_state.selection = selection_pid.and_then(|p| { + self.processes + .iter() + .position(|item| item.pid() == p) + }); } } 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::Bottom => self.selection_bottom(selection), - MoveSelection::Top => self.selection_top(selection), - }; - - self.selection = new_idx; - } + self.selection_state.move_selection(dir, self.processes.len()); } - fn selection_down(&self, current_idx: usize, lines: usize) -> Option { - let mut new_idx = current_idx; - let max_idx = self.items.len().saturating_sub(1); - - 'a: for _ in 0..lines { - if new_idx >= max_idx { - break 'a; - } - new_idx = new_idx.saturating_add(1); + pub fn toggle_follow_selection(&mut self) { + if self.processes.len() == 0 { + // nothing to follow: do not toggle, enforce false + self.selection_state.set_follow(false); } - - Some(new_idx) - } - - 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_idx == min_idx { - break 'a; - } - new_idx = new_idx.saturating_sub(1); + else { + self.selection_state.set_follow(!self.selection_state.follow_selection); } - - Some(new_idx) - } - - fn selection_bottom(&self, _current_idx: usize) -> Option { - let max_idx = self.items.len().saturating_sub(1); - - Some(max_idx) - } - fn selection_top(&self, _current_idx: usize) -> Option { - let min_idx = 0; - - Some(min_idx) - } - - pub fn toggle_follow_selection(&mut self) { - self.follow_selection = !self.follow_selection - } - + // GETTERS pub fn is_follow_selection(&self) -> bool { - self.follow_selection + self.selection_state.follow_selection } pub fn selection(&self) -> Option { - self.selection + self.selection_state.selection } pub fn is_empty(&self) -> bool { - self.items.len() == 0 + self.processes.len() == 0 } pub fn len(&self) -> usize { - self.items.len() + self.processes.len() } - pub fn selected_item(&self) -> Option<&ProcessListItem> { - if let Some(selection) = self.selection { - let selected_item = self.items.get_item(selection); - return selected_item + pub fn selection_item(&self) -> Option<&ProcessItem> { + if let Some(selection) = self.selection_state.selection { + let selection_item = self.processes.get(selection); + return selection_item } None } - pub fn selected_pid(&self) -> Option { - if let Some(selection) = self.selection { - if let Some(item) = self.items.get_item(selection) { + pub fn selection_pid(&self) -> Option { + if let Some(selection) = self.selection_state.selection { + if let Some(item) = self.processes.get(selection) { return Some(item.pid()) } else { @@ -203,13 +195,12 @@ impl ProcessList { None } - pub fn sort_order(&self) -> &ListSortOrder { + pub fn sort_order(&self) -> &ProcessItemSortOrder { &self.sort } - 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) + pub fn iterate(&self, start_idx: usize, max_amount: usize) -> ProcessItemIterator { + ProcessItemIterator::new(&self.processes, self.selection_state.selection, start_idx, max_amount) } } From 18a2bcd3a04ac38a13db0a5c4fcdc4b11e896d6c Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Fri, 11 Jul 2025 16:07:31 -0500 Subject: [PATCH 2/5] working on generic list state --- src/main.rs | 1 + src/state/list_state.rs | 237 ++++++++++++++++++++++++++++++++ src/state/mod.rs | 3 + src/state/process_sort_order.rs | 28 ++++ src/state/selection_state.rs | 21 +++ 5 files changed, 290 insertions(+) create mode 100644 src/state/list_state.rs create mode 100644 src/state/mod.rs create mode 100644 src/state/process_sort_order.rs create mode 100644 src/state/selection_state.rs diff --git a/src/main.rs b/src/main.rs index 0084187..ebd5f94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ pub mod config; pub mod components; pub mod events; pub mod models; +pub mod state; fn main() -> Result<()> { enable_raw_mode()?; diff --git a/src/state/list_state.rs b/src/state/list_state.rs new file mode 100644 index 0000000..25468f5 --- /dev/null +++ b/src/state/list_state.rs @@ -0,0 +1,237 @@ +// Wrapper around Vec that provides selection state +// Data here is unstructured: To get structured (sorted) +// data use iterators + +// In cases where the ListState is dynamically updated +// set +use crate::components::utils::selection::SelectionState; + +pub struct ListState { + items: T, + selection_state: SelectionState, +} + +impl ListState> { + // CONSTRUCTORS::BEGIN + pub fn new(items: Vec) -> Self { + let selection_state = if items.is_empty() { SelectionState::new(None) } else { SelectionState::new(Some(0)) }; + + Self { + items, + selection_state, + } + } + + pub fn filter(&self, mut predicate: F) -> Self + where + T: Clone, + F: FnMut(&T) -> bool + { + let filtered_items: Vec = self.items + .iter() + .filter(|x| predicate(x)) + .cloned() + .collect(); + + let selection_state = if filtered_items.is_empty() { + SelectionState::new(None) + } + else { + SelectionState::new(Some(0)) + }; + + Self { + items: filtered_items, + selection_state, + } + } + // CONSTRUCTORS::END + + // MUTATORS::BEGIN + pub fn replace(&mut self, new_items: Vec) { + if new_items.is_empty() { + self.set_selection(None); + } + else { + self.set_selection(Some(0)); + } + + self.items = new_items; + } + + pub fn set_selection(&mut self, idx: Option) { + let len = self.items.len(); + + // items empty case + if len == 0 { + self.selection_state.set_selection(None); + return; + } + + if let Some(idx) = idx { + // index within length: set selection to index + if idx < len { + self.selection_state.set_selection(Some(idx)); + } + else { + // index greater than or equal to length: clamp selection to max_index + let max_idx = len.saturating_sub(1); + self.selection_state.set_selection(Some(max_idx)); + } + } + else { + self.selection_state.set_selection(None); + } + } + + pub fn select_next(&mut self) { + let len = self.items.len(); + + if let Some(idx) = self.selection_state.selection { + if idx.saturating_add(1) < len { + self.selection_state.set_selection(Some(idx.saturating_add(1))); + } + } + } + + pub fn select_prev(&mut self) { + if let Some(idx) = self.selection_state.selection { + if idx > 0 { + self.selection_state.set_selection(Some(idx.saturating_sub(1))); + } + } + } + + pub fn select_first(&mut self) { + if !self.items.is_empty() { + self.selection_state.set_selection(Some(0)); + } + } + + pub fn select_last(&mut self) { + if !self.items.is_empty() { + self.selection_state.set_selection(Some(self.items.len().saturating_sub(1))); + } + } + + pub fn toggle_follow_selection(&mut self) { + if !self.items.is_empty() { + // toggle + self.selection_state.set_follow(!self.selection_state.follow_selection); + } + else { + // no items: must be false + self.selection_state.set_follow(false); + } + } + // MUTATORS::END + + // GETTERS::BEGIN + pub fn selected(&self) -> Option<&T> { + self.selection_state.selection.and_then(|i| self.items.get(i)) + } + + pub fn len(&self) -> usize { + self.items.len() + } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + pub fn items(&self) -> &[T] { + &self.items + } + // GETTERS::END + + // ITERATORS + pub fn iter_sorted<'a, F>(&'a self, mut compare: F) -> impl Iterator + 'a + where F: FnMut(&T, &T) -> std::cmp::Ordering, + { + // generate indices 0..n + let mut indices: Vec = (0..self.items.len()).collect(); + + // sort indices based on comparing the underlying items + indices.sort_by(|&i, &j| compare(&self.items[i], &self.items[j])); + + // return an iterator of &T in sorted order + indices.into_iter().map(move |i| &self.items[i]) + } +} + +impl ListState> { + pub fn replace_with_follow(&mut self, new_items: Vec) + where T: Clone + { + let old_selection = self.selection_state.selection.and_then(|idx| self.items.get(idx)).cloned(); + self.items = new_items; + + if self.selection_state.follow_selection { + if let Some(selected_item) = old_selection { + self.selection_state.set_selection({ + self.items + .iter() + .position(|x| *x == selected_item) + }); + } + else { + self.selection_state.set_selection(Some(0)) + } + } + else { + if self.items.is_empty() { + self.selection_state.set_selection(None) + } + else { + self.selection_state.set_selection(Some(0)) + } + } + } +} + +impl IntoIterator for ListState> { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.items.into_iter() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_list_state_with_items() { + let items = vec![1, 2, 3]; + let state = ListState::new(items.clone()); + assert_eq!(state.items, items); + assert_eq!(state.selection_state.selection, Some(0)); + } + + #[test] + fn test_set_selection_clamps() { + let items = vec![1, 2, 3]; + let mut state = ListState::new(items); + state.set_selection(Some(10)); // out of bounds + assert_eq!(state.selection_state.selection, Some(2)); + } + + #[test] + fn test_filter() { + let items = vec![1, 2, 3, 4, 5]; + let state = ListState::new(items); + let filtered = state.filter(|x| *x % 2 == 0); + assert_eq!(filtered.items, vec![2, 4]); + assert_eq!(state.selection_state.selection, Some(0)); + } + + #[test] + fn test_iter_sorted() { + let items = vec![3, 1, 2]; + let state = ListState::new(items); + let sorted: Vec<_> = state.iter_sorted(|a, b| a.cmp(b)).copied().collect(); + assert_eq!(sorted, vec![1, 2, 3]); + } +} \ No newline at end of file diff --git a/src/state/mod.rs b/src/state/mod.rs new file mode 100644 index 0000000..b9eb922 --- /dev/null +++ b/src/state/mod.rs @@ -0,0 +1,3 @@ +pub mod list_state; +pub mod selection_state; +pub mod process_sort_order; \ No newline at end of file diff --git a/src/state/process_sort_order.rs b/src/state/process_sort_order.rs new file mode 100644 index 0000000..3c644ef --- /dev/null +++ b/src/state/process_sort_order.rs @@ -0,0 +1,28 @@ +use std::cmp::Ordering; +use crate::models::process_list::process_item::ProcessItem; + +pub enum ProcessSortOrder { + PidInc, + PidDec, + NameInc, + NameDec, + CpuUsageInc, + CpuUsageDec, + MemoryUsageInc, + MemoryUsageDec, +} + +impl ProcessSortOrder { + pub fn compare(&self, a: &ProcessItem, b: &ProcessItem) -> Ordering { + match self { + ProcessSortOrder::PidInc => a.pid().cmp(&b.pid()), + ProcessSortOrder::PidDec => b.pid().cmp(&a.pid()), + ProcessSortOrder::NameInc => a.name().cmp(&b.name()), + ProcessSortOrder::NameDec => b.name().cmp(&a.name()), + ProcessSortOrder::CpuUsageInc => a.cpu_usage().partial_cmp(&b.cpu_usage()).unwrap_or(Ordering::Equal), + ProcessSortOrder::CpuUsageDec => b.cpu_usage().partial_cmp(&a.cpu_usage()).unwrap_or(Ordering::Equal), + ProcessSortOrder::MemoryUsageInc => a.memory_usage().cmp(&b.memory_usage()), + ProcessSortOrder::MemoryUsageDec => b.memory_usage().cmp(&a.memory_usage()), + } + } +} \ No newline at end of file diff --git a/src/state/selection_state.rs b/src/state/selection_state.rs new file mode 100644 index 0000000..c0c8307 --- /dev/null +++ b/src/state/selection_state.rs @@ -0,0 +1,21 @@ +pub struct SelectionState { + pub selection: Option, + pub follow_selection: bool, +} + +impl SelectionState { + pub fn new(idx: Option) -> Self { + Self { + selection: idx, + follow_selection: false, + } + } + + pub fn set_selection(&mut self, idx: Option) { + self.selection = idx; + } + + pub fn set_follow(&mut self, follow: bool) { + self.follow_selection = follow; + } +} \ No newline at end of file From 091d50d65d0fb872fa00b834b3486e1cadbe9aa8 Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Wed, 16 Jul 2025 14:45:11 -0500 Subject: [PATCH 3/5] working on model, state, component architecture and mouse capture --- src/app.rs | 88 ++++- src/components/cpu.rs | 14 +- src/components/filter.rs | 9 +- src/components/memory.rs | 6 +- src/components/mod.rs | 43 +-- src/components/process.rs | 267 +++++++------ src/components/temp.rs | 6 +- src/components/utils/mod.rs | 12 +- src/components/utils/selection.rs | 15 +- src/config.rs | 23 +- src/events/event.rs | 20 +- src/main.rs | 19 +- .../{bounded_queue => }/bounded_queue.rs | 0 src/models/bounded_queue/mod.rs | 1 - src/models/items/mod.rs | 3 +- .../{process_list => items}/process_item.rs | 37 +- src/models/mod.rs | 12 +- src/models/process_list/mod.rs | 37 -- src/models/process_list/process_item_iter.rs | 37 -- src/models/process_list/process_list.rs | 363 ------------------ src/models/vec_model.rs | 42 ++ src/services/mod.rs | 5 + .../sysinfo_service.rs} | 67 +++- src/state/list_state.rs | 237 ------------ src/state/mod.rs | 3 - src/state/process_sort_order.rs | 28 -- src/state/selection_state.rs | 21 - src/states/README.md | 30 ++ src/states/mod.rs | 1 + src/states/vec_state.rs | 130 +++++++ 30 files changed, 611 insertions(+), 965 deletions(-) rename src/models/{bounded_queue => }/bounded_queue.rs (100%) delete mode 100644 src/models/bounded_queue/mod.rs rename src/models/{process_list => items}/process_item.rs (81%) delete mode 100644 src/models/process_list/mod.rs delete mode 100644 src/models/process_list/process_item_iter.rs delete mode 100644 src/models/process_list/process_list.rs create mode 100644 src/models/vec_model.rs create mode 100644 src/services/mod.rs rename src/{components/sysinfo_wrapper.rs => services/sysinfo_service.rs} (70%) delete mode 100644 src/state/list_state.rs delete mode 100644 src/state/mod.rs delete mode 100644 src/state/process_sort_order.rs delete mode 100644 src/state/selection_state.rs create mode 100644 src/states/README.md create mode 100644 src/states/mod.rs create mode 100644 src/states/vec_state.rs diff --git a/src/app.rs b/src/app.rs index 1769739..123e0b3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,20 +1,24 @@ +use std::collections::HashMap; + use anyhow::{Ok, Result}; -use crossterm::event::KeyEvent; +use crossterm::event::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}; use ratatui::prelude::*; use crate::components::temp::TempComponent; +use crate::components::Refreshable; use crate::config::Config; use crate::components::{ cpu::CPUComponent, memory::MemoryComponent, process::ProcessComponent, - sysinfo_wrapper::SysInfoWrapper, error::ErrorComponent, Component, EventState, DrawableComponent, help::HelpComponent, }; +use crate::services::sysinfo_service::SysInfoService; +#[derive(PartialEq, Eq, Hash, Clone, Copy)] enum MainFocus { CPU, Process, @@ -24,8 +28,9 @@ enum MainFocus { pub struct App { focus: MainFocus, + focus_rects: HashMap, expand: bool, - system_wrapper: SysInfoWrapper, + service: SysInfoService, process: ProcessComponent, cpu: CPUComponent, memory: MemoryComponent, @@ -37,18 +42,22 @@ pub struct App { impl App { pub fn new(config: Config) -> Self { - let mut system_wrapper = SysInfoWrapper::new(config.clone()); - system_wrapper.refresh_all(); + let mut service = SysInfoService::new(config.clone()); + service.refresh_all(); + + let process = ProcessComponent::new(config.clone(), &service); + let memory = MemoryComponent::new(config.clone(), &service); + let cpu = CPUComponent::new(config.clone(), &service); + let temp = TempComponent::new(config.clone(), &service); - let process = ProcessComponent::new(config.clone(), &system_wrapper); - let memory = MemoryComponent::new(config.clone(), &system_wrapper); - let cpu = CPUComponent::new(config.clone(), &system_wrapper); - let temp = TempComponent::new(config.clone(), &system_wrapper); + let focus = MainFocus::Process; + let focus_rects = HashMap::new(); Self { - focus: MainFocus::Process, + focus, + focus_rects, expand: false, - system_wrapper, + service, process, cpu, memory, @@ -60,12 +69,12 @@ impl App { } pub fn refresh_event(&mut self) -> Result { - self.system_wrapper.refresh_all(); + self.service.refresh_all(); - self.process.update(&self.system_wrapper); - self.memory.update(&self.system_wrapper); - self.cpu.update(&self.system_wrapper); - self.temp.update(&self.system_wrapper); + self.process.refresh(&self.service); + self.memory.update(&self.service); + self.cpu.update(&self.service); + self.temp.update(&self.service); Ok(EventState::Consumed) } @@ -90,6 +99,29 @@ impl App { Ok(EventState::NotConsumed) } + pub fn mouse_event(&mut self, mouse: MouseEvent) -> Result { + if self.move_focus_mouse_test(mouse)?.is_consumed() { + return Ok(EventState::Consumed) + } + + Ok(EventState::NotConsumed) + } + + fn move_focus_mouse_test(&mut self, mouse: MouseEvent) -> Result { + if let MouseEventKind::Down(MouseButton::Left) = mouse.kind { + let col = mouse.column; + let row = mouse.row; + + for (focus, rect) in &self.focus_rects { + if rect.contains(col, row) { + self.focus = *focus; + return Ok(EventState::Consumed) + } + } + } + return Ok(EventState::NotConsumed) + } + fn component_event(&mut self, key: KeyEvent) -> Result { if self.error.event(key)?.is_consumed() { return Ok(EventState::Consumed) @@ -121,7 +153,7 @@ impl App { } // terminate case if key.code == self.config.key_config.terminate { - self.process.terminate_process(&self.system_wrapper); + //self.process.terminate_process(&self.system_wrapper); return Ok(EventState::Consumed) } @@ -171,6 +203,7 @@ impl App { chunks[0], true, )?; + self.focus_rects.insert(MainFocus::Process, chunks[0]); } if matches!(self.focus, MainFocus::CPU) { @@ -179,6 +212,7 @@ impl App { chunks[0], true, )?; + self.focus_rects.insert(MainFocus::CPU, chunks[0]); } if matches!(self.focus, MainFocus::Memory) { @@ -187,6 +221,7 @@ impl App { chunks[0], true, )?; + self.focus_rects.insert(MainFocus::Memory, chunks[0]); } if matches!(self.focus, MainFocus::Temp) { @@ -195,6 +230,7 @@ impl App { chunks[0], true, )?; + self.focus_rects.insert(MainFocus::Temp, chunks[0]); } } else { @@ -226,12 +262,14 @@ impl App { vertical_chunks[2], matches!(self.focus, MainFocus::Process) )?; + self.focus_rects.insert(MainFocus::Process, vertical_chunks[2]); self.cpu.draw( f, vertical_chunks[0], matches!(self.focus, MainFocus::CPU) )?; + self.focus_rects.insert(MainFocus::CPU, vertical_chunks[0]); self.memory.draw( f, @@ -239,15 +277,17 @@ impl App { //vertical_chunks[1], matches!(self.focus, MainFocus::Memory) )?; + self.focus_rects.insert(MainFocus::Memory, horizontal_chunks[1][0]); self.temp.draw( f, horizontal_chunks[1][1], matches!(self.focus, MainFocus::Temp) )?; + self.focus_rects.insert(MainFocus::Temp, horizontal_chunks[1][1]); } - self.help.draw(f, Rect::default(), false)?; + //self.help.draw(f, Rect::default(), false)?; //TODO: re-implement return Ok(()) } @@ -272,3 +312,15 @@ impl App { res }*/ } + +trait Contains { + fn contains(&self, col: u16, row: u16) -> bool; +} +impl Contains for ratatui::layout::Rect { + fn contains(&self, col: u16, row: u16) -> bool { + col >= self.x + && col < self.x + self.width + && row >= self.y + && row < self.y + self.height + } +} \ No newline at end of file diff --git a/src/components/cpu.rs b/src/components/cpu.rs index c93ea0b..4848eb2 100644 --- a/src/components/cpu.rs +++ b/src/components/cpu.rs @@ -7,9 +7,9 @@ use anyhow::{Ok, Result}; use crossterm::event::KeyEvent; use super::EventState; use crate::components::common_nav; -use crate::components::sysinfo_wrapper::SysInfoWrapper; -use crate::components::utils::selection::SelectionState; -use crate::models::bounded_queue::bounded_queue::BoundedQueue; +use crate::services::sysinfo_service::SysInfoService; +use crate::components::utils::selection::UISelection; +use crate::models::bounded_queue::BoundedQueue; use crate::models::items::cpu_item::CpuItem; use crate::config::Config; use super::{Component, DrawableComponent}; @@ -69,13 +69,13 @@ impl ColorWheel { pub struct CPUComponent { cpus: BTreeMap>, - selection_state: SelectionState, + selection_state: UISelection, selection_offset: usize, config: Config, } impl CPUComponent { - pub fn new(config: Config, sysinfo: &SysInfoWrapper) -> Self { + pub fn new(config: Config, sysinfo: &SysInfoService) -> Self { let mut cpus: BTreeMap> = BTreeMap::new(); for cpu in sysinfo.get_cpus() { @@ -89,7 +89,7 @@ impl CPUComponent { perf_q.add_item(cpu); } - let selection_state = if cpus.len() > 0 { SelectionState::new(Some(0)) } else { SelectionState::new(None) }; + let selection_state = if cpus.len() > 0 { UISelection::new(Some(0)) } else { UISelection::new(None) }; // this is the offset between the SelectionState Selection (ui selection) & cpu_selection (backend) // this offset is present because an option to display ALL cpus is present in the ui list that is // not present in the CPU list @@ -104,7 +104,7 @@ impl CPUComponent { } // has ownership - pub fn update(&mut self, sysinfo: &SysInfoWrapper) { + pub fn update(&mut self, sysinfo: &SysInfoService) { for cpu in sysinfo.get_cpus() { let id = cpu.id(); diff --git a/src/components/filter.rs b/src/components/filter.rs index 35c8330..0d94c4b 100644 --- a/src/components/filter.rs +++ b/src/components/filter.rs @@ -26,13 +26,18 @@ impl FilterComponent { self.input_str.clear(); } - pub fn input_str(&mut self) -> &str { + pub fn input_str(&self) -> &str { &self.input_str } - pub fn is_filter_empty(&mut self) -> bool { + pub fn is_filter_empty(&self) -> bool { self.input_str.is_empty() } + + pub fn filter_contents(&self) -> Option<&str> { + if self.input_str.is_empty() { return None } + else { return Some(&self.input_str) } + } } impl Component for FilterComponent { diff --git a/src/components/memory.rs b/src/components/memory.rs index 0594b67..35faf38 100644 --- a/src/components/memory.rs +++ b/src/components/memory.rs @@ -5,7 +5,7 @@ use ratatui::{ widgets::{Block, Gauge}, }; use crossterm::event::KeyEvent; -use crate::components::sysinfo_wrapper::SysInfoWrapper; +use crate::services::sysinfo_service::SysInfoService; use crate::components::DrawableComponent; use crate::models::items::memory_item::MemoryItem; use crate::config::Config; @@ -18,7 +18,7 @@ pub struct MemoryComponent { } impl MemoryComponent { - pub fn new(config: Config, sysinfo: &SysInfoWrapper) -> Self { + pub fn new(config: Config, sysinfo: &SysInfoService) -> Self { let mut memory = MemoryItem::default(); sysinfo.get_memory(&mut memory); @@ -28,7 +28,7 @@ impl MemoryComponent { } } - pub fn update(&mut self, sysinfo: &SysInfoWrapper) { + pub fn update(&mut self, sysinfo: &SysInfoService) { sysinfo.get_memory(&mut self.memory); } } diff --git a/src/components/mod.rs b/src/components/mod.rs index bef2b6f..34c2c2f 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -2,8 +2,6 @@ use anyhow::Result; use crossterm::event::KeyEvent; use ratatui::prelude::*; use super::config::KeyConfig; -use crate::components::utils::MoveSelection; -pub mod sysinfo_wrapper; pub mod filter; pub mod help; pub mod error; @@ -22,6 +20,10 @@ pub trait Component { fn event(&mut self, key: KeyEvent) -> Result; } +pub trait Refreshable { + fn refresh(&mut self, service: &S); +} + #[derive(PartialEq)] pub enum EventState { Consumed, @@ -51,34 +53,11 @@ pub fn common_nav(key: KeyEvent, key_config: &KeyConfig) -> Option Option { - if key.code == key_config.sort_cpu_usage_dec { - Some(ListSortOrder::CpuUsageDec) - } - else if key.code == key_config.sort_cpu_usage_inc { - Some(ListSortOrder::CpuUsageInc) - } - else if key.code == key_config.sort_memory_usage_dec { - Some(ListSortOrder::MemoryUsageDec) - } - else if key.code == key_config.sort_memory_usage_inc { - Some(ListSortOrder::MemoryUsageInc) - } - else if key.code == key_config.sort_pid_dec { - Some(ListSortOrder::PidDec) - } - else if key.code == key_config.sort_pid_inc { - Some(ListSortOrder::PidInc) - } - else if key.code == key_config.sort_name_dec { - Some(ListSortOrder::NameDec) - } - else if key.code == key_config.sort_name_inc { - Some(ListSortOrder::NameInc) - } - else { - None - } -}*/ +#[derive(Copy, Clone)] +pub enum MoveSelection { + Up, + Down, + Top, + Bottom, +} \ No newline at end of file diff --git a/src/components/process.rs b/src/components/process.rs index fa4a30e..a0bba0f 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -1,20 +1,12 @@ use anyhow::{Ok, Result}; use crossterm::event::KeyEvent; -use ratatui::{ - Frame, - prelude::*, -}; -use crate::components::{ - sysinfo_wrapper::SysInfoWrapper, - common_nav, DrawableComponent, Component, EventState, - utils::vertical_scroll::VerticalScroll, - filter::FilterComponent, -}; +use ratatui::{Frame, prelude::*,}; +use crate::services::{sysinfo_service::SysInfoService, ListProvider}; use crate::config::{Config, KeyConfig}; -use crate::models::process_list::process_item_iter::ProcessItemIterator; -use crate::models::process_list::process_list::ProcessList; -use crate::models::process_list::ProcessItemSortOrder; -use crate::models::process_list::map_key_to_process_sort; +use crate::components::{common_nav, DrawableComponent, Component, EventState, Refreshable}; +use crate::components::{utils::{selection::UISelection, vertical_scroll::VerticalScroll}, filter::FilterComponent}; +use crate::states::vec_state::VecState; +use crate::models::items::process_item::{ProcessItem, ProcessItemSortOrder}; #[derive(PartialEq, Clone)] pub enum Focus { @@ -23,110 +15,137 @@ pub enum Focus { } pub struct ProcessComponent { - focus: Focus, - list: ProcessList, - filter: FilterComponent, - filtered_list: Option, + vec_state: VecState, + ui_selection: UISelection, + sort: Option, scroll: VerticalScroll, + filter_component: FilterComponent, + focus: Focus, pub config: Config, } impl ProcessComponent { - pub fn new(config: Config, sysinfo: &SysInfoWrapper) -> Self { + pub fn new(config: Config, sysinfo: &SysInfoService) -> Self { + let processes: Vec = sysinfo.fetch_items(); + let ui_selection: UISelection = if processes.is_empty() { UISelection::new(None) } else { UISelection::new(Some(0)) }; + let state_selection: Option = ui_selection.selection; + let filter: Option = None; + let sort: Option = None; + let vec_state: VecState = VecState::new(processes, state_selection, sort.clone(), filter); + + let scroll: VerticalScroll = VerticalScroll::new(); + let filter_component: FilterComponent = FilterComponent::new(config.clone()); + let focus: Focus = Focus::List; + Self { - focus: Focus::List, - list: ProcessList::new(sysinfo), - filter: FilterComponent::new(config.clone()), - filtered_list: None, - scroll: VerticalScroll::new(), + vec_state, + ui_selection, + sort, + scroll, + filter_component, + focus, config, } } +} - pub fn update(&mut self, sysinfo: &SysInfoWrapper) { - self.list.update(sysinfo); - - if let Some(filtered_list) = self.filtered_list.as_mut() { - filtered_list.update(sysinfo); +impl Refreshable for ProcessComponent +where + S: ListProvider +{ + fn refresh(&mut self, service: &S) { + let processes: Vec = service.fetch_items(); + self.vec_state.replace(processes); + + let len = self.vec_state.view_indices().len(); + if len == 0 { + self.ui_selection.set_selection(None); + self.vec_state.set_selection(self.ui_selection.selection); + return; } - } - pub fn terminate_process(&mut self, sysinfo: &SysInfoWrapper) -> bool { - self.filtered_list - .as_ref() - .and_then(|f| f.selection_pid()) - .or_else(|| self.list.selection_pid()) - .map(|pid| { - sysinfo.terminate_process(pid); - true - }) - .unwrap_or(false) + // ui_selection iterates over vec_state.view_indices() + if let Some(ui_selection) = self.ui_selection.selection { + let max_idx = len.saturating_sub(1); + + if ui_selection > max_idx { + self.ui_selection.set_selection(Some(max_idx)); + let idx = self.vec_state.view_indices().get(max_idx).cloned(); + self.vec_state.set_selection(idx); + } + else { + let idx = self.vec_state.view_indices().get(ui_selection).cloned(); + self.vec_state.set_selection(idx); + } + } } } + impl Component for ProcessComponent { fn event(&mut self, key: KeyEvent) -> Result { - if key.code == self.config.key_config.filter && self.focus == Focus::List { + if key.code == self.config.key_config.filter && + matches!(self.focus,Focus::List) + { self.focus = Focus::Filter; - return Ok(EventState::Consumed) } if matches!(self.focus, Focus::Filter) { - if self.filter.event(key)?.is_consumed() { - self.filtered_list = if self.filter.input_str().is_empty() { - None + + if self.filter_component.event(key)?.is_consumed() { + self.vec_state.set_filter(self.filter_component.filter_contents()); + + if self.vec_state.view_indices().len() > 0 { + // set ui_selection to beginning of view_indices + self.ui_selection.set_selection(Some(0)); + // set vec_state selection to the index at view_indices[0] + let idx = self.vec_state.view_indices().get(0).cloned(); + self.vec_state.set_selection(idx); } - else { - Some(self.list.filter(self.filter.input_str())) - }; return Ok(EventState::Consumed) } if key.code == self.config.key_config.enter { self.focus = Focus::List; - return Ok(EventState::Consumed) } } if matches!(self.focus, Focus::List) { - if list_nav( - if let Some(list) = self.filtered_list.as_mut() { - list + if let Some(move_dir) = common_nav(key, &self.config.key_config) { + let len = self.vec_state.view_indices().len(); + self.ui_selection.move_selection(move_dir, len); // if len == 0, ui_selection.selection is set to None here + + if let Some(ui_selection) = self.ui_selection.selection { + let idx = self.vec_state.view_indices().get(ui_selection).cloned(); + self.vec_state.set_selection(idx); } else { - &mut self.list - }, - key, - &self.config.key_config - ) { - return Ok(EventState::Consumed); + self.vec_state.set_selection(None); + } + + return Ok(EventState::Consumed) } - if key.code == self.config.key_config.follow_selection { - if let Some(filtered_list) = self.filtered_list.as_mut() { - filtered_list.toggle_follow_selection(); + if let Some(sort_order) = process_sort(key, &self.config.key_config) { + self.sort = Some(sort_order); + self.vec_state.set_sort(self.sort.clone()); + + if let Some(ui_selection) = self.ui_selection.selection { + let idx = self.vec_state.view_indices().get(ui_selection).cloned(); + self.vec_state.set_selection(idx); } else { - self.list.toggle_follow_selection(); + self.vec_state.set_selection(None); } return Ok(EventState::Consumed) } - if list_sort( - if let Some(list) = self.filtered_list.as_mut() { - list - } - else { - &mut self.list - }, - key, - &self.config.key_config - )? { - return Ok(EventState::Consumed); + if key.code == self.config.key_config.follow_selection { // TODO: implement follow selection? + return Ok(EventState::Consumed) } } @@ -134,27 +153,17 @@ impl Component for ProcessComponent { } } -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); - - true - } - else { - false - } -} - -fn list_sort(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> Result { - // sorting is not common between componenets - if let Some(sort) = map_key_to_process_sort(key, key_config) { - list.sort(&sort); - - Ok(true) - } - else { - Ok(false) - } +fn process_sort(key: KeyEvent, key_config: &KeyConfig) -> Option { + if key.code == key_config.sort_pid_inc { return Some(ProcessItemSortOrder::PidInc) } + if key.code == key_config.sort_pid_dec { return Some(ProcessItemSortOrder::PidDec) } + if key.code == key_config.sort_cpu_usage_inc { return Some(ProcessItemSortOrder::CpuUsageInc) } + if key.code == key_config.sort_cpu_usage_dec { return Some(ProcessItemSortOrder::CpuUsageDec) } + if key.code == key_config.sort_memory_usage_inc { return Some(ProcessItemSortOrder::MemoryUsageInc) } + if key.code == key_config.sort_memory_usage_dec { return Some(ProcessItemSortOrder::MemoryUsageDec) } + if key.code == key_config.sort_name_inc { return Some(ProcessItemSortOrder::NameInc) } + if key.code == key_config.sort_name_dec { return Some(ProcessItemSortOrder::NameDec) } + + return None } impl DrawableComponent for ProcessComponent { @@ -168,37 +177,25 @@ impl DrawableComponent for ProcessComponent { let visible_list_height = horizontal_chunks[0].height.saturating_sub(3) as usize; - let list = if let Some(filtered_list) = self.filtered_list.as_ref() { - filtered_list - } - else { - &self.list - }; - - // update vert scroll - list.selection().map_or_else( + // update vertical scroll + let indices = self.vec_state.view_indices(); + let len = indices.len(); + self.ui_selection.selection.map_or_else( { || self.scroll.reset() - }, |selection| { - self.scroll.update( - selection, - list.len(), - visible_list_height, - ); - }, - ); + }, |idx| { + self.scroll.update(idx, len, visible_list_height,); + },); - let visible_items = list - .iterate( - self.scroll.get_top(), - visible_list_height, - ); + let visible_items = self.vec_state + .iter_with_selection() + .skip(self.scroll.get_top()) + .take(visible_list_height); draw_process_list( f, horizontal_chunks[0], - visible_items, - list.is_follow_selection(), + visible_items, if focused { matches!(self.focus, Focus::List) } @@ -206,7 +203,7 @@ impl DrawableComponent for ProcessComponent { false }, self.config.theme_config.clone(), - list.sort_order(), + self.sort.clone(), ); self.scroll.draw( @@ -220,7 +217,7 @@ impl DrawableComponent for ProcessComponent { }, )?; - self.filter.draw( + self.filter_component.draw( f, horizontal_chunks[1], if focused { @@ -236,37 +233,37 @@ impl DrawableComponent for ProcessComponent { } use ratatui::widgets::{block::*, *}; -//use crate::models::process_list::list_iter::ListIterator; use crate::config::ThemeConfig; -fn draw_process_list<'a>( +fn draw_process_list<'a, I>( f: &mut Frame, area: Rect, - visible_items: ProcessItemIterator, - follow_selection: bool, + visible_items: I, focus: bool, theme_config: ThemeConfig, - sort_order: &ProcessItemSortOrder, + sort_order: Option, ) +where + I: Iterator, { - let follow_flag = follow_selection; + let follow_flag = false; // setting header let header = ["", - if matches!(sort_order, ProcessItemSortOrder::PidInc) { "PID ▲" } - else if matches!(sort_order, ProcessItemSortOrder::PidDec) { "PID ▼" } + if matches!(sort_order, Some(ProcessItemSortOrder::PidInc)) { "PID ▲" } + else if matches!(sort_order, Some(ProcessItemSortOrder::PidDec)) { "PID ▼" } else { "PID" }, - if matches!(sort_order, ProcessItemSortOrder::NameInc) { "Name ▲" } - else if matches!(sort_order, ProcessItemSortOrder::NameDec) { "Name ▼" } + if matches!(sort_order, Some(ProcessItemSortOrder::NameInc)) { "Name ▲" } + else if matches!(sort_order, Some(ProcessItemSortOrder::NameDec)) { "Name ▼" } else { "Name" }, - if matches!(sort_order, ProcessItemSortOrder::CpuUsageInc) { "CPU (%) ▲" } - else if matches!(sort_order, ProcessItemSortOrder::CpuUsageDec) { "CPU (%) ▼" } + if matches!(sort_order, Some(ProcessItemSortOrder::CpuUsageInc)) { "CPU (%) ▲" } + else if matches!(sort_order, Some(ProcessItemSortOrder::CpuUsageDec)) { "CPU (%) ▼" } else { "CPU (%)" }, - if matches!(sort_order, ProcessItemSortOrder::MemoryUsageInc) { "Memory (MB) ▲" } - else if matches!(sort_order, ProcessItemSortOrder::MemoryUsageDec) { "Memory (MB) ▼" } + if matches!(sort_order, Some(ProcessItemSortOrder::MemoryUsageInc)) { "Memory (MB) ▲" } + else if matches!(sort_order, Some(ProcessItemSortOrder::MemoryUsageDec)) { "Memory (MB) ▼" } else { "Memory (MB)" }, "Run (hh:mm:ss)", @@ -287,7 +284,7 @@ fn draw_process_list<'a>( // setting rows let rows = visible_items - .map(|(item, selected)| { + .map(|(idx, item, selected)| { let style = if focus && selected && follow_flag { theme_config.style_item_selected_followed diff --git a/src/components/temp.rs b/src/components/temp.rs index 4b356ef..3cd9a20 100644 --- a/src/components/temp.rs +++ b/src/components/temp.rs @@ -5,7 +5,7 @@ use ratatui::Frame; use ratatui::widgets::Cell; use ratatui::widgets::Row; use crossterm::event::KeyEvent; -use crate::components::sysinfo_wrapper::SysInfoWrapper; +use crate::services::sysinfo_service::SysInfoService; use crate::components::utils::vertical_scroll::VerticalScroll; use crate::models::items::temp_item::TempItem; use crate::{components::EventState, config::Config}; @@ -19,7 +19,7 @@ pub struct TempComponent { } impl TempComponent { - pub fn new(config: Config, sysinfo: &SysInfoWrapper) -> Self { + pub fn new(config: Config, sysinfo: &SysInfoService) -> Self { let mut temps = Vec::new(); sysinfo.get_temps(&mut temps); @@ -31,7 +31,7 @@ impl TempComponent { } } - pub fn update(&mut self, sysinfo: &SysInfoWrapper) { + pub fn update(&mut self, sysinfo: &SysInfoService) { sysinfo.get_temps(&mut self.temps); } } diff --git a/src/components/utils/mod.rs b/src/components/utils/mod.rs index 7d232ca..97350fe 100644 --- a/src/components/utils/mod.rs +++ b/src/components/utils/mod.rs @@ -1,12 +1,2 @@ pub mod vertical_scroll; -pub mod selection; - -#[derive(Copy, Clone)] -pub enum MoveSelection { - Up, - Down, - MultipleUp, - MultipleDown, - Top, - Bottom, -} \ No newline at end of file +pub mod selection; \ No newline at end of file diff --git a/src/components/utils/selection.rs b/src/components/utils/selection.rs index 21a9b8a..ab2e423 100644 --- a/src/components/utils/selection.rs +++ b/src/components/utils/selection.rs @@ -1,11 +1,12 @@ use crate::components::MoveSelection; -pub struct SelectionState { +// currently this is only being used by CpuComponent +pub struct UISelection { pub selection: Option, pub follow_selection: bool, } -impl SelectionState { +impl UISelection { pub fn new(idx: Option) -> Self { Self { selection: idx, @@ -21,13 +22,15 @@ impl SelectionState { self.follow_selection = follow; } - pub fn move_selection(&mut self, dir: MoveSelection, len: usize) { + pub fn move_selection(&mut self, move_selection: MoveSelection, len: usize) { + if matches!(len, 0) { + self.selection = None + } + if let Some(selection) = self.selection { - let new_idx = match dir { + let new_idx = match move_selection { MoveSelection::Down => self.selection_down(selection, 1, len), - MoveSelection::MultipleDown => self.selection_down(selection, 10, len), MoveSelection::Up => self.selection_up(selection, 1), - MoveSelection::MultipleUp => self.selection_up(selection, 10), MoveSelection::Bottom => self.selection_bottom(selection, len), MoveSelection::Top => self.selection_top(selection), }; diff --git a/src/config.rs b/src/config.rs index fc7a9d9..1a03f68 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,10 @@ -use crossterm::event::KeyCode; +use crossterm::event::{KeyCode, MouseButton, MouseEventKind}; use serde::{Deserialize,Serialize}; #[derive(Clone,Serialize,Deserialize)] pub struct Config { pub key_config: KeyConfig, + pub mouse_config: MouseConfig, pub theme_config: ThemeConfig, refresh_rate: u64, min_as_s: u64, @@ -16,6 +17,7 @@ impl Default for Config { fn default() -> Self { Self { key_config: KeyConfig::default(), + mouse_config: MouseConfig::default(), theme_config: ThemeConfig::default(), refresh_rate: 2000, min_as_s: 60000/ 1000, @@ -102,6 +104,25 @@ impl Default for KeyConfig { } } +#[derive(Clone,Serialize,Deserialize)] +pub struct MouseConfig { + pub left_click: MouseButton, + pub right_click: MouseButton, + pub scroll_up: MouseEventKind, + pub scroll_down: MouseEventKind, +} + +impl Default for MouseConfig { + fn default() -> Self { + Self { + left_click: MouseButton::Left, + right_click: MouseButton::Right, + scroll_up: MouseEventKind::ScrollUp, + scroll_down: MouseEventKind::ScrollDown, + } + } +} + use ratatui::prelude::{Color, Modifier, Style}; #[derive(Clone,PartialEq,Serialize,Deserialize)] diff --git a/src/events/event.rs b/src/events/event.rs index dfe1dbf..a3bd38a 100644 --- a/src/events/event.rs +++ b/src/events/event.rs @@ -1,8 +1,10 @@ use crossterm::event::{ self, KeyEvent, + KeyEventKind, + MouseEvent, + MouseEventKind, Event as CEvent, - KeyEventKind }; use std::{ @@ -28,15 +30,16 @@ impl Default for EventConfig { #[derive(Clone, Copy)] -pub enum Event { - Input(K), +pub enum Event { + KeyInput(K), + MouseInput(M), Tick, Refresh, } pub struct Events { - rx: mpsc::Receiver>, - _tx: mpsc::Sender>, + rx: mpsc::Receiver>, + _tx: mpsc::Sender>, } impl Events { @@ -59,9 +62,12 @@ impl Events { if let Ok(event) = event::read() { if let CEvent::Key(key) = event { if key.kind == KeyEventKind::Press { - input_tx.send(Event::Input(key)).unwrap(); + input_tx.send(Event::KeyInput(key)).unwrap(); } } + if let CEvent::Mouse(mouse) = event { + input_tx.send(Event::MouseInput(mouse)).unwrap(); + } } } }); @@ -79,7 +85,7 @@ impl Events { Events { rx, _tx: tx } } - pub fn next(&self) -> Result, mpsc::RecvError> { + pub fn next(&self) -> Result, mpsc::RecvError> { self.rx.recv() } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ebd5f94..e6f3bf8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use anyhow::Result; -use std::io; +use crossterm::event::EnableMouseCapture; +use std::io::{stdout}; use crossterm::ExecutableCommand; use crossterm::{ execute, @@ -19,13 +20,15 @@ pub mod config; pub mod components; pub mod events; pub mod models; -pub mod state; +pub mod states; +pub mod services; fn main() -> Result<()> { enable_raw_mode()?; - io::stdout().execute(EnterAlternateScreen)?; + execute!(stdout(), EnterAlternateScreen, EnableMouseCapture)?; + //stdout().execute(EnterAlternateScreen)?; - let backend = CrosstermBackend::new(io::stdout()); + let backend = CrosstermBackend::new(stdout()); let mut terminal = Terminal::new(backend)?; let config = config::Config::default(); @@ -50,7 +53,7 @@ fn main() -> Result<()> { })?; match events.next()? { - Event::Input(key) => match app.key_event(key) { + Event::KeyInput(key) => match app.key_event(key) { Ok(state) => { if !state.is_consumed() && key.code == app.config.key_config.exit { break; @@ -60,6 +63,12 @@ fn main() -> Result<()> { app.error.set(err.to_string())?; } } + Event::MouseInput(mouse) => match app.mouse_event(mouse) { + Ok(_state) => {} + Err(err) => { + app.error.set(err.to_string())?; + } + } Event::Refresh => match app.refresh_event() { Ok(_state) => {} Err(err) => { diff --git a/src/models/bounded_queue/bounded_queue.rs b/src/models/bounded_queue.rs similarity index 100% rename from src/models/bounded_queue/bounded_queue.rs rename to src/models/bounded_queue.rs diff --git a/src/models/bounded_queue/mod.rs b/src/models/bounded_queue/mod.rs deleted file mode 100644 index c07b676..0000000 --- a/src/models/bounded_queue/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod bounded_queue; \ No newline at end of file diff --git a/src/models/items/mod.rs b/src/models/items/mod.rs index 82f6fde..c5689cf 100644 --- a/src/models/items/mod.rs +++ b/src/models/items/mod.rs @@ -1,3 +1,4 @@ pub mod cpu_item; pub mod memory_item; -pub mod temp_item; \ No newline at end of file +pub mod temp_item; +pub mod process_item; \ No newline at end of file diff --git a/src/models/process_list/process_item.rs b/src/models/items/process_item.rs similarity index 81% rename from src/models/process_list/process_item.rs rename to src/models/items/process_item.rs index cb72dd1..cf40910 100644 --- a/src/models/process_list/process_item.rs +++ b/src/models/items/process_item.rs @@ -1,3 +1,17 @@ +use crate::{models::{Filterable, Sortable}}; + +#[derive(Clone, PartialEq)] +pub enum ProcessItemSortOrder { + PidInc, + PidDec, + NameInc, + NameDec, + CpuUsageInc, + CpuUsageDec, + MemoryUsageInc, + MemoryUsageDec, +} + #[derive(Default, Clone)] pub struct ProcessItem { pid: u32, @@ -83,7 +97,7 @@ impl ProcessItem { &self.path } - // STATIC COMPARATORS + // STATIC COMPARATORS -- get rid of these pub fn cmp_pid_inc(a: &Self, b: &Self) -> std::cmp::Ordering { a.pid.cmp(&b.pid) } @@ -134,6 +148,27 @@ impl PartialEq for ProcessItem { } } +impl Filterable for ProcessItem { + fn matches_filter(&self, filter: &str) -> bool { + self.name.contains(filter) + } +} + +impl Sortable for ProcessItem { + fn cmp_with(&self, other: &Self, sort: &ProcessItemSortOrder) -> std::cmp::Ordering { + match sort { + ProcessItemSortOrder::PidInc => self.pid.cmp(&other.pid), + ProcessItemSortOrder::PidDec => other.pid.cmp(&self.pid), + ProcessItemSortOrder::NameInc => self.name.cmp(&other.name), + ProcessItemSortOrder::NameDec => other.name.cmp(&self.name), + ProcessItemSortOrder::CpuUsageInc => self.cpu_usage.partial_cmp(&other.cpu_usage).unwrap_or(std::cmp::Ordering::Equal), + ProcessItemSortOrder::CpuUsageDec => other.cpu_usage.partial_cmp(&self.cpu_usage).unwrap_or(std::cmp::Ordering::Equal), + ProcessItemSortOrder::MemoryUsageInc => self.memory_usage.cmp(&other.memory_usage), + ProcessItemSortOrder::MemoryUsageDec => other.memory_usage.cmp(&self.memory_usage), + } + } +} + #[cfg(test)] pub mod test { use super::ProcessItem; diff --git a/src/models/mod.rs b/src/models/mod.rs index f494c30..5752a3a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,11 @@ -pub mod process_list; pub mod items; -pub mod bounded_queue; \ No newline at end of file +pub mod bounded_queue; +pub mod vec_model; + +pub trait Filterable { + fn matches_filter(&self, filter: &str) -> bool; +} + +pub trait Sortable { + fn cmp_with(&self, other: &Self, sort: &S) -> std::cmp::Ordering; +} \ No newline at end of file diff --git a/src/models/process_list/mod.rs b/src/models/process_list/mod.rs deleted file mode 100644 index f59234f..0000000 --- a/src/models/process_list/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -pub mod process_list; -pub mod process_item; -pub mod process_item_iter; - -// possible set of values (known at compile time) -// Variants correspond to static comparator functions -// found at ../items/process_list_item.rs -#[derive(PartialEq, Clone)] -pub enum ProcessItemSortOrder { - PidInc, - PidDec, - NameInc, - NameDec, - CpuUsageInc, - CpuUsageDec, - MemoryUsageInc, - MemoryUsageDec, -} - -// This function maps user's dynamic key press to static sorting strategy. -// Supported mappings are explicitly stated in function match statement. -// Returns Some(ProcessItemSortOrder::Variant) or None. -use crossterm::event::KeyEvent; -use crate::config::KeyConfig; -pub fn map_key_to_process_sort(key: KeyEvent, key_config: &KeyConfig) -> Option { - match key.code { - code if code == key_config.sort_cpu_usage_dec => Some(ProcessItemSortOrder::CpuUsageDec), - code if code == key_config.sort_cpu_usage_inc => Some(ProcessItemSortOrder::CpuUsageInc), - code if code == key_config.sort_memory_usage_dec => Some(ProcessItemSortOrder::MemoryUsageDec), - code if code == key_config.sort_memory_usage_inc => Some(ProcessItemSortOrder::MemoryUsageInc), - code if code == key_config.sort_pid_dec => Some(ProcessItemSortOrder::PidDec), - code if code == key_config.sort_pid_inc => Some(ProcessItemSortOrder::PidInc), - code if code == key_config.sort_name_dec => Some(ProcessItemSortOrder::NameDec), - code if code == key_config.sort_name_inc => Some(ProcessItemSortOrder::NameInc), - _ => None, - } -} \ No newline at end of file diff --git a/src/models/process_list/process_item_iter.rs b/src/models/process_list/process_item_iter.rs deleted file mode 100644 index 33cf907..0000000 --- a/src/models/process_list/process_item_iter.rs +++ /dev/null @@ -1,37 +0,0 @@ -use super::process_item::ProcessItem; - -pub struct ProcessItemIterator<'a> { - list: &'a Vec, - selection: Option, - start_idx: usize, - end_idx: usize, -} - -impl<'a> ProcessItemIterator<'a> { - pub fn new(list: &'a Vec, selection: Option, start_idx: usize, max_iter: usize) -> Self { - let end_idx = usize::min(start_idx + max_iter, list.len()); - Self { - list, - selection, - start_idx, - end_idx, - } - } -} - -impl<'a> Iterator for ProcessItemIterator<'a> { - type Item = (&'a ProcessItem, bool); - - fn next(&mut self) -> Option { - if self.start_idx >= self.end_idx { - return None; - } - - let item = &self.list[self.start_idx]; - let is_selected = Some(self.start_idx) == self.selection; - - self.start_idx += 1; - - Some((item, is_selected)) - } -} \ No newline at end of file diff --git a/src/models/process_list/process_list.rs b/src/models/process_list/process_list.rs deleted file mode 100644 index eeb944e..0000000 --- a/src/models/process_list/process_list.rs +++ /dev/null @@ -1,363 +0,0 @@ -use super::process_item::ProcessItem; -use super::ProcessItemSortOrder; -use crate::components::utils::selection::SelectionState; -use crate::components::utils::MoveSelection; -use crate::components::sysinfo_wrapper::SysInfoWrapper; -use crate::models::process_list::process_item_iter::ProcessItemIterator; - -// A ProcessList can be constructed as an "unfiltered list" with the new(&SysInfoWrapper) constructor -// or as a "filtered list" with filter(&str) constructor. -// Additionally, the filter(&str) constructor uses an existing instance of a ProcessList, -// thus the only constructor that interacts with the sysinfo backend is new(&SysInfoWrapper). -// If filter = None, then the List is Unfiltered, if Some() then the List if Filtered. -pub struct ProcessList { - processes: Vec, - sort: ProcessItemSortOrder, - selection_state: SelectionState, - filter: Option, -} - -impl ProcessList { - // constructor - pub fn new(sysinfo: &SysInfoWrapper) -> Self { - let mut processes: Vec = Vec::new(); - // sysinfo.get_processes(&mut vec) populates argument Vec with system processes. See /components/sysinfo_wrapper.rs for implementation details. - sysinfo.get_processes(&mut processes); - - // setting defaults explicitly - let sort: ProcessItemSortOrder = ProcessItemSortOrder::CpuUsageDec; - let selection_state: SelectionState = if processes.len() > 0 { SelectionState::new(Some(0)) } else { SelectionState::new(None) }; - let filter: Option = None; - - Self { - processes, - sort, - selection_state, - filter, - } - } - - // filter constructor - pub fn filter(&self, filter_text: &str) -> Self { - // filtering by process name--case insensitive - let processes: Vec = self.processes - .iter() - .filter(|item| { - item.name().to_lowercase().contains(&filter_text.to_lowercase()) - }) - .cloned() - .collect(); - - // setting defaults explicitly - let sort: ProcessItemSortOrder = ProcessItemSortOrder::CpuUsageDec; - let selection_state: SelectionState = if processes.len() > 0 { SelectionState::new(Some(0)) } else { SelectionState::new(None) }; - let filter: Option = Some(String::from(filter_text)); - - Self { - processes, - sort, - selection_state, - filter, - } - } - - - pub fn update(&mut self, sysinfo: &SysInfoWrapper) { - // storing reference to selected item and deep copy of it's PID before updating processes - let selection_item: Option<&ProcessItem> = self.processes.get(self.selection_state.selection.unwrap_or_default()); - let selection_pid: Option = selection_item.map(|item| item.pid()); - - // get new processes - sysinfo.get_processes(&mut self.processes); - // filter if this is a "filtered list" - if let Some(filter) = &self.filter { - self.processes.retain(|item| { - item.name().to_lowercase().contains(&filter.to_lowercase()) - }); - } - - // return if update resulted in no processes - if self.processes.len() == 0 { - self.selection_state.set_selection(None); - self.selection_state.set_follow(false); - return - } - - // sort order is lost when getting new processes - self.sort(&self.sort.clone()); - - // set selection after update - let selection = if self.selection_state.follow_selection { - selection_pid.and_then(|p| { - self.processes - .iter() - .position(|item| item.pid() == p) - }) - } - else { - if let Some(selection) = self.selection_state.selection { - // check upper bound (lowerbound is effectively checked when checking for NO processes) - let max_idx = self.processes.len().saturating_sub(1); - if selection > max_idx { - Some(max_idx) - } - else { - Some(selection) - } - } - else { - None - } - }; - - self.selection_state.set_selection(selection); - } - - pub fn sort(&mut self, sort: &ProcessItemSortOrder) { - let selection_item: Option<&ProcessItem> = self.processes.get(self.selection_state.selection.unwrap_or_default()); - let selection_pid: Option = selection_item.map(|item| item.pid()); - - // mapping variants to corresponding static comparator functions (see /process_item.rs) - match sort { - ProcessItemSortOrder::PidInc => self.processes.sort_by(ProcessItem::cmp_pid_inc), - ProcessItemSortOrder::PidDec => self.processes.sort_by(ProcessItem::cmp_pid_dec), - ProcessItemSortOrder::NameInc => self.processes.sort_by(ProcessItem::cmp_name_inc), - ProcessItemSortOrder::NameDec => self.processes.sort_by(ProcessItem::cmp_name_dec), - ProcessItemSortOrder::CpuUsageInc => self.processes.sort_by(ProcessItem::cmp_cpu_inc), - ProcessItemSortOrder::CpuUsageDec => self.processes.sort_by(ProcessItem::cmp_cpu_dec), - ProcessItemSortOrder::MemoryUsageInc => self.processes.sort_by(ProcessItem::cmp_mem_inc), - ProcessItemSortOrder::MemoryUsageDec => self.processes.sort_by(ProcessItem::cmp_mem_dec), - } - - // assign field to new sort variant - self.sort = sort.clone(); - - // update selection if following - if self.selection_state.follow_selection { - self.selection_state.selection = selection_pid.and_then(|p| { - self.processes - .iter() - .position(|item| item.pid() == p) - }); - } - } - - pub fn move_selection(&mut self, dir: MoveSelection) { - self.selection_state.move_selection(dir, self.processes.len()); - } - - pub fn toggle_follow_selection(&mut self) { - if self.processes.len() == 0 { - // nothing to follow: do not toggle, enforce false - self.selection_state.set_follow(false); - } - else { - self.selection_state.set_follow(!self.selection_state.follow_selection); - } - } - - // GETTERS - pub fn is_follow_selection(&self) -> bool { - self.selection_state.follow_selection - } - - pub fn selection(&self) -> Option { - self.selection_state.selection - } - - pub fn is_empty(&self) -> bool { - self.processes.len() == 0 - } - - pub fn len(&self) -> usize { - self.processes.len() - } - - pub fn selection_item(&self) -> Option<&ProcessItem> { - if let Some(selection) = self.selection_state.selection { - let selection_item = self.processes.get(selection); - return selection_item - } - - None - } - - pub fn selection_pid(&self) -> Option { - if let Some(selection) = self.selection_state.selection { - if let Some(item) = self.processes.get(selection) { - return Some(item.pid()) - } - else { - return None - } - } - - None - } - - pub fn sort_order(&self) -> &ProcessItemSortOrder { - &self.sort - } - - pub fn iterate(&self, start_idx: usize, max_amount: usize) -> ProcessItemIterator { - ProcessItemIterator::new(&self.processes, self.selection_state.selection, start_idx, max_amount) - } -} - -/* -#[cfg(test)] -mod test { - use crate::models::process_list::{ProcessList, ListSortOrder, MoveSelection}; - use crate::models::process_list_item::ProcessListItem; - - #[test] - fn test_constructors() { - // Default constructor. - let empty_instance = ProcessList::default(); - assert!(empty_instance.is_empty()); - assert_eq!(empty_instance.selection(), None); - - // New constructor. - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test"), String::from("test")); - let items = vec![item_0, item_1]; - let instance = ProcessList::new(items); - 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.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.is_empty()); - assert_eq!(filter_instance.selection(), Some(0)); - } - - #[test] - fn test_update() { - // Update with empty list of items. - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test"), String::from("test")); - let items = vec![item_0, item_1]; - let mut instance = ProcessList::new(items); - let empty_items = vec![]; - let _ = instance.update(empty_items); - assert!(instance.is_empty()); - assert!(instance.selection().is_none()); - - // Update with non-empty list of items. - let item_0 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test"), 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, 0, 10, 10, String::from("test"), 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, 0, 10, 10, String::from("test"), String::from("test")); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test"), String::from("test")); - let items = vec![item_0, item_1]; - let mut instance = ProcessList::new(items); - let _ = instance.toggle_follow_selection(); - let empty_items = vec![]; - let _ = instance.update(empty_items); - assert!(instance.is_empty()); - 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, 0, 10, 10, String::from("test"), String::from("test")); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test"), String::from("test")); - let items = vec![item_0, item_1]; - let mut instance = ProcessList::new(items); - let _ = instance.toggle_follow_selection(); - let item_2 = ProcessListItem::new(3, String::from("c"), 3.0, 3, 0, 10, 10, String::from("test"), 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, 0, 10, 10, String::from("test"), String::from("test")); - let item_1 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test"), String::from("test")); - let items = vec![item_0, item_1]; - let mut instance = ProcessList::new(items); - let _ = instance.toggle_follow_selection(); - let item_2 = ProcessListItem::new(2, String::from("b"), 2.0, 2, 0, 10, 10, String::from("test"), String::from("test")); - let item_3 = ProcessListItem::new(3, String::from("c"), 3.0, 3, 0, 10, 10, String::from("test"), String::from("test")); - let new_items = vec![item_2, item_3]; - let _ = instance.update(new_items); - assert!(!instance.is_empty()); - assert_eq!(instance.selection(), Some(0)); - } - - #[test] - fn test_sort() { - // Test sort when follow_selection = false. - let item_0 = ProcessListItem::new(1, String::from("a"), 2.0, 2, 0, 10, 10, String::from("test"), String::from("test")); - let item_1 = ProcessListItem::new(2, String::from("b"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); - let items = vec![item_1, item_0]; - let mut instance = ProcessList::new(items); - assert!(instance.sort == ListSortOrder::CpuUsageDec); - assert!(!instance.is_follow_selection()); - assert_eq!(instance.selection(), Some(0)); - let _ = instance.sort(&ListSortOrder::CpuUsageInc); - assert_eq!(instance.selection(), Some(0)); - - - let item_0 = ProcessListItem::new(1, String::from("a"), 2.0, 2, 0, 10, 10, String::from("test"), String::from("test")); - let item_1 = ProcessListItem::new(2, String::from("b"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); - let items = vec![item_0, item_1]; - let mut instance = ProcessList::new(items); - assert!(instance.sort == ListSortOrder::CpuUsageDec); - let _ = instance.toggle_follow_selection(); - assert!(instance.is_follow_selection()); - assert_eq!(instance.selection(), Some(0)); - let _ = instance.sort(&ListSortOrder::CpuUsageInc); - assert_eq!(instance.selection(), Some(1)); - } - - #[test] - fn test_selection() { - let mut empty_instance = ProcessList::default(); - empty_instance.move_selection(MoveSelection::Down); - assert_eq!(empty_instance.selection(), None); - - let item_0 = ProcessListItem::new(1, String::from("a"), 2.0, 2, 0, 10, 10, String::from("test"), String::from("test")); - let item_1 = ProcessListItem::new(2, String::from("b"), 1.0, 1, 0, 10, 10, String::from("test"), String::from("test")); - let items = vec![item_0, item_1]; - let mut instance = ProcessList::new(items); - assert_eq!(instance.selection(), Some(0)); - instance.move_selection(MoveSelection::Down); - instance.move_selection(MoveSelection::Down); - assert_eq!(instance.selection(), Some(1)); - - instance.move_selection(MoveSelection::Up); - instance.move_selection(MoveSelection::Up); - assert_eq!(instance.selection(), Some(0)); - - instance.move_selection(MoveSelection::End); - instance.move_selection(MoveSelection::End); - assert_eq!(instance.selection(), Some(1)); - - instance.move_selection(MoveSelection::Top); - instance.move_selection(MoveSelection::Top); - assert_eq!(instance.selection(), Some(0)); - - instance.move_selection(MoveSelection::MultipleDown); - instance.move_selection(MoveSelection::MultipleDown); - assert_eq!(instance.selection(), Some(1)); - - 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/models/vec_model.rs b/src/models/vec_model.rs new file mode 100644 index 0000000..b714c1e --- /dev/null +++ b/src/models/vec_model.rs @@ -0,0 +1,42 @@ +// Simple wrapper over Vector +pub struct VecModel { + items: Vec +} + +impl VecModel { + pub fn new(items: Vec) -> Self { + Self { + items, + } + } + + // MUTATORS + pub fn push(&mut self, item: T) { + self.items.push(item); + } + + pub fn pop(&mut self) -> Option { + self.items.pop() + } + + pub fn clear(&mut self) { + self.items.clear(); + } + + pub fn replace(&mut self, new_items: Vec) { + self.items = new_items; + } + + // GETTERS + pub fn len(&self) -> usize { + self.items.len() + } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + pub fn items(&self) -> &[T] { + &self.items + } +} \ No newline at end of file diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..2e4a202 --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1,5 @@ +pub mod sysinfo_service; + +pub trait ListProvider { + fn fetch_items(&self) -> Vec; +} \ No newline at end of file diff --git a/src/components/sysinfo_wrapper.rs b/src/services/sysinfo_service.rs similarity index 70% rename from src/components/sysinfo_wrapper.rs rename to src/services/sysinfo_service.rs index ed996c9..d07cf84 100644 --- a/src/components/sysinfo_wrapper.rs +++ b/src/services/sysinfo_service.rs @@ -1,17 +1,17 @@ use sysinfo::{Pid, System, Components}; -use crate::models::process_list::process_item::ProcessItem; -use crate::models::items::{memory_item::MemoryItem, temp_item::TempItem, cpu_item::CpuItem}; +use crate::models::items::{memory_item::MemoryItem, temp_item::TempItem, cpu_item::CpuItem, process_item::ProcessItem}; use crate::config::Config; +use crate::services::ListProvider; // 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 SysInfoWrapper { +pub struct SysInfoService { system: System, components: Components, pub _config: Config } -impl SysInfoWrapper { +impl SysInfoService { pub fn new(config: Config) -> Self { Self { system: System::new_all(), @@ -155,4 +155,63 @@ impl SysInfoWrapper { res } +} + +impl ListProvider for SysInfoService { + fn fetch_items(&self) -> Vec { + let mut processes: Vec = Vec::new(); + + for (pid, process) in self.system.processes() { + 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 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 path = if let Some(path) = process.exe() { + if let Some(path) = path.to_str() { + path.to_string() + } + else { + String::from("Non-valid Unicode") + } + } + else { + String::from("Permission Denied") + }; + + let item = ProcessItem::new( + pid.as_u32(), + name, + cpu_usage, + memory_usage, + start_time, + run_time, + accumulated_cpu_time, + status, + path, + ); + + processes.push(item); + } + + return processes; + } } \ No newline at end of file diff --git a/src/state/list_state.rs b/src/state/list_state.rs deleted file mode 100644 index 25468f5..0000000 --- a/src/state/list_state.rs +++ /dev/null @@ -1,237 +0,0 @@ -// Wrapper around Vec that provides selection state -// Data here is unstructured: To get structured (sorted) -// data use iterators - -// In cases where the ListState is dynamically updated -// set -use crate::components::utils::selection::SelectionState; - -pub struct ListState { - items: T, - selection_state: SelectionState, -} - -impl ListState> { - // CONSTRUCTORS::BEGIN - pub fn new(items: Vec) -> Self { - let selection_state = if items.is_empty() { SelectionState::new(None) } else { SelectionState::new(Some(0)) }; - - Self { - items, - selection_state, - } - } - - pub fn filter(&self, mut predicate: F) -> Self - where - T: Clone, - F: FnMut(&T) -> bool - { - let filtered_items: Vec = self.items - .iter() - .filter(|x| predicate(x)) - .cloned() - .collect(); - - let selection_state = if filtered_items.is_empty() { - SelectionState::new(None) - } - else { - SelectionState::new(Some(0)) - }; - - Self { - items: filtered_items, - selection_state, - } - } - // CONSTRUCTORS::END - - // MUTATORS::BEGIN - pub fn replace(&mut self, new_items: Vec) { - if new_items.is_empty() { - self.set_selection(None); - } - else { - self.set_selection(Some(0)); - } - - self.items = new_items; - } - - pub fn set_selection(&mut self, idx: Option) { - let len = self.items.len(); - - // items empty case - if len == 0 { - self.selection_state.set_selection(None); - return; - } - - if let Some(idx) = idx { - // index within length: set selection to index - if idx < len { - self.selection_state.set_selection(Some(idx)); - } - else { - // index greater than or equal to length: clamp selection to max_index - let max_idx = len.saturating_sub(1); - self.selection_state.set_selection(Some(max_idx)); - } - } - else { - self.selection_state.set_selection(None); - } - } - - pub fn select_next(&mut self) { - let len = self.items.len(); - - if let Some(idx) = self.selection_state.selection { - if idx.saturating_add(1) < len { - self.selection_state.set_selection(Some(idx.saturating_add(1))); - } - } - } - - pub fn select_prev(&mut self) { - if let Some(idx) = self.selection_state.selection { - if idx > 0 { - self.selection_state.set_selection(Some(idx.saturating_sub(1))); - } - } - } - - pub fn select_first(&mut self) { - if !self.items.is_empty() { - self.selection_state.set_selection(Some(0)); - } - } - - pub fn select_last(&mut self) { - if !self.items.is_empty() { - self.selection_state.set_selection(Some(self.items.len().saturating_sub(1))); - } - } - - pub fn toggle_follow_selection(&mut self) { - if !self.items.is_empty() { - // toggle - self.selection_state.set_follow(!self.selection_state.follow_selection); - } - else { - // no items: must be false - self.selection_state.set_follow(false); - } - } - // MUTATORS::END - - // GETTERS::BEGIN - pub fn selected(&self) -> Option<&T> { - self.selection_state.selection.and_then(|i| self.items.get(i)) - } - - pub fn len(&self) -> usize { - self.items.len() - } - - pub fn is_empty(&self) -> bool { - self.items.is_empty() - } - - pub fn items(&self) -> &[T] { - &self.items - } - // GETTERS::END - - // ITERATORS - pub fn iter_sorted<'a, F>(&'a self, mut compare: F) -> impl Iterator + 'a - where F: FnMut(&T, &T) -> std::cmp::Ordering, - { - // generate indices 0..n - let mut indices: Vec = (0..self.items.len()).collect(); - - // sort indices based on comparing the underlying items - indices.sort_by(|&i, &j| compare(&self.items[i], &self.items[j])); - - // return an iterator of &T in sorted order - indices.into_iter().map(move |i| &self.items[i]) - } -} - -impl ListState> { - pub fn replace_with_follow(&mut self, new_items: Vec) - where T: Clone - { - let old_selection = self.selection_state.selection.and_then(|idx| self.items.get(idx)).cloned(); - self.items = new_items; - - if self.selection_state.follow_selection { - if let Some(selected_item) = old_selection { - self.selection_state.set_selection({ - self.items - .iter() - .position(|x| *x == selected_item) - }); - } - else { - self.selection_state.set_selection(Some(0)) - } - } - else { - if self.items.is_empty() { - self.selection_state.set_selection(None) - } - else { - self.selection_state.set_selection(Some(0)) - } - } - } -} - -impl IntoIterator for ListState> { - type Item = T; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.items.into_iter() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_new_list_state_with_items() { - let items = vec![1, 2, 3]; - let state = ListState::new(items.clone()); - assert_eq!(state.items, items); - assert_eq!(state.selection_state.selection, Some(0)); - } - - #[test] - fn test_set_selection_clamps() { - let items = vec![1, 2, 3]; - let mut state = ListState::new(items); - state.set_selection(Some(10)); // out of bounds - assert_eq!(state.selection_state.selection, Some(2)); - } - - #[test] - fn test_filter() { - let items = vec![1, 2, 3, 4, 5]; - let state = ListState::new(items); - let filtered = state.filter(|x| *x % 2 == 0); - assert_eq!(filtered.items, vec![2, 4]); - assert_eq!(state.selection_state.selection, Some(0)); - } - - #[test] - fn test_iter_sorted() { - let items = vec![3, 1, 2]; - let state = ListState::new(items); - let sorted: Vec<_> = state.iter_sorted(|a, b| a.cmp(b)).copied().collect(); - assert_eq!(sorted, vec![1, 2, 3]); - } -} \ No newline at end of file diff --git a/src/state/mod.rs b/src/state/mod.rs deleted file mode 100644 index b9eb922..0000000 --- a/src/state/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod list_state; -pub mod selection_state; -pub mod process_sort_order; \ No newline at end of file diff --git a/src/state/process_sort_order.rs b/src/state/process_sort_order.rs deleted file mode 100644 index 3c644ef..0000000 --- a/src/state/process_sort_order.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::cmp::Ordering; -use crate::models::process_list::process_item::ProcessItem; - -pub enum ProcessSortOrder { - PidInc, - PidDec, - NameInc, - NameDec, - CpuUsageInc, - CpuUsageDec, - MemoryUsageInc, - MemoryUsageDec, -} - -impl ProcessSortOrder { - pub fn compare(&self, a: &ProcessItem, b: &ProcessItem) -> Ordering { - match self { - ProcessSortOrder::PidInc => a.pid().cmp(&b.pid()), - ProcessSortOrder::PidDec => b.pid().cmp(&a.pid()), - ProcessSortOrder::NameInc => a.name().cmp(&b.name()), - ProcessSortOrder::NameDec => b.name().cmp(&a.name()), - ProcessSortOrder::CpuUsageInc => a.cpu_usage().partial_cmp(&b.cpu_usage()).unwrap_or(Ordering::Equal), - ProcessSortOrder::CpuUsageDec => b.cpu_usage().partial_cmp(&a.cpu_usage()).unwrap_or(Ordering::Equal), - ProcessSortOrder::MemoryUsageInc => a.memory_usage().cmp(&b.memory_usage()), - ProcessSortOrder::MemoryUsageDec => b.memory_usage().cmp(&a.memory_usage()), - } - } -} \ No newline at end of file diff --git a/src/state/selection_state.rs b/src/state/selection_state.rs deleted file mode 100644 index c0c8307..0000000 --- a/src/state/selection_state.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub struct SelectionState { - pub selection: Option, - pub follow_selection: bool, -} - -impl SelectionState { - pub fn new(idx: Option) -> Self { - Self { - selection: idx, - follow_selection: false, - } - } - - pub fn set_selection(&mut self, idx: Option) { - self.selection = idx; - } - - pub fn set_follow(&mut self, follow: bool) { - self.follow_selection = follow; - } -} \ No newline at end of file diff --git a/src/states/README.md b/src/states/README.md new file mode 100644 index 0000000..c56c512 --- /dev/null +++ b/src/states/README.md @@ -0,0 +1,30 @@ +### Overall Design + +/components: UI rendering -- how state is drawn & responds to events +/state: UI state -- logical state that drives the UI +/models: Domain data & business logic -- set of rules, computations, and workflows that define how the app solves real-world problems + +/state: + ListState: selection, sort, vec, filter + +/models: + ProcessItem + ProcessList + +### Design +- state/: contains UI state, not rendering code. + +#### UI + + +---- + +#### Backend +1. Backend should provide indices to UI views + 1. E.g., UI calls for sorted representation of ProcessList, Backend should return a Vec where each element referes to an index in the original Vec + 1. Idea: Implement iterator "functions?" that created the correct references vectors + 2. E.g., Vec = [1, 3, 4, 2]; UI calls to sort, backend returns: Vec = [0, 3, 1, 2]; + 3. the Vec in ListState should be immutable (Not mut methods allowed) + 4. Idea: It might be worth storing a reference to the selected item in Vec + +ListState: Vec \ No newline at end of file diff --git a/src/states/mod.rs b/src/states/mod.rs new file mode 100644 index 0000000..7c58275 --- /dev/null +++ b/src/states/mod.rs @@ -0,0 +1 @@ +pub mod vec_state; \ No newline at end of file diff --git a/src/states/vec_state.rs b/src/states/vec_state.rs new file mode 100644 index 0000000..1803968 --- /dev/null +++ b/src/states/vec_state.rs @@ -0,0 +1,130 @@ +use crate::models::vec_model::VecModel; +use crate::models::{Filterable, Sortable}; + +pub struct VecState { + model: VecModel, + selection: Option, + sort: Option, + filter: Option, +} + +impl VecState { + pub fn new(model: Vec, selection: Option, sort: Option, filter: Option) -> Self { + let model = VecModel::new(model); + + Self { + model, + selection, + sort, + filter, + } + } + + // MUTATORS + pub fn set_selection(&mut self, selection: Option) { + self.selection = selection; + } + + pub fn set_sort(&mut self, sort: Option) { + self.sort = sort; + } + + pub fn set_filter(&mut self, filter: Option<&str>) { + self.filter = if let Some(filter) = filter { + Some(String::from(filter)) + } + else { + None + }; + } + + pub fn push(&mut self, item: T) { + self.model.push(item); + } + + pub fn pop(&mut self) -> Option { + self.model.pop() + } + + pub fn clear(&mut self) { + self.model.clear(); + } + + pub fn replace(&mut self, new_items: Vec) { + self.model.replace(new_items); + } + + // GETTERS + pub fn len(&self) -> usize { + self.model.len() + } + + pub fn is_empty(&self) -> bool { + self.model.is_empty() + } + + pub fn list(&self) -> &[T] { + &self.model.items() + } +} + +impl VecState +where + T: Filterable + Sortable +{ + pub fn view_indices(&self) -> Vec { + let mut indices: Vec = (0..self.model.items().len()).collect(); + + if let Some(filter) = &self.filter { + indices.retain(|&i| self.model.items()[i].matches_filter(filter)); + } + + if let Some(sort) = &self.sort { + indices.sort_by(|&i, &j| self.model.items()[i].cmp_with(&self.model.items()[j], sort)); + } + + indices + } + + pub fn iter_with_selection(&self) -> impl Iterator { + let indices = self.view_indices(); + let selected = self.selection; + + let res = indices.into_iter().map(move |i| { + let is_selected = Some(i) == selected; + (i, &self.model.items()[i], is_selected) + }); + + res + } +} + +#[cfg(test)] +mod tests { + use std::vec; + use crate::models::items::process_item::{ProcessItem, ProcessItemSortOrder}; + use super::*; + + #[test] + fn test() { + let items: Vec = vec![ + ProcessItem::new(0, String::from("Discord"), 12.0, 12, 12, 12, 12, String::from("Runnable"), String::from("test/")), + ProcessItem::new(9, String::from("Discord-Helper"), 20.0, 20, 20, 20, 20, String::from("Runnable"), String::from("test/")), + ProcessItem::new(3, String::from("iTerm"), 9.0, 9, 9, 9, 9, String::from("Runnable"), String::from("test/")), + ProcessItem::new(11, String::from("process-display"), 2.0, 2, 2, 2, 2, String::from("Runnable"), String::from("test/")), + ]; + + let sort: Option = Some(ProcessItemSortOrder::CpuUsageDec); + let selection: Option = Some(0); + let filter: Option = None; + + let list: VecState = VecState::new(items, selection, sort, filter); + + let view = list.iter_with_selection(); + view.for_each(|(idx, item, sel)| { + + let text = format!("idx={}, item.name={}, sel={}", idx, item.name(), sel); + println!("{text}"); + }); + } +} \ No newline at end of file From 1f6597c4a1171040a1a39e9ff8878e77f5b284c7 Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Fri, 18 Jul 2025 17:10:58 -0500 Subject: [PATCH 4/5] adding key & mouse input adapter for crossterm events --- src/app.rs | 61 ++-- src/components/command.rs | 31 -- src/components/cpu.rs | 8 +- src/components/error.rs | 12 +- src/components/filter.rs | 16 +- src/components/help.rs | 17 +- src/components/memory.rs | 20 +- src/components/mod.rs | 24 +- src/components/process.rs | 487 +++++++++++++++++++++---------- src/components/temp.rs | 27 +- src/config.rs | 129 ++++---- src/events/event.rs | 37 +-- src/input.rs | 65 +++++ src/main.rs | 3 +- src/models/items/process_item.rs | 2 +- src/services/mod.rs | 9 +- src/services/sysinfo_service.rs | 97 +++--- src/states/vec_state.rs | 20 +- 18 files changed, 664 insertions(+), 401 deletions(-) create mode 100644 src/input.rs diff --git a/src/app.rs b/src/app.rs index 123e0b3..7244a6b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; - use anyhow::{Ok, Result}; use crossterm::event::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}; use ratatui::prelude::*; +use crate::input::{Key, Mouse, MouseKind}; use crate::components::temp::TempComponent; use crate::components::Refreshable; use crate::config::Config; @@ -11,8 +11,8 @@ use crate::components::{ memory::MemoryComponent, process::ProcessComponent, error::ErrorComponent, - Component, EventState, + Component, DrawableComponent, help::HelpComponent, }; @@ -74,7 +74,7 @@ impl App { self.process.refresh(&self.service); self.memory.update(&self.service); self.cpu.update(&self.service); - self.temp.update(&self.service); + self.temp.refresh(&self.service); Ok(EventState::Consumed) } @@ -83,14 +83,14 @@ impl App { self.expand = !self.expand } - pub fn key_event(&mut self, key: KeyEvent) -> Result { + pub fn key_event(&mut self, key: Key) -> Result { if self.component_event(key)?.is_consumed() { return Ok(EventState::Consumed); } - else if self.move_focus(key)?.is_consumed() { + else if self.move_focus_key(key)?.is_consumed() { return Ok(EventState::Consumed); } - else if key.code == self.config.key_config.expand { + else if key == self.config.key_config.expand { self.toggle_expand(); return Ok(EventState::Consumed); @@ -99,16 +99,34 @@ impl App { Ok(EventState::NotConsumed) } - pub fn mouse_event(&mut self, mouse: MouseEvent) -> Result { - if self.move_focus_mouse_test(mouse)?.is_consumed() { + pub fn mouse_event(&mut self, mouse: Mouse) -> Result { + //TODO: + // 1. change component_event -> component_key_event + // 2. create component_mouse_event + // 3. figure out how to select Cells? e.g., selecting a process in the process list + match self.focus { + MainFocus::Process => { + if self.process.mouse_event(mouse)?.is_consumed() { + return Ok(EventState::Consumed) + } + } + _ => {} + } + + if self.move_focus_mouse(mouse)?.is_consumed() { + return Ok(EventState::Consumed) + } + + if matches!(mouse.kind, MouseKind::MiddleClick) { + self.toggle_expand(); return Ok(EventState::Consumed) } Ok(EventState::NotConsumed) } - fn move_focus_mouse_test(&mut self, mouse: MouseEvent) -> Result { - if let MouseEventKind::Down(MouseButton::Left) = mouse.kind { + fn move_focus_mouse(&mut self, mouse: Mouse) -> Result { + if matches!(mouse.kind, MouseKind::LeftClick) { let col = mouse.column; let row = mouse.row; @@ -119,40 +137,41 @@ impl App { } } } + return Ok(EventState::NotConsumed) } - fn component_event(&mut self, key: KeyEvent) -> Result { - if self.error.event(key)?.is_consumed() { + fn component_event(&mut self, key: Key) -> Result { + if self.error.key_event(key)?.is_consumed() { return Ok(EventState::Consumed) } - if self.help.event(key)?.is_consumed() { + if self.help.key_event(key)?.is_consumed() { return Ok(EventState::Consumed) } match self.focus { MainFocus::CPU => { - if self.cpu.event(key)?.is_consumed() { + if self.cpu.key_event(key)?.is_consumed() { return Ok(EventState::Consumed) } } MainFocus::Memory => { - if self.memory.event(key)?.is_consumed() { + if self.memory.key_event(key)?.is_consumed() { return Ok(EventState::Consumed) } } MainFocus::Temp => { - if self.temp.event(key)?.is_consumed() { + if self.temp.key_event(key)?.is_consumed() { return Ok(EventState::Consumed) } } MainFocus::Process => { - if self.process.event(key)?.is_consumed() { + if self.process.key_event(key)?.is_consumed() { return Ok(EventState::Consumed) } // terminate case - if key.code == self.config.key_config.terminate { + if key == self.config.key_config.terminate { //self.process.terminate_process(&self.system_wrapper); return Ok(EventState::Consumed) @@ -163,8 +182,8 @@ impl App { Ok(EventState::NotConsumed) } - fn move_focus(&mut self, key: KeyEvent) -> Result { - if key.code == self.config.key_config.tab { + fn move_focus_key(&mut self, key: Key) -> Result { + if key == self.config.key_config.tab { match self.focus { MainFocus::CPU => { self.focus = MainFocus::Memory @@ -316,7 +335,7 @@ impl App { trait Contains { fn contains(&self, col: u16, row: u16) -> bool; } -impl Contains for ratatui::layout::Rect { +impl Contains for Rect { fn contains(&self, col: u16, row: u16) -> bool { col >= self.x && col < self.x + self.width diff --git a/src/components/command.rs b/src/components/command.rs index 29572d4..af36979 100644 --- a/src/components/command.rs +++ b/src/components/command.rs @@ -60,17 +60,6 @@ pub fn filter_submit(key: &KeyConfig) -> CommandText { ) } -pub fn change_tab(key: &KeyConfig) -> CommandText { - CommandText::new( - format!( - "Move tab left/right [{:?}/{:?}]", - key.tab_left, - key.tab_right, - ), - CMD_GROUP_GENERAL - ) -} - pub fn exit_popup(key: &KeyConfig) -> CommandText { CommandText::new( format!( @@ -143,24 +132,4 @@ pub fn sort_list_by_memory_usage(key: &KeyConfig) -> CommandText { ), CMD_GROUP_GENERAL ) -} - -pub fn follow_selection(key: &KeyConfig) -> CommandText { - CommandText::new( - format!( - "Toggle follow selection [{:?}]", - key.follow_selection, - ), - 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 index 4848eb2..cadc3a1 100644 --- a/src/components/cpu.rs +++ b/src/components/cpu.rs @@ -4,7 +4,7 @@ use ratatui::prelude::*; use ratatui::widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, List, ListItem, ListState}; use std::str::FromStr; use anyhow::{Ok, Result}; -use crossterm::event::KeyEvent; +use crate::input::*; use super::EventState; use crate::components::common_nav; use crate::services::sysinfo_service::SysInfoService; @@ -119,7 +119,7 @@ impl CPUComponent { } impl Component for CPUComponent { - fn event(&mut self, key: KeyEvent) -> Result { + fn key_event(&mut self, key: Key) -> Result { if let Some(dir) = common_nav(key, &self.config.key_config) { self.selection_state.move_selection(dir, self.cpus.len() + self.selection_offset); @@ -128,6 +128,10 @@ impl Component for CPUComponent { Ok(super::EventState::NotConsumed) } + + fn mouse_event(&mut self, _mouse: Mouse) -> Result { + Ok(EventState::NotConsumed) + } } impl DrawableComponent for CPUComponent { diff --git a/src/components/error.rs b/src/components/error.rs index 6bc5467..4178d99 100644 --- a/src/components/error.rs +++ b/src/components/error.rs @@ -1,5 +1,5 @@ -use anyhow::Result; -use crossterm::event::KeyEvent; +use anyhow::{Ok, Result}; +use crate::input::*; use ratatui::{ Frame, prelude::*, @@ -43,9 +43,9 @@ impl ErrorComponent { } impl Component for ErrorComponent { - fn event(&mut self, key: KeyEvent) -> Result { + fn key_event(&mut self, key: Key) -> Result { if self.visible { - if key.code == self.config.key_config.exit { + if key == self.config.key_config.exit { self.error = String::new(); self.hide()?; return Ok(EventState::Consumed); @@ -54,6 +54,10 @@ impl Component for ErrorComponent { } return Ok(EventState::NotConsumed) } + + fn mouse_event(&mut self, _mouse: Mouse) -> Result { + Ok(EventState::NotConsumed) + } } impl DrawableComponent for ErrorComponent { diff --git a/src/components/filter.rs b/src/components/filter.rs index 0d94c4b..5f4906d 100644 --- a/src/components/filter.rs +++ b/src/components/filter.rs @@ -1,5 +1,5 @@ -use anyhow::Result; -use crossterm::event::{KeyEvent, KeyCode}; +use anyhow::{Ok, Result}; +use crate::input::*; use ratatui::{ Frame, prelude::*, @@ -41,19 +41,23 @@ impl FilterComponent { } impl Component for FilterComponent { - fn event(&mut self, key: KeyEvent) -> Result { - match key.code { - KeyCode::Char(c) => { + fn key_event(&mut self, key: Key) -> Result { + match key { + Key::Char(c) => { self.input_str.push(c); Ok(EventState::Consumed) } - KeyCode::Backspace => { + Key::Backspace => { self.input_str.pop(); Ok(EventState::Consumed) } _ => Ok(EventState::NotConsumed) } } + + fn mouse_event(&mut self, _mouse: Mouse) -> Result { + Ok(EventState::NotConsumed) + } } impl DrawableComponent for FilterComponent { diff --git a/src/components/help.rs b/src/components/help.rs index 2d4ec63..6bbbd24 100644 --- a/src/components/help.rs +++ b/src/components/help.rs @@ -1,6 +1,7 @@ +use anyhow::Ok; use anyhow::Result; use itertools::Itertools; -use crossterm::event::KeyEvent; +use crate::input::*; use ratatui::{ Frame, prelude::*, @@ -102,27 +103,31 @@ impl HelpComponent { } impl Component for HelpComponent { - fn event(&mut self, key: KeyEvent) -> Result { + fn key_event(&mut self, key: Key) -> Result { if self.visible { - if key.code == self.config.key_config.exit { + if key == self.config.key_config.exit { self.hide(); return Ok(EventState::Consumed); } - else if key.code == self.config.key_config.move_down { + else if key == self.config.key_config.move_down { self.scroll_selection(true); return Ok(EventState::Consumed); } - else if key.code == self.config.key_config.move_up { + else if key == self.config.key_config.move_up { self.scroll_selection(false); return Ok(EventState::Consumed); } } - else if key.code == self.config.key_config.open_help { + else if key == self.config.key_config.open_help { self.show()?; return Ok(EventState::Consumed); } Ok(EventState::NotConsumed) } + + fn mouse_event(&mut self, _mouse: Mouse) -> Result { + Ok(EventState::NotConsumed) + } } impl DrawableComponent for HelpComponent { diff --git a/src/components/memory.rs b/src/components/memory.rs index 35faf38..919cb4b 100644 --- a/src/components/memory.rs +++ b/src/components/memory.rs @@ -1,10 +1,10 @@ use anyhow::{Ok, Result}; use ratatui::{ - layout::{Layout, Direction, Constraint}, + layout::{Constraint, Direction, Layout}, style::{Style, Stylize}, - widgets::{Block, Gauge}, + widgets::{Block, Borders, Gauge}, }; -use crossterm::event::KeyEvent; +use crate::input::*; use crate::services::sysinfo_service::SysInfoService; use crate::components::DrawableComponent; use crate::models::items::memory_item::MemoryItem; @@ -34,7 +34,11 @@ impl MemoryComponent { } impl Component for MemoryComponent { - fn event(&mut self, _key: KeyEvent) -> Result { + fn key_event(&mut self, _key: Key) -> Result { + Ok(EventState::NotConsumed) + } + + fn mouse_event(&mut self, _mouse: Mouse) -> Result { Ok(EventState::NotConsumed) } } @@ -62,8 +66,8 @@ impl DrawableComponent for MemoryComponent { let ram_title = format!(" {:<15} {:.2} GB / {:.2} GB ", ram_label, self.memory.used_memory_gb(), self.memory.total_memory_gb()); let g_ram = Gauge::default() - .block(Block::bordered().style(style).title(ram_title)) - .gauge_style(Style::new().red().on_black().italic()) + .block(Block::default().borders(Borders::LEFT | Borders::TOP | Borders::RIGHT).style(style).title(ram_title)) + .gauge_style(Style::new().light_cyan().on_black().italic()) .percent(ram_percent as u16); // swap widget @@ -72,8 +76,8 @@ impl DrawableComponent for MemoryComponent { let swap_title = format!(" {:<15} {:.2} GB / {:.2} GB ", swap_label, self.memory.used_swap_gb(), self.memory.total_swap_gb()); let g_swap = Gauge::default() - .block(Block::bordered().style(style).title(swap_title)) - .gauge_style(Style::new().magenta().on_black().italic()) + .block(Block::default().borders(Borders::LEFT | Borders::BOTTOM | Borders::RIGHT).style(style).title(swap_title)) + .gauge_style(Style::new().light_magenta().on_black().italic()) .percent(swap_percent as u16); f.render_widget(g_ram, vertical_chunks[0]); diff --git a/src/components/mod.rs b/src/components/mod.rs index 34c2c2f..e30950e 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use crossterm::event::KeyEvent; +use crate::input::{Key, Mouse}; use ratatui::prelude::*; use super::config::KeyConfig; pub mod filter; @@ -17,9 +17,19 @@ pub trait DrawableComponent { } pub trait Component { - fn event(&mut self, key: KeyEvent) -> Result; + fn key_event(&mut self, key: Key) -> Result; + fn mouse_event(&mut self, mouse: Mouse) -> Result; } +// trait Refreshable details: +// +// Refreshable is meant to be implemented in components that are refreshable +// via a service (e.g., components/process.rs). Currently, there is only +// one service available and can be found in services/sysinfo_service.rs-- +// this is essentially just a wrapper around the sysinfo crate. +// For more information on what sysinfo service provides to components, +// see trait VecProvider in services/mod.rs. +// pub trait Refreshable { fn refresh(&mut self, service: &S); } @@ -36,17 +46,17 @@ impl EventState { } } -pub fn common_nav(key: KeyEvent, key_config: &KeyConfig) -> Option { - if key.code == key_config.move_down { +pub fn common_nav(key: Key, key_config: &KeyConfig) -> Option { + if key == key_config.move_down { Some(MoveSelection::Down) } - else if key.code == key_config.move_bottom { + else if key == key_config.move_bottom { Some(MoveSelection::Bottom) } - else if key.code == key_config.move_up { + else if key == key_config.move_up { Some(MoveSelection::Up) } - else if key.code == key_config.move_top { + else if key == key_config.move_top { Some(MoveSelection::Top) } else { diff --git a/src/components/process.rs b/src/components/process.rs index a0bba0f..c83ef82 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -1,14 +1,16 @@ use anyhow::{Ok, Result}; -use crossterm::event::KeyEvent; -use ratatui::{Frame, prelude::*,}; -use crate::services::{sysinfo_service::SysInfoService, ListProvider}; -use crate::config::{Config, KeyConfig}; -use crate::components::{common_nav, DrawableComponent, Component, EventState, Refreshable}; +use ratatui::{Frame, prelude::*}; +use ratatui::widgets::{block::*, *}; +use crate::services::VecProvider; +use crate::config::*; +use crate::components::{common_nav, Component, DrawableComponent, EventState, MoveSelection, Refreshable}; use crate::components::{utils::{selection::UISelection, vertical_scroll::VerticalScroll}, filter::FilterComponent}; use crate::states::vec_state::VecState; use crate::models::items::process_item::{ProcessItem, ProcessItemSortOrder}; -#[derive(PartialEq, Clone)] +use crate::input::{Key, Mouse, MouseKind}; + +#[derive(PartialEq, Clone, Debug)] pub enum Focus { Filter, List, @@ -25,14 +27,15 @@ pub struct ProcessComponent { } impl ProcessComponent { - pub fn new(config: Config, sysinfo: &SysInfoService) -> Self { - let processes: Vec = sysinfo.fetch_items(); + pub fn new(config: Config, service: &S) -> Self + where S: VecProvider + { + let processes: Vec = service.fetch_items(); let ui_selection: UISelection = if processes.is_empty() { UISelection::new(None) } else { UISelection::new(Some(0)) }; let state_selection: Option = ui_selection.selection; let filter: Option = None; let sort: Option = None; let vec_state: VecState = VecState::new(processes, state_selection, sort.clone(), filter); - let scroll: VerticalScroll = VerticalScroll::new(); let filter_component: FilterComponent = FilterComponent::new(config.clone()); let focus: Focus = Focus::List; @@ -47,44 +50,86 @@ impl ProcessComponent { config, } } -} -impl Refreshable for ProcessComponent -where - S: ListProvider -{ - fn refresh(&mut self, service: &S) { - let processes: Vec = service.fetch_items(); - self.vec_state.replace(processes); + fn handle_move_selection(&mut self, dir: MoveSelection) { + let len = self.vec_state.view_indices().len(); + // move ui selection by dir + self.ui_selection.move_selection(dir, len); + // map ui selection -> vec state index + let vec_idx = self.compute_vec_state_idx(); + // update vec state selection to index + self.vec_state.set_selection(vec_idx); + } + fn handle_refresh_selection(&mut self) { let len = self.vec_state.view_indices().len(); + let max_idx = len.saturating_sub(1); + + let new_ui_selection: Option = if len == 0 { - self.ui_selection.set_selection(None); - self.vec_state.set_selection(self.ui_selection.selection); - return; + None } + else { + match self.ui_selection.selection { + Some(ui_selection) => { + // if out of bounds, clamp to max index + if ui_selection > max_idx { + Some(max_idx) + } + else { + Some(ui_selection) + } + } + // occurs when going from empty -> non-empty list + None => Some(0), + } + }; - // ui_selection iterates over vec_state.view_indices() - if let Some(ui_selection) = self.ui_selection.selection { - let max_idx = len.saturating_sub(1); + self.ui_selection.set_selection(new_ui_selection); + let vec_idx = self.compute_vec_state_idx(); + self.vec_state.set_selection(vec_idx); + } - if ui_selection > max_idx { - self.ui_selection.set_selection(Some(max_idx)); - let idx = self.vec_state.view_indices().get(max_idx).cloned(); - self.vec_state.set_selection(idx); - } - else { - let idx = self.vec_state.view_indices().get(ui_selection).cloned(); - self.vec_state.set_selection(idx); - } + fn handle_filter_selection(&mut self) { + let len = self.vec_state.view_indices().len(); + + let new_ui_selection: Option = + if len == 0 { + None } + else { + Some(0) + }; + + self.ui_selection.set_selection(new_ui_selection); + let vec_idx = self.compute_vec_state_idx(); + self.vec_state.set_selection(vec_idx); + } + + fn compute_vec_state_idx(&self) -> Option { + // map ui_selection.selection to vec_state + let vec_idx = self.ui_selection.selection + .and_then(|ui_selection| self.vec_state.view_indices().get(ui_selection).cloned()); + + vec_idx + } +} + +impl Refreshable for ProcessComponent +where + S: VecProvider +{ + fn refresh(&mut self, service: &S) { + let processes: Vec = service.fetch_items(); + self.vec_state.replace(processes); + self.handle_refresh_selection(); } } impl Component for ProcessComponent { - fn event(&mut self, key: KeyEvent) -> Result { - if key.code == self.config.key_config.filter && + fn key_event(&mut self, key: Key) -> Result { + if key == self.config.key_config.filter && matches!(self.focus,Focus::List) { self.focus = Focus::Filter; @@ -92,22 +137,13 @@ impl Component for ProcessComponent { } if matches!(self.focus, Focus::Filter) { - - if self.filter_component.event(key)?.is_consumed() { + if self.filter_component.key_event(key)?.is_consumed() { self.vec_state.set_filter(self.filter_component.filter_contents()); - - if self.vec_state.view_indices().len() > 0 { - // set ui_selection to beginning of view_indices - self.ui_selection.set_selection(Some(0)); - // set vec_state selection to the index at view_indices[0] - let idx = self.vec_state.view_indices().get(0).cloned(); - self.vec_state.set_selection(idx); - } - + self.handle_filter_selection(); return Ok(EventState::Consumed) } - - if key.code == self.config.key_config.enter { + + if key == self.config.key_config.enter { self.focus = Focus::List; return Ok(EventState::Consumed) } @@ -115,53 +151,50 @@ impl Component for ProcessComponent { if matches!(self.focus, Focus::List) { if let Some(move_dir) = common_nav(key, &self.config.key_config) { - let len = self.vec_state.view_indices().len(); - self.ui_selection.move_selection(move_dir, len); // if len == 0, ui_selection.selection is set to None here - - if let Some(ui_selection) = self.ui_selection.selection { - let idx = self.vec_state.view_indices().get(ui_selection).cloned(); - self.vec_state.set_selection(idx); - } - else { - self.vec_state.set_selection(None); - } - + self.handle_move_selection(move_dir); return Ok(EventState::Consumed) } - if let Some(sort_order) = process_sort(key, &self.config.key_config) { + if let Some(sort_order) = match_sort_order_key(key, &self.config.key_config) { self.sort = Some(sort_order); self.vec_state.set_sort(self.sort.clone()); - - if let Some(ui_selection) = self.ui_selection.selection { - let idx = self.vec_state.view_indices().get(ui_selection).cloned(); - self.vec_state.set_selection(idx); - } - else { - self.vec_state.set_selection(None); - } - + self.handle_refresh_selection(); // handle_refresh() also works for handling sort return Ok(EventState::Consumed) } + } + + Ok(EventState::NotConsumed) + } - if key.code == self.config.key_config.follow_selection { // TODO: implement follow selection? - return Ok(EventState::Consumed) + fn mouse_event(&mut self, mouse: Mouse) -> Result { + match mouse.kind { + MouseKind::ScrollDown | MouseKind::ScrollUp => { + let dir = match mouse.kind { + MouseKind::ScrollDown => MoveSelection::Down, + MouseKind::ScrollUp => MoveSelection::Up, + _ => unreachable!(), // safeguarded by conditional + }; + + self.handle_move_selection(dir); + return Ok(EventState::Consumed); } + _ => {} } - + Ok(EventState::NotConsumed) } } -fn process_sort(key: KeyEvent, key_config: &KeyConfig) -> Option { - if key.code == key_config.sort_pid_inc { return Some(ProcessItemSortOrder::PidInc) } - if key.code == key_config.sort_pid_dec { return Some(ProcessItemSortOrder::PidDec) } - if key.code == key_config.sort_cpu_usage_inc { return Some(ProcessItemSortOrder::CpuUsageInc) } - if key.code == key_config.sort_cpu_usage_dec { return Some(ProcessItemSortOrder::CpuUsageDec) } - if key.code == key_config.sort_memory_usage_inc { return Some(ProcessItemSortOrder::MemoryUsageInc) } - if key.code == key_config.sort_memory_usage_dec { return Some(ProcessItemSortOrder::MemoryUsageDec) } - if key.code == key_config.sort_name_inc { return Some(ProcessItemSortOrder::NameInc) } - if key.code == key_config.sort_name_dec { return Some(ProcessItemSortOrder::NameDec) } +// maps key to ProcessItemSortOrder +fn match_sort_order_key(key: Key, key_config: &KeyConfig) -> Option { + if key == key_config.sort_pid_inc { return Some(ProcessItemSortOrder::PidInc) } + if key == key_config.sort_pid_dec { return Some(ProcessItemSortOrder::PidDec) } + if key == key_config.sort_cpu_usage_inc { return Some(ProcessItemSortOrder::CpuUsageInc) } + if key == key_config.sort_cpu_usage_dec { return Some(ProcessItemSortOrder::CpuUsageDec) } + if key == key_config.sort_memory_usage_inc { return Some(ProcessItemSortOrder::MemoryUsageInc) } + if key == key_config.sort_memory_usage_dec { return Some(ProcessItemSortOrder::MemoryUsageDec) } + if key == key_config.sort_name_inc { return Some(ProcessItemSortOrder::NameInc) } + if key == key_config.sort_name_dec { return Some(ProcessItemSortOrder::NameDec) } return None } @@ -175,7 +208,8 @@ impl DrawableComponent for ProcessComponent { Constraint::Length(3), //filter ]).split(area); - let visible_list_height = horizontal_chunks[0].height.saturating_sub(3) as usize; + // sub 3 here for table header (1 line) and borders (2 lines) + let visible_list_height = horizontal_chunks[0].height.saturating_sub(4) as usize; // update vertical scroll let indices = self.vec_state.view_indices(); @@ -184,7 +218,7 @@ impl DrawableComponent for ProcessComponent { { || self.scroll.reset() }, |idx| { - self.scroll.update(idx, len, visible_list_height,); + self.scroll.update(idx, len, visible_list_height); },); let visible_items = self.vec_state @@ -232,9 +266,6 @@ impl DrawableComponent for ProcessComponent { } } -use ratatui::widgets::{block::*, *}; -use crate::config::ThemeConfig; - fn draw_process_list<'a, I>( f: &mut Frame, area: Rect, @@ -246,77 +277,42 @@ fn draw_process_list<'a, I>( where I: Iterator, { - let follow_flag = false; // setting header - let header = ["", - if matches!(sort_order, Some(ProcessItemSortOrder::PidInc)) { "PID ▲" } - else if matches!(sort_order, Some(ProcessItemSortOrder::PidDec)) { "PID ▼" } - else { "PID" }, - - if matches!(sort_order, Some(ProcessItemSortOrder::NameInc)) { "Name ▲" } - else if matches!(sort_order, Some(ProcessItemSortOrder::NameDec)) { "Name ▼" } - else { "Name" }, - - if matches!(sort_order, Some(ProcessItemSortOrder::CpuUsageInc)) { "CPU (%) ▲" } - else if matches!(sort_order, Some(ProcessItemSortOrder::CpuUsageDec)) { "CPU (%) ▼" } - else { "CPU (%)" }, - - if matches!(sort_order, Some(ProcessItemSortOrder::MemoryUsageInc)) { "Memory (MB) ▲" } - else if matches!(sort_order, Some(ProcessItemSortOrder::MemoryUsageDec)) { "Memory (MB) ▼" } - else { "Memory (MB)" }, - + let header_labels = [ + "", + &header_with_sort(&sort_order, &ProcessItemSortOrder::PidInc, &ProcessItemSortOrder::PidDec, "PID"), + &header_with_sort(&sort_order, &ProcessItemSortOrder::NameInc, &ProcessItemSortOrder::NameDec, "Name"), + &header_with_sort(&sort_order, &ProcessItemSortOrder::CpuUsageInc, &ProcessItemSortOrder::CpuUsageDec, "CPU (%)"), + &header_with_sort(&sort_order, &ProcessItemSortOrder::MemoryUsageInc, &ProcessItemSortOrder::MemoryUsageDec, "Memory (MB)"), "Run (hh:mm:ss)", "Status", - "Path"] + "Path", + ]; + + let header = header_labels .into_iter() .map(Cell::from) .collect::() - .style( - if focus { - theme_config.style_border_focused - } - else { - theme_config.style_border_not_focused - } - ) - .height(1); + .style(if focus {theme_config.style_border_focused} else {theme_config.style_border_not_focused}) + .height(2); // setting rows let rows = visible_items - .map(|(idx, item, selected)| { - let style = - if focus && selected && follow_flag { - theme_config.style_item_selected_followed - } - else if focus && selected && !follow_flag { - theme_config.style_item_selected - } - else if focus { - theme_config.style_item_focused - } - else if !focus && selected & follow_flag { - theme_config.style_item_selected_followed_not_focused - } - else if !focus && selected & !follow_flag { - theme_config.style_item_selected_not_focused - } - else { - theme_config.style_item_not_focused - }; - - let cells: Vec = vec![ - if style == theme_config.style_item_selected || - style == theme_config.style_item_selected_followed || - style == theme_config.style_item_selected_followed_not_focused || - style == theme_config.style_item_selected_not_focused { - Cell::from(String::from("->")) - } - else { - Cell::from(String::from("")) - }, + .map(|(_idx, item, selected)| { + let style = compute_row_style(focus, selected, &theme_config); + + let is_selected_style = [ + theme_config.style_item_selected, + theme_config.style_item_selected_followed, + theme_config.style_item_selected_followed_not_focused, + theme_config.style_item_selected_not_focused, + ].contains(&style); + + let cells = vec![ + Cell::from(if is_selected_style {"->"} else { "" }), Cell::from(item.pid().to_string()), - Cell::from(item.name().to_string()), + Cell::from(item.name()), Cell::from(format!("{:.2}", item.cpu_usage())), Cell::from(format!("{:.2}", item.memory_usage()/1000000)), Cell::from(item.run_time_hh_mm_ss()), @@ -327,10 +323,10 @@ where }) .collect::>(); - // setting the width constraints. + // setting width constraints let widths = vec![ - Constraint::Length(2), + Constraint::Length(2), // arrow Constraint::Length(10), // pid Constraint::Length(50), // name Constraint::Length(15), // cpu usage @@ -342,13 +338,7 @@ where // setting block information let block_title: &str = " Process List "; - let block_style = - if focus { - theme_config.style_border_focused - } - else { - theme_config.style_border_not_focused - }; + let block_style = if focus { theme_config.style_border_focused } else { theme_config.style_border_not_focused }; // setting the table let table = Table::new(rows, widths) @@ -360,7 +350,192 @@ where f.render_widget(table, area); } +// helper function for building header labels +fn header_with_sort( + current: &Option, + inc: &ProcessItemSortOrder, + dec: &ProcessItemSortOrder, + base: &str, +) -> String { + match current { + Some(s) if s == inc => format!("{base} ▲"), + Some(s) if s == dec => format!("{base} ▼"), + _ => base.to_string(), + } +} + +// helper function for determining row style +fn compute_row_style(focus: bool, selected: bool, theme: &ThemeConfig) -> Style { + match (focus, selected) { + (true, true) => theme.style_item_selected, + (true, _,) => theme.style_item_focused, + (false, true) => theme.style_item_selected_not_focused, + _ => theme.style_item_not_focused, + } +} + #[cfg(test)] mod test { - // TODO: write tests -} + use super::*; + use crate::services::VecProvider; + use crate::models::items::process_item::ProcessItem; + + struct DummyService { + // index into test data + idx: usize + } + impl DummyService { + fn new() -> Self { + Self { + idx: 0, + } + } + + // move idx to load new test data + fn set(&mut self, idx: usize) { + self.idx = idx; + } + } + + impl VecProvider for DummyService { + fn fetch_items(&self) -> Vec { + test_data(self.idx) + } + } + + + #[test] + fn test_constructor_with_data() { + let mut service = DummyService::new(); + // 0=data, see test_data() + service.set(0); + let config = Config::default(); + let component = ProcessComponent::new(config.clone(), &service); + + // check vec_state contains expected number of items + assert_eq!(component.vec_state.len(), test_data(service.idx).len()); + // check that ui_selection is Some(0) since there is data + assert_eq!(component.ui_selection.selection, Some(0)); + // check that sort and filter are None + assert!(component.sort.is_none()); + assert!(component.vec_state.filter().is_none()); + // check focus + assert_eq!(component.focus, Focus::List); + } + + #[test] + fn test_constructor_with_no_data() { + let mut service = DummyService::new(); + // 2=no data, see test_data() + service.set(2); + let config = Config::default(); + let component = ProcessComponent::new(config.clone(), &service); + + // check vec_state contains expected number of items + assert_eq!(component.vec_state.len(), test_data(service.idx).len()); + // check that ui_selection is Some(0) since there is data + assert_eq!(component.ui_selection.selection, None); + // check that sort and filter are None + assert!(component.sort.is_none()); + assert!(component.vec_state.filter().is_none()); + // check focus + assert_eq!(component.focus, Focus::List); + } + + #[test] + fn test_handle_move_selection() { + let mut service = DummyService::new(); + service.set(0); + let config = Config::default(); + let mut component = ProcessComponent::new(config.clone(), &service); + + // testing boundaries + component.handle_move_selection(MoveSelection::Up); + assert_eq!(component.ui_selection.selection, Some(0)); + component.handle_move_selection(MoveSelection::Down); + assert_eq!(component.ui_selection.selection, Some(1)); + component.handle_move_selection(MoveSelection::Bottom); + assert_eq!(component.ui_selection.selection, Some(component.vec_state.view_indices().len().saturating_sub(1))); + component.handle_move_selection(MoveSelection::Down); + assert_eq!(component.ui_selection.selection, Some(component.vec_state.view_indices().len().saturating_sub(1))); + } + + #[test] + fn test_handle_refresh_selection() { + let mut service = DummyService::new(); + service.set(0); + let config = Config::default(); + let mut component = ProcessComponent::new(config.clone(), &service); + + // emulate refresh from non-empty to non-empty list + let ui_selection = component.ui_selection.selection; + service.set(1); + component.refresh(&service); + // ensure ui selection is same index (do not want ui selection to change unless out of bounds) + assert_eq!(component.ui_selection.selection, ui_selection); + assert!(component.vec_state.selection().is_some()); + + // emulate refresh from non-empty to empty list + service.set(2); + component.refresh(&service); + assert!(component.ui_selection.selection.is_none()); + assert!(component.vec_state.selection().is_none()); + + // emulate refresh from empty to empty list + service.set(2); + component.refresh(&service); + assert!(component.ui_selection.selection.is_none()); + assert!(component.vec_state.selection().is_none()); + + // emulate refresh from empty to non-empty list + service.set(0); + component.refresh(&service); + assert_eq!(component.ui_selection.selection, Some(0)); + assert!(component.vec_state.selection().is_some()); + } + + fn test_data(idx: usize) -> Vec { + match idx { + 0 => { + return vec![ + ProcessItem::new(0, String::from("Discord"), 12.0, 12, 12, 12, 12, String::from("Runnable"), String::from("test/")), + ProcessItem::new(1, String::from("Slack"), 8.5, 15, 15, 15, 15, String::from("Sleeping"), String::from("test/")), + ProcessItem::new(2, String::from("Chrome"), 25.3, 40, 40, 40, 40, String::from("Runnable"), String::from("test/")), + ProcessItem::new(3, String::from("iTerm"), 9.0, 9, 9, 9, 9, String::from("Runnable"), String::from("test/")), + ProcessItem::new(4, String::from("Spotify"), 7.2, 22, 22, 22, 22, String::from("Sleeping"), String::from("test/")), + ProcessItem::new(5, String::from("VSCode"), 18.1, 35, 35, 35, 35, String::from("Runnable"), String::from("test/")), + ProcessItem::new(6, String::from("SystemUIServer"), 1.5, 5, 5, 5, 5, String::from("Runnable"), String::from("test/")), + ProcessItem::new(7, String::from("Dock"), 0.8, 3, 3, 3, 3, String::from("Sleeping"), String::from("test/")), + ProcessItem::new(8, String::from("Finder"), 4.4, 18, 18, 18, 18, String::from("Runnable"), String::from("test/")), + ProcessItem::new(9, String::from("Discord-Helper"), 20.0, 20, 20, 20, 20, String::from("Runnable"), String::from("test/")), + ProcessItem::new(10, String::from("Photos"), 3.1, 12, 12, 12, 12, String::from("Sleeping"), String::from("test/")), + ProcessItem::new(11, String::from("process-display"), 2.0, 2, 2, 2, 2, String::from("Runnable"), String::from("test/")), + ProcessItem::new(12, String::from("Mail"), 1.2, 7, 7, 7, 7, String::from("Runnable"), String::from("test/")), + ProcessItem::new(13, String::from("Calendar"), 0.6, 6, 6, 6, 6, String::from("Sleeping"), String::from("test/")), + ProcessItem::new(14, String::from("Notes"), 0.4, 4, 4, 4, 4, String::from("Sleeping"), String::from("test/")), + ProcessItem::new(15, String::from("Preview"), 0.9, 8, 8, 8, 8, String::from("Runnable"), String::from("test/")), + ProcessItem::new(16, String::from("Safari"), 11.0, 30, 30, 30, 30, String::from("Runnable"), String::from("test/")), + ProcessItem::new(17, String::from("Terminal"), 5.7, 10, 10, 10, 10, String::from("Runnable"), String::from("test/")), + ProcessItem::new(18, String::from("Activity Monitor"), 2.9, 14, 14, 14, 14, String::from("Runnable"), String::from("test/")), + ProcessItem::new(19, String::from("Xcode"), 14.3, 50, 50, 50, 50, String::from("Runnable"), String::from("test/")), + ]; + } + 1 => { + return vec![ + ProcessItem::new(0, String::from("Discord"), 12.0, 12, 12, 12, 12, String::from("Runnable"), String::from("test/")), + ProcessItem::new(1, String::from("Slack"), 8.5, 15, 15, 15, 15, String::from("Sleeping"), String::from("test/")), + ProcessItem::new(2, String::from("Chrome"), 25.3, 40, 40, 40, 40, String::from("Runnable"), String::from("test/")), + ProcessItem::new(3, String::from("iTerm"), 9.0, 9, 9, 9, 9, String::from("Runnable"), String::from("test/")), + ProcessItem::new(4, String::from("Spotify"), 7.2, 22, 22, 22, 22, String::from("Sleeping"), String::from("test/")), + ProcessItem::new(5, String::from("VSCode"), 18.1, 35, 35, 35, 35, String::from("Runnable"), String::from("test/")), + ProcessItem::new(6, String::from("SystemUIServer"), 1.5, 5, 5, 5, 5, String::from("Runnable"), String::from("test/")), + ProcessItem::new(7, String::from("Dock"), 0.8, 3, 3, 3, 3, String::from("Sleeping"), String::from("test/")), + ProcessItem::new(8, String::from("Finder"), 4.4, 18, 18, 18, 18, String::from("Runnable"), String::from("test/")), + ProcessItem::new(9, String::from("Discord-Helper"), 20.0, 20, 20, 20, 20, String::from("Runnable"), String::from("test/")), + ProcessItem::new(10, String::from("Photos"), 3.1, 12, 12, 12, 12, String::from("Sleeping"), String::from("test/")), + ]; + } + _ => { return vec![]; } + } + } +} \ No newline at end of file diff --git a/src/components/temp.rs b/src/components/temp.rs index 3cd9a20..cfb9c01 100644 --- a/src/components/temp.rs +++ b/src/components/temp.rs @@ -4,10 +4,14 @@ use ratatui::prelude::*; use ratatui::Frame; use ratatui::widgets::Cell; use ratatui::widgets::Row; -use crossterm::event::KeyEvent; + +use crate::input::*; + +use crate::components::Refreshable; use crate::services::sysinfo_service::SysInfoService; use crate::components::utils::vertical_scroll::VerticalScroll; use crate::models::items::temp_item::TempItem; +use crate::services::VecProvider; use crate::{components::EventState, config::Config}; use super::{Component, DrawableComponent}; @@ -20,8 +24,7 @@ pub struct TempComponent { impl TempComponent { pub fn new(config: Config, sysinfo: &SysInfoService) -> Self { - let mut temps = Vec::new(); - sysinfo.get_temps(&mut temps); + let temps: Vec = sysinfo.fetch_items(); Self { config, @@ -30,29 +33,37 @@ impl TempComponent { vertical_scroll: VerticalScroll::new(), } } +} - pub fn update(&mut self, sysinfo: &SysInfoService) { - sysinfo.get_temps(&mut self.temps); +impl Refreshable for TempComponent +where S: VecProvider +{ + fn refresh(&mut self, service: &S) { + self.temps = service.fetch_items(); } } impl Component for TempComponent { - fn event(&mut self, key: KeyEvent) -> Result { + fn key_event(&mut self, key: Key) -> Result { let temps_max_idx = self.temps.len().saturating_sub(1); - if key.code == self.config.key_config.move_down { + if key == self.config.key_config.move_down { if self.ui_selection < temps_max_idx { self.ui_selection = self.ui_selection.saturating_add(1); } return Ok(super::EventState::Consumed); } - if key.code == self.config.key_config.move_up { + if key == self.config.key_config.move_up { self.ui_selection = self.ui_selection.saturating_sub(1); return Ok(super::EventState::Consumed); } Ok(super::EventState::NotConsumed) } + + fn mouse_event(&mut self, _mouse: Mouse) -> Result { + Ok(EventState::NotConsumed) + } } //TODO: take a closer look at: diff --git a/src/config.rs b/src/config.rs index 1a03f68..efcfb7f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,6 @@ -use crossterm::event::{KeyCode, MouseButton, MouseEventKind}; use serde::{Deserialize,Serialize}; -#[derive(Clone,Serialize,Deserialize)] +#[derive(Clone)] pub struct Config { pub key_config: KeyConfig, pub mouse_config: MouseConfig, @@ -45,86 +44,79 @@ impl Config { } } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone)] pub struct KeyConfig { - pub move_up: KeyCode, - pub move_top: KeyCode, - pub move_down: KeyCode, - pub move_bottom: KeyCode, - pub enter: KeyCode, - pub tab: KeyCode, - pub filter: KeyCode, - pub terminate: KeyCode, - pub tab_right: KeyCode, - pub tab_left: KeyCode, - pub open_help: KeyCode, - pub exit: KeyCode, - pub sort_name_inc: KeyCode, - pub sort_name_dec: KeyCode, - pub sort_pid_inc: KeyCode, - pub sort_pid_dec: KeyCode, - pub sort_cpu_usage_inc: KeyCode, - pub sort_cpu_usage_dec: KeyCode, - pub sort_memory_usage_inc: KeyCode, - pub sort_memory_usage_dec: KeyCode, - pub follow_selection: KeyCode, - pub toggle_themes: KeyCode, - pub process_info: KeyCode, - pub expand: KeyCode, + pub move_up: Key, + pub move_top: Key, + pub move_down: Key, + pub move_bottom: Key, + pub enter: Key, + pub tab: Key, + pub filter: Key, + pub terminate: Key, + pub open_help: Key, + pub exit: Key, + pub sort_name_inc: Key, + pub sort_name_dec: Key, + pub sort_pid_inc: Key, + pub sort_pid_dec: Key, + pub sort_cpu_usage_inc: Key, + pub sort_cpu_usage_dec: Key, + pub sort_memory_usage_inc: Key, + pub sort_memory_usage_dec: Key, + pub follow_selection: Key, + pub expand: Key, } impl Default for KeyConfig { fn default() -> Self { Self { - move_up: KeyCode::Up, - move_top: KeyCode::Char('W'), - move_down: KeyCode::Down, - move_bottom: KeyCode::Char('S'), - enter: KeyCode::Enter, - tab: KeyCode::Tab, - filter: KeyCode::Char('/'), - terminate: KeyCode::Char('T'), - tab_right: KeyCode::Right, - tab_left: KeyCode::Left, - open_help: KeyCode::Char('?'), - exit: KeyCode::Esc, - sort_name_inc: KeyCode::Char('n'), - sort_name_dec: KeyCode::Char('N'), - sort_pid_inc: KeyCode::Char('p'), - sort_pid_dec: KeyCode::Char('P'), - sort_cpu_usage_inc: KeyCode::Char('c'), - sort_cpu_usage_dec: KeyCode::Char('C'), - sort_memory_usage_inc: KeyCode::Char('m'), - sort_memory_usage_dec: KeyCode::Char('M'), - follow_selection: KeyCode::Char('f'), - toggle_themes: KeyCode::Char('t'), - process_info: KeyCode::Enter, - expand: KeyCode::Char('e'), + move_up: Key::Up, + move_top: Key::Char('W'), + move_down: Key::Down, + move_bottom: Key::Char('S'), + enter: Key::Enter, + tab: Key::Tab, + filter: Key::Char('/'), + terminate: Key::Char('T'), + open_help: Key::Char('?'), + exit: Key::Esc, + sort_name_inc: Key::Char('n'), + sort_name_dec: Key::Char('N'), + sort_pid_inc: Key::Char('p'), + sort_pid_dec: Key::Char('P'), + sort_cpu_usage_inc: Key::Char('c'), + sort_cpu_usage_dec: Key::Char('C'), + sort_memory_usage_inc: Key::Char('m'), + sort_memory_usage_dec: Key::Char('M'), + follow_selection: Key::Char('f'), + expand: Key::Char('e'), } } } -#[derive(Clone,Serialize,Deserialize)] +#[derive(Clone)] pub struct MouseConfig { - pub left_click: MouseButton, - pub right_click: MouseButton, - pub scroll_up: MouseEventKind, - pub scroll_down: MouseEventKind, + pub left_click: MouseKind, + pub middle_click: MouseKind, + pub scroll_up: MouseKind, + pub scroll_down: MouseKind, } impl Default for MouseConfig { fn default() -> Self { Self { - left_click: MouseButton::Left, - right_click: MouseButton::Right, - scroll_up: MouseEventKind::ScrollUp, - scroll_down: MouseEventKind::ScrollDown, + left_click: MouseKind::LeftClick, + middle_click: MouseKind::MiddleClick, + scroll_up: MouseKind::ScrollUp, + scroll_down: MouseKind::ScrollDown, } } } +use ratatui::{prelude::{Color, Modifier, Style}, style::Stylize}; -use ratatui::prelude::{Color, Modifier, Style}; +use crate::input::{Key, MouseKind}; #[derive(Clone,PartialEq,Serialize,Deserialize)] pub struct ThemeConfig { pub style_border_focused: Style, @@ -140,14 +132,17 @@ pub struct ThemeConfig { impl Default for ThemeConfig { fn default() -> Self { Self { - style_border_focused: Style::default().fg(Color::LightGreen), + style_border_focused: Style::default().fg(Color::LightGreen).bold(), style_border_not_focused: Style::default().fg(Color::DarkGray), + style_item_focused: Style::default().fg(Color::White), - style_item_not_focused: Style::default().fg(Color::DarkGray), - style_item_selected: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD), - style_item_selected_not_focused: Style::default().bg(Color::Gray).add_modifier(Modifier::BOLD), + style_item_not_focused: Style::default().fg(Color::Gray), + + style_item_selected: Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD), + style_item_selected_not_focused: Style::default().fg(Color::White).add_modifier(Modifier::BOLD), + style_item_selected_followed: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED), - style_item_selected_followed_not_focused: Style::default().bg(Color::Gray).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED), + style_item_selected_followed_not_focused: Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED), } } -} +} \ No newline at end of file diff --git a/src/events/event.rs b/src/events/event.rs index a3bd38a..26bfffa 100644 --- a/src/events/event.rs +++ b/src/events/event.rs @@ -1,17 +1,6 @@ -use crossterm::event::{ - self, - KeyEvent, - KeyEventKind, - MouseEvent, - MouseEventKind, - Event as CEvent, -}; - -use std::{ - thread, - time::Duration, - sync::mpsc -}; +use crossterm::event::{self, KeyEventKind, Event as CEvent}; +use std::{thread, time::Duration, sync::mpsc}; +use crate::input::{Key, Mouse}; #[derive(Clone, Copy)] pub struct EventConfig { @@ -28,18 +17,17 @@ impl Default for EventConfig { } } - -#[derive(Clone, Copy)] -pub enum Event { - KeyInput(K), - MouseInput(M), +#[derive(Clone)] +pub enum Event { + KeyInput(Key), + MouseInput(Mouse), Tick, Refresh, } pub struct Events { - rx: mpsc::Receiver>, - _tx: mpsc::Sender>, + rx: mpsc::Receiver, + _tx: mpsc::Sender, } impl Events { @@ -61,12 +49,13 @@ impl Events { if event::poll(config.tick_rate).unwrap() { if let Ok(event) = event::read() { if let CEvent::Key(key) = event { + // guard needed for Windows if key.kind == KeyEventKind::Press { - input_tx.send(Event::KeyInput(key)).unwrap(); + input_tx.send(Event::KeyInput(Key::from(key))).unwrap(); } } if let CEvent::Mouse(mouse) = event { - input_tx.send(Event::MouseInput(mouse)).unwrap(); + input_tx.send(Event::MouseInput(Mouse::from(mouse))).unwrap(); } } } @@ -85,7 +74,7 @@ impl Events { Events { rx, _tx: tx } } - pub fn next(&self) -> Result, mpsc::RecvError> { + pub fn next(&self) -> Result { self.rx.recv() } } \ No newline at end of file diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..e35a5a5 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,65 @@ +use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind}; + +// wrapping key and mouse inputs to decouple application logic from crossterm +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Key { + Enter, + Esc, + Char(char), + Backspace, + Up, + Down, + Tab, + Unkown, +} + +// adapter from crossterm key event type to application event model +impl From for Key { + fn from(event: KeyEvent) -> Self { + match event.code { + KeyCode::Enter => Key::Enter, + KeyCode::Esc => Key::Esc, + KeyCode::Char(char) => Key::Char(char), + KeyCode::Backspace => Key::Backspace, + KeyCode::Up => Key::Up, + KeyCode::Down => Key::Down, + KeyCode::Tab => Key::Tab, + _ => Key::Unkown, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum MouseKind { + LeftClick, + MiddleClick, + ScrollUp, + ScrollDown, + Unkown, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Mouse { + pub kind: MouseKind, + pub column: u16, + pub row: u16, +} + +// adapter from crossterm mouse event type to application event model +impl From for Mouse { + fn from(event: MouseEvent) -> Self { + let kind = match event.kind { + MouseEventKind::Down(MouseButton::Left) => MouseKind::LeftClick, + MouseEventKind::Down(MouseButton::Middle) => MouseKind::MiddleClick, + MouseEventKind::ScrollUp => MouseKind::ScrollUp, + MouseEventKind::ScrollDown => MouseKind::ScrollDown, + _ => MouseKind::Unkown, + }; + + Self { + kind, + column: event.column, + row: event.row, + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e6f3bf8..132c3a1 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 input; pub mod components; pub mod events; pub mod models; @@ -55,7 +56,7 @@ fn main() -> Result<()> { match events.next()? { Event::KeyInput(key) => match app.key_event(key) { Ok(state) => { - if !state.is_consumed() && key.code == app.config.key_config.exit { + if !state.is_consumed() && key == app.config.key_config.exit { break; } } diff --git a/src/models/items/process_item.rs b/src/models/items/process_item.rs index cf40910..5a95a04 100644 --- a/src/models/items/process_item.rs +++ b/src/models/items/process_item.rs @@ -1,6 +1,6 @@ use crate::{models::{Filterable, Sortable}}; -#[derive(Clone, PartialEq)] +#[derive(Clone, Copy, PartialEq)] pub enum ProcessItemSortOrder { PidInc, PidDec, diff --git a/src/services/mod.rs b/src/services/mod.rs index 2e4a202..5d7bbc5 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,5 +1,12 @@ pub mod sysinfo_service; -pub trait ListProvider { +// trait VecProvider details: +// +// VecProvider is implemented in SysInfoService for each item `T` in +// models/items/. The served vectors are used in creating & refreshing(updating) +// models (e.g., vec_model.rs). For more information on refreshing models +// see the trait Refreshable in components/mod.rs. +// +pub trait VecProvider { fn fetch_items(&self) -> Vec; } \ No newline at end of file diff --git a/src/services/sysinfo_service.rs b/src/services/sysinfo_service.rs index d07cf84..e2a44c3 100644 --- a/src/services/sysinfo_service.rs +++ b/src/services/sysinfo_service.rs @@ -1,7 +1,7 @@ use sysinfo::{Pid, System, Components}; use crate::models::items::{memory_item::MemoryItem, temp_item::TempItem, cpu_item::CpuItem, process_item::ProcessItem}; use crate::config::Config; -use crate::services::ListProvider; +use crate::services::VecProvider; // 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 @@ -47,61 +47,6 @@ impl SysInfoService { cpus } - pub fn get_processes(&self, processes: &mut Vec) { - processes.clear(); - - for (pid, process) in self.system.processes() { - 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 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 path = if let Some(path) = process.exe() { - if let Some(path) = path.to_str() { - path.to_string() - } - else { - String::from("Non-valid Unicode") - } - } - else { - String::from("Permission Denied") - }; - - let item = ProcessItem::new( - pid.as_u32(), - name, - cpu_usage, - memory_usage, - start_time, - run_time, - accumulated_cpu_time, - status, - path, - ); - - processes.push(item); - } - } - pub fn get_memory(&self, memory: &mut MemoryItem) { let total_memory = self.system.total_memory(); // total memory is size of RAM in bytes let used_memory = self.system.used_memory(); // used memory is allocated memory @@ -157,7 +102,45 @@ impl SysInfoService { } } -impl ListProvider for SysInfoService { +impl VecProvider for SysInfoService { + fn fetch_items(&self) -> Vec { + let mut temps: Vec = Vec::new(); + + for component in &self.components { + let temp = if let Some(temp) = component.temperature() { + temp + } + else { + 0_f32 + }; + + let max_temp = if let Some(max_temp) = component.max() { + max_temp + } + else { + 0_f32 + }; + + let critical_temp = if let Some(critical_temp) = component.critical() { + critical_temp + } + else { + 0_f32 + }; + + let label = component.label().to_string(); + + + let item = TempItem::new(temp, max_temp, critical_temp, label); + + temps.push(item); + } + + temps + } +} + +impl VecProvider for SysInfoService { fn fetch_items(&self) -> Vec { let mut processes: Vec = Vec::new(); diff --git a/src/states/vec_state.rs b/src/states/vec_state.rs index 1803968..a1dfb74 100644 --- a/src/states/vec_state.rs +++ b/src/states/vec_state.rs @@ -8,7 +8,9 @@ pub struct VecState { filter: Option, } -impl VecState { +impl VecState +where S: Clone, +{ pub fn new(model: Vec, selection: Option, sort: Option, filter: Option) -> Self { let model = VecModel::new(model); @@ -38,6 +40,7 @@ impl VecState { }; } + // ACCESS TO MODEL MUTATORS pub fn push(&mut self, item: T) { self.model.push(item); } @@ -66,12 +69,25 @@ impl VecState { pub fn list(&self) -> &[T] { &self.model.items() } + + pub fn selection(&self) -> Option { + self.selection + } + + pub fn sort(&self) -> &Option { + &self.sort + } + + pub fn filter(&self) -> Option<&str> { + self.filter.as_deref() + } } impl VecState where T: Filterable + Sortable { + // Vec mapping viewable indices(e.g., rows when rendering a table) -> immutable model indices after sort/filter pub fn view_indices(&self) -> Vec { let mut indices: Vec = (0..self.model.items().len()).collect(); @@ -86,6 +102,8 @@ where indices } + // Returns an iterator where Item = (usize:"mapping to immutable model index", + // &T:"reference to current item", bool:"does the current item map to the selected index?") pub fn iter_with_selection(&self) -> impl Iterator { let indices = self.view_indices(); let selected = self.selection; From e8d1de7f5abd56009fd75f8045b6b0de724c2677 Mon Sep 17 00:00:00 2001 From: rhasler1 Date: Sat, 26 Jul 2025 18:22:15 -0500 Subject: [PATCH 5/5] adding network component --- Cargo.toml | 3 +- src/app.rs | 217 +++++++----- src/components/command.rs | 58 ++-- src/components/cpu.rs | 235 ++++++++++--- src/components/help.rs | 7 +- src/components/memory.rs | 208 ++++++++--- src/components/mod.rs | 1 + src/components/network.rs | 214 ++++++++++++ src/components/process.rs | 324 ++++++++++++++---- src/components/utils/vertical_scroll.rs | 15 +- src/config.rs | 88 ++--- src/main.rs | 2 +- ...ounded_queue.rs => bounded_queue_model.rs} | 14 +- src/models/items/memory_item.rs | 20 ++ src/models/items/mod.rs | 13 +- src/models/items/network_item.rs | 40 +++ src/models/items/process_item.rs | 202 ++++++----- src/models/mod.rs | 2 +- src/services/mod.rs | 4 + src/services/sysinfo_service.rs | 80 ++--- src/states/bounded_queue_state.rs | 65 ++++ src/states/mod.rs | 3 +- src/states/vec_state.rs | 9 +- src/ui/fitted_sparkline.rs | 199 +++++++++++ src/ui/mod.rs | 1 + 25 files changed, 1548 insertions(+), 476 deletions(-) create mode 100644 src/components/network.rs rename src/models/{bounded_queue.rs => bounded_queue_model.rs} (91%) create mode 100644 src/models/items/network_item.rs create mode 100644 src/states/bounded_queue_state.rs create mode 100644 src/ui/fitted_sparkline.rs create mode 100644 src/ui/mod.rs diff --git a/Cargo.toml b/Cargo.toml index d1452ca..a8963af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,5 +17,4 @@ ratatui = { version = "0.26.0", features = ["serde", "macros"] } serde = { version = "1.0.188", features = ["derive"] } anyhow = "1.0.97" itertools = "0.10.0" -dynamic-list = {path = "./dynamic-list", version = "0.1.0"} -clippy = "0.0.302" +clippy = "0.0.302" \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 7244a6b..685b73f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,21 +1,22 @@ use std::collections::HashMap; use anyhow::{Ok, Result}; -use crossterm::event::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}; use ratatui::prelude::*; +use crate::components::help::HelpComponent; use crate::input::{Key, Mouse, MouseKind}; -use crate::components::temp::TempComponent; -use crate::components::Refreshable; -use crate::config::Config; +use crate::components::{command, Refreshable}; +use crate::config::{Config, KeyConfig, MouseConfig}; use crate::components::{ cpu::CPUComponent, memory::MemoryComponent, + network::NetworkComponent, process::ProcessComponent, error::ErrorComponent, EventState, Component, DrawableComponent, - help::HelpComponent, + //help::HelpComponent, }; +use crate::components::command::CommandInfo; use crate::services::sysinfo_service::SysInfoService; #[derive(PartialEq, Eq, Hash, Clone, Copy)] @@ -23,7 +24,7 @@ enum MainFocus { CPU, Process, Memory, - Temp, + Network, } pub struct App { @@ -34,7 +35,8 @@ pub struct App { process: ProcessComponent, cpu: CPUComponent, memory: MemoryComponent, - temp: TempComponent, + network: NetworkComponent, + //temp: TempComponent, help: HelpComponent, pub error: ErrorComponent, pub config: Config, @@ -48,7 +50,12 @@ impl App { let process = ProcessComponent::new(config.clone(), &service); let memory = MemoryComponent::new(config.clone(), &service); let cpu = CPUComponent::new(config.clone(), &service); - let temp = TempComponent::new(config.clone(), &service); + let network = NetworkComponent::new(config.clone(), &service); + //let temp = TempComponent::new(config.clone(), &service); + + let help_config = config.clone(); + let mut help = HelpComponent::new(help_config.clone()); + help.set_commands(commands(&help_config.key_config, &help_config.mouse_config)); let focus = MainFocus::Process; let focus_rects = HashMap::new(); @@ -61,8 +68,9 @@ impl App { process, cpu, memory, - temp, - help: HelpComponent::new(config.clone()), + network, + //temp, + help, error: ErrorComponent::new(config.clone()), config: config.clone(), } @@ -72,9 +80,9 @@ impl App { self.service.refresh_all(); self.process.refresh(&self.service); - self.memory.update(&self.service); + self.memory.refresh(&self.service); self.cpu.update(&self.service); - self.temp.refresh(&self.service); + self.network.refresh(&self.service); Ok(EventState::Consumed) } @@ -84,68 +92,29 @@ impl App { } pub fn key_event(&mut self, key: Key) -> Result { - if self.component_event(key)?.is_consumed() { - return Ok(EventState::Consumed); - } - else if self.move_focus_key(key)?.is_consumed() { - return Ok(EventState::Consumed); + if self.help.is_visible() { + let _ = self.help.key_event(key)?.is_consumed(); + return Ok(EventState::Consumed) } - else if key == self.config.key_config.expand { - self.toggle_expand(); + if self.key_component_event(key)?.is_consumed() { return Ok(EventState::Consumed); } - - Ok(EventState::NotConsumed) - } - - pub fn mouse_event(&mut self, mouse: Mouse) -> Result { - //TODO: - // 1. change component_event -> component_key_event - // 2. create component_mouse_event - // 3. figure out how to select Cells? e.g., selecting a process in the process list - match self.focus { - MainFocus::Process => { - if self.process.mouse_event(mouse)?.is_consumed() { - return Ok(EventState::Consumed) - } - } - _ => {} - } - - if self.move_focus_mouse(mouse)?.is_consumed() { - return Ok(EventState::Consumed) + if self.move_focus_key(key)?.is_consumed() { + return Ok(EventState::Consumed); } - - if matches!(mouse.kind, MouseKind::MiddleClick) { + if key == self.config.key_config.expand { self.toggle_expand(); - return Ok(EventState::Consumed) + return Ok(EventState::Consumed); } Ok(EventState::NotConsumed) } - fn move_focus_mouse(&mut self, mouse: Mouse) -> Result { - if matches!(mouse.kind, MouseKind::LeftClick) { - let col = mouse.column; - let row = mouse.row; - - for (focus, rect) in &self.focus_rects { - if rect.contains(col, row) { - self.focus = *focus; - return Ok(EventState::Consumed) - } - } - } - - return Ok(EventState::NotConsumed) - } - - fn component_event(&mut self, key: Key) -> Result { + fn key_component_event(&mut self, key: Key) -> Result { if self.error.key_event(key)?.is_consumed() { return Ok(EventState::Consumed) } - if self.help.key_event(key)?.is_consumed() { return Ok(EventState::Consumed) } @@ -161,8 +130,8 @@ impl App { return Ok(EventState::Consumed) } } - MainFocus::Temp => { - if self.temp.key_event(key)?.is_consumed() { + MainFocus::Network => { + if self.network.key_event(key)?.is_consumed() { return Ok(EventState::Consumed) } } @@ -172,8 +141,7 @@ impl App { } // terminate case if key == self.config.key_config.terminate { - //self.process.terminate_process(&self.system_wrapper); - + return Ok(EventState::Consumed) } } @@ -189,23 +157,84 @@ impl App { self.focus = MainFocus::Memory } MainFocus::Memory => { - self.focus = MainFocus::Temp + self.focus = MainFocus::Network } - MainFocus::Temp => { + MainFocus::Network => { self.focus = MainFocus::Process } MainFocus::Process => { self.focus = MainFocus::CPU } } + return Ok(EventState::Consumed) + } + Ok(EventState::NotConsumed) + } + + pub fn mouse_event(&mut self, mouse: Mouse) -> Result { + // mouse event can result in multiple state changes + // for example: The app's main focus is on CPU. A left-click + // mouse event occurs on the proces list. This one event will + // first change the app's main focus to the ProcessComponent, + // then the ProcessComponent will handle the mouse click to potentially + // change it's own focus state (List, Filter) or selection state. + if self.help.is_visible() { + let _ = self.help.mouse_event(mouse)?.is_consumed(); + return Ok(EventState::Consumed) + } + + let move_focus_res = self.move_focus_mouse(mouse)?.is_consumed(); + + match self.focus { + MainFocus::Process => { + if self.process.mouse_event(mouse)?.is_consumed() { + return Ok(EventState::Consumed) + } + } + MainFocus::CPU => { + if self.cpu.mouse_event(mouse)?.is_consumed() { + return Ok(EventState::Consumed) + } + } + MainFocus::Memory => { + if self.memory.mouse_event(mouse)?.is_consumed() { + return Ok(EventState::Consumed) + } + } + MainFocus::Network => { + if self.network.mouse_event(mouse)?.is_consumed() { + return Ok(EventState::Consumed) + } + } + } + + if move_focus_res { return Ok(EventState::Consumed) } Ok(EventState::NotConsumed) } + fn move_focus_mouse(&mut self, mouse: Mouse) -> Result { + if matches!(mouse.kind, MouseKind::LeftClick) { + let col = mouse.column; + let row = mouse.row; + + for (focus, rect) in &self.focus_rects { + if rect.contains(col, row) { + self.focus = *focus; + return Ok(EventState::Consumed) + } + } + } + + return Ok(EventState::NotConsumed) + } + pub fn draw(&mut self, f: &mut Frame) -> Result<()> { + self.focus_rects.clear(); + let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -214,6 +243,11 @@ impl App { .split(f.size()); self.error.draw(f, chunks[0], false)?; + + if self.help.is_visible() { + self.help.draw(f, chunks[0], false)?; + return Ok(()) + } if self.expand { if matches!(self.focus, MainFocus::Process) { @@ -243,13 +277,13 @@ impl App { self.focus_rects.insert(MainFocus::Memory, chunks[0]); } - if matches!(self.focus, MainFocus::Temp) { - self.temp.draw( + if matches!(self.focus, MainFocus::Network) { + self.network.draw( f, chunks[0], true, )?; - self.focus_rects.insert(MainFocus::Temp, chunks[0]); + self.focus_rects.insert(MainFocus::Network, chunks[0]); } } else { @@ -293,43 +327,38 @@ impl App { self.memory.draw( f, horizontal_chunks[1][0], - //vertical_chunks[1], matches!(self.focus, MainFocus::Memory) )?; self.focus_rects.insert(MainFocus::Memory, horizontal_chunks[1][0]); - self.temp.draw( + self.network.draw( f, horizontal_chunks[1][1], - matches!(self.focus, MainFocus::Temp) + matches!(self.focus, MainFocus::Network) )?; - self.focus_rects.insert(MainFocus::Temp, horizontal_chunks[1][1]); + self.focus_rects.insert(MainFocus::Network, horizontal_chunks[1][1]); } - //self.help.draw(f, Rect::default(), false)?; //TODO: re-implement - return Ok(()) } +} - /* - fn commands(&self) -> Vec { - let res = vec![ - CommandInfo::new(command::help(&self.config.key_config)), - CommandInfo::new(command::exit_popup(&self.config.key_config)), - CommandInfo::new(command::change_tab(&self.config.key_config)), - CommandInfo::new(command::move_selection(&self.config.key_config)), - CommandInfo::new(command::selection_to_top_bottom(&self.config.key_config)), - CommandInfo::new(command::follow_selection(&self.config.key_config)), - CommandInfo::new(command::sort_list_by_name(&self.config.key_config)), - CommandInfo::new(command::sort_list_by_pid(&self.config.key_config)), - CommandInfo::new(command::sort_list_by_cpu_usage(&self.config.key_config)), - CommandInfo::new(command::sort_list_by_memory_usage(&self.config.key_config)), - CommandInfo::new(command::filter_submit(&self.config.key_config)), - CommandInfo::new(command::terminate_process(&self.config.key_config)), - ]; - - res - }*/ +fn commands(key_config: &KeyConfig, mouse_config: &MouseConfig) -> Vec { + let res = vec![ + CommandInfo::new(command::help(key_config)), + CommandInfo::new(command::exit_popup(key_config)), + //CommandInfo::new(command::change_tab(&self.config.key_config)), + CommandInfo::new(command::move_selection(key_config)), + CommandInfo::new(command::selection_to_top_bottom(key_config)), + CommandInfo::new(command::sort_list_by_name(key_config, mouse_config)), + CommandInfo::new(command::sort_list_by_pid(key_config, mouse_config)), + CommandInfo::new(command::sort_list_by_cpu_usage(key_config, mouse_config)), + CommandInfo::new(command::sort_list_by_memory_usage(key_config, mouse_config)), + CommandInfo::new(command::filter_submit(key_config)), + CommandInfo::new(command::terminate_process(key_config)), + ]; + + res } trait Contains { diff --git a/src/components/command.rs b/src/components/command.rs index af36979..3a5e811 100644 --- a/src/components/command.rs +++ b/src/components/command.rs @@ -1,6 +1,10 @@ -use crate::config::KeyConfig; +use crate::config::{KeyConfig, MouseConfig}; static CMD_GROUP_GENERAL: &str = "-- General --"; +static CMD_GROUP_PROCESS: &str = "-- Process --"; +static CMD_GROUP_CPU: &str = "-- CPU --"; +static CMD_GROUP_MEMORY: &str = "-- Memory --"; +static CMD_GROUP_NETWORK: &str = "-- Network --"; #[derive(Clone, PartialEq, PartialOrd, Ord, Eq)] pub struct CommandText { @@ -74,7 +78,7 @@ pub fn help(key: &KeyConfig) -> CommandText { CommandText::new( format!( "Help [{:?}]", - key.open_help, + key.help, ), CMD_GROUP_GENERAL ) @@ -90,46 +94,54 @@ pub fn terminate_process(key: &KeyConfig) -> CommandText { ) } -pub fn sort_list_by_name(key: &KeyConfig) -> CommandText { +// Process specific::begin +pub fn sort_list_by_name(key: &KeyConfig, mouse: &MouseConfig) -> CommandText { CommandText::new( format!( - "Sort by name dec/inc [{:?}/{:?}]", - key.sort_name_dec, - key.sort_name_inc, + "Sort by name toggle [{:?}]", + key.sort_name_toggle, ), - CMD_GROUP_GENERAL + CMD_GROUP_PROCESS ) } -pub fn sort_list_by_pid(key: &KeyConfig) -> CommandText { +pub fn sort_list_by_pid(key: &KeyConfig, mouse: &MouseConfig) -> CommandText { CommandText::new( format!( - "Sort by PID dec/inc [{:?}/{:?}]", - key.sort_pid_dec, - key.sort_pid_inc, + "Sort by PID toggle [{:?}]", + key.sort_pid_toggle ), - CMD_GROUP_GENERAL + CMD_GROUP_PROCESS ) } -pub fn sort_list_by_cpu_usage(key: &KeyConfig) -> CommandText { +pub fn sort_list_by_cpu_usage(key: &KeyConfig, mouse: &MouseConfig) -> CommandText { CommandText::new( format!( - "Sort by cpu usage dec/inc [{:?}/{:?}]", - key.sort_cpu_usage_dec, - key.sort_cpu_usage_inc, + "Sort by cpu usage toggle [{:?}]", + key.sort_cpu_toggle ), - CMD_GROUP_GENERAL + CMD_GROUP_PROCESS ) } -pub fn sort_list_by_memory_usage(key: &KeyConfig) -> CommandText { +pub fn sort_list_by_memory_usage(key: &KeyConfig, mouse: &MouseConfig) -> CommandText { CommandText::new( format!( - "Sort by memory usage dec/inc [{:?}/{:?}]", - key.sort_memory_usage_dec, - key.sort_memory_usage_inc, + "Sort by memory usage toggle [{:?}]", + key.sort_memory_toggle ), - CMD_GROUP_GENERAL + CMD_GROUP_PROCESS ) -} \ No newline at end of file +} + +pub fn select_process(key: &KeyConfig, mouse: &MouseConfig) -> CommandText { + CommandText::new( + format!( + "Select process by mouse [{:?}] | [{:?}/{:?}]", + mouse.left_click, mouse.scroll_down, mouse.scroll_up, + ), + CMD_GROUP_PROCESS + ) +} +// Process specific::end \ No newline at end of file diff --git a/src/components/cpu.rs b/src/components/cpu.rs index cadc3a1..ac8c551 100644 --- a/src/components/cpu.rs +++ b/src/components/cpu.rs @@ -1,5 +1,7 @@ +use std::cmp::{max, min}; use std::collections::BTreeMap; use ratatui::Frame; +use ratatui::layout::Position; use ratatui::prelude::*; use ratatui::widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, List, ListItem, ListState}; use std::str::FromStr; @@ -9,45 +11,49 @@ use super::EventState; use crate::components::common_nav; use crate::services::sysinfo_service::SysInfoService; use crate::components::utils::selection::UISelection; -use crate::models::bounded_queue::BoundedQueue; +use crate::components::*; +use crate::models::bounded_queue_model::BoundedQueueModel; use crate::models::items::cpu_item::CpuItem; use crate::config::Config; -use super::{Component, DrawableComponent}; +use crate::config::*; #[derive(PartialEq, Clone, Copy)] pub enum ColorWheel { - Red, + LightRed, Blue, Cyan, Green, LightGreen, - Magenta, + LightYellow, + LightMagenta, } impl Default for ColorWheel { fn default() -> Self { - ColorWheel::Red + ColorWheel::LightRed } } impl ColorWheel { - const ALL: [ColorWheel; 6] = [ - ColorWheel::Red, + const ALL: [ColorWheel; 7] = [ + ColorWheel::LightRed, ColorWheel::Blue, ColorWheel::Cyan, ColorWheel::Green, ColorWheel::LightGreen, - ColorWheel::Magenta, + ColorWheel::LightYellow, + ColorWheel::LightMagenta, ]; pub fn as_str(&self) -> &'static str { match self { - ColorWheel::Red => "red", + ColorWheel::LightRed => "lightred", ColorWheel::Blue => "blue", ColorWheel::Cyan => "cyan", ColorWheel::Green => "green", ColorWheel::LightGreen => "lightgreen", - ColorWheel::Magenta => "magenta", + ColorWheel::LightYellow => "lightyellow", + ColorWheel::LightMagenta => "lightmagenta", } } @@ -67,22 +73,35 @@ impl ColorWheel { } } +pub enum Focus { + Chart, + CPUList, +} + pub struct CPUComponent { - cpus: BTreeMap>, + cpus: BTreeMap>, selection_state: UISelection, selection_offset: usize, + data_window_time_scale: u64, + chart_area: Option, + list_area: Option, + focus: Focus, config: Config, } impl CPUComponent { pub fn new(config: Config, sysinfo: &SysInfoService) -> Self { - let mut cpus: BTreeMap> = BTreeMap::new(); + let mut cpus: BTreeMap> = BTreeMap::new(); + + let focus: Focus = Focus::CPUList; + let data_window_time_scale = config.min_time_scale(); + let capacity = ( config.max_time_scale() / config.refresh_rate() ) as usize; for cpu in sysinfo.get_cpus() { let id = cpu.id(); let perf_q = cpus.entry(id).or_insert_with(|| { - BoundedQueue::new(config.events_per_min() as usize) + BoundedQueueModel::new(capacity) }); // passes ownership @@ -95,41 +114,140 @@ impl CPUComponent { // not present in the CPU list let selection_offset = 1; + let chart_area = None; + let list_area = None; + Self { cpus, selection_state, selection_offset, + data_window_time_scale, + chart_area, + list_area, + focus, config, - } + } + } + + fn handle_mouse_click_on_chart(&mut self, click_x: u16, click_y: u16) -> bool { + if self.chart_area.is_none() { return false; } + let chart_area = self.chart_area.unwrap(); + + if chart_area.contains(Position {x: click_x, y: click_y}) { + self.focus = Focus::Chart; + return true + } + + false + } + + fn handle_mouse_click_on_list(&mut self, click_x: u16, click_y: u16) -> bool { + if self.list_area.is_none() { return false; } + let list_area = self.list_area.unwrap(); + + if list_area.contains(Position { x: click_x, y: click_y }) { + self.focus = Focus::CPUList; + return true + } + + return false } // has ownership pub fn update(&mut self, sysinfo: &SysInfoService) { + let capacity = ( self.config.max_time_scale() / self.config.refresh_rate() ) as usize; + for cpu in sysinfo.get_cpus() { let id = cpu.id(); let perf_q = self.cpus.entry(id).or_insert_with(|| { - BoundedQueue::new(self.config.events_per_min() as usize) + BoundedQueueModel::new(capacity) }); // passes ownership perf_q.add_item(cpu); } } + + fn handle_move_selection(&mut self, dir: MoveSelection) { + let len = self.cpus.len(); + let offset = self.selection_offset; + self.selection_state.move_selection(dir, len + offset); + } } impl Component for CPUComponent { fn key_event(&mut self, key: Key) -> Result { - if let Some(dir) = common_nav(key, &self.config.key_config) { - self.selection_state.move_selection(dir, self.cpus.len() + self.selection_offset); - + let key_config = &self.config.key_config; + + // key event to change selection / data window + match self.focus { + Focus::CPUList => { + if let Some(dir) = common_nav(key, key_config) { + self.handle_move_selection(dir); + return Ok(EventState::Consumed) + } + } + Focus::Chart => { + if key == key_config.move_down { + self.data_window_time_scale = min(self.data_window_time_scale.saturating_add(self.config.time_inc()), self.config.max_time_scale()); + return Ok(EventState::Consumed) + } + if key == key_config.move_up { + self.data_window_time_scale = max(self.data_window_time_scale.saturating_sub(self.config.time_inc()), self.config.min_time_scale()); + return Ok(EventState::Consumed) + } + } + } + + // key event to move focus + if key == key_config.filter { + match self.focus { + Focus::CPUList => { self.focus = Focus::Chart } + Focus::Chart => { self.focus = Focus::CPUList } + } return Ok(EventState::Consumed) } - + Ok(super::EventState::NotConsumed) } - fn mouse_event(&mut self, _mouse: Mouse) -> Result { + fn mouse_event(&mut self, mouse: Mouse) -> Result { + // mouse events to change selection / data window + match self.focus { + Focus::CPUList => { + match mouse.kind { + MouseKind::ScrollDown => { self.handle_move_selection(MoveSelection::Down); } + MouseKind::ScrollUp => { self.handle_move_selection(MoveSelection::Up); } + //TODO: LeftClick + _ => {} + } + } + Focus::Chart => { + match mouse.kind { + MouseKind::ScrollDown => { + self.data_window_time_scale = min(self.data_window_time_scale.saturating_add(self.config.time_inc()), self.config.max_time_scale()); + return Ok(EventState::Consumed) + } + MouseKind::ScrollUp => { + self.data_window_time_scale = max(self.data_window_time_scale.saturating_sub(self.config.time_inc()), self.config.min_time_scale()); + return Ok(EventState::Consumed) + } + _ => {} + } + } + } + + // move focus + if matches!(mouse.kind, MouseKind::LeftClick) { + if self.handle_mouse_click_on_chart(mouse.column, mouse.row) { + return Ok(EventState::Consumed) + } + if self.handle_mouse_click_on_list(mouse.column, mouse.row) { + return Ok(EventState::Consumed) + } + } + Ok(EventState::NotConsumed) } } @@ -137,23 +255,32 @@ impl Component for CPUComponent { impl DrawableComponent for CPUComponent { // draw function has some magic numbers relating to render position: TODO-research a fix/better approach fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { - // split screen let horizontal_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Fill(1), // chart - Constraint::Length(16), // list + Constraint::Length(20), // list ]).split(area); + + // saving chart and list area for mouse clicks + self.chart_area = Some(horizontal_chunks[0]); + self.list_area = Some(horizontal_chunks[1]); + + let refresh_rate = self.config.refresh_rate(); // default = 2,000 ms + let time_scale = self.data_window_time_scale; // default = 60,000 ms + let data_window = (time_scale / refresh_rate) as usize; // default = 30 + let max_idx = data_window.saturating_sub(1); // + let cpu_focus = &self.focus; // 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 + /*let perf_q_max_idx = self.cpus .get(&0) .map(|q| q.capacity().saturating_sub(1)) - .unwrap_or(0); + .unwrap_or(0); */ // The UICPUList will look like: // All ui_selection=0 cpu_selection=None @@ -181,8 +308,9 @@ impl DrawableComponent for CPUComponent { let data: Vec<(f64, f64)> = queue .iter() .rev() + .take(data_window) .enumerate() - .map(|(i, item)| ((perf_q_max_idx - i) as f64, item.usage() as f64)) + .map(|(idx, cpu_item)| ((max_idx - idx) as f64, cpu_item.usage() as f64)) .collect(); all_data.push((*id as u32, data)); @@ -195,8 +323,9 @@ impl DrawableComponent for CPUComponent { let data: Vec<(f64, f64)> = queue .iter() .rev() + .take(data_window) .enumerate() - .map(|(i, item)| ((perf_q_max_idx - i) as f64, item.usage() as f64)) + .map(|(idx, cpu_item)| ((max_idx - idx) as f64, cpu_item.usage() as f64)) .collect(); all_data.push((id as u32, data)); @@ -216,7 +345,7 @@ impl DrawableComponent for CPUComponent { } // populate names for UIList to draw cpu list - let mut names: Vec = self.cpus + let mut names: Vec<(usize, String)> = self.cpus .iter() .map(|(key, queue)| { let usage = queue.back().unwrap().usage(); @@ -228,45 +357,57 @@ impl DrawableComponent for CPUComponent { format!("CPU {}", key.saturating_sub(1)) }; - let title = format!("{:<7} {:.2}", label, usage); - - ListItem::new(title).style(Color::from_str(ColorWheel::from_index(*key).as_str()).unwrap()) + (*key, format!("{:<7} {:.2}", label, usage)) + //ListItem::new(title).style(Color::from_str(ColorWheel::from_index(*key).as_str()).unwrap()) }) .collect(); - // insert All option into UI list - let title = format!("{:<7} {}", String::from("All"), String::from("%")); - names.insert(0, ListItem::new(title)); + let all_option = format!("{:<7} {}", String::from("All"), String::from("%")); + // assigning random key to all for coloring, colorwheel::7783 => magenta + names.insert(0, (7783, all_option)); + + // map Vec to Vec + let names: Vec = names + .iter() + .enumerate() + .map(|(i, (key, name))| { + if Some(i) == self.selection_state.selection { + ListItem::new(format!("-> {}", name)).style(Color::from_str(ColorWheel::from_index(*key).as_str()).unwrap()) + } + else { + ListItem::new(format!(" {}", name)).style(Color::from_str(ColorWheel::from_index(*key).as_str()).unwrap()) + } + }).collect(); // render chart let chart = Chart::new(datasets) .block( { - if !focused { + if focused && matches!(cpu_focus, Focus::Chart) { Block::default() .borders(Borders::ALL) - .title(" CPU % ") - .style(self.config.theme_config.style_border_not_focused) + .title(" CPU ") + .style(self.config.theme_config.style_border_focused) } else { Block::default() .borders(Borders::ALL) - .title(" CPU % ") - .style(self.config.theme_config.style_border_focused) + .title(" CPU ") + .style(self.config.theme_config.style_border_not_focused) } } ) .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")]) + .bounds([0.0, max_idx as f64]) + .labels(vec![Span::raw(format!("{}s", ms_to_s(time_scale))), Span::raw("now")]) .labels_alignment(Alignment::Right), ) .y_axis( Axis::default() .bounds([0.0, 100.0]) .labels(vec![ - Span::raw("0"), + Span::raw("0%"), Span::raw("50"), Span::raw("100"), ]) @@ -282,19 +423,11 @@ impl DrawableComponent for CPUComponent { let cpu_list = List::new(names) .scroll_padding(horizontal_chunks[1].height as usize / 2) .block( - if !focused { - Block::default().borders(Borders::ALL).style(self.config.theme_config.style_border_not_focused) - } - else { + if focused && matches!(cpu_focus, Focus::CPUList) { Block::default().borders(Borders::ALL).style(self.config.theme_config.style_border_focused) } - ) - .highlight_style( - if !focused { - self.config.theme_config.style_item_selected_not_focused - } else { - self.config.theme_config.style_item_selected + Block::default().borders(Borders::ALL).style(self.config.theme_config.style_border_not_focused) } ); diff --git a/src/components/help.rs b/src/components/help.rs index 6bbbd24..3691374 100644 --- a/src/components/help.rs +++ b/src/components/help.rs @@ -100,12 +100,15 @@ impl HelpComponent { Ok(()) } + pub fn is_visible(&self) -> bool { + self.visible + } } impl Component for HelpComponent { fn key_event(&mut self, key: Key) -> Result { if self.visible { - if key == self.config.key_config.exit { + if key == self.config.key_config.help { self.hide(); return Ok(EventState::Consumed); } @@ -118,7 +121,7 @@ impl Component for HelpComponent { return Ok(EventState::Consumed); } } - else if key == self.config.key_config.open_help { + else if key == self.config.key_config.help { self.show()?; return Ok(EventState::Consumed); } diff --git a/src/components/memory.rs b/src/components/memory.rs index 919cb4b..38c968b 100644 --- a/src/components/memory.rs +++ b/src/components/memory.rs @@ -1,87 +1,207 @@ +use std::cmp::{max, min}; use anyhow::{Ok, Result}; -use ratatui::{ - layout::{Constraint, Direction, Layout}, - style::{Style, Stylize}, - widgets::{Block, Borders, Gauge}, -}; -use crate::input::*; -use crate::services::sysinfo_service::SysInfoService; +use ratatui::{Frame, prelude::*, widgets::*}; +use crate::{components::Refreshable, input::*, services::ItemProvider, states::bounded_queue_state::BoundedQueueState}; use crate::components::DrawableComponent; use crate::models::items::memory_item::MemoryItem; use crate::config::Config; use super::Component; use super::EventState; +use crate::config::*; pub struct MemoryComponent { config: Config, - memory: MemoryItem, + queue_state: BoundedQueueState, + data_window_time_scale: u64, } impl MemoryComponent { - pub fn new(config: Config, sysinfo: &SysInfoService) -> Self { - let mut memory = MemoryItem::default(); - sysinfo.get_memory(&mut memory); + pub fn new(config: Config, service: &S) -> Self + where S: ItemProvider + { + let capacity = ( config.max_time_scale() / config.refresh_rate() ) as usize; + let selection = None; + let refresh_bool = true; + let data_window_time_scale = config.min_time_scale(); + + let mut queue_state = BoundedQueueState::new(capacity, selection, refresh_bool); + let memory = service.fetch_item(); + // adding first item + queue_state.add_item(memory); Self { config, - memory, + queue_state, + data_window_time_scale, } } +} - pub fn update(&mut self, sysinfo: &SysInfoService) { - sysinfo.get_memory(&mut self.memory); +impl Refreshable for MemoryComponent +where + S: ItemProvider +{ + fn refresh(&mut self, service: &S) { + let memory_item: MemoryItem = service.fetch_item(); + self.queue_state.add_item(memory_item); } } impl Component for MemoryComponent { - fn key_event(&mut self, _key: Key) -> Result { + fn key_event(&mut self, key: Key) -> Result { + let key_config = &self.config.key_config; + if key == key_config.move_down { + self.data_window_time_scale = min(self.data_window_time_scale.saturating_add(self.config.time_inc()), self.config.max_time_scale()); + return Ok(EventState::Consumed) + } + if key == key_config.move_up { + self.data_window_time_scale = max(self.data_window_time_scale.saturating_sub(self.config.time_inc()), self.config.min_time_scale()); + return Ok(EventState::Consumed) + } + Ok(EventState::NotConsumed) } - fn mouse_event(&mut self, _mouse: Mouse) -> Result { + fn mouse_event(&mut self, mouse: Mouse) -> Result { + match mouse.kind { + MouseKind::ScrollDown => { + self.data_window_time_scale = min(self.data_window_time_scale.saturating_add(self.config.time_inc()), self.config.max_time_scale()); + return Ok(EventState::Consumed) + } + MouseKind::ScrollUp => { + self.data_window_time_scale = max(self.data_window_time_scale.saturating_sub(self.config.time_inc()), self.config.min_time_scale()); + return Ok(EventState::Consumed) + } + _ => {} + } + Ok(EventState::NotConsumed) } } impl DrawableComponent for MemoryComponent { - fn draw(&mut self, f: &mut ratatui::Frame, area: ratatui::prelude::Rect, focused: bool) -> Result<()> { - let vertical_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Ratio(1, 2), - Constraint::Ratio(1, 2), - ].as_ref()) - .split(area); - - let style = if focused { + fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { + let vertical_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(2), + Constraint::Min(0), + ]).split(area); + + let refresh_rate = self.config.refresh_rate(); // default = 2,000 ms + let time_scale = self.data_window_time_scale; // default = 60,000 ms + let data_window = (time_scale / refresh_rate) as usize; // default = 30 + let max_idx = data_window.saturating_sub(1); // + + + // building data sets + let ram_percent_usage_data: Vec<(f64, f64)> = self.queue_state + .iter() + .rev() + .take(data_window) + .enumerate() + .map(|(idx, memory_item)| { + ( + max_idx.saturating_sub(idx) as f64, + memory_item.percent_memory_usage(), + ) + }) + .collect(); + + let swap_percent_usage_data: Vec<(f64, f64)> = self.queue_state + .iter() + .rev() + .take(data_window) + .enumerate() + .map(|(idx, memory_item)| { + ( + max_idx.saturating_sub(idx) as f64, + memory_item.percent_swap_usage(), + ) + }) + .collect(); + + let datasets = vec![ + Dataset::default() + .data(&ram_percent_usage_data) + .graph_type(GraphType::Line) + .marker(symbols::Marker::Braille) + .style(Style::new().light_red()), + + Dataset::default() + .data(&swap_percent_usage_data) + .graph_type(GraphType::Line) + .marker(symbols::Marker::Braille) + .style(Style::new().light_magenta()), + ]; + + // set block style + let block_style = if focused { self.config.theme_config.style_border_focused } else { self.config.theme_config.style_border_not_focused }; - // ram widget - let ram_percent = ( self.memory.used_memory_gb() / self.memory.total_memory_gb() ) * 100_f64; - let ram_label = "RAM Usage"; - let ram_title = format!(" {:<15} {:.2} GB / {:.2} GB ", ram_label, self.memory.used_memory_gb(), self.memory.total_memory_gb()); + // building chart + let x_axis = Axis::default() + .bounds([0.0, max_idx as f64]) + .labels(vec![Span::raw(format!("{}s", ms_to_s(time_scale))), Span::raw("now")]) + .labels_alignment(Alignment::Right); - let g_ram = Gauge::default() - .block(Block::default().borders(Borders::LEFT | Borders::TOP | Borders::RIGHT).style(style).title(ram_title)) - .gauge_style(Style::new().light_cyan().on_black().italic()) - .percent(ram_percent as u16); + let y_axis = Axis::default() + .bounds([0.0, 100.0]) + .labels(vec![ + Span::raw("0%"), + Span::raw("50"), + Span::raw("100"), + ]) + .labels_alignment(Alignment::Right); + + let chart_title = format!(" Memory "); + let chart = Chart::new(datasets) + .block(Block::default() + .borders(Borders::LEFT|Borders::BOTTOM|Borders::RIGHT) + .style(block_style) + ) + .x_axis(x_axis) + .y_axis(y_axis); - // swap widget - let swap_percent = ( self.memory.used_swap_gb() / self.memory.total_swap_gb() ) * 100_f64; - let swap_label = "Swap Usage"; - let swap_title = format!(" {:<15} {:.2} GB / {:.2} GB ", swap_label, self.memory.used_swap_gb(), self.memory.total_swap_gb()); + f.render_widget(chart, vertical_chunks[1]); - let g_swap = Gauge::default() - .block(Block::default().borders(Borders::LEFT | Borders::BOTTOM | Borders::RIGHT).style(style).title(swap_title)) - .gauge_style(Style::new().light_magenta().on_black().italic()) - .percent(swap_percent as u16); - f.render_widget(g_ram, vertical_chunks[0]); - f.render_widget(g_swap, vertical_chunks[1]); + // building legend + let memory_item = if let Some(item) = self.queue_state.back() { + item + } + else { + &MemoryItem::default() + }; + let ram_legend = format!(" RAM :: {:.0}/{:.0}GB :: {:.0}%", + memory_item.used_memory_gb(), + memory_item.total_memory_gb(), + memory_item.percent_memory_usage(), + ); + let swap_legend = format!(" SWAP :: {:.0}/{:.0}GB :: {:.0}%", + memory_item.used_swap_gb(), + memory_item.total_swap_gb(), + memory_item.percent_swap_usage(), + ); + + let legend = Paragraph::new( + Line::from(vec![ + Span::styled(ram_legend, Style::default().fg(Color::LightRed)), + Span::raw(" "), + Span::styled(swap_legend, Style::default().fg(Color::LightMagenta)), + ]) + .right_aligned()) + .block(Block::new() + .borders(Borders::LEFT|Borders::TOP|Borders::RIGHT) + .style(block_style) + .title(chart_title) + ); + + f.render_widget(legend, vertical_chunks[0]); Ok(()) } diff --git a/src/components/mod.rs b/src/components/mod.rs index e30950e..4d9bc1a 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -11,6 +11,7 @@ pub mod command; pub mod cpu; pub mod memory; pub mod temp; +pub mod network; pub trait DrawableComponent { fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()>; diff --git a/src/components/network.rs b/src/components/network.rs new file mode 100644 index 0000000..eaca285 --- /dev/null +++ b/src/components/network.rs @@ -0,0 +1,214 @@ + +use std::cmp::{max, min}; + +use anyhow::{Ok, Result}; +use ratatui::{Frame, prelude::*, widgets::*}; + +use crate::components::Refreshable; +use crate::input::MouseKind; +use crate::services::ItemProvider; +use crate::states::bounded_queue_state::BoundedQueueState; +use crate::models::items::network_item::NetworkItem; +use crate::models::items::*; +use crate::config::Config; +use crate::components::*; +use crate::config::*; + +pub struct NetworkComponent { + config: Config, + queue_state: BoundedQueueState, + data_window_time_scale: u64, +} + +impl NetworkComponent { + pub fn new(config: Config, service: &S) -> Self + where S: ItemProvider + { + let capacity = ( config.max_time_scale() / config.refresh_rate() ) as usize; + let selection = None; + let refresh_bool = true; + let data_window_time_scale = config.min_time_scale(); + + let mut queue_state = BoundedQueueState::new(capacity, selection, refresh_bool); + let network = service.fetch_item(); + + queue_state.add_item(network); + + Self { + config, + queue_state, + data_window_time_scale, + } + } +} + +impl Refreshable for NetworkComponent +where + S: ItemProvider +{ + fn refresh(&mut self, service: &S) { + let network_item: NetworkItem = service.fetch_item(); + self.queue_state.add_item(network_item); + } +} + +impl Component for NetworkComponent { + fn key_event(&mut self, key: Key) -> Result { + let key_config = &self.config.key_config; + if key == key_config.move_down { + self.data_window_time_scale = min(self.data_window_time_scale.saturating_add(self.config.time_inc()), self.config.max_time_scale()); + return Ok(EventState::Consumed) + } + if key == key_config.move_up { + self.data_window_time_scale = max(self.data_window_time_scale.saturating_sub(self.config.time_inc()), self.config.min_time_scale()); + return Ok(EventState::Consumed) + } + + Ok(EventState::NotConsumed) + } + + fn mouse_event(&mut self, mouse: Mouse) -> Result { + match mouse.kind { + MouseKind::ScrollDown => { + self.data_window_time_scale = min(self.data_window_time_scale.saturating_add(self.config.time_inc()), self.config.max_time_scale()); + return Ok(EventState::Consumed) + } + MouseKind::ScrollUp => { + self.data_window_time_scale = max(self.data_window_time_scale.saturating_sub(self.config.time_inc()), self.config.min_time_scale()); + return Ok(EventState::Consumed) + } + _ => {} + } + + Ok(EventState::NotConsumed) + } +} + +impl DrawableComponent for NetworkComponent { + fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { + let vertical_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(2), + Constraint::Min(0), + ]).split(area); + + let refresh_rate = self.config.refresh_rate(); // default = 2,000 ms + let time_scale = self.data_window_time_scale; // default = 60,000 ms + let data_window = (time_scale / refresh_rate) as usize; // default = 30 + let max_idx = data_window.saturating_sub(1); // + let y_max_scale_factor = 1.5; // used to scale y-upper bound + + let tx_data: Vec<(f64, f64)> = self.queue_state + .iter() + .rev() + .take(data_window) + .enumerate() + .map(|(idx, network_item)| { + ( + max_idx.saturating_sub(idx) as f64, + byte_to_kb(network_item.tx()) as f64, + ) + }) + .collect(); + + let rx_data: Vec<(f64, f64)> = self.queue_state + .iter() + .rev() + .take(data_window) + .enumerate() + .map(|(idx, network_item)| { + ( + max_idx.saturating_sub(idx) as f64, + byte_to_kb(network_item.rx()) as f64, + ) + }) + .collect(); + + // getting upper bound on y + let max_y = tx_data + .iter() + .chain(rx_data.iter()) + .map(|tuple| tuple.1) + .fold(0.0, f64::max); + + // scaling + let max_y = max_y * y_max_scale_factor; + + let datasets = vec![ + Dataset::default() + .data(&tx_data) + .graph_type(GraphType::Line) + .marker(Marker::Braille) + .style(Style::new().light_blue()), + + Dataset::default() + .data(&rx_data) + .graph_type(GraphType::Line) + .marker(Marker::Braille) + .style(Style::new().light_yellow()) + ]; + + // set block style + let block_style = if focused { + self.config.theme_config.style_border_focused + } + else { + self.config.theme_config.style_border_not_focused + }; + + // building chart + let x_axis = Axis::default() + .bounds([0.0, max_idx as f64]) + .labels(vec![Span::raw(format!("{}s", ms_to_s(time_scale))), Span::raw("now")]) + .labels_alignment(Alignment::Right); + + let y_axis = Axis::default() + .bounds([0.0, max_y]) + .labels(vec![ + Span::raw("0KB"), + Span::raw(format!("{}", max_y)), + ]) + .labels_alignment(Alignment::Right); + + let chart_title = " Network "; + let chart = Chart::new(datasets) + .block(Block::default() + .borders(Borders::LEFT|Borders::BOTTOM|Borders::RIGHT) + .style(block_style) + ) + .x_axis(x_axis) + .y_axis(y_axis); + + f.render_widget(chart, vertical_chunks[1]); + + let network_item = if let Some(item) = self.queue_state.back() { + item + } + else { + &NetworkItem::default() + }; + + let tx_per_s = network_item.tx() / ms_to_s(refresh_rate); + let rx_per_s = network_item.rx() / ms_to_s(refresh_rate); + let tx_legend = format!("TX/s {}KB :: TOTAL TX {}MB", byte_to_kb(tx_per_s), byte_to_mb(network_item.total_tx())); + let rx_legend = format!("RX/s {}KB :: TOTAL RX {}MB", byte_to_kb(rx_per_s), byte_to_mb(network_item.total_rx())); + + let legend = Paragraph::new( + Line::from(vec![ + Span::styled(tx_legend, Style::default().fg(Color::LightBlue)), + Span::raw(" "), + Span::styled(rx_legend, Style::default().fg(Color::LightYellow)), + ]) + .right_aligned()) + .block(Block::new() + .borders(Borders::LEFT|Borders::TOP|Borders::RIGHT) + .style(block_style) + .title(chart_title) + ); + + f.render_widget(legend, vertical_chunks[0]); + + Ok(()) + } +} \ No newline at end of file diff --git a/src/components/process.rs b/src/components/process.rs index c83ef82..584e016 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -1,14 +1,15 @@ use anyhow::{Ok, Result}; -use ratatui::{Frame, prelude::*}; -use ratatui::widgets::{block::*, *}; -use crate::services::VecProvider; +use ratatui::{Frame, prelude::*, widgets::*}; +use ratatui::layout::Position; use crate::config::*; -use crate::components::{common_nav, Component, DrawableComponent, EventState, MoveSelection, Refreshable}; -use crate::components::{utils::{selection::UISelection, vertical_scroll::VerticalScroll}, filter::FilterComponent}; +use crate::input::{Key, Mouse, MouseKind}; +use crate::services::VecProvider; +use crate::components::utils::{selection::UISelection, vertical_scroll::VerticalScroll}; +use crate::components::filter::FilterComponent; +use crate::components::*; use crate::states::vec_state::VecState; use crate::models::items::process_item::{ProcessItem, ProcessItemSortOrder}; - -use crate::input::{Key, Mouse, MouseKind}; +use crate::models::items::*; #[derive(PartialEq, Clone, Debug)] pub enum Focus { @@ -17,10 +18,16 @@ pub enum Focus { } pub struct ProcessComponent { - vec_state: VecState, - ui_selection: UISelection, - sort: Option, + vec_state: VecState, // underlying the "processlist" is a VecState: see states/vec_state.rs for details + // ui stuff + ui_selection: UISelection, // manages ui selection state and movement + table_area: Option, // table area is dynamic, updated by draw, init to None + filter_area: Option, // filter area is dynamic, updated by draw, init to None + header_height: u16, // header height is static, initialize in constructor to 1 + border_height: u16, // border height is static, initialize in constructor to 1, e.g., If Border::BOTTOM | Border::TOP, then total height = 2 scroll: VerticalScroll, + // + sort: ProcessItemSortOrder, filter_component: FilterComponent, focus: Focus, pub config: Config, @@ -31,11 +38,16 @@ impl ProcessComponent { where S: VecProvider { let processes: Vec = service.fetch_items(); + let ui_selection: UISelection = if processes.is_empty() { UISelection::new(None) } else { UISelection::new(Some(0)) }; + let table_area: Option = None; + let filter_area: Option = None; + let header_height = 1 as u16; + let border_height = 1 as u16; let state_selection: Option = ui_selection.selection; let filter: Option = None; - let sort: Option = None; - let vec_state: VecState = VecState::new(processes, state_selection, sort.clone(), filter); + let sort: ProcessItemSortOrder = ProcessItemSortOrder::CpuUsageDec; + let vec_state: VecState = VecState::new(processes, state_selection, Some(sort.clone()), filter); let scroll: VerticalScroll = VerticalScroll::new(); let filter_component: FilterComponent = FilterComponent::new(config.clone()); let focus: Focus = Focus::List; @@ -43,6 +55,10 @@ impl ProcessComponent { Self { vec_state, ui_selection, + table_area, + filter_area, + header_height, + border_height, sort, scroll, filter_component, @@ -51,6 +67,7 @@ impl ProcessComponent { } } + // SELECTION HANDLERS::begin fn handle_move_selection(&mut self, dir: MoveSelection) { let len = self.vec_state.view_indices().len(); // move ui selection by dir @@ -106,6 +123,132 @@ impl ProcessComponent { self.vec_state.set_selection(vec_idx); } + // toggles sort if already sorting by specified field + // else sets sort to decrementing of specified field + fn handle_sort(&mut self, key: Key) -> bool { + let key_config = &self.config.key_config; + let sort = self.sort; + if key == key_config.sort_pid_toggle { + if matches!(sort, ProcessItemSortOrder::PidDec) { + self.sort = ProcessItemSortOrder::PidInc; + } + else { + self.sort = ProcessItemSortOrder::PidDec; + } + self.vec_state.set_sort(Some(self.sort.clone())); + return true; + } + else if key == key_config.sort_name_toggle { + if matches!(sort, ProcessItemSortOrder::NameDec) { + self.sort = ProcessItemSortOrder::NameInc; + } + else { + self.sort = ProcessItemSortOrder::NameDec; + } + self.vec_state.set_sort(Some(self.sort.clone())); + return true + } + else if key == key_config.sort_cpu_toggle { + if matches!(sort, ProcessItemSortOrder::CpuUsageDec) { + self.sort = ProcessItemSortOrder::CpuUsageInc; + } + else { + self.sort = ProcessItemSortOrder::CpuUsageDec; + } + self.vec_state.set_sort(Some(self.sort.clone())); + return true; + } + else if key == key_config.sort_memory_toggle { + if matches!(sort, ProcessItemSortOrder::MemoryUsageDec) { + self.sort = ProcessItemSortOrder::MemoryUsageInc; + } + else { + self.sort = ProcessItemSortOrder::MemoryUsageDec; + } + self.vec_state.set_sort(Some(self.sort.clone())); + return true; + } + + false + + } + // SELECTION HANDLERS::end + + // MOUSE CLICK HANDLERS::begin + + /* computes the max/min y-coordinates of the process list and checks if 'click_y' is within the range + if click_y is within this range, then focus is set to List. if click_y is within the max/min y-coordinates + of visible items contained in the process list, then ui selection is set to the correspoding item index + and vec state selection is updated. + returns true if click_y is contained in the process list, else false + */ + fn handle_mouse_click_on_list(&mut self, click_y: u16) -> bool { + if self.table_area.is_none() { return false; } + let table_area = self.table_area.unwrap(); + let table_top = table_area.top(); // y-coordinate to top of table area + let table_height = table_area.height; // height of table area + let header_height = self.header_height; // height of table header + let border_height = self.border_height; // height of a single border + + let visible_list_height = table_height.saturating_sub(header_height).saturating_sub(border_height).saturating_sub(border_height); + // checking if click is NOT within list component constraints + if click_y < table_top.saturating_add(border_height).saturating_add(header_height) || + click_y >= table_top.saturating_add(border_height).saturating_add(header_height).saturating_add(visible_list_height) { + return false; + } + self.focus = Focus::List; + + + // determining the maximum number of visible items + let scroll_offset = self.scroll.get_top(); + let list_len = self.scroll.get_count(); + let max_visible_items = (list_len.saturating_sub(scroll_offset)) as u16; + let visible_list_height = if max_visible_items < visible_list_height { + max_visible_items + } + else { + visible_list_height + }; + + // checking if click is within list items constraints + if click_y < table_top.saturating_add(border_height).saturating_add(header_height) || + click_y >= table_top.saturating_add(border_height).saturating_add(header_height).saturating_add(visible_list_height) { + // return true here, click is considered consumed when focus is changed after the first check + return true; + } + + // calculating row index and corresponding item index + let row_idx = (click_y.saturating_sub(table_top).saturating_sub(header_height).saturating_sub(border_height)) as usize; + let item_idx = scroll_offset.saturating_add(row_idx); + + // setting selection and vec state + self.ui_selection.set_selection(Some(item_idx)); + let vec_idx = self.compute_vec_state_idx(); + self.vec_state.set_selection(vec_idx); + + true + } + + fn handle_mouse_click_on_filter(&mut self, click_y: u16) -> bool { + if self.filter_area.is_none() { return false; } + let filter_area = self.filter_area.unwrap(); + let filter_top = filter_area.top(); + let filter_height = filter_area.height; + let border_height = self.border_height; + + if click_y < filter_top.saturating_add(border_height) || + click_y >= filter_top.saturating_add(border_height).saturating_add(filter_height) { + return false; + } + + // click was on filter, set focus to filter + self.focus = Focus::Filter; + true + } + // MOUSE CLICK HANDLERS::end + + // HELPERS::begin + /* computes amd returns vector state index corresponding to ui selection */ fn compute_vec_state_idx(&self) -> Option { // map ui_selection.selection to vec_state let vec_idx = self.ui_selection.selection @@ -113,6 +256,7 @@ impl ProcessComponent { vec_idx } + // HELPERS::end } impl Refreshable for ProcessComponent @@ -126,7 +270,6 @@ where } } - impl Component for ProcessComponent { fn key_event(&mut self, key: Key) -> Result { if key == self.config.key_config.filter && @@ -155,10 +298,9 @@ impl Component for ProcessComponent { return Ok(EventState::Consumed) } - if let Some(sort_order) = match_sort_order_key(key, &self.config.key_config) { - self.sort = Some(sort_order); - self.vec_state.set_sort(self.sort.clone()); - self.handle_refresh_selection(); // handle_refresh() also works for handling sort + if self.handle_sort(key) { + // logic for handling selection after sort is similar enough to refresh + self.handle_refresh_selection(); return Ok(EventState::Consumed) } } @@ -168,16 +310,25 @@ impl Component for ProcessComponent { fn mouse_event(&mut self, mouse: Mouse) -> Result { match mouse.kind { - MouseKind::ScrollDown | MouseKind::ScrollUp => { - let dir = match mouse.kind { - MouseKind::ScrollDown => MoveSelection::Down, - MouseKind::ScrollUp => MoveSelection::Up, - _ => unreachable!(), // safeguarded by conditional - }; - - self.handle_move_selection(dir); + MouseKind::ScrollDown => { + self.handle_move_selection(MoveSelection::Down); + return Ok(EventState::Consumed); + } + MouseKind::ScrollUp => { + self.handle_move_selection(MoveSelection::Up); return Ok(EventState::Consumed); } + MouseKind::LeftClick => { + if self.handle_mouse_click_on_list(mouse.row) { + return Ok(EventState::Consumed); + } + if self.handle_mouse_click_on_filter(mouse.row) { + return Ok(EventState::Consumed); + } + /*if self.handle_mouse_click_on_header(mouse.column, mouse.row) { + return Ok(EventState::Consumed) + }*/ + } _ => {} } @@ -185,16 +336,24 @@ impl Component for ProcessComponent { } } -// maps key to ProcessItemSortOrder -fn match_sort_order_key(key: Key, key_config: &KeyConfig) -> Option { - if key == key_config.sort_pid_inc { return Some(ProcessItemSortOrder::PidInc) } - if key == key_config.sort_pid_dec { return Some(ProcessItemSortOrder::PidDec) } - if key == key_config.sort_cpu_usage_inc { return Some(ProcessItemSortOrder::CpuUsageInc) } - if key == key_config.sort_cpu_usage_dec { return Some(ProcessItemSortOrder::CpuUsageDec) } - if key == key_config.sort_memory_usage_inc { return Some(ProcessItemSortOrder::MemoryUsageInc) } - if key == key_config.sort_memory_usage_dec { return Some(ProcessItemSortOrder::MemoryUsageDec) } - if key == key_config.sort_name_inc { return Some(ProcessItemSortOrder::NameInc) } - if key == key_config.sort_name_dec { return Some(ProcessItemSortOrder::NameDec) } +// helper function to match key to sort variant +fn handle_sort(key: Key, key_config: &KeyConfig, sort: &ProcessItemSortOrder) -> Option { + if key == key_config.sort_pid_toggle { + if matches!(sort, ProcessItemSortOrder::PidDec) { return Some(ProcessItemSortOrder::PidInc)}; + return Some(ProcessItemSortOrder::PidDec); + } + if key == key_config.sort_cpu_toggle { + if matches!(sort, ProcessItemSortOrder::PidDec) { return Some(ProcessItemSortOrder::PidInc)}; + return Some(ProcessItemSortOrder::PidDec); + } + + + + if key == key_config.sort_cpu_toggle { return Some(ProcessItemSortOrder::CpuUsageDec) } + if key == key_config.sort_memory_toggle { return Some(ProcessItemSortOrder::MemoryUsageInc) } + if key == key_config.sort_memory_toggle { return Some(ProcessItemSortOrder::MemoryUsageDec) } + if key == key_config.sort_name_toggle { return Some(ProcessItemSortOrder::NameInc) } + if key == key_config.sort_name_toggle { return Some(ProcessItemSortOrder::NameDec) } return None } @@ -204,20 +363,34 @@ impl DrawableComponent for ProcessComponent { let horizontal_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Fill(1), //list + Constraint::Fill(1), //table(process list) Constraint::Length(3), //filter ]).split(area); - // sub 3 here for table header (1 line) and borders (2 lines) - let visible_list_height = horizontal_chunks[0].height.saturating_sub(4) as usize; + // set heights + let header_height = self.header_height; + let border_height = self.border_height; + let visible_list_height = horizontal_chunks[0] + .height + .saturating_sub(border_height) + .saturating_sub(border_height) + .saturating_sub(header_height) as usize; + + // saving table and filter area to process mouse clicks + // set table area + self.table_area = Some(horizontal_chunks[0]); + // set filter area + self.filter_area = Some(horizontal_chunks[1]); // update vertical scroll let indices = self.vec_state.view_indices(); let len = indices.len(); self.ui_selection.selection.map_or_else( { || + // if selection is none self.scroll.reset() }, |idx| { + // if selection is some self.scroll.update(idx, len, visible_list_height); },); @@ -272,22 +445,21 @@ fn draw_process_list<'a, I>( visible_items: I, focus: bool, theme_config: ThemeConfig, - sort_order: Option, + sort_order: ProcessItemSortOrder, ) where I: Iterator, { - // setting header let header_labels = [ "", - &header_with_sort(&sort_order, &ProcessItemSortOrder::PidInc, &ProcessItemSortOrder::PidDec, "PID"), - &header_with_sort(&sort_order, &ProcessItemSortOrder::NameInc, &ProcessItemSortOrder::NameDec, "Name"), - &header_with_sort(&sort_order, &ProcessItemSortOrder::CpuUsageInc, &ProcessItemSortOrder::CpuUsageDec, "CPU (%)"), - &header_with_sort(&sort_order, &ProcessItemSortOrder::MemoryUsageInc, &ProcessItemSortOrder::MemoryUsageDec, "Memory (MB)"), - "Run (hh:mm:ss)", - "Status", - "Path", + &header_with_sort(&sort_order, &ProcessItemSortOrder::PidInc, &ProcessItemSortOrder::PidDec, "PID(p)"), + &header_with_sort(&sort_order, &ProcessItemSortOrder::NameInc, &ProcessItemSortOrder::NameDec, "NAME(n)"), + &header_with_sort(&sort_order, &ProcessItemSortOrder::CpuUsageInc, &ProcessItemSortOrder::CpuUsageDec, "CPU(c)(%)"), + &header_with_sort(&sort_order, &ProcessItemSortOrder::MemoryUsageInc, &ProcessItemSortOrder::MemoryUsageDec, "MEM(m)(MB)"), + "STATUS", + "RUNTIME", + //"PATH", ]; let header = header_labels @@ -295,29 +467,27 @@ where .map(Cell::from) .collect::() .style(if focus {theme_config.style_border_focused} else {theme_config.style_border_not_focused}) - .height(2); + .height(1); // setting rows let rows = visible_items .map(|(_idx, item, selected)| { let style = compute_row_style(focus, selected, &theme_config); - - let is_selected_style = [ - theme_config.style_item_selected, - theme_config.style_item_selected_followed, - theme_config.style_item_selected_followed_not_focused, - theme_config.style_item_selected_not_focused, - ].contains(&style); + let indicator = if style == theme_config.style_item_selected { + "->" + } else { + "" + }; let cells = vec![ - Cell::from(if is_selected_style {"->"} else { "" }), + Cell::from(indicator), Cell::from(item.pid().to_string()), - Cell::from(item.name()), + Cell::from(format!("{:.40}", item.name())), Cell::from(format!("{:.2}", item.cpu_usage())), - Cell::from(format!("{:.2}", item.memory_usage()/1000000)), - Cell::from(item.run_time_hh_mm_ss()), + Cell::from(format!("{}", byte_to_mb(item.memory_usage()))), Cell::from(item.status()), - Cell::from(item.path()), + Cell::from(item.run_time_dd_hh_mm_ss()), + //Cell::from(item.path()), ]; Row::new(cells).style(style) }) @@ -327,13 +497,12 @@ where let widths = vec![ Constraint::Length(2), // arrow - Constraint::Length(10), // pid - Constraint::Length(50), // name - Constraint::Length(15), // cpu usage - Constraint::Length(15), // memory usage - Constraint::Length(20), // run time - Constraint::Length(15), // status - Constraint::Min(0), // path + Constraint::Percentage(10), // pid + Constraint::Percentage(30), // name + Constraint::Percentage(15), // cpu usage + Constraint::Percentage(15), // memory usage + Constraint::Percentage(15), // status + Constraint::Percentage(15), // run time ]; // setting block information @@ -352,14 +521,14 @@ where // helper function for building header labels fn header_with_sort( - current: &Option, + current: &ProcessItemSortOrder, inc: &ProcessItemSortOrder, dec: &ProcessItemSortOrder, base: &str, ) -> String { match current { - Some(s) if s == inc => format!("{base} ▲"), - Some(s) if s == dec => format!("{base} ▼"), + s if s == inc => format!("{base} ▲"), + s if s == dec => format!("{base} ▼"), _ => base.to_string(), } } @@ -368,12 +537,16 @@ fn header_with_sort( fn compute_row_style(focus: bool, selected: bool, theme: &ThemeConfig) -> Style { match (focus, selected) { (true, true) => theme.style_item_selected, - (true, _,) => theme.style_item_focused, + (true, false) => theme.style_item_focused, (false, true) => theme.style_item_selected_not_focused, _ => theme.style_item_not_focused, } } + + + +// TEST MODULE #[cfg(test)] mod test { use super::*; @@ -402,7 +575,8 @@ mod test { test_data(self.idx) } } - + + #[test] fn test_constructor_with_data() { @@ -417,7 +591,7 @@ mod test { // check that ui_selection is Some(0) since there is data assert_eq!(component.ui_selection.selection, Some(0)); // check that sort and filter are None - assert!(component.sort.is_none()); + //assert!(component.sort.is_none()); assert!(component.vec_state.filter().is_none()); // check focus assert_eq!(component.focus, Focus::List); @@ -436,7 +610,7 @@ mod test { // check that ui_selection is Some(0) since there is data assert_eq!(component.ui_selection.selection, None); // check that sort and filter are None - assert!(component.sort.is_none()); + //assert!(component.sort.is_none()); assert!(component.vec_state.filter().is_none()); // check focus assert_eq!(component.focus, Focus::List); @@ -493,6 +667,8 @@ mod test { assert_eq!(component.ui_selection.selection, Some(0)); assert!(component.vec_state.selection().is_some()); } + + //TODO: add tests for mouse_event() and key_event() fn test_data(idx: usize) -> Vec { match idx { diff --git a/src/components/utils/vertical_scroll.rs b/src/components/utils/vertical_scroll.rs index dd08534..65f11b3 100644 --- a/src/components/utils/vertical_scroll.rs +++ b/src/components/utils/vertical_scroll.rs @@ -24,6 +24,10 @@ impl VerticalScroll { self.top.get() } + pub fn get_count(&self) -> usize { + self.count.get() + } + pub fn reset(&self) { self.top.set(0); } @@ -51,14 +55,11 @@ const fn calc_scroll_top( return 0; } - let padding = visual_height / 2; - let min_top = selection.saturating_sub(padding); - - if selection < current_top + padding { - min_top + if selection < current_top { + selection } - else if selection >= current_top + visual_height - padding { - min_top + else if selection >= current_top + visual_height { + selection.saturating_sub(visual_height.saturating_sub(1)) } else { current_top diff --git a/src/config.rs b/src/config.rs index efcfb7f..d794510 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use std::ops::Div; use serde::{Deserialize,Serialize}; #[derive(Clone)] @@ -6,22 +7,30 @@ pub struct Config { pub mouse_config: MouseConfig, pub theme_config: ThemeConfig, refresh_rate: u64, - min_as_s: u64, - events_per_min: u64, + max_time_scale: u64, + min_time_scale: u64, + time_inc: u64, tick_rate: u64, } -//times are in ms impl Default for Config { fn default() -> Self { + let refresh_rate = 2000; // ms (2 seconds) + let max_time_scale = 300000; // ms (5 minutes) + let min_time_scale = 60000; // ms (60 seconds) + let time_inc = 30000; // ms (30 seconds) + let tick_rate = 250; // ms + + Self { key_config: KeyConfig::default(), mouse_config: MouseConfig::default(), theme_config: ThemeConfig::default(), - refresh_rate: 2000, - min_as_s: 60000/ 1000, - events_per_min: 60000 / 2000, - tick_rate: 250, + refresh_rate, + max_time_scale, + min_time_scale, + time_inc, + tick_rate, } } } @@ -31,17 +40,25 @@ impl Config { self.refresh_rate } - pub fn tick_rate(&self) -> u64 { - self.tick_rate + pub fn max_time_scale(&self) -> u64 { + self.max_time_scale } - pub fn min_as_s(&self) -> u64 { - self.min_as_s + pub fn min_time_scale(&self) -> u64 { + self.min_time_scale } - - pub fn events_per_min(&self) -> u64 { - self.events_per_min + + pub fn time_inc(&self) -> u64 { + self.time_inc } + + pub fn tick_rate(&self) -> u64 { + self.tick_rate + } +} + +pub fn ms_to_s(data_ms: u64) -> u64 { + data_ms.div(1000) } #[derive(Clone)] @@ -54,16 +71,12 @@ pub struct KeyConfig { pub tab: Key, pub filter: Key, pub terminate: Key, - pub open_help: Key, + pub help: Key, pub exit: Key, - pub sort_name_inc: Key, - pub sort_name_dec: Key, - pub sort_pid_inc: Key, - pub sort_pid_dec: Key, - pub sort_cpu_usage_inc: Key, - pub sort_cpu_usage_dec: Key, - pub sort_memory_usage_inc: Key, - pub sort_memory_usage_dec: Key, + pub sort_name_toggle: Key, + pub sort_pid_toggle: Key, + pub sort_cpu_toggle: Key, + pub sort_memory_toggle: Key, pub follow_selection: Key, pub expand: Key, } @@ -79,16 +92,12 @@ impl Default for KeyConfig { tab: Key::Tab, filter: Key::Char('/'), terminate: Key::Char('T'), - open_help: Key::Char('?'), + help: Key::Char('?'), exit: Key::Esc, - sort_name_inc: Key::Char('n'), - sort_name_dec: Key::Char('N'), - sort_pid_inc: Key::Char('p'), - sort_pid_dec: Key::Char('P'), - sort_cpu_usage_inc: Key::Char('c'), - sort_cpu_usage_dec: Key::Char('C'), - sort_memory_usage_inc: Key::Char('m'), - sort_memory_usage_dec: Key::Char('M'), + sort_name_toggle: Key::Char('n'), + sort_pid_toggle: Key::Char('p'), + sort_cpu_toggle: Key::Char('c'), + sort_memory_toggle: Key::Char('m'), follow_selection: Key::Char('f'), expand: Key::Char('e'), } @@ -114,9 +123,9 @@ impl Default for MouseConfig { } } -use ratatui::{prelude::{Color, Modifier, Style}, style::Stylize}; - +use ratatui::prelude::{Color, Style}; use crate::input::{Key, MouseKind}; + #[derive(Clone,PartialEq,Serialize,Deserialize)] pub struct ThemeConfig { pub style_border_focused: Style, @@ -125,24 +134,19 @@ pub struct ThemeConfig { pub style_item_not_focused: Style, pub style_item_selected: Style, pub style_item_selected_not_focused: Style, - pub style_item_selected_followed: Style, - pub style_item_selected_followed_not_focused: Style, } impl Default for ThemeConfig { fn default() -> Self { Self { - style_border_focused: Style::default().fg(Color::LightGreen).bold(), + style_border_focused: Style::default().fg(Color::LightGreen), style_border_not_focused: Style::default().fg(Color::DarkGray), style_item_focused: Style::default().fg(Color::White), style_item_not_focused: Style::default().fg(Color::Gray), - style_item_selected: Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD), - style_item_selected_not_focused: Style::default().fg(Color::White).add_modifier(Modifier::BOLD), - - style_item_selected_followed: Style::default().bg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED), - style_item_selected_followed_not_focused: Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED), + style_item_selected: Style::default().fg(Color::LightBlue), + style_item_selected_not_focused: Style::default().fg(Color::White), } } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 132c3a1..5d68cb0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ use anyhow::Result; use crossterm::event::EnableMouseCapture; use std::io::{stdout}; -use crossterm::ExecutableCommand; use crossterm::{ execute, terminal::{enable_raw_mode, EnterAlternateScreen}, @@ -19,6 +18,7 @@ pub mod app; pub mod config; pub mod input; pub mod components; +pub mod ui; pub mod events; pub mod models; pub mod states; diff --git a/src/models/bounded_queue.rs b/src/models/bounded_queue_model.rs similarity index 91% rename from src/models/bounded_queue.rs rename to src/models/bounded_queue_model.rs index a864f79..77982ac 100644 --- a/src/models/bounded_queue.rs +++ b/src/models/bounded_queue_model.rs @@ -1,13 +1,12 @@ use std::collections::VecDeque; #[derive(Default)] -pub struct BoundedQueue { - pub items: VecDeque, +pub struct BoundedQueueModel { + items: VecDeque, capacity: usize, } -// Clone trait is required for T to clone elements when adding items. -impl BoundedQueue { +impl BoundedQueueModel { pub fn new(capacity: usize) -> Self { Self { items: VecDeque::with_capacity(capacity), @@ -15,6 +14,7 @@ impl BoundedQueue { } } + // MUTATORS pub fn add_item(&mut self, item: T) { let len = self.items.len(); @@ -33,6 +33,7 @@ impl BoundedQueue { } } + // GETTERS pub fn front(&self) -> Option<&T> { self.items.front() } @@ -45,6 +46,11 @@ impl BoundedQueue { self.capacity } + pub fn items(&self) -> &VecDeque { + &self.items + } + + // ITERATORS pub fn iter(&self) -> std::collections::vec_deque::Iter<'_, T> { self.items.iter() } diff --git a/src/models/items/memory_item.rs b/src/models/items/memory_item.rs index 9f856de..35b4249 100644 --- a/src/models/items/memory_item.rs +++ b/src/models/items/memory_item.rs @@ -39,6 +39,16 @@ impl MemoryItem { self.used_memory as f64 / 1000000000_f64 } + pub fn percent_memory_usage(&self) -> f64 { + let percent = (self.used_memory_gb() / self.total_memory_gb()) * 100_f64; + if percent.is_nan() { + return 0_f64 + } + else { + return percent + } + } + pub fn total_swap(&self) -> u64 { self.total_swap } @@ -54,6 +64,16 @@ impl MemoryItem { pub fn used_swap_gb(&self) -> f64 { self.used_swap as f64 / 1000000000_f64 } + + pub fn percent_swap_usage(&self) -> f64 { + let percent = (self.used_swap_gb() / self.total_swap_gb()) * 100_f64; + if percent.is_nan() { + return 0_f64 + } + else { + return percent + } + } } #[cfg(test)] diff --git a/src/models/items/mod.rs b/src/models/items/mod.rs index c5689cf..8fd5cb5 100644 --- a/src/models/items/mod.rs +++ b/src/models/items/mod.rs @@ -1,4 +1,15 @@ +use std::ops::Div; + pub mod cpu_item; pub mod memory_item; pub mod temp_item; -pub mod process_item; \ No newline at end of file +pub mod process_item; +pub mod network_item; + +pub fn byte_to_kb(data: u64) -> u64 { + data.div(1024) +} + +pub fn byte_to_mb(data: u64) -> u64 { + data.div(1048576) +} \ No newline at end of file diff --git a/src/models/items/network_item.rs b/src/models/items/network_item.rs new file mode 100644 index 0000000..d5fce41 --- /dev/null +++ b/src/models/items/network_item.rs @@ -0,0 +1,40 @@ +#[derive(Default)] +pub struct NetworkItem { + tx: u64, + rx: u64, + total_tx: u64, + total_rx: u64, +} + +impl NetworkItem { + pub fn new( + tx: u64, + rx: u64, + total_tx: u64, + total_rx: u64, + ) -> Self { + Self { + tx, + rx, + total_tx, + total_rx, + } + } + + // GETTERS + pub fn tx(&self) -> u64 { + self.tx + } + + pub fn rx(&self) -> u64 { + self.rx + } + + pub fn total_tx(&self) -> u64 { + self.total_tx + } + + pub fn total_rx(&self) -> u64 { + self.total_rx + } +} \ No newline at end of file diff --git a/src/models/items/process_item.rs b/src/models/items/process_item.rs index 5a95a04..cb6ddeb 100644 --- a/src/models/items/process_item.rs +++ b/src/models/items/process_item.rs @@ -1,3 +1,5 @@ +use std::ops::Div; + use crate::{models::{Filterable, Sortable}}; #[derive(Clone, Copy, PartialEq)] @@ -10,38 +12,54 @@ pub enum ProcessItemSortOrder { CpuUsageDec, MemoryUsageInc, MemoryUsageDec, + StatusInc, + StatusDec, + RuntimeInc, + RuntimeDec, } #[derive(Default, Clone)] pub struct ProcessItem { - pid: u32, - name: String, - cpu_usage: f32, - memory_usage: u64, - start_time: u64, - run_time: u64, - accumulated_cpu_time: u64, - status: String, - path: String, + pid: u32, + name: String, + cpu_usage: f32, + memory_usage: u64, + //read_bytes: u64, + //written_bytes: u64, + //total_read_bytes: u64, + //total_written_bytes: u64, + start_time: u64, + run_time: u64, + accumulated_cpu_time: u64, + status: String, + path: String, } impl ProcessItem { 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, - path: String, + pid: u32, + name: String, + cpu_usage: f32, + memory_usage: u64, + //read_bytes: u64, + //written_bytes: u64, + //total_read_bytes: u64, + //total_written_bytes: u64, + start_time: u64, + run_time: u64, + accumulated_cpu_time: u64, + status: String, + path: String, ) -> Self { Self { pid, name, cpu_usage, memory_usage, + //read_bytes, + //written_bytes, + //total_read_bytes, + //total_written_bytes, start_time, run_time, accumulated_cpu_time, @@ -67,6 +85,22 @@ impl ProcessItem { self.memory_usage } + /*pub fn read_bytes(&self) -> u64 { + self.read_bytes + } + + pub fn written_bytes(&self) -> u64 { + self.written_bytes + } + + pub fn total_read_bytes(&self) -> u64 { + self.total_read_bytes + } + + pub fn total_written_bytes(&self) -> u64 { + self.total_written_bytes + }*/ + pub fn start_time(&self) -> u64 { self.start_time } @@ -75,14 +109,15 @@ impl ProcessItem { self.run_time } - pub fn run_time_hh_mm_ss(&self) -> String { + pub fn run_time_dd_hh_mm_ss(&self) -> String { let time_in_s = self.run_time; let ss = time_in_s % 60; let mm = (time_in_s / 60) % 60; - let hh = (time_in_s / 60) / 60; + let hh = (time_in_s / 3600) % 24; + let dd = hh / 86400; - format!("{:0>2}:{:0>2}:{:0>2}", hh, mm, ss) + format!("{:0>2}D {:0>2}H {:0>2}M {:0>2}S", dd, hh, mm, ss) } pub fn accumulated_cpu_time(&self) -> u64 { @@ -96,49 +131,6 @@ impl ProcessItem { pub fn path(&self) -> &str { &self.path } - - // STATIC COMPARATORS -- get rid of these - pub fn cmp_pid_inc(a: &Self, b: &Self) -> std::cmp::Ordering { - a.pid.cmp(&b.pid) - } - - pub fn cmp_pid_dec(a: &Self, b: &Self) -> std::cmp::Ordering { - b.pid.cmp(&a.pid) - } - - pub fn cmp_name_inc(a: &Self, b: &Self) -> std::cmp::Ordering { - a.name.cmp(&b.name) - } - - pub fn cmp_name_dec(a: &Self, b: &Self) -> std::cmp::Ordering { - b.name.cmp(&a.name) - } - - pub fn cmp_cpu_inc(a: &Self, b: &Self) -> std::cmp::Ordering { - // ordering cannot always be determined with f32 - // in case where f32 comparison returns None this - // function will return Equal - a.cpu_usage - .partial_cmp(&b.cpu_usage) - .unwrap_or(std::cmp::Ordering::Equal) - } - - pub fn cmp_cpu_dec(a: &Self, b: &Self) -> std::cmp::Ordering { - // ordering cannot always be determined with f32 - // in case where f32 comparison returns None this - // function will return Equal - b.cpu_usage - .partial_cmp(&a.cpu_usage) - .unwrap_or(std::cmp::Ordering::Equal) - } - - pub fn cmp_mem_inc(a: &Self, b: &Self) -> std::cmp::Ordering { - a.memory_usage.cmp(&b.memory_usage) - } - - pub fn cmp_mem_dec(a: &Self, b: &Self) -> std::cmp::Ordering { - b.memory_usage.cmp(&a.memory_usage) - } } // PartialEq is needed for comparison, e.g., calling contains @@ -150,27 +142,79 @@ impl PartialEq for ProcessItem { impl Filterable for ProcessItem { fn matches_filter(&self, filter: &str) -> bool { - self.name.contains(filter) + // by pid + if let Some(pid_str) = filter.strip_prefix("pid>") { + if let Ok(threshold) = pid_str.trim().parse::() { + return self.pid() > threshold; + } + } else if let Some(pid_str) = filter.strip_prefix("pid<") { + if let Ok(threshold) = pid_str.trim().parse::() { + return self.pid() < threshold; + } + } else if let Some(pid_str) = filter.strip_prefix("pid=") { + if let Ok(target) = pid_str.trim().parse::() { + return self.pid() == target; + } + } + + // by cpu + if let Some(cpu_str) = filter.strip_prefix("cpu>") { + if let Ok(threshold) = cpu_str.trim().parse::() { + return self.cpu_usage() > threshold; + } + } else if let Some(cpu_str) = filter.strip_prefix("cpu<") { + if let Ok(threshold) = cpu_str.trim().parse::() { + return self.cpu_usage() < threshold; + } + } else if let Some(cpu_str) = filter.strip_prefix("cpu=") { + if let Ok(target) = cpu_str.trim().parse::() { + return self.cpu_usage() == target; + } + } + + // by mem + if let Some(mem_str) = filter.strip_prefix("mem>") { + if let Ok(threshold) = mem_str.trim().parse::() { + return (self.memory_usage().div(1000000)) > threshold; + } + } else if let Some(mem_str) = filter.strip_prefix("mem<") { + if let Ok(threshold) = mem_str.trim().parse::() { + return self.memory_usage().div(1000000) < threshold; + } + } else if let Some(mem_str) = filter.strip_prefix("mem=") { + if let Ok(target) = mem_str.trim().parse::() { + return self.memory_usage().div(1000000) == target; + } + } + + // by name + self.name.to_lowercase().contains(&filter.to_lowercase()) } } impl Sortable for ProcessItem { fn cmp_with(&self, other: &Self, sort: &ProcessItemSortOrder) -> std::cmp::Ordering { match sort { - ProcessItemSortOrder::PidInc => self.pid.cmp(&other.pid), - ProcessItemSortOrder::PidDec => other.pid.cmp(&self.pid), - ProcessItemSortOrder::NameInc => self.name.cmp(&other.name), - ProcessItemSortOrder::NameDec => other.name.cmp(&self.name), - ProcessItemSortOrder::CpuUsageInc => self.cpu_usage.partial_cmp(&other.cpu_usage).unwrap_or(std::cmp::Ordering::Equal), - ProcessItemSortOrder::CpuUsageDec => other.cpu_usage.partial_cmp(&self.cpu_usage).unwrap_or(std::cmp::Ordering::Equal), - ProcessItemSortOrder::MemoryUsageInc => self.memory_usage.cmp(&other.memory_usage), - ProcessItemSortOrder::MemoryUsageDec => other.memory_usage.cmp(&self.memory_usage), + ProcessItemSortOrder::PidInc => self.pid.cmp(&other.pid), + ProcessItemSortOrder::PidDec => other.pid.cmp(&self.pid), + ProcessItemSortOrder::NameInc => self.name.cmp(&other.name), + ProcessItemSortOrder::NameDec => other.name.cmp(&self.name), + ProcessItemSortOrder::CpuUsageInc => self.cpu_usage.partial_cmp(&other.cpu_usage).unwrap_or(std::cmp::Ordering::Equal), + ProcessItemSortOrder::CpuUsageDec => other.cpu_usage.partial_cmp(&self.cpu_usage).unwrap_or(std::cmp::Ordering::Equal), + ProcessItemSortOrder::MemoryUsageInc => self.memory_usage.cmp(&other.memory_usage), + ProcessItemSortOrder::MemoryUsageDec => other.memory_usage.cmp(&self.memory_usage), + ProcessItemSortOrder::StatusInc => self.status.cmp(&other.status), + ProcessItemSortOrder::StatusDec => other.status.cmp(&self.status), + ProcessItemSortOrder::RuntimeInc => self.run_time.cmp(&other.run_time), + ProcessItemSortOrder::RuntimeDec => other.run_time.cmp(&self.run_time), } } } #[cfg(test)] pub mod test { + use crate::models::Filterable; + use super::ProcessItem; #[test] @@ -210,9 +254,9 @@ pub mod test { 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_eq!(instance_0.is_match("a"), false); - //assert_eq!(instance_0.is_match(&instance_0.pid.to_string()), true); + assert_eq!(instance_0.matches_filter(""), true); + assert_eq!(instance_0.matches_filter("a"), false); + assert_eq!(instance_0.matches_filter(&format!("pid={}", &instance_0.pid())), true); assert_eq!(instance_1.pid(), instance_1.pid); assert_eq!(instance_1.name(), instance_1.name); @@ -222,8 +266,8 @@ pub mod test { 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_eq!(instance_1.is_match("aa"), false); - //assert_eq!(instance_1.is_match(&instance_1.pid.to_string()), true); + assert_eq!(instance_1.matches_filter("a"), true); + assert_eq!(instance_1.matches_filter("aa"), false); + assert_eq!(instance_1.matches_filter(&format!("pid={}", &instance_1.pid.to_string())), true); } } \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs index 5752a3a..9bf9be4 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,5 @@ pub mod items; -pub mod bounded_queue; +pub mod bounded_queue_model; pub mod vec_model; pub trait Filterable { diff --git a/src/services/mod.rs b/src/services/mod.rs index 5d7bbc5..03f7c43 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -9,4 +9,8 @@ pub mod sysinfo_service; // pub trait VecProvider { fn fetch_items(&self) -> Vec; +} + +pub trait ItemProvider { + fn fetch_item(&self) -> T; } \ No newline at end of file diff --git a/src/services/sysinfo_service.rs b/src/services/sysinfo_service.rs index e2a44c3..e72f4d9 100644 --- a/src/services/sysinfo_service.rs +++ b/src/services/sysinfo_service.rs @@ -1,13 +1,15 @@ -use sysinfo::{Pid, System, Components}; +use sysinfo::{Components, Networks, Pid, System}; +use crate::models::items::network_item::NetworkItem; use crate::models::items::{memory_item::MemoryItem, temp_item::TempItem, cpu_item::CpuItem, process_item::ProcessItem}; use crate::config::Config; -use crate::services::VecProvider; +use crate::services::{ItemProvider, VecProvider}; // 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 SysInfoService { system: System, components: Components, + networks: Networks, pub _config: Config } @@ -16,6 +18,7 @@ impl SysInfoService { Self { system: System::new_all(), components: Components::new_with_refreshed_list(), + networks: Networks::new_with_refreshed_list(), _config: config } } @@ -23,6 +26,7 @@ impl SysInfoService { pub fn refresh_all(&mut self) { self.system.refresh_all(); self.components.refresh(false); + self.networks.refresh(true); } pub fn get_cpus(&self) -> Vec { @@ -47,49 +51,6 @@ impl SysInfoService { cpus } - pub fn get_memory(&self, memory: &mut MemoryItem) { - let total_memory = self.system.total_memory(); // total memory is size of RAM in bytes - let used_memory = self.system.used_memory(); // used memory is allocated memory - let total_swap = self.system.total_swap(); - let used_swap = self.system.used_swap(); - - memory.update(total_memory, used_memory, total_swap, used_swap); - } - - pub fn get_temps(&self, temps: &mut Vec) { - temps.clear(); - - for component in &self.components { - let temp = if let Some(temp) = component.temperature() { - temp - } - else { - 0_f32 - }; - - let max_temp = if let Some(max_temp) = component.max() { - max_temp - } - else { - 0_f32 - }; - - let critical_temp = if let Some(critical_temp) = component.critical() { - critical_temp - } - else { - 0_f32 - }; - - let label = component.label().to_string(); - - - let item = TempItem::new(temp, max_temp, critical_temp, label); - - temps.push(item); - } - } - pub fn terminate_process(&self, pid: u32) -> bool { let mut res = false; @@ -102,6 +63,35 @@ impl SysInfoService { } } +impl ItemProvider for SysInfoService { + fn fetch_item(&self) -> NetworkItem { + let mut tx = 0; + let mut rx = 0; + let mut total_tx = 0; + let mut total_rx = 0; + + for (_interface_name, network) in &self.networks { + tx += network.transmitted(); + rx += network.received(); + total_tx += network.total_transmitted(); + total_rx += network.total_received(); + } + + NetworkItem::new(tx, rx, total_tx, total_rx) + } +} + +impl ItemProvider for SysInfoService { + fn fetch_item(&self) -> MemoryItem { + let total_memory = self.system.total_memory(); // total memory is size of RAM in bytes + let used_memory = self.system.used_memory(); // used memory is allocated memory + let total_swap = self.system.total_swap(); + let used_swap = self.system.used_swap(); + + MemoryItem::new(total_memory, used_memory, total_swap, used_swap) + } +} + impl VecProvider for SysInfoService { fn fetch_items(&self) -> Vec { let mut temps: Vec = Vec::new(); diff --git a/src/states/bounded_queue_state.rs b/src/states/bounded_queue_state.rs new file mode 100644 index 0000000..18f8cb7 --- /dev/null +++ b/src/states/bounded_queue_state.rs @@ -0,0 +1,65 @@ +use crate::models::bounded_queue_model::BoundedQueueModel; +use std::collections::VecDeque; + +pub struct BoundedQueueState { + model: BoundedQueueModel, + selection: Option, + refresh_bool: bool, +} + +impl BoundedQueueState { + pub fn new( + capacity: usize, + selection: Option, + refresh_bool: bool + ) -> Self { + let model = BoundedQueueModel::new(capacity); + + Self { + model, + selection, + refresh_bool, + } + } + + // MUTATORS + pub fn set_selection(&mut self, selection: Option) { + self.selection = selection; + } + + pub fn toggle_refresh(&mut self) { + self.refresh_bool = !self.refresh_bool; + } + + pub fn add_item(&mut self, item: T) { + if self.refresh_bool { + self.model.add_item(item); + } + } + + // GETTERS + pub fn front(&self) -> Option<&T> { + self.model.front() + } + + pub fn back(&self) -> Option<&T> { + self.model.back() + } + + pub fn capacity(&self) -> usize { + self.model.capacity() + } + + pub fn model_items(&self) -> &VecDeque { + self.model.items() + } + + /*pub fn as_vec(&self) -> &[T] { + self.model.items.into_iter().collect() + }*/ + + // ITERS + pub fn iter(&self) -> std::collections::vec_deque::Iter<'_, T> { + self.model.iter() + } +} \ No newline at end of file diff --git a/src/states/mod.rs b/src/states/mod.rs index 7c58275..74d0a08 100644 --- a/src/states/mod.rs +++ b/src/states/mod.rs @@ -1 +1,2 @@ -pub mod vec_state; \ No newline at end of file +pub mod vec_state; +pub mod bounded_queue_state; \ No newline at end of file diff --git a/src/states/vec_state.rs b/src/states/vec_state.rs index a1dfb74..4ab1ad3 100644 --- a/src/states/vec_state.rs +++ b/src/states/vec_state.rs @@ -5,12 +5,10 @@ pub struct VecState { model: VecModel, selection: Option, sort: Option, - filter: Option, + filter: Option, } -impl VecState -where S: Clone, -{ +impl VecState { pub fn new(model: Vec, selection: Option, sort: Option, filter: Option) -> Self { let model = VecModel::new(model); @@ -117,6 +115,7 @@ where } } +/* #[cfg(test)] mod tests { use std::vec; @@ -145,4 +144,4 @@ mod tests { println!("{text}"); }); } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/src/ui/fitted_sparkline.rs b/src/ui/fitted_sparkline.rs new file mode 100644 index 0000000..1f71d26 --- /dev/null +++ b/src/ui/fitted_sparkline.rs @@ -0,0 +1,199 @@ +use ratatui::{prelude::*, widgets::{Block, RenderDirection, Widget}}; + +pub struct FittedSparkline<'a> { + pub data: &'a [u64], + pub num_data_points: Option, + pub max: Option, + pub block: Option>, + pub style: Style, + pub direction: RenderDirection, +} + +impl<'a> Default for FittedSparkline<'a> { + fn default() -> Self { + Self { + data: &[], + num_data_points: None, + max: None, + block: None, + style: Style::default(), + direction: RenderDirection::LeftToRight, + } + } +} + +// builder-style implementation +impl<'a> FittedSparkline<'a> { + pub fn data(mut self, data: &'a [u64]) -> Self { + self.data = data; + self + } + + pub fn num_data_points(mut self, num_data_points: Option) -> Self { + self.num_data_points = num_data_points; + self + } + + pub fn max(mut self, max: u64) -> Self { + self.max = Some(max); + self + } + + pub fn block(mut self, block: Block<'a>) -> Self { + self.block = Some(block); + self + } + + pub fn style(mut self, style: Style) -> Self { + self.style = style; + self + } + + pub fn direction(mut self, direction: RenderDirection) -> Self { + self.direction = direction; + self + } +} + +impl Widget for FittedSparkline<'_> { + fn render(self, area: Rect, buf: &mut Buffer) + where Self: Sized + { + let spark_area = if let Some(block) = self.block { + let inner = block.inner(area); + block.render(area, buf); + inner + } + else { + area + }; + + let data_width = self.data.len() as u16; + + if spark_area.height == 0 || spark_area.width == 0 { return; } + if data_width > spark_area.width { return; } // currently does not support truncation + + let data = self.data; + let max = self.max.unwrap_or_else(|| data.iter().copied().max().unwrap_or(1)); + + let fitted_data = fit_data_to_spark_area(data, self.num_data_points, spark_area.width); + + // render data points + for (i, &value) in fitted_data + .iter() + .enumerate() { + let x = if self.direction == RenderDirection::LeftToRight { + spark_area.left().saturating_add(i as u16) + } + else { + spark_area.right().saturating_sub(1).saturating_sub(i as u16) + }; + + let height_ratio = value as f64 / max as f64; + let bar_height = (height_ratio * spark_area.height as f64).round() as u16; + + for dy in 0..bar_height { + let y = spark_area.bottom().saturating_sub(1).saturating_sub(dy); + buf.get_mut(x, y).set_bg(self.style.bg.unwrap_or(Color::White)); + } + + } + + //assert_eq!(fitted_data.len() as u16, spark_area.width); + + // render avg line + let avg_line = compute_avg(data); + for i in 0..spark_area.width { + let x = if self.direction == RenderDirection::LeftToRight { + spark_area.left().saturating_add(i as u16) + } + else { + spark_area.right().saturating_sub(1).saturating_sub(i as u16) + }; + + let height_ratio = avg_line as f64 / max as f64; + let bar_height = (height_ratio * spark_area.height as f64).round() as u16; + + let y = spark_area.bottom().saturating_sub(1).saturating_sub(bar_height); + buf.get_mut(x, y).set_symbol("_").set_fg(Color::Blue); + } + } +} + +fn compute_avg(data: &[u64]) -> u64 { + let mut avg: u64 = 0; + for &value in data { + avg += value; + } + avg = avg / data.len() as u64; + + avg +} + +fn fit_data_to_spark_area(data: &[u64], num_data_points: Option, spark_area_width: u16) -> Vec { + let (times, remainder) = if let Some(capacity) = num_data_points { + ( spark_area_width / capacity, spark_area_width % capacity ) + } + else { + ( spark_area_width / data.len() as u16, spark_area_width % data.len() as u16 ) + }; + + // fill times + let mut result = Vec::new(); + for &value in data { + for _ in 0..times { + result.push(value); + } + } + + // fill remainder + if let Some(last_data_point) = data.last() { + for _ in 0..remainder { + result.push(*last_data_point); + } + } + + result +} + + +/* +// Experimenting with custom Widget, needs some work + fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { + // preparing data to be drawn + let ram_percent_usage_data: Vec = self.queue_state + .iter() + .rev() + .map(|memory_item| { + ( memory_item.used_memory_gb() / memory_item.total_memory_gb() * 100_f64 ) as u64 + }) + .collect(); + + let block_style = if focused { + self.config.theme_config.style_border_focused + } + else { + self.config.theme_config.style_border_not_focused + }; + + let title: String = if let Some(memory_item) = self.queue_state.back() { + format!(" RAM :: {:.2}/{:.2} GB :: {:.2} % ", memory_item.used_memory_gb(), memory_item.total_memory_gb(), (memory_item.used_memory_gb()/memory_item.total_memory_gb()*100_f64)) + } + else { + format!(" RAM ") + }; + + // see ui/fitted_sparkline.rs for implementation details + let widget = FittedSparkline::default() + .data(&ram_percent_usage_data) + .num_data_points(Some(self.queue_state.capacity() as u16)) + .max(100) + .direction(RenderDirection::RightToLeft) + .block(Block::new().borders(Borders::ALL).title(title).style(block_style)) + .style(Style::new().on_red()); + + f.render_widget(widget, area); + + Ok(()) + } +*/ \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..e95505e --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1 @@ +pub mod fitted_sparkline; \ No newline at end of file