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