diff --git a/src/app/data_harvester.rs b/src/app/data_harvester.rs new file mode 100644 index 00000000..d5ff467d --- /dev/null +++ b/src/app/data_harvester.rs @@ -0,0 +1,364 @@ +//! This is the main file to house data collection functions. + +use std::time::Instant; + +#[cfg(target_os = "linux")] +use fxhash::FxHashMap; + +#[cfg(not(target_os = "linux"))] +use sysinfo::{System, SystemExt}; + +use battery::{Battery, Manager}; + +use futures::join; + +use super::{DataFilters, UsedWidgets}; + +pub mod batteries; +pub mod cpu; +pub mod disks; +pub mod memory; +pub mod network; +pub mod processes; +pub mod temperature; + +#[derive(Clone, Debug)] +pub struct Data { + pub last_collection_time: Instant, + pub cpu: Option, + pub load_avg: Option, + pub memory: Option, + pub swap: Option, + pub temperature_sensors: Option>, + pub network: Option, + pub list_of_processes: Option>, + pub disks: Option>, + pub io: Option, + pub list_of_batteries: Option>, +} + +impl Default for Data { + fn default() -> Self { + Data { + last_collection_time: Instant::now(), + cpu: None, + load_avg: None, + memory: None, + swap: None, + temperature_sensors: None, + list_of_processes: None, + disks: None, + io: None, + network: None, + list_of_batteries: None, + } + } +} + +impl Data { + pub fn cleanup(&mut self) { + self.io = None; + self.temperature_sensors = None; + self.list_of_processes = None; + self.disks = None; + self.memory = None; + self.swap = None; + self.cpu = None; + self.load_avg = None; + + if let Some(network) = &mut self.network { + network.first_run_cleanup(); + } + } +} + +#[derive(Debug)] +pub struct DataCollector { + pub data: Data, + #[cfg(not(target_os = "linux"))] + sys: System, + previous_cpu_times: Vec<(cpu::PastCpuWork, cpu::PastCpuTotal)>, + previous_average_cpu_time: Option<(cpu::PastCpuWork, cpu::PastCpuTotal)>, + #[cfg(target_os = "linux")] + pid_mapping: FxHashMap, + #[cfg(target_os = "linux")] + prev_idle: f64, + #[cfg(target_os = "linux")] + prev_non_idle: f64, + mem_total_kb: u64, + temperature_type: temperature::TemperatureType, + use_current_cpu_total: bool, + last_collection_time: Instant, + total_rx: u64, + total_tx: u64, + show_average_cpu: bool, + widgets_to_harvest: UsedWidgets, + battery_manager: Option, + battery_list: Option>, + filters: DataFilters, +} + +impl DataCollector { + pub fn new(filters: DataFilters) -> Self { + DataCollector { + data: Data::default(), + #[cfg(not(target_os = "linux"))] + sys: System::new_with_specifics(sysinfo::RefreshKind::new()), + previous_cpu_times: vec![], + previous_average_cpu_time: None, + #[cfg(target_os = "linux")] + pid_mapping: FxHashMap::default(), + #[cfg(target_os = "linux")] + prev_idle: 0_f64, + #[cfg(target_os = "linux")] + prev_non_idle: 0_f64, + mem_total_kb: 0, + temperature_type: temperature::TemperatureType::Celsius, + use_current_cpu_total: false, + last_collection_time: Instant::now(), + total_rx: 0, + total_tx: 0, + show_average_cpu: false, + widgets_to_harvest: UsedWidgets::default(), + battery_manager: None, + battery_list: None, + filters, + } + } + + pub fn init(&mut self) { + #[cfg(target_os = "linux")] + { + futures::executor::block_on(self.initialize_memory_size()); + } + #[cfg(not(target_os = "linux"))] + { + self.sys.refresh_memory(); + self.mem_total_kb = self.sys.get_total_memory(); + + // TODO: Would be good to get this and network list running on a timer instead...? + // Refresh components list once... + if self.widgets_to_harvest.use_temp { + self.sys.refresh_components_list(); + } + + // Refresh network list once... + if cfg!(target_os = "windows") && self.widgets_to_harvest.use_net { + self.sys.refresh_networks_list(); + } + } + + if self.widgets_to_harvest.use_battery { + if let Ok(battery_manager) = Manager::new() { + if let Ok(batteries) = battery_manager.batteries() { + let battery_list: Vec = batteries.filter_map(Result::ok).collect(); + if !battery_list.is_empty() { + self.battery_list = Some(battery_list); + self.battery_manager = Some(battery_manager); + } + } + } + } + + futures::executor::block_on(self.update_data()); + + std::thread::sleep(std::time::Duration::from_millis(250)); + + self.data.cleanup(); + + // trace!("Enabled widgets to harvest: {:#?}", self.widgets_to_harvest); + } + + #[cfg(target_os = "linux")] + async fn initialize_memory_size(&mut self) { + self.mem_total_kb = if let Ok(mem) = heim::memory::memory().await { + mem.total().get::() + } else { + 1 + }; + } + + pub fn set_collected_data(&mut self, used_widgets: UsedWidgets) { + self.widgets_to_harvest = used_widgets; + } + + pub fn set_temperature_type(&mut self, temperature_type: temperature::TemperatureType) { + self.temperature_type = temperature_type; + } + + pub fn set_use_current_cpu_total(&mut self, use_current_cpu_total: bool) { + self.use_current_cpu_total = use_current_cpu_total; + } + + pub fn set_show_average_cpu(&mut self, show_average_cpu: bool) { + self.show_average_cpu = show_average_cpu; + } + + pub async fn update_data(&mut self) { + #[cfg(not(target_os = "linux"))] + { + if self.widgets_to_harvest.use_proc { + self.sys.refresh_processes(); + } + if self.widgets_to_harvest.use_temp { + self.sys.refresh_components(); + } + + if cfg!(target_os = "windows") && self.widgets_to_harvest.use_net { + self.sys.refresh_networks(); + } + } + + let current_instant = std::time::Instant::now(); + + // CPU + if self.widgets_to_harvest.use_cpu { + if let Ok(cpu_data) = cpu::get_cpu_data_list( + self.show_average_cpu, + &mut self.previous_cpu_times, + &mut self.previous_average_cpu_time, + ) + .await + { + self.data.cpu = Some(cpu_data); + } + + #[cfg(target_family = "unix")] + { + // Load Average + if let Ok(load_avg_data) = cpu::get_load_avg().await { + self.data.load_avg = Some(load_avg_data); + } + } + } + + // Batteries + if let Some(battery_manager) = &self.battery_manager { + if let Some(battery_list) = &mut self.battery_list { + self.data.list_of_batteries = + Some(batteries::refresh_batteries(battery_manager, battery_list)); + } + } + + if self.widgets_to_harvest.use_proc { + if let Ok(process_list) = { + #[cfg(target_os = "linux")] + { + processes::get_process_data( + &mut self.prev_idle, + &mut self.prev_non_idle, + &mut self.pid_mapping, + self.use_current_cpu_total, + current_instant + .duration_since(self.last_collection_time) + .as_secs(), + self.mem_total_kb, + ) + } + #[cfg(not(target_os = "linux"))] + { + processes::get_process_data( + &self.sys, + self.use_current_cpu_total, + self.mem_total_kb, + ) + } + } { + self.data.list_of_processes = Some(process_list); + } + } + + let network_data_fut = { + #[cfg(target_os = "windows")] + { + network::get_network_data( + &self.sys, + self.last_collection_time, + &mut self.total_rx, + &mut self.total_tx, + current_instant, + self.widgets_to_harvest.use_net, + &self.filters.net_filter, + ) + } + #[cfg(not(target_os = "windows"))] + { + network::get_network_data( + self.last_collection_time, + &mut self.total_rx, + &mut self.total_tx, + current_instant, + self.widgets_to_harvest.use_net, + &self.filters.net_filter, + ) + } + }; + let mem_data_fut = memory::get_mem_data(self.widgets_to_harvest.use_mem); + let disk_data_fut = disks::get_disk_usage( + self.widgets_to_harvest.use_disk, + &self.filters.disk_filter, + &self.filters.mount_filter, + ); + let disk_io_usage_fut = disks::get_io_usage(self.widgets_to_harvest.use_disk); + let temp_data_fut = { + #[cfg(not(target_os = "linux"))] + { + temperature::get_temperature_data( + &self.sys, + &self.temperature_type, + self.widgets_to_harvest.use_temp, + &self.filters.temp_filter, + ) + } + + #[cfg(target_os = "linux")] + { + temperature::get_temperature_data( + &self.temperature_type, + self.widgets_to_harvest.use_temp, + &self.filters.temp_filter, + ) + } + }; + + let (net_data, mem_res, disk_res, io_res, temp_res) = join!( + network_data_fut, + mem_data_fut, + disk_data_fut, + disk_io_usage_fut, + temp_data_fut + ); + + if let Ok(net_data) = net_data { + if let Some(net_data) = &net_data { + self.total_rx = net_data.total_rx; + self.total_tx = net_data.total_tx; + } + self.data.network = net_data; + } + + if let Ok(memory) = mem_res.0 { + self.data.memory = memory; + } + + if let Ok(swap) = mem_res.1 { + self.data.swap = swap; + } + + if let Ok(disks) = disk_res { + self.data.disks = disks; + } + + if let Ok(io) = io_res { + self.data.io = io; + } + + if let Ok(temp) = temp_res { + self.data.temperature_sensors = temp; + } + + // Update time + self.data.last_collection_time = current_instant; + self.last_collection_time = current_instant; + } +} diff --git a/src/app/data_harvester/batteries.rs b/src/app/data_harvester/batteries.rs new file mode 100644 index 00000000..8c0e4a92 --- /dev/null +++ b/src/app/data_harvester/batteries.rs @@ -0,0 +1,10 @@ +//! Data collection for batteries. +//! +//! For Linux, macOS, Windows, FreeBSD, Dragonfly, and iOS, this is handled by the battery crate. + +cfg_if::cfg_if! { + if #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "ios"))] { + pub mod battery; + pub use self::battery::*; + } +} diff --git a/src/app/data_harvester/cpu.rs b/src/app/data_harvester/cpu.rs new file mode 100644 index 00000000..81a0db4c --- /dev/null +++ b/src/app/data_harvester/cpu.rs @@ -0,0 +1,14 @@ +//! Data collection for CPU usage and load average. +//! +//! For CPU usage, Linux, macOS, and Windows are handled by Heim. +//! +//! For load average, macOS and Linux are supported through Heim. + +cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] { + pub mod heim; + pub use self::heim::*; + } +} + +pub type LoadAvgHarvest = [f32; 3]; diff --git a/src/app/data_harvester/cpu/heim.rs b/src/app/data_harvester/cpu/heim.rs new file mode 100644 index 00000000..6941dd0c --- /dev/null +++ b/src/app/data_harvester/cpu/heim.rs @@ -0,0 +1,170 @@ +//! CPU stats through heim. +//! Supports macOS, Linux, and Windows. + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub mod linux; + pub use linux::*; + } else if #[cfg(any(target_os = "macos", target_os = "windows"))] { + pub mod windows_macos; + pub use windows_macos::*; + } +} + +cfg_if::cfg_if! { + if #[cfg(target_family = "unix")] { + pub mod unix; + pub use unix::*; + } +} + +#[derive(Default, Debug, Clone)] +pub struct CpuData { + pub cpu_prefix: String, + pub cpu_count: Option, + pub cpu_usage: f64, +} + +pub type CpuHarvest = Vec; + +pub type PastCpuWork = f64; +pub type PastCpuTotal = f64; + +use futures::StreamExt; +use std::collections::VecDeque; + +pub async fn get_cpu_data_list( + show_average_cpu: bool, previous_cpu_times: &mut Vec<(PastCpuWork, PastCpuTotal)>, + previous_average_cpu_time: &mut Option<(PastCpuWork, PastCpuTotal)>, +) -> crate::error::Result { + fn calculate_cpu_usage_percentage( + (previous_working_time, previous_total_time): (f64, f64), + (current_working_time, current_total_time): (f64, f64), + ) -> f64 { + ((if current_working_time > previous_working_time { + current_working_time - previous_working_time + } else { + 0.0 + }) * 100.0) + / (if current_total_time > previous_total_time { + current_total_time - previous_total_time + } else { + 1.0 + }) + } + + // Get all CPU times... + let cpu_times = heim::cpu::times().await?; + futures::pin_mut!(cpu_times); + + let mut cpu_deque: VecDeque = if previous_cpu_times.is_empty() { + // Must initialize ourselves. Use a very quick timeout to calculate an initial. + futures_timer::Delay::new(std::time::Duration::from_millis(100)).await; + + let second_cpu_times = heim::cpu::times().await?; + futures::pin_mut!(second_cpu_times); + + let mut new_cpu_times: Vec<(PastCpuWork, PastCpuTotal)> = Vec::new(); + let mut cpu_deque: VecDeque = VecDeque::new(); + let mut collected_zip = cpu_times.zip(second_cpu_times).enumerate(); // Gotta move it here, can't on while line. + + while let Some((itx, (past, present))) = collected_zip.next().await { + if let (Ok(past), Ok(present)) = (past, present) { + let present_times = convert_cpu_times(&present); + new_cpu_times.push(present_times); + cpu_deque.push_back(CpuData { + cpu_prefix: "CPU".to_string(), + cpu_count: Some(itx), + cpu_usage: calculate_cpu_usage_percentage( + convert_cpu_times(&past), + present_times, + ), + }); + } else { + new_cpu_times.push((0.0, 0.0)); + cpu_deque.push_back(CpuData { + cpu_prefix: "CPU".to_string(), + cpu_count: Some(itx), + cpu_usage: 0.0, + }); + } + } + + *previous_cpu_times = new_cpu_times; + cpu_deque + } else { + let (new_cpu_times, cpu_deque): (Vec<(PastCpuWork, PastCpuTotal)>, VecDeque) = + cpu_times + .collect::>() + .await + .iter() + .zip(&*previous_cpu_times) + .enumerate() + .map(|(itx, (current_cpu, (past_cpu_work, past_cpu_total)))| { + if let Ok(cpu_time) = current_cpu { + let present_times = convert_cpu_times(cpu_time); + + ( + present_times, + CpuData { + cpu_prefix: "CPU".to_string(), + cpu_count: Some(itx), + cpu_usage: calculate_cpu_usage_percentage( + (*past_cpu_work, *past_cpu_total), + present_times, + ), + }, + ) + } else { + ( + (*past_cpu_work, *past_cpu_total), + CpuData { + cpu_prefix: "CPU".to_string(), + cpu_count: Some(itx), + cpu_usage: 0.0, + }, + ) + } + }) + .unzip(); + + *previous_cpu_times = new_cpu_times; + cpu_deque + }; + + // Get average CPU if needed... and slap it at the top + if show_average_cpu { + let cpu_time = heim::cpu::time().await?; + + let (cpu_usage, new_average_cpu_time) = if let Some((past_cpu_work, past_cpu_total)) = + previous_average_cpu_time + { + let present_times = convert_cpu_times(&cpu_time); + ( + calculate_cpu_usage_percentage((*past_cpu_work, *past_cpu_total), present_times), + present_times, + ) + } else { + // Again, we need to do a quick timeout... + futures_timer::Delay::new(std::time::Duration::from_millis(100)).await; + let second_cpu_time = heim::cpu::time().await?; + + let present_times = convert_cpu_times(&second_cpu_time); + ( + calculate_cpu_usage_percentage(convert_cpu_times(&cpu_time), present_times), + present_times, + ) + }; + + *previous_average_cpu_time = Some(new_average_cpu_time); + cpu_deque.push_front(CpuData { + cpu_prefix: "AVG".to_string(), + cpu_count: None, + cpu_usage, + }) + } + + // Ok(Vec::from(cpu_deque.drain(0..3).collect::>())) // For artificially limiting the CPU results + + Ok(Vec::from(cpu_deque)) +} diff --git a/src/app/data_harvester/disks.rs b/src/app/data_harvester/disks.rs new file mode 100644 index 00000000..e5a52336 --- /dev/null +++ b/src/app/data_harvester/disks.rs @@ -0,0 +1,10 @@ +//! Data collection for disks (IO, usage, space, etc.). +//! +//! For Linux, macOS, and Windows, this is handled by heim. + +cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] { + pub mod heim; + pub use self::heim::*; + } +} diff --git a/src/app/data_harvester/disks/heim.rs b/src/app/data_harvester/disks/heim.rs new file mode 100644 index 00000000..a79d00db --- /dev/null +++ b/src/app/data_harvester/disks/heim.rs @@ -0,0 +1,154 @@ +use crate::app::Filter; + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub mod linux; + pub use linux::*; + } else if #[cfg(any(target_os = "macos", target_os = "windows"))] { + pub mod windows_macos; + pub use windows_macos::*; + } +} + +#[derive(Debug, Clone, Default)] +pub struct DiskHarvest { + pub name: String, + pub mount_point: String, + pub free_space: Option, + pub used_space: Option, + pub total_space: Option, +} + +#[derive(Clone, Debug)] +pub struct IoData { + pub read_bytes: u64, + pub write_bytes: u64, +} + +pub type IoHarvest = std::collections::HashMap>; + +pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result> { + if !actually_get { + return Ok(None); + } + + use futures::StreamExt; + + let mut io_hash: std::collections::HashMap> = + std::collections::HashMap::new(); + + let counter_stream = heim::disk::io_counters().await?; + futures::pin_mut!(counter_stream); + + while let Some(io) = counter_stream.next().await { + if let Ok(io) = io { + let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable"); + + io_hash.insert( + mount_point.to_string(), + Some(IoData { + read_bytes: io.read_bytes().get::(), + write_bytes: io.write_bytes().get::(), + }), + ); + } + } + + Ok(Some(io_hash)) +} + +pub async fn get_disk_usage( + actually_get: bool, disk_filter: &Option, mount_filter: &Option, +) -> crate::utils::error::Result>> { + if !actually_get { + return Ok(None); + } + + use futures::StreamExt; + + let mut vec_disks: Vec = Vec::new(); + let partitions_stream = heim::disk::partitions_physical().await?; + futures::pin_mut!(partitions_stream); + + while let Some(part) = partitions_stream.next().await { + if let Ok(partition) = part { + let name = get_device_name(&partition); + + let mount_point = (partition + .mount_point() + .to_str() + .unwrap_or("Name Unavailable")) + .to_string(); + + // Precedence ordering in the case where name and mount filters disagree, "allow" takes precedence over "deny". + // + // For implementation, we do this as follows: + // 1. Is the entry allowed through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `false`? If so, we always keep this entry. + // 2. Is the entry denied through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `true`? If so, we always deny this entry. + // 3. Anything else is allowed. + + let filter_check_map = [(disk_filter, &name), (mount_filter, &mount_point)]; + + // This represents case 1. That is, if there is a match in an allowing list - if there is, then + // immediately allow it! + let matches_allow_list = filter_check_map.iter().any(|(filter, text)| { + if let Some(filter) = filter { + if !filter.is_list_ignored { + for r in &filter.list { + if r.is_match(text) { + return true; + } + } + } + } + false + }); + + let to_keep = if matches_allow_list { + true + } else { + // If it doesn't match an allow list, then check if it is denied. + // That is, if it matches in a reject filter, then reject. Otherwise, we always keep it. + !filter_check_map.iter().any(|(filter, text)| { + if let Some(filter) = filter { + if filter.is_list_ignored { + for r in &filter.list { + if r.is_match(text) { + return true; + } + } + } + } + false + }) + }; + + if to_keep { + // The usage line can fail in some cases (for example, if you use Void Linux + LUKS, + // see https://github.com/ClementTsang/bottom/issues/419 for details). As such, check + // it like this instead. + if let Ok(usage) = heim::disk::usage(partition.mount_point().to_path_buf()).await { + vec_disks.push(DiskHarvest { + free_space: Some(usage.free().get::()), + used_space: Some(usage.used().get::()), + total_space: Some(usage.total().get::()), + mount_point, + name, + }); + } else { + vec_disks.push(DiskHarvest { + free_space: None, + used_space: None, + total_space: None, + mount_point, + name, + }); + } + } + } + } + + vec_disks.sort_by(|a, b| a.name.cmp(&b.name)); + + Ok(Some(vec_disks)) +} diff --git a/src/app/data_harvester/memory.rs b/src/app/data_harvester/memory.rs new file mode 100644 index 00000000..25fccf59 --- /dev/null +++ b/src/app/data_harvester/memory.rs @@ -0,0 +1,10 @@ +//! Data collection for memory. +//! +//! For Linux, macOS, and Windows, this is handled by Heim. + +cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] { + pub mod general; + pub use self::general::*; + } +} diff --git a/src/app/data_harvester/network.rs b/src/app/data_harvester/network.rs new file mode 100644 index 00000000..c717e6ac --- /dev/null +++ b/src/app/data_harvester/network.rs @@ -0,0 +1,30 @@ +//! Data collection for network usage/IO. +//! +//! For Linux and macOS, this is handled by Heim. +//! For Windows, this is handled by sysinfo. + +cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "macos"))] { + pub mod heim; + pub use self::heim::*; + } else if #[cfg(target_os = "windows")] { + pub mod sysinfo; + pub use self::sysinfo::*; + } +} + +#[derive(Default, Clone, Debug)] +/// All units in bits. +pub struct NetworkHarvest { + pub rx: u64, + pub tx: u64, + pub total_rx: u64, + pub total_tx: u64, +} + +impl NetworkHarvest { + pub fn first_run_cleanup(&mut self) { + self.rx = 0; + self.tx = 0; + } +} diff --git a/src/app/data_harvester/processes.rs b/src/app/data_harvester/processes.rs new file mode 100644 index 00000000..283080b3 --- /dev/null +++ b/src/app/data_harvester/processes.rs @@ -0,0 +1,97 @@ +//! Data collection for processes. +//! +//! For Linux, this is handled by a custom set of functions. +//! For Windows and macOS, this is handled by sysinfo. + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub mod linux; + pub use self::linux::*; + } else if #[cfg(target_os = "macos")] { + pub mod macos; + pub use self::macos::*; + } else if #[cfg(target_os = "windows")] { + pub mod windows; + pub use self::windows::*; + } +} + +cfg_if::cfg_if! { + if #[cfg(target_family = "unix")] { + pub mod unix; + pub use self::unix::*; + } +} + +use crate::Pid; + +// TODO: Add value so we know if it's sorted ascending or descending by default? +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub enum ProcessSorting { + CpuPercent, + Mem, + MemPercent, + Pid, + ProcessName, + Command, + ReadPerSecond, + WritePerSecond, + TotalRead, + TotalWrite, + State, + User, + Count, +} + +impl std::fmt::Display for ProcessSorting { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match &self { + ProcessSorting::CpuPercent => "CPU%", + ProcessSorting::MemPercent => "Mem%", + ProcessSorting::Mem => "Mem", + ProcessSorting::ReadPerSecond => "R/s", + ProcessSorting::WritePerSecond => "W/s", + ProcessSorting::TotalRead => "T.Read", + ProcessSorting::TotalWrite => "T.Write", + ProcessSorting::State => "State", + ProcessSorting::ProcessName => "Name", + ProcessSorting::Command => "Command", + ProcessSorting::Pid => "PID", + ProcessSorting::Count => "Count", + ProcessSorting::User => "User", + } + ) + } +} + +impl Default for ProcessSorting { + fn default() -> Self { + ProcessSorting::CpuPercent + } +} + +#[derive(Debug, Clone, Default)] +pub struct ProcessHarvest { + pub pid: Pid, + pub parent_pid: Option, // Remember, parent_pid 0 is root... + pub cpu_usage_percent: f64, + pub mem_usage_percent: f64, + pub mem_usage_bytes: u64, + // pub rss_kb: u64, + // pub virt_kb: u64, + pub name: String, + pub command: String, + pub read_bytes_per_sec: u64, + pub write_bytes_per_sec: u64, + pub total_read_bytes: u64, + pub total_write_bytes: u64, + pub process_state: String, + pub process_state_char: char, + + /// This is the *effective* user ID. + #[cfg(target_family = "unix")] + pub uid: Option, +} diff --git a/src/app/data_harvester/temperature.rs b/src/app/data_harvester/temperature.rs new file mode 100644 index 00000000..8f3b776e --- /dev/null +++ b/src/app/data_harvester/temperature.rs @@ -0,0 +1,73 @@ +//! Data collection for temperature metrics. +//! +//! For Linux and macOS, this is handled by Heim. +//! For Windows, this is handled by sysinfo. + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub mod heim; + pub use self::heim::*; + } else if #[cfg(any(target_os = "macos", target_os = "windows"))] { + pub mod sysinfo; + pub use self::sysinfo::*; + } +} + +use std::cmp::Ordering; + +use crate::app::Filter; + +#[derive(Default, Debug, Clone)] +pub struct TempHarvest { + pub name: String, + pub temperature: f32, +} + +#[derive(Clone, Debug)] +pub enum TemperatureType { + Celsius, + Kelvin, + Fahrenheit, +} + +impl Default for TemperatureType { + fn default() -> Self { + TemperatureType::Celsius + } +} + +fn is_temp_filtered(filter: &Option, text: &str) -> bool { + if let Some(filter) = filter { + if filter.is_list_ignored { + let mut ret = true; + for r in &filter.list { + if r.is_match(text) { + ret = false; + break; + } + } + ret + } else { + true + } + } else { + true + } +} + +fn temp_vec_sort(temperature_vec: &mut Vec) { + // By default, sort temperature, then by alphabetically! + // TODO: [TEMPS] Allow users to control this. + + // Note we sort in reverse here; we want greater temps to be higher priority. + temperature_vec.sort_by(|a, b| match a.temperature.partial_cmp(&b.temperature) { + Some(x) => match x { + Ordering::Less => Ordering::Greater, + Ordering::Greater => Ordering::Less, + Ordering::Equal => Ordering::Equal, + }, + None => Ordering::Equal, + }); + + temperature_vec.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap_or(Ordering::Equal)); +} diff --git a/src/app/widgets.rs b/src/app/widgets.rs new file mode 100644 index 00000000..d5ba3586 --- /dev/null +++ b/src/app/widgets.rs @@ -0,0 +1,210 @@ +use std::time::Instant; + +use crossterm::event::{KeyEvent, MouseEvent}; +use enum_dispatch::enum_dispatch; +use tui::{layout::Rect, widgets::TableState}; + +use crate::{ + app::{ + event::{EventResult, SelectionAction}, + layout_manager::BottomWidgetType, + }, + constants, +}; + +pub mod base; +pub use base::*; + +pub mod process; +pub use process::*; + +pub mod net; +pub use net::*; + +pub mod mem; +pub use mem::*; + +pub mod cpu; +pub use cpu::*; + +pub mod disk; +pub use disk::*; + +pub mod battery; +pub use self::battery::*; + +pub mod temp; +pub use temp::*; + +/// A trait for things that are drawn with state. +#[enum_dispatch] +#[allow(unused_variables)] +pub trait Component { + /// Handles a [`KeyEvent`]. + /// + /// Defaults to returning [`EventResult::NoRedraw`], indicating nothing should be done. + fn handle_key_event(&mut self, event: KeyEvent) -> EventResult { + EventResult::NoRedraw + } + + /// Handles a [`MouseEvent`]. + /// + /// Defaults to returning [`EventResult::Continue`], indicating nothing should be done. + fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { + EventResult::NoRedraw + } + + /// Returns a [`Component`]'s bounding box. Note that these are defined in *global*, *absolute* + /// coordinates. + fn bounds(&self) -> Rect; + + /// Updates a [`Component`]s bounding box to `new_bounds`. + fn set_bounds(&mut self, new_bounds: Rect); +} + +/// A trait for actual fully-fledged widgets to be displayed in bottom. +#[enum_dispatch] +pub trait Widget { + /// Updates a [`Widget`] given some data. Defaults to doing nothing. + fn update(&mut self) {} + + /// Handles what to do when trying to respond to a widget selection movement to the left. + /// Defaults to just moving to the next-possible widget in that direction. + fn handle_widget_selection_left(&mut self) -> SelectionAction { + SelectionAction::NotHandled + } + + /// Handles what to do when trying to respond to a widget selection movement to the right. + /// Defaults to just moving to the next-possible widget in that direction. + fn handle_widget_selection_right(&mut self) -> SelectionAction { + SelectionAction::NotHandled + } + + /// Handles what to do when trying to respond to a widget selection movement upward. + /// Defaults to just moving to the next-possible widget in that direction. + fn handle_widget_selection_up(&mut self) -> SelectionAction { + SelectionAction::NotHandled + } + + /// Handles what to do when trying to respond to a widget selection movement downward. + /// Defaults to just moving to the next-possible widget in that direction. + fn handle_widget_selection_down(&mut self) -> SelectionAction { + SelectionAction::NotHandled + } + + fn get_pretty_name(&self) -> &'static str; +} + +/// The "main" widgets that are used by bottom to display information! +#[enum_dispatch(Component, Widget)] +pub enum TmpBottomWidget { + MemGraph, + TempTable, + DiskTable, + CpuGraph, + NetGraph, + OldNetGraph, + ProcessManager, + BatteryTable, +} + +// ----- Old stuff below ----- + +#[derive(Debug)] +pub enum ScrollDirection { + // UP means scrolling up --- this usually DECREMENTS + Up, + // DOWN means scrolling down --- this usually INCREMENTS + Down, +} + +impl Default for ScrollDirection { + fn default() -> Self { + ScrollDirection::Down + } +} + +#[derive(Debug)] +pub enum CursorDirection { + Left, + Right, +} + +/// AppScrollWidgetState deals with fields for a scrollable app's current state. +#[derive(Default)] +pub struct AppScrollWidgetState { + pub current_scroll_position: usize, + pub previous_scroll_position: usize, + pub scroll_direction: ScrollDirection, + pub table_state: TableState, +} + +#[derive(PartialEq)] +pub enum KillSignal { + Cancel, + Kill(usize), +} + +impl Default for KillSignal { + #[cfg(target_family = "unix")] + fn default() -> Self { + KillSignal::Kill(15) + } + #[cfg(target_os = "windows")] + fn default() -> Self { + KillSignal::Kill(1) + } +} + +#[derive(Default)] +pub struct AppDeleteDialogState { + pub is_showing_dd: bool, + pub selected_signal: KillSignal, + /// tl x, tl y, br x, br y, index/signal + pub button_positions: Vec<(u16, u16, u16, u16, usize)>, + pub keyboard_signal_select: usize, + pub last_number_press: Option, + pub scroll_pos: usize, +} + +pub struct AppHelpDialogState { + pub is_showing_help: bool, + pub scroll_state: ParagraphScrollState, + pub index_shortcuts: Vec, +} + +impl Default for AppHelpDialogState { + fn default() -> Self { + AppHelpDialogState { + is_showing_help: false, + scroll_state: ParagraphScrollState::default(), + index_shortcuts: vec![0; constants::HELP_TEXT.len()], + } + } +} + +/// Meant for canvas operations involving table column widths. +#[derive(Default)] +pub struct CanvasTableWidthState { + pub desired_column_widths: Vec, + pub calculated_column_widths: Vec, +} + +pub struct BasicTableWidgetState { + // Since this is intended (currently) to only be used for ONE widget, that's + // how it's going to be written. If we want to allow for multiple of these, + // then we can expand outwards with a normal BasicTableState and a hashmap + pub currently_displayed_widget_type: BottomWidgetType, + pub currently_displayed_widget_id: u64, + pub widget_id: i64, + pub left_tlc: Option<(u16, u16)>, + pub left_brc: Option<(u16, u16)>, + pub right_tlc: Option<(u16, u16)>, + pub right_brc: Option<(u16, u16)>, +} + +#[derive(Default)] +pub struct ParagraphScrollState { + pub current_scroll_index: u16, + pub max_scroll_index: u16, +} diff --git a/src/app/widgets/base.rs b/src/app/widgets/base.rs new file mode 100644 index 00000000..851baf24 --- /dev/null +++ b/src/app/widgets/base.rs @@ -0,0 +1,16 @@ +//! A collection of basic components. + +pub mod text_table; +pub use text_table::TextTable; + +pub mod time_graph; +pub use time_graph::TimeGraph; + +pub mod scrollable; +pub use scrollable::Scrollable; + +pub mod text_input; +pub use text_input::TextInput; + +pub mod carousel; +pub use carousel::Carousel;