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 9ca01a1..685b73f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,35 +1,42 @@ +use std::collections::HashMap; use anyhow::{Ok, Result}; -use crossterm::event::KeyEvent; use ratatui::prelude::*; -use crate::components::temp::TempComponent; -use crate::config::Config; +use crate::components::help::HelpComponent; +use crate::input::{Key, Mouse, MouseKind}; +use crate::components::{command, Refreshable}; +use crate::config::{Config, KeyConfig, MouseConfig}; use crate::components::{ cpu::CPUComponent, memory::MemoryComponent, + network::NetworkComponent, process::ProcessComponent, - sysinfo_wrapper::SysInfoWrapper, error::ErrorComponent, - Component, EventState, + Component, DrawableComponent, - help::HelpComponent, + //help::HelpComponent, }; +use crate::components::command::CommandInfo; +use crate::services::sysinfo_service::SysInfoService; +#[derive(PartialEq, Eq, Hash, Clone, Copy)] enum MainFocus { CPU, Process, Memory, - Temp, + Network, } pub struct App { focus: MainFocus, + focus_rects: HashMap, expand: bool, - system_wrapper: SysInfoWrapper, + service: SysInfoService, process: ProcessComponent, cpu: CPUComponent, memory: MemoryComponent, - temp: TempComponent, + network: NetworkComponent, + //temp: TempComponent, help: HelpComponent, pub error: ErrorComponent, pub config: Config, @@ -37,35 +44,45 @@ 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(), &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 process = ProcessComponent::new(config.clone(), &service); + let memory = MemoryComponent::new(config.clone(), &service); + let cpu = CPUComponent::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(); Self { - focus: MainFocus::Process, + focus, + focus_rects, expand: false, - system_wrapper, + service, process, cpu, memory, - temp, - help: HelpComponent::new(config.clone()), + network, + //temp, + help, error: ErrorComponent::new(config.clone()), config: config.clone(), } } 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.refresh(&self.service); + self.cpu.update(&self.service); + self.network.refresh(&self.service); Ok(EventState::Consumed) } @@ -74,55 +91,57 @@ impl App { self.expand = !self.expand } - pub fn key_event(&mut self, key: KeyEvent) -> Result { - if self.component_event(key)?.is_consumed() { + pub fn key_event(&mut self, key: Key) -> Result { + if self.help.is_visible() { + let _ = self.help.key_event(key)?.is_consumed(); + return Ok(EventState::Consumed) + } + + if self.key_component_event(key)?.is_consumed() { return Ok(EventState::Consumed); } - else if self.move_focus(key)?.is_consumed() { + if self.move_focus_key(key)?.is_consumed() { return Ok(EventState::Consumed); } - else if key.code == self.config.key_config.expand { + if key == self.config.key_config.expand { self.toggle_expand(); - return Ok(EventState::Consumed); } Ok(EventState::NotConsumed) } - fn component_event(&mut self, key: KeyEvent) -> Result { - if self.error.event(key)?.is_consumed() { + fn key_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() { + MainFocus::Network => { + if self.network.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 { - self.process.terminate_process(&self.system_wrapper); - + if key == self.config.key_config.terminate { + return Ok(EventState::Consumed) } } @@ -131,30 +150,91 @@ 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 } 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([ @@ -163,7 +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) { @@ -172,6 +256,7 @@ impl App { chunks[0], true, )?; + self.focus_rects.insert(MainFocus::Process, chunks[0]); } if matches!(self.focus, MainFocus::CPU) { @@ -180,6 +265,7 @@ impl App { chunks[0], true, )?; + self.focus_rects.insert(MainFocus::CPU, chunks[0]); } if matches!(self.focus, MainFocus::Memory) { @@ -188,14 +274,16 @@ impl App { chunks[0], true, )?; + 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::Network, chunks[0]); } } else { @@ -227,49 +315,60 @@ 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, 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::Network, horizontal_chunks[1][1]); } - self.help.draw(f, Rect::default(), false)?; - 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 { + fn contains(&self, col: u16, row: u16) -> bool; +} +impl Contains for 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/command.rs b/src/components/command.rs index 29572d4..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 { @@ -60,17 +64,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!( @@ -85,7 +78,7 @@ pub fn help(key: &KeyConfig) -> CommandText { CommandText::new( format!( "Help [{:?}]", - key.open_help, + key.help, ), CMD_GROUP_GENERAL ) @@ -101,66 +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 ) } -pub fn follow_selection(key: &KeyConfig) -> CommandText { +pub fn select_process(key: &KeyConfig, mouse: &MouseConfig) -> CommandText { CommandText::new( format!( - "Toggle follow selection [{:?}]", - key.follow_selection, + "Select process by mouse [{:?}] | [{:?}/{:?}]", + mouse.left_click, mouse.scroll_down, mouse.scroll_up, ), - CMD_GROUP_GENERAL + CMD_GROUP_PROCESS ) } - -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 +// Process specific::end \ No newline at end of file diff --git a/src/components/cpu.rs b/src/components/cpu.rs index f226586..ac8c551 100644 --- a/src/components/cpu.rs +++ b/src/components/cpu.rs @@ -1,48 +1,59 @@ +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; -use anyhow::Ok; -use crate::components::sysinfo_wrapper::SysInfoWrapper; -use crate::models::b_queue::bounded_queue::BoundedQueue; +use anyhow::{Ok, Result}; +use crate::input::*; +use super::EventState; +use crate::components::common_nav; +use crate::services::sysinfo_service::SysInfoService; +use crate::components::utils::selection::UISelection; +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", } } @@ -62,87 +73,214 @@ impl ColorWheel { } } -#[derive(Default)] +pub enum Focus { + Chart, + CPUList, +} + pub struct CPUComponent { - cpus: BTreeMap>, - ui_selection: usize, + 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: &SysInfoWrapper) -> Self { - let mut cpus: BTreeMap> = BTreeMap::new(); - let ui_selection: usize = 0; + pub fn new(config: Config, sysinfo: &SysInfoService) -> Self { + 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 perf_q.add_item(cpu); } + 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 + let selection_offset = 1; + + let chart_area = None; + let list_area = None; + Self { cpus, - ui_selection, + 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: &SysInfoWrapper) { + 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 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); + fn key_event(&mut self, key: Key) -> Result { + 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) + } } - 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); + + // 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 { + // 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) + } } impl DrawableComponent for CPUComponent { - fn draw(&mut self, f: &mut ratatui::Frame, area: ratatui::prelude::Rect, focused: bool) -> anyhow::Result<()> { - // split screen + // 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<()> { 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 @@ -153,42 +291,45 @@ 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() + .take(data_window) + .enumerate() + .map(|(idx, cpu_item)| ((max_idx - idx) as f64, cpu_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() + .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)); - } - } - 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)); + } } } @@ -204,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(); @@ -216,40 +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 { - Block::default().borders(Borders::ALL).title(" CPU % ").style(self.config.theme_config.style_border_not_focused) + if focused && matches!(cpu_focus, Focus::Chart) { + Block::default() + .borders(Borders::ALL) + .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) + Block::default() + .borders(Borders::ALL) + .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"), ]) @@ -260,25 +418,16 @@ 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) .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/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 35c8330..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::*, @@ -26,29 +26,38 @@ 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 { - 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..3691374 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::*, @@ -99,30 +100,37 @@ impl HelpComponent { Ok(()) } + pub fn is_visible(&self) -> bool { + self.visible + } } 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.help { 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.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 0594b67..38c968b 100644 --- a/src/components/memory.rs +++ b/src/components/memory.rs @@ -1,83 +1,207 @@ +use std::cmp::{max, min}; use anyhow::{Ok, Result}; -use ratatui::{ - layout::{Layout, Direction, Constraint}, - style::{Style, Stylize}, - widgets::{Block, Gauge}, -}; -use crossterm::event::KeyEvent; -use crate::components::sysinfo_wrapper::SysInfoWrapper; +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: &SysInfoWrapper) -> 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: &SysInfoWrapper) { - 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 event(&mut self, _key: KeyEvent) -> 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 { + 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 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); - let g_ram = Gauge::default() - .block(Block::bordered().style(style).title(ram_title)) - .gauge_style(Style::new().red().on_black().italic()) - .percent(ram_percent as u16); + f.render_widget(chart, vertical_chunks[1]); - // 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()); - let g_swap = Gauge::default() - .block(Block::bordered().style(style).title(swap_title)) - .gauge_style(Style::new().magenta().on_black().italic()) - .percent(swap_percent as u16); + // 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(), + ); - f.render_widget(g_ram, vertical_chunks[0]); - f.render_widget(g_swap, vertical_chunks[1]); + 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 601746a..4d9bc1a 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,9 +1,7 @@ use anyhow::Result; -use crossterm::event::KeyEvent; +use crate::input::{Key, Mouse}; use ratatui::prelude::*; -use crate::models::p_list::process_list::{ListSortOrder, MoveSelection}; use super::config::KeyConfig; -pub mod sysinfo_wrapper; pub mod filter; pub mod help; pub mod error; @@ -13,13 +11,28 @@ 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<()>; } 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); } #[derive(PartialEq)] @@ -34,17 +47,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 { @@ -52,32 +65,10 @@ 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/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 3a42d05..584e016 100644 --- a/src/components/process.rs +++ b/src/components/process.rs @@ -1,156 +1,361 @@ use anyhow::{Ok, Result}; -use crossterm::event::KeyEvent; -use ratatui::{ - Frame, - prelude::*, -}; -use crate::components::{ - sysinfo_wrapper::SysInfoWrapper, - common_nav, common_sort, DrawableComponent, Component, EventState, - utils::vertical_scroll::VerticalScroll, - filter::FilterComponent, -}; -use crate::config::{Config, KeyConfig}; -use crate::models::p_list::process_list::{ListSortOrder, ProcessList}; - -#[derive(PartialEq, Clone)] +use ratatui::{Frame, prelude::*, widgets::*}; +use ratatui::layout::Position; +use crate::config::*; +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::models::items::*; + +#[derive(PartialEq, Clone, Debug)] pub enum Focus { Filter, List, } pub struct ProcessComponent { - focus: Focus, - list: ProcessList, - filter: FilterComponent, - filtered_list: 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, } impl ProcessComponent { - pub fn new(config: Config, sysinfo: &SysInfoWrapper) -> Self { + 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 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: 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; + Self { - focus: Focus::List, - list: ProcessList::new(sysinfo), - filter: FilterComponent::new(config.clone()), - filtered_list: None, - scroll: VerticalScroll::new(), + vec_state, + ui_selection, + table_area, + filter_area, + header_height, + border_height, + sort, + scroll, + filter_component, + focus, config, } } - pub fn update(&mut self, sysinfo: &SysInfoWrapper) { - self.list.update(sysinfo, ""); + // SELECTION HANDLERS::begin + 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 { + 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), + } + }; + + self.ui_selection.set_selection(new_ui_selection); + let vec_idx = self.compute_vec_state_idx(); + self.vec_state.set_selection(vec_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); + } + + // 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 + }; - if let Some(filtered_list) = self.filtered_list.as_mut() { - filtered_list.update(sysinfo, self.filter.input_str()); + // 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 } - pub fn terminate_process(&mut self, sysinfo: &SysInfoWrapper) -> bool { - self.filtered_list - .as_ref() - .and_then(|f| f.selected_pid()) - .or_else(|| self.list.selected_pid()) - .map(|pid| { - sysinfo.terminate_process(pid); - true - }) - .unwrap_or(false) + 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 + .and_then(|ui_selection| self.vec_state.view_indices().get(ui_selection).cloned()); + + vec_idx + } + // HELPERS::end +} + +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 && self.focus == Focus::List { + fn key_event(&mut self, key: Key) -> Result { + if key == 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 - } - else { - Some(self.list.filter(self.filter.input_str())) - }; - + if self.filter_component.key_event(key)?.is_consumed() { + self.vec_state.set_filter(self.filter_component.filter_contents()); + self.handle_filter_selection(); return Ok(EventState::Consumed) } - - if key.code == self.config.key_config.enter { - self.focus = Focus::List; + if key == 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 - } - else { - &mut self.list - }, - key, - &self.config.key_config - ) { - return Ok(EventState::Consumed); + if let Some(move_dir) = common_nav(key, &self.config.key_config) { + self.handle_move_selection(move_dir); + 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(); - } - else { - self.list.toggle_follow_selection(); - } - + if self.handle_sort(key) { + // logic for handling selection after sort is similar enough to refresh + self.handle_refresh_selection(); return Ok(EventState::Consumed) } + } + + Ok(EventState::NotConsumed) + } - if list_sort( - if let Some(list) = self.filtered_list.as_mut() { - list - } - else { - &mut self.list - }, - key, - &self.config.key_config - )? { + fn mouse_event(&mut self, mouse: Mouse) -> Result { + match mouse.kind { + 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) + }*/ + } + _ => {} } - + Ok(EventState::NotConsumed) } } -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 +// 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); } - else { - false + if key == key_config.sort_cpu_toggle { + if matches!(sort, ProcessItemSortOrder::PidDec) { return Some(ProcessItemSortOrder::PidInc)}; + return Some(ProcessItemSortOrder::PidDec); } -} -fn list_sort(list: &mut ProcessList, key: KeyEvent, key_config: &KeyConfig) -> Result { - if let Some(sort) = common_sort(key, key_config) { - list.sort(&sort); - Ok(true) - } - else { - Ok(false) - } + + 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 } impl DrawableComponent for ProcessComponent { @@ -158,43 +363,46 @@ 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); - 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( + // 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() - }, |selection| { - self.scroll.update( - selection, - list.len(), - visible_list_height, - ); - }, - ); + }, |idx| { + // if selection is some + 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) } @@ -202,7 +410,7 @@ impl DrawableComponent for ProcessComponent { false }, self.config.theme_config.clone(), - list.sort_order(), + self.sort.clone(), ); self.scroll.draw( @@ -216,7 +424,7 @@ impl DrawableComponent for ProcessComponent { }, )?; - self.filter.draw( + self.filter_component.draw( f, horizontal_chunks[1], if focused { @@ -231,122 +439,75 @@ impl DrawableComponent for ProcessComponent { } } -use ratatui::widgets::{block::*, *}; -use crate::models::p_list::list_iter::ListIterator; -use crate::config::ThemeConfig; - -fn draw_process_list( +fn draw_process_list<'a, I>( f: &mut Frame, area: Rect, - visible_items: ListIterator<'_>, - follow_selection: bool, + visible_items: I, focus: bool, theme_config: ThemeConfig, - sort_order: &ListSortOrder, -) { - let follow_flag = follow_selection; - + sort_order: ProcessItemSortOrder, +) +where + I: Iterator, +{ // setting header - let header = ["", - if matches!(sort_order, ListSortOrder::PidInc) { "PID â–²" } - else if matches!(sort_order, ListSortOrder::PidDec) { "PID â–¼" } - else { "PID" }, - - if matches!(sort_order, ListSortOrder::NameInc) { "Name â–²" } - else if matches!(sort_order, ListSortOrder::NameDec) { "Name â–¼" } - else { "Name" }, - - if matches!(sort_order, ListSortOrder::CpuUsageInc) { "CPU (%) â–²" } - else if matches!(sort_order, ListSortOrder::CpuUsageDec) { "CPU (%) â–¼" } - else { "CPU (%)" }, - - if matches!(sort_order, ListSortOrder::MemoryUsageInc) { "Memory (MB) â–²" } - else if matches!(sort_order, ListSortOrder::MemoryUsageDec) { "Memory (MB) â–¼" } - else { "Memory (MB)" }, - - "Run (hh:mm:ss)", - "Status", - "Path"] + let header_labels = [ + "", + &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 .into_iter() .map(Cell::from) .collect::() - .style( - if focus { - theme_config.style_border_focused - } - else { - theme_config.style_border_not_focused - } - ) + .style(if focus {theme_config.style_border_focused} else {theme_config.style_border_not_focused}) .height(1); // setting rows let rows = visible_items - .map(|(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 indicator = if style == theme_config.style_item_selected { + "->" + } else { + "" + }; + + let cells = vec![ + Cell::from(indicator), Cell::from(item.pid().to_string()), - Cell::from(item.name().to_string()), + 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) }) .collect::>(); - // setting the width constraints. + // setting width constraints let widths = vec![ - Constraint::Length(2), - 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::Length(2), // arrow + 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 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) @@ -358,7 +519,199 @@ fn draw_process_list( f.render_widget(table, area); } +// helper function for building header labels +fn header_with_sort( + current: &ProcessItemSortOrder, + inc: &ProcessItemSortOrder, + dec: &ProcessItemSortOrder, + base: &str, +) -> String { + match current { + s if s == inc => format!("{base} â–²"), + 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, false) => theme.style_item_focused, + (false, true) => theme.style_item_selected_not_focused, + _ => theme.style_item_not_focused, + } +} + + + + +// TEST MODULE #[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()); + } + + //TODO: add tests for mouse_event() and key_event() + + 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 ba49e3d..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::components::sysinfo_wrapper::SysInfoWrapper; + +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}; @@ -19,9 +23,8 @@ pub struct TempComponent { } impl TempComponent { - pub fn new(config: Config, sysinfo: &SysInfoWrapper) -> Self { - let mut temps = Vec::new(); - sysinfo.get_temps(&mut temps); + pub fn new(config: Config, sysinfo: &SysInfoService) -> Self { + let temps: Vec = sysinfo.fetch_items(); Self { config, @@ -30,29 +33,37 @@ impl TempComponent { vertical_scroll: VerticalScroll::new(), } } +} - pub fn update(&mut self, sysinfo: &SysInfoWrapper) { - 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 { - let temps_max_idx = self.temps.len() - 1; + 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/components/utils/mod.rs b/src/components/utils/mod.rs index d838e6b..97350fe 100644 --- a/src/components/utils/mod.rs +++ b/src/components/utils/mod.rs @@ -1 +1,2 @@ -pub mod vertical_scroll; \ No newline at end of file +pub mod vertical_scroll; +pub mod selection; \ 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..ab2e423 --- /dev/null +++ b/src/components/utils/selection.rs @@ -0,0 +1,81 @@ +use crate::components::MoveSelection; + +// currently this is only being used by CpuComponent +pub struct UISelection { + pub selection: Option, + pub follow_selection: bool, +} + +impl UISelection { + 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, move_selection: MoveSelection, len: usize) { + if matches!(len, 0) { + self.selection = None + } + + if let Some(selection) = self.selection { + let new_idx = match move_selection { + MoveSelection::Down => self.selection_down(selection, 1, len), + MoveSelection::Up => self.selection_up(selection, 1), + 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/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 fc7a9d9..d794510 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,26 +1,36 @@ -use crossterm::event::KeyCode; +use std::ops::Div; use serde::{Deserialize,Serialize}; -#[derive(Clone,Serialize,Deserialize)] +#[derive(Clone)] pub struct Config { pub key_config: KeyConfig, + 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, } } } @@ -30,80 +40,92 @@ 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 + } } -#[derive(Clone, Serialize, Deserialize)] +pub fn ms_to_s(data_ms: u64) -> u64 { + data_ms.div(1000) +} + +#[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 help: Key, + pub exit: 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, } 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'), + help: Key::Char('?'), + exit: Key::Esc, + 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'), } } } +#[derive(Clone)] +pub struct MouseConfig { + 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: MouseKind::LeftClick, + middle_click: MouseKind::MiddleClick, + scroll_up: MouseKind::ScrollUp, + scroll_down: MouseKind::ScrollDown, + } + } +} + +use ratatui::prelude::{Color, Style}; +use crate::input::{Key, MouseKind}; -use ratatui::prelude::{Color, Modifier, Style}; #[derive(Clone,PartialEq,Serialize,Deserialize)] pub struct ThemeConfig { pub style_border_focused: Style, @@ -112,8 +134,6 @@ 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 { @@ -121,12 +141,12 @@ impl Default for ThemeConfig { Self { 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::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_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_not_focused: Style::default().fg(Color::Gray), + + 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/events/event.rs b/src/events/event.rs index dfe1dbf..26bfffa 100644 --- a/src/events/event.rs +++ b/src/events/event.rs @@ -1,15 +1,6 @@ -use crossterm::event::{ - self, - KeyEvent, - Event as CEvent, - KeyEventKind -}; - -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 { @@ -26,17 +17,17 @@ impl Default for EventConfig { } } - -#[derive(Clone, Copy)] -pub enum Event { - Input(K), +#[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 { @@ -58,10 +49,14 @@ 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::Input(key)).unwrap(); + input_tx.send(Event::KeyInput(Key::from(key))).unwrap(); } } + if let CEvent::Mouse(mouse) = event { + input_tx.send(Event::MouseInput(Mouse::from(mouse))).unwrap(); + } } } }); @@ -79,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 6edd703..5d68cb0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use std::io; -use crossterm::ExecutableCommand; +use crossterm::event::EnableMouseCapture; +use std::io::{stdout}; use crossterm::{ execute, terminal::{enable_raw_mode, EnterAlternateScreen}, @@ -16,16 +16,20 @@ use crate::app::App; pub mod app; pub mod config; +pub mod input; pub mod components; +pub mod ui; pub mod events; pub mod models; +pub mod states; +pub mod services; -// 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)?; + 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,9 +54,9 @@ 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 { + if !state.is_consumed() && key == app.config.key_config.exit { break; } } @@ -60,6 +64,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/b_queue/mod.rs b/src/models/b_queue/mod.rs deleted file mode 100644 index c07b676..0000000 --- a/src/models/b_queue/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod bounded_queue; \ No newline at end of file diff --git a/src/models/b_queue/bounded_queue.rs b/src/models/bounded_queue_model.rs similarity index 91% rename from src/models/b_queue/bounded_queue.rs rename to src/models/bounded_queue_model.rs index a864f79..77982ac 100644 --- a/src/models/b_queue/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 82f6fde..8fd5cb5 100644 --- a/src/models/items/mod.rs +++ b/src/models/items/mod.rs @@ -1,3 +1,15 @@ +use std::ops::Div; + 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; +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 new file mode 100644 index 0000000..cb6ddeb --- /dev/null +++ b/src/models/items/process_item.rs @@ -0,0 +1,273 @@ +use std::ops::Div; + +use crate::{models::{Filterable, Sortable}}; + +#[derive(Clone, Copy, PartialEq)] +pub enum ProcessItemSortOrder { + PidInc, + PidDec, + NameInc, + NameDec, + CpuUsageInc, + CpuUsageDec, + MemoryUsageInc, + MemoryUsageDec, + StatusInc, + StatusDec, + RuntimeInc, + RuntimeDec, +} + +#[derive(Default, Clone)] +pub struct ProcessItem { + 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, + //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, + status, + path, + } + } + + // GETTERS + pub fn pid(&self) -> u32 { + self.pid + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn cpu_usage(&self) -> f32 { + self.cpu_usage + } + + pub fn memory_usage(&self) -> u64 { + 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 + } + + pub fn run_time(&self) -> u64 { + self.run_time + } + + 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 / 3600) % 24; + let dd = hh / 86400; + + format!("{:0>2}D {:0>2}H {:0>2}M {:0>2}S", dd, hh, mm, ss) + } + + pub fn accumulated_cpu_time(&self) -> u64 { + self.accumulated_cpu_time + } + + pub fn status(&self) -> &str { + &self.status + } + + pub fn path(&self) -> &str { + &self.path + } +} + +// PartialEq is needed for comparison, e.g., calling contains +impl PartialEq for ProcessItem { + fn eq(&self, other: &Self) -> bool { + self.pid.eq(&other.pid) + } +} + +impl Filterable for ProcessItem { + fn matches_filter(&self, filter: &str) -> bool { + // 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::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] + fn test_constructors() { + let instance = ProcessItem::default(); + assert_eq!(instance.pid, 0); + assert!(String::is_empty(&instance.name)); + assert_eq!(instance.cpu_usage, 0.0); + assert_eq!(instance.memory_usage, 0); + assert_eq!(instance.start_time, 0); + assert_eq!(instance.run_time, 0); + assert_eq!(instance.accumulated_cpu_time, 0); + assert!(String::is_empty(&instance.status)); + + let instance = 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); + assert_eq!(instance.memory_usage, 1); + assert_eq!(instance.start_time, 0); + assert_eq!(instance.run_time, 10); + assert_eq!(instance.accumulated_cpu_time, 10); + assert_eq!(instance.status, String::from("test")); + + } + + #[test] + fn test_instance_functions() { + let instance_0 = 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); + assert_eq!(instance_0.cpu_usage(), instance_0.cpu_usage); + assert_eq!(instance_0.memory_usage(), instance_0.memory_usage); + assert_eq!(instance_0.start_time(), instance_0.start_time); + assert_eq!(instance_0.run_time(), instance_0.run_time); + assert_eq!(instance_0.accumulated_cpu_time(), instance_0.accumulated_cpu_time); + assert_eq!(instance_0.status(), instance_0.status); + assert_eq!(instance_0.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); + assert_eq!(instance_1.cpu_usage(), instance_1.cpu_usage); + assert_eq!(instance_1.memory_usage(), instance_1.memory_usage); + assert_eq!(instance_0.start_time(), instance_0.start_time); + assert_eq!(instance_0.run_time(), instance_0.run_time); + assert_eq!(instance_0.accumulated_cpu_time(), instance_0.accumulated_cpu_time); + assert_eq!(instance_0.status(), instance_0.status); + assert_eq!(instance_1.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 b039fa3..9bf9be4 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,11 @@ -pub mod p_list; pub mod items; -pub mod b_queue; \ No newline at end of file +pub mod bounded_queue_model; +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/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.rs b/src/models/p_list/process_list.rs deleted file mode 100644 index 77c5c82..0000000 --- a/src/models/p_list/process_list.rs +++ /dev/null @@ -1,372 +0,0 @@ -use super::process_list_items::ProcessListItems; -use super::process_list_item::ProcessListItem; -use super::list_iter::ListIterator; -use crate::components::sysinfo_wrapper::SysInfoWrapper; - -#[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)] -pub struct ProcessList { - items: ProcessListItems, - sort: ListSortOrder, - follow_selection: bool, - pub selection: Option, -} - -impl ProcessList { - pub fn new(sysinfo: &SysInfoWrapper) -> Self { - Self { - items: ProcessListItems::new(sysinfo), - sort: ListSortOrder::default(), - follow_selection: false, - selection: Some(0), - } - } - - pub fn filter(&self, filter_text: &str) -> Self { - let items = self.items.filter(filter_text); - let len = items.len(); - - Self { - items, - sort: ListSortOrder::default(), - follow_selection: false, - selection: if len > 0 { - Some(0) - } - else { - None - }, - } - } - - 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()); - - - self.items.update(sysinfo, filter_text); - - self.items.sort_items(&self.sort); - - if self.items.len() == 0 { - self.selection = None; - return - } - - if self.follow_selection { - self.selection = pid.and_then(|p| self.items.get_idx(p)); - } - else { - if let Some(selection) = self.selection { - let max_idx = self.items.len().saturating_sub(1); - if selection > max_idx { - self.selection = Some(max_idx) - } - } - } - - if self.selection.is_none() { - self.selection = Some(0) - } - } - - 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); - - self.sort = sort.clone(); - - if self.follow_selection { - self.selection = pid.and_then(|p| self.items.get_idx(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; - } - } - - 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); - } - - 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) -> 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 - } - - pub fn is_follow_selection(&self) -> bool { - self.follow_selection - } - - pub fn selection(&self) -> Option { - self.selection - } - - pub fn is_empty(&self) -> bool { - self.items.len() == 0 - } - - pub fn len(&self) -> usize { - self.items.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 - } - - None - } - - pub fn selected_pid(&self) -> Option { - if let Some(selection) = self.selection { - if let Some(item) = self.items.get_item(selection) { - return Some(item.pid()) - } - else { - return None - } - } - - None - } - - pub fn sort_order(&self) -> &ListSortOrder { - &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) - } -} - -/* -#[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/p_list/process_list_item.rs b/src/models/p_list/process_list_item.rs deleted file mode 100644 index 00ae816..0000000 --- a/src/models/p_list/process_list_item.rs +++ /dev/null @@ -1,156 +0,0 @@ -#[derive(Default, Clone, Debug)] -pub struct ProcessListItem { - pid: u32, - name: String, - cpu_usage: f32, - memory_usage: u64, - start_time: u64, - run_time: u64, - accumulated_cpu_time: u64, - status: String, - path: String, -} - -impl ProcessListItem { - 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, - ) -> Self { - Self { - pid, - name, - cpu_usage, - memory_usage, - start_time, - run_time, - accumulated_cpu_time, - status, - path, - } - } - - // 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) - //} - - pub fn pid(&self) -> u32 { - self.pid - } - - pub fn name(&self) -> &str { - &self.name - } - - pub fn cpu_usage(&self) -> f32 { - self.cpu_usage - } - - pub fn memory_usage(&self) -> u64 { - self.memory_usage - } - - pub fn start_time(&self) -> u64 { - self.start_time - } - - pub fn run_time(&self) -> u64 { - self.run_time - } - - pub fn run_time_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; - - format!("{:0>2}:{:0>2}:{:0>2}", hh, mm, ss) - } - - pub fn accumulated_cpu_time(&self) -> u64 { - self.accumulated_cpu_time - } - - pub fn status(&self) -> &str { - &self.status - } - - pub fn path(&self) -> &str { - &self.path - } -} - -// PartialEq is needed for comparison, e.g., calling contains -impl PartialEq for ProcessListItem { - fn eq(&self, other: &Self) -> bool { - self.pid.eq(&other.pid) - } -} - -#[cfg(test)] -pub mod test { - use super::ProcessListItem; - - #[test] - fn test_constructors() { - let instance = ProcessListItem::default(); - assert_eq!(instance.pid, 0); - assert!(String::is_empty(&instance.name)); - assert_eq!(instance.cpu_usage, 0.0); - assert_eq!(instance.memory_usage, 0); - assert_eq!(instance.start_time, 0); - assert_eq!(instance.run_time, 0); - assert_eq!(instance.accumulated_cpu_time, 0); - assert!(String::is_empty(&instance.status)); - - let instance = ProcessListItem::new(1, String::from("a"), 1.0, 1, 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); - assert_eq!(instance.memory_usage, 1); - assert_eq!(instance.start_time, 0); - assert_eq!(instance.run_time, 10); - assert_eq!(instance.accumulated_cpu_time, 10); - assert_eq!(instance.status, String::from("test")); - - } - - #[test] - fn test_instance_functions() { - let instance_0 = ProcessListItem::default(); - let instance_1 = ProcessListItem::new(1, String::from("a"), 1.0, 1, 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); - assert_eq!(instance_0.cpu_usage(), instance_0.cpu_usage); - assert_eq!(instance_0.memory_usage(), instance_0.memory_usage); - assert_eq!(instance_0.start_time(), instance_0.start_time); - assert_eq!(instance_0.run_time(), instance_0.run_time); - assert_eq!(instance_0.accumulated_cpu_time(), instance_0.accumulated_cpu_time); - assert_eq!(instance_0.status(), instance_0.status); - //assert_eq!(instance_0.is_match(""), true); - //assert_eq!(instance_0.is_match("a"), false); - //assert_eq!(instance_0.is_match(&instance_0.pid.to_string()), true); - - assert_eq!(instance_1.pid(), instance_1.pid); - assert_eq!(instance_1.name(), instance_1.name); - assert_eq!(instance_1.cpu_usage(), instance_1.cpu_usage); - assert_eq!(instance_1.memory_usage(), instance_1.memory_usage); - assert_eq!(instance_0.start_time(), instance_0.start_time); - assert_eq!(instance_0.run_time(), instance_0.run_time); - assert_eq!(instance_0.accumulated_cpu_time(), instance_0.accumulated_cpu_time); - assert_eq!(instance_0.status(), instance_0.status); - //assert_eq!(instance_1.is_match("a"), true); - //assert_eq!(instance_1.is_match("aa"), false); - //assert_eq!(instance_1.is_match(&instance_1.pid.to_string()), true); - } -} \ No newline at end of file diff --git a/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/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..03f7c43 --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1,16 @@ +pub mod sysinfo_service; + +// 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; +} + +pub trait ItemProvider { + fn fetch_item(&self) -> T; +} \ No newline at end of file diff --git a/src/components/sysinfo_wrapper.rs b/src/services/sysinfo_service.rs similarity index 73% rename from src/components/sysinfo_wrapper.rs rename to src/services/sysinfo_service.rs index 7f49503..e72f4d9 100644 --- a/src/components/sysinfo_wrapper.rs +++ b/src/services/sysinfo_service.rs @@ -1,21 +1,24 @@ -use sysinfo::{Pid, System, Components}; -use crate::models::p_list::process_list_item::ProcessListItem; -use crate::models::items::{memory_item::MemoryItem, temp_item::TempItem, cpu_item::CpuItem}; +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::{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 SysInfoWrapper { +pub struct SysInfoService { system: System, components: Components, + networks: Networks, pub _config: Config } -impl SysInfoWrapper { +impl SysInfoService { pub fn new(config: Config) -> Self { Self { system: System::new_all(), components: Components::new_with_refreshed_list(), + networks: Networks::new_with_refreshed_list(), _config: config } } @@ -23,6 +26,7 @@ impl SysInfoWrapper { 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,8 +51,88 @@ impl SysInfoWrapper { cpus } - pub fn get_processes(&self, processes: &mut Vec) { - processes.clear(); + + pub fn terminate_process(&self, pid: u32) -> bool { + let mut res = false; + + if let Some(process) = self.system.process(Pid::from_u32(pid)) { + res = process.kill(); + } + + res + } +} + +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(); + + 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(); for (pid, process) in self.system.processes() { let name = if let Some(name) = process.name().to_str() { @@ -86,7 +170,7 @@ impl SysInfoWrapper { String::from("Permission Denied") }; - let item = ProcessListItem::new( + let item = ProcessItem::new( pid.as_u32(), name, cpu_usage, @@ -100,59 +184,7 @@ impl SysInfoWrapper { 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 - 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; - - if let Some(process) = self.system.process(Pid::from_u32(pid)) { - res = process.kill(); - } - - res + return processes; } } \ 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/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 new file mode 100644 index 0000000..74d0a08 --- /dev/null +++ b/src/states/mod.rs @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..4ab1ad3 --- /dev/null +++ b/src/states/vec_state.rs @@ -0,0 +1,147 @@ +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 + }; + } + + // ACCESS TO MODEL MUTATORS + 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() + } + + 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(); + + 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 + } + + // 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; + + 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 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