diff --git a/Cargo.lock b/Cargo.lock index bb8569a0..0cead99b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,7 +231,7 @@ dependencies = [ [[package]] name = "bottom" -version = "0.6.4" +version = "0.7.0" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index a0505a43..d6380949 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bottom" -version = "0.6.4" +version = "0.7.0" authors = ["Clement Tsang "] edition = "2018" repository = "https://github.com/ClementTsang/bottom" diff --git a/src/app.rs b/src/app.rs index 8cd9c019..d0237d94 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,6 +13,9 @@ use std::{ time::Instant, }; +use crossterm::event::{KeyEvent, KeyModifiers}; +use fxhash::FxHashMap; +use indextree::{Arena, NodeId}; use unicode_segmentation::GraphemeCursor; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; @@ -29,9 +32,11 @@ use crate::{ constants::{self, MAX_SIGNAL}, units::data_units::DataUnit, utils::error::{BottomError, Result}, - Pid, + BottomEvent, Pid, }; +use self::event::{does_point_intersect_rect, EventResult, ReturnSignal}; + const MAX_SEARCH_LENGTH: usize = 200; #[derive(Debug, Clone)] @@ -40,6 +45,17 @@ pub enum AxisScaling { Linear, } +#[derive(Clone, Default, Debug)] +pub struct UsedWidgets { + pub use_cpu: bool, + pub use_mem: bool, + pub use_net: bool, + pub use_proc: bool, + pub use_disk: bool, + pub use_temp: bool, + pub use_battery: bool, +} + /// AppConfigFields is meant to cover basic fields that would normally be set /// by config files or launch options. #[derive(Debug)] @@ -67,14 +83,9 @@ pub struct AppConfigFields { pub network_use_binary_prefix: bool, } +// FIXME: Get rid of TypedBuilder here! #[derive(TypedBuilder)] pub struct AppState { - #[builder(default = false, setter(skip))] - awaiting_second_char: bool, - - #[builder(default, setter(skip))] - second_char: Option, - #[builder(default, setter(skip))] pub dd_err: Option, @@ -93,12 +104,6 @@ pub struct AppState { #[builder(default, setter(skip))] pub data_collection: DataCollection, - #[builder(default, setter(skip))] - pub delete_dialog_state: AppDeleteDialogState, - - #[builder(default, setter(skip))] - pub help_dialog_state: AppHelpDialogState, - #[builder(default = false, setter(skip))] pub is_expanded: bool, @@ -115,6 +120,18 @@ pub struct AppState { #[builder(default, setter(skip))] pub user_table: processes::UserTable, + pub used_widgets: UsedWidgets, + pub filters: DataFilters, + pub app_config_fields: AppConfigFields, + + // --- Possibly delete? --- + #[builder(default, setter(skip))] + pub delete_dialog_state: AppDeleteDialogState, + + #[builder(default, setter(skip))] + pub help_dialog_state: AppHelpDialogState, + + // --- TO DELETE--- pub cpu_state: CpuState, pub mem_state: MemState, pub net_state: NetState, @@ -123,14 +140,31 @@ pub struct AppState { pub disk_state: DiskState, pub battery_state: BatteryState, pub basic_table_widget_state: Option, - pub app_config_fields: AppConfigFields, pub widget_map: HashMap, pub current_widget: BottomWidget, - pub used_widgets: UsedWidgets, - pub filters: DataFilters, + + #[builder(default = false, setter(skip))] + awaiting_second_char: bool, + + #[builder(default, setter(skip))] + second_char: Option, + + // --- NEW STUFF --- + pub selected_widget: NodeId, + pub widget_lookup_map: FxHashMap, + pub layout_tree: Arena, + pub layout_tree_root: NodeId, } impl AppState { + /// Creates a new [`AppState`]. + pub fn new( + _app_config_fields: AppConfigFields, _filters: DataFilters, + _layout_tree_output: LayoutCreationOutput, + ) -> Self { + todo!() + } + pub fn reset(&mut self) { // Reset multi self.reset_multi_tap_keys(); @@ -176,6 +210,79 @@ impl AppState { self.dd_err = None; } + /// Handles a global [`KeyEvent`], and returns [`Some(EventResult)`] if the global shortcut is consumed by some global + /// shortcut. If not, it returns [`None`]. + fn handle_global_shortcut(&mut self, event: KeyEvent) -> Option { + // TODO: Write this. + + if event.modifiers.is_empty() { + todo!() + } else if let KeyModifiers::ALT = event.modifiers { + todo!() + } else { + None + } + } + + /// Handles a [`BottomEvent`] and updates the [`AppState`] if needed. Returns an [`EventResult`] indicating + /// whether the app now requires a redraw. + pub fn handle_event(&mut self, event: BottomEvent) -> EventResult { + match event { + BottomEvent::KeyInput(event) => { + if let Some(event_result) = self.handle_global_shortcut(event) { + // See if it's caught by a global shortcut first... + event_result + } else if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) { + // If it isn't, send it to the current widget! + widget.handle_key_event(event) + } else { + EventResult::NoRedraw + } + } + BottomEvent::MouseInput(event) => { + // Not great, but basically a blind lookup through the table for anything that clips the click location. + // TODO: Would be cool to use a kd-tree or something like that in the future. + + let x = event.column; + let y = event.row; + + for (id, widget) in self.widget_lookup_map.iter_mut() { + if does_point_intersect_rect(x, y, widget.bounds()) { + if self.selected_widget == *id { + self.selected_widget = *id; + return widget.handle_mouse_event(event); + } else { + // If the aren't equal, *force* a redraw. + self.selected_widget = *id; + widget.handle_mouse_event(event); + return EventResult::Redraw; + } + } + } + + EventResult::NoRedraw + } + BottomEvent::Update(new_data) => { + if !self.is_frozen { + // TODO: Update all data, and redraw. + EventResult::Redraw + } else { + EventResult::NoRedraw + } + } + BottomEvent::Clean => { + self.data_collection + .clean_data(constants::STALE_MAX_MILLISECONDS); + EventResult::NoRedraw + } + } + } + + /// Handles a [`ReturnSignal`], and returns + pub fn handle_return_signal(&mut self, return_signal: ReturnSignal) -> EventResult { + todo!() + } + pub fn on_esc(&mut self) { self.reset_multi_tap_keys(); if self.is_in_dialog() { @@ -200,6 +307,7 @@ impl AppState { .process_search_state .search_state .is_enabled = false; + current_proc_state.is_sort_open = false; self.is_force_redraw = true; return; diff --git a/src/app/data_harvester/batteries/mod.rs b/src/app/data_harvester/batteries/mod.rs deleted file mode 100644 index 8c0e4a92..00000000 --- a/src/app/data_harvester/batteries/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! 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/heim/mod.rs b/src/app/data_harvester/cpu/heim/mod.rs deleted file mode 100644 index 6941dd0c..00000000 --- a/src/app/data_harvester/cpu/heim/mod.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! 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/cpu/mod.rs b/src/app/data_harvester/cpu/mod.rs deleted file mode 100644 index 81a0db4c..00000000 --- a/src/app/data_harvester/cpu/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! 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/disks/heim/mod.rs b/src/app/data_harvester/disks/heim/mod.rs deleted file mode 100644 index a79d00db..00000000 --- a/src/app/data_harvester/disks/heim/mod.rs +++ /dev/null @@ -1,154 +0,0 @@ -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/disks/mod.rs b/src/app/data_harvester/disks/mod.rs deleted file mode 100644 index e5a52336..00000000 --- a/src/app/data_harvester/disks/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! 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/memory/mod.rs b/src/app/data_harvester/memory/mod.rs deleted file mode 100644 index 25fccf59..00000000 --- a/src/app/data_harvester/memory/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! 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/mod.rs b/src/app/data_harvester/mod.rs deleted file mode 100644 index 32482537..00000000 --- a/src/app/data_harvester/mod.rs +++ /dev/null @@ -1,366 +0,0 @@ -//! 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 crate::app::layout_manager::UsedWidgets; - -use futures::join; - -use super::DataFilters; - -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/network/mod.rs b/src/app/data_harvester/network/mod.rs deleted file mode 100644 index c717e6ac..00000000 --- a/src/app/data_harvester/network/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! 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/mod.rs b/src/app/data_harvester/processes/mod.rs deleted file mode 100644 index 283080b3..00000000 --- a/src/app/data_harvester/processes/mod.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! 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/mod.rs b/src/app/data_harvester/temperature/mod.rs deleted file mode 100644 index 8f3b776e..00000000 --- a/src/app/data_harvester/temperature/mod.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! 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/event.rs b/src/app/event.rs index d51a490e..19d1fb4e 100644 --- a/src/app/event.rs +++ b/src/app/event.rs @@ -1,25 +1,45 @@ use std::time::{Duration, Instant}; +use tui::layout::Rect; + const MAX_TIMEOUT: Duration = Duration::from_millis(400); +/// These are "signals" that are sent along with an [`EventResult`] to signify a potential additional action +/// that the caller must do, along with the "core" result of either drawing or redrawing. +pub enum ReturnSignal { + /// Do nothing. + Nothing, + /// A signal returned when some process widget was told to try to kill a process (or group of processes). + KillProcess, +} + +/// The results of handling a [`ReturnSignal`]. +pub enum ReturnSignalResult { + /// Kill the program. + Quit, + /// Trigger a redraw. + Redraw, + /// Don't trigger a redraw. + NoRedraw, +} + /// The results of handling some user input event, like a mouse or key event, signifying what /// the program should then do next. pub enum EventResult { /// Kill the program. Quit, - /// Trigger a redraw. Redraw, - /// Don't trigger a redraw. NoRedraw, + /// Return a signal. + Signal(ReturnSignal), } /// How a widget should handle a widget selection request. pub enum SelectionAction { /// This event occurs if the widget internally handled the selection action. Handled, - /// This event occurs if the widget did not handle the selection action; the caller must handle it. NotHandled, } @@ -28,7 +48,6 @@ pub enum SelectionAction { enum MultiKeyState { /// Currently not waiting on any next input. Idle, - /// Waiting for the next input, with a given trigger [`Instant`]. Waiting { /// When it was triggered. @@ -43,10 +62,8 @@ enum MultiKeyState { pub enum MultiKeyResult { /// Returned when a character was *accepted*, but has not completed the sequence required. Accepted, - /// Returned when a character is accepted and completes the sequence. Completed, - /// Returned if a character breaks the sequence or if it has already timed out. Rejected, } @@ -124,3 +141,8 @@ impl MultiKey { } } } + +/// Checks whether points `(x, y)` intersect a given [`Rect`]. +pub fn does_point_intersect_rect(x: u16, y: u16, rect: Rect) -> bool { + x >= rect.left() && x <= rect.right() && y >= rect.top() && y <= rect.bottom() +} diff --git a/src/app/layout_manager.rs b/src/app/layout_manager.rs index d4b9272d..065b89fb 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -1,14 +1,24 @@ -use crate::error::{BottomError, Result}; +use crate::{ + app::{DiskTable, MemGraph, NetGraph, OldNetGraph, ProcessManager, TempTable}, + error::{BottomError, Result}, + options::layout_options::{Row, RowChildren}, +}; +use fxhash::FxHashMap; +use indextree::{Arena, NodeId}; use std::collections::BTreeMap; +use tui::layout::Constraint; use typed_builder::*; +use crate::app::widgets::Widget; use crate::constants::DEFAULT_WIDGET_ID; +use super::{event::SelectionAction, CpuGraph, TextTable, TimeGraph, TmpBottomWidget}; + /// Represents a more usable representation of the layout, derived from the /// config. #[derive(Clone, Debug)] pub struct BottomLayout { - pub rows: Vec, + pub rows: Vec, pub total_row_height_ratio: u32, } @@ -535,7 +545,7 @@ impl BottomLayout { pub fn init_basic_default(use_battery: bool) -> Self { let table_widgets = if use_battery { vec![ - BottomCol::builder() + OldBottomCol::builder() .canvas_handle_width(true) .children(vec![BottomColRow::builder() .canvas_handle_height(true) @@ -549,7 +559,7 @@ impl BottomLayout { .build()]) .build()]) .build(), - BottomCol::builder() + OldBottomCol::builder() .canvas_handle_width(true) .children(vec![ BottomColRow::builder() @@ -593,7 +603,7 @@ impl BottomLayout { .build(), ]) .build(), - BottomCol::builder() + OldBottomCol::builder() .canvas_handle_width(true) .children(vec![BottomColRow::builder() .canvas_handle_height(true) @@ -607,7 +617,7 @@ impl BottomLayout { .build()]) .build()]) .build(), - BottomCol::builder() + OldBottomCol::builder() .canvas_handle_width(true) .children(vec![BottomColRow::builder() .canvas_handle_height(true) @@ -624,7 +634,7 @@ impl BottomLayout { ] } else { vec![ - BottomCol::builder() + OldBottomCol::builder() .canvas_handle_width(true) .children(vec![BottomColRow::builder() .canvas_handle_height(true) @@ -638,7 +648,7 @@ impl BottomLayout { .build()]) .build()]) .build(), - BottomCol::builder() + OldBottomCol::builder() .canvas_handle_width(true) .children(vec![ BottomColRow::builder() @@ -679,7 +689,7 @@ impl BottomLayout { .build(), ]) .build(), - BottomCol::builder() + OldBottomCol::builder() .canvas_handle_width(true) .children(vec![BottomColRow::builder() .canvas_handle_height(true) @@ -699,9 +709,9 @@ impl BottomLayout { BottomLayout { total_row_height_ratio: 3, rows: vec![ - BottomRow::builder() + OldBottomRow::builder() .canvas_handle_height(true) - .children(vec![BottomCol::builder() + .children(vec![OldBottomCol::builder() .canvas_handle_width(true) .children(vec![BottomColRow::builder() .canvas_handle_height(true) @@ -714,9 +724,9 @@ impl BottomLayout { .build()]) .build()]) .build(), - BottomRow::builder() + OldBottomRow::builder() .canvas_handle_height(true) - .children(vec![BottomCol::builder() + .children(vec![OldBottomCol::builder() .canvas_handle_width(true) .children(vec![BottomColRow::builder() .canvas_handle_height(true) @@ -741,9 +751,9 @@ impl BottomLayout { .build()]) .build()]) .build(), - BottomRow::builder() + OldBottomRow::builder() .canvas_handle_height(true) - .children(vec![BottomCol::builder() + .children(vec![OldBottomCol::builder() .canvas_handle_width(true) .children(vec![BottomColRow::builder() .canvas_handle_height(true) @@ -756,7 +766,7 @@ impl BottomLayout { .build()]) .build()]) .build(), - BottomRow::builder() + OldBottomRow::builder() .canvas_handle_height(true) .children(table_widgets) .build(), @@ -767,8 +777,8 @@ impl BottomLayout { /// Represents a single row in the layout. #[derive(Clone, Debug, TypedBuilder)] -pub struct BottomRow { - pub children: Vec, +pub struct OldBottomRow { + pub children: Vec, #[builder(default = 1)] pub total_col_ratio: u32, @@ -787,7 +797,7 @@ pub struct BottomRow { /// contains only ONE element, it is still a column (rather than either a col or /// a widget, as per the config, for simplicity's sake). #[derive(Clone, Debug, TypedBuilder)] -pub struct BottomCol { +pub struct OldBottomCol { pub children: Vec, #[builder(default = 1)] @@ -972,13 +982,479 @@ Supported widget names: } } -#[derive(Clone, Default, Debug)] -pub struct UsedWidgets { - pub use_cpu: bool, - pub use_mem: bool, - pub use_net: bool, - pub use_proc: bool, - pub use_disk: bool, - pub use_temp: bool, - pub use_battery: bool, +// --- New stuff --- + +/// Represents a row in the layout tree. +#[derive(PartialEq, Eq)] +pub struct RowLayout { + last_selected_index: usize, + pub constraint: Constraint, +} + +impl Default for RowLayout { + fn default() -> Self { + Self { + last_selected_index: 0, + constraint: Constraint::Min(0), + } + } +} + +/// Represents a column in the layout tree. +#[derive(PartialEq, Eq)] +pub struct ColLayout { + last_selected_index: usize, + pub constraint: Constraint, +} + +impl Default for ColLayout { + fn default() -> Self { + Self { + last_selected_index: 0, + constraint: Constraint::Min(0), + } + } +} + +/// Represents a widget in the layout tree. +#[derive(PartialEq, Eq)] +pub struct WidgetLayout { + pub constraint: Constraint, +} + +impl Default for WidgetLayout { + fn default() -> Self { + Self { + constraint: Constraint::Min(0), + } + } +} + +/// A [`LayoutNode`] represents a single node in the overall widget hierarchy. Each node is one of: +/// - [`LayoutNode::Row`] (a a non-leaf that distributes its children horizontally) +/// - [`LayoutNode::Col`] (a non-leaf node that distributes its children vertically) +/// - [`LayoutNode::Widget`] (a leaf node that contains the ID of the widget it is associated with) +#[derive(PartialEq, Eq)] +pub enum LayoutNode { + Row(RowLayout), + Col(ColLayout), + Widget(WidgetLayout), +} + +/// Relative movement direction from the currently selected widget. +pub enum MovementDirection { + Left, + Right, + Up, + Down, +} + +/// A wrapper struct to simplify the output of [`create_layout_tree`]. +pub struct LayoutCreationOutput { + pub layout_tree: Arena, + pub root: NodeId, + pub widget_lookup_map: FxHashMap, + pub selected: Option, +} + +/// Creates a new [`Arena`] from the given config and returns it, along with the [`NodeId`] representing +/// the root of the newly created [`Arena`], a mapping from [`NodeId`]s to [`BottomWidget`]s, and optionally, a default +/// selected [`NodeId`]. +// FIXME: This is currently jury-rigged "glue" just to work with the existing config system! We are NOT keeping it like this, it's too awful to keep like this! +pub fn create_layout_tree( + rows: &[Row], process_defaults: crate::options::ProcessDefaults, + app_config_fields: &super::AppConfigFields, +) -> Result { + fn add_widget_to_map( + widget_lookup_map: &mut FxHashMap, widget_type: &str, + widget_id: NodeId, process_defaults: &crate::options::ProcessDefaults, + app_config_fields: &super::AppConfigFields, + ) -> Result<()> { + match widget_type.parse::()? { + BottomWidgetType::Cpu => { + let graph = TimeGraph::from_config(app_config_fields); + let legend = TextTable::new(vec![("CPU", None, false), ("Use%", None, false)]); + let legend_position = super::CpuGraphLegendPosition::Right; + + widget_lookup_map.insert( + widget_id, + CpuGraph::new(graph, legend, legend_position).into(), + ); + } + BottomWidgetType::Mem => { + let graph = TimeGraph::from_config(app_config_fields); + widget_lookup_map.insert(widget_id, MemGraph::new(graph).into()); + } + BottomWidgetType::Net => { + let graph = TimeGraph::from_config(app_config_fields); + if app_config_fields.use_old_network_legend { + widget_lookup_map.insert(widget_id, OldNetGraph::new(graph).into()); + } else { + widget_lookup_map.insert(widget_id, NetGraph::new(graph).into()); + } + } + BottomWidgetType::Proc => { + widget_lookup_map.insert( + widget_id, + ProcessManager::new(process_defaults.is_tree).into(), + ); + } + BottomWidgetType::Temp => { + let table = TextTable::new(vec![("Sensor", None, false), ("Temp", None, false)]); + widget_lookup_map.insert(widget_id, TempTable::new(table).into()); + } + BottomWidgetType::Disk => { + let table = TextTable::new(vec![ + ("Disk", None, false), + ("Mount", None, false), + ("Used", None, false), + ("Free", None, false), + ("Total", None, false), + ("R/s", None, false), + ("W/s", None, false), + ]); + widget_lookup_map.insert(widget_id, DiskTable::new(table).into()); + } + BottomWidgetType::Battery => {} + _ => {} + } + + Ok(()) + } + + let mut layout_tree = Arena::new(); + let root_id = layout_tree.new_node(LayoutNode::Col(ColLayout::default())); + let mut widget_lookup_map = FxHashMap::default(); + let mut selected = None; + + let row_sum: u32 = rows.iter().map(|row| row.ratio.unwrap_or(1)).sum(); + for row in rows { + let ratio = row.ratio.unwrap_or(1); + let layout_node = LayoutNode::Row(RowLayout { + constraint: Constraint::Ratio(ratio, row_sum), + ..Default::default() + }); + let row_id = layout_tree.new_node(layout_node); + root_id.append(row_id, &mut layout_tree); + + if let Some(cols) = &row.child { + let col_sum: u32 = cols + .iter() + .map(|col| match col { + RowChildren::Widget(widget) => widget.ratio.unwrap_or(1), + RowChildren::Col { ratio, child: _ } => ratio.unwrap_or(1), + }) + .sum(); + + for col in cols { + match col { + RowChildren::Widget(widget) => { + let widget_node = LayoutNode::Widget(WidgetLayout { + constraint: Constraint::Ratio(widget.ratio.unwrap_or(1), col_sum), + }); + let widget_id = layout_tree.new_node(widget_node); + row_id.append(widget_id, &mut layout_tree); + + if let Some(true) = widget.default { + selected = Some(widget_id); + } + add_widget_to_map( + &mut widget_lookup_map, + &widget.widget_type, + widget_id, + &process_defaults, + app_config_fields, + )?; + } + RowChildren::Col { + ratio, + child: children, + } => { + let col_node = LayoutNode::Col(ColLayout { + constraint: Constraint::Ratio(ratio.unwrap_or(1), col_sum), + ..Default::default() + }); + let col_id = layout_tree.new_node(col_node); + row_id.append(col_id, &mut layout_tree); + + let child_sum: u32 = + children.iter().map(|child| child.ratio.unwrap_or(1)).sum(); + + for child in children { + let widget_node = LayoutNode::Widget(WidgetLayout { + constraint: Constraint::Ratio(child.ratio.unwrap_or(1), child_sum), + }); + let widget_id = layout_tree.new_node(widget_node); + col_id.append(widget_id, &mut layout_tree); + + if let Some(true) = child.default { + selected = Some(widget_id); + } + add_widget_to_map( + &mut widget_lookup_map, + &child.widget_type, + widget_id, + &process_defaults, + app_config_fields, + )?; + } + } + } + } + } + } + + Ok(LayoutCreationOutput { + layout_tree, + root: root_id, + widget_lookup_map, + selected, + }) +} + +/// Attempts to find and return the selected [`BottomWidgetId`] after moving in a direction. +/// +/// Note this function assumes a properly built tree - if not, bad things may happen! We generally assume that: +/// - Only [`LayoutNode::Widget`]s are leaves. +/// - Only [`LayoutNode::Row`]s or [`LayoutNode::Col`]s are non-leaves. +pub fn move_widget_selection( + layout_tree: &mut Arena, current_widget: &mut TmpBottomWidget, + current_widget_id: NodeId, direction: MovementDirection, +) -> NodeId { + // We first give our currently-selected widget a chance to react to the movement - it may handle it internally! + let handled = match direction { + MovementDirection::Left => current_widget.handle_widget_selection_left(), + MovementDirection::Right => current_widget.handle_widget_selection_right(), + MovementDirection::Up => current_widget.handle_widget_selection_up(), + MovementDirection::Down => current_widget.handle_widget_selection_down(), + }; + + match handled { + SelectionAction::Handled => { + // If it was handled by the widget, then we don't have to do anything - return the current one. + current_widget_id + } + SelectionAction::NotHandled => { + /// Keeps traversing up the `layout_tree` until it hits a parent where `current_id` is a child and parent + /// is a [`LayoutNode::Row`], returning its parent's [`NodeId`] and the child's [`NodeId`] (in that order). + /// If this crawl fails (i.e. hits a root, it is an invalid tree for some reason), it returns [`None`]. + fn find_first_row( + layout_tree: &Arena, current_id: NodeId, + ) -> Option<(NodeId, NodeId)> { + layout_tree + .get(current_id) + .and_then(|current_node| current_node.parent()) + .and_then(|parent_id| { + layout_tree + .get(parent_id) + .map(|parent_node| (parent_id, parent_node)) + }) + .and_then(|(parent_id, parent_node)| match parent_node.get() { + LayoutNode::Row(_) => Some((parent_id, current_id)), + LayoutNode::Col(_) => find_first_row(layout_tree, parent_id), + LayoutNode::Widget(_) => None, + }) + } + + /// Keeps traversing up the `layout_tree` until it hits a parent where `current_id` is a child and parent + /// is a [`LayoutNode::Col`], returning its parent's [`NodeId`] and the child's [`NodeId`] (in that order). + /// If this crawl fails (i.e. hits a root, it is an invalid tree for some reason), it returns [`None`]. + fn find_first_col( + layout_tree: &Arena, current_id: NodeId, + ) -> Option<(NodeId, NodeId)> { + layout_tree + .get(current_id) + .and_then(|current_node| current_node.parent()) + .and_then(|parent_id| { + layout_tree + .get(parent_id) + .map(|parent_node| (parent_id, parent_node)) + }) + .and_then(|(parent_id, parent_node)| match parent_node.get() { + LayoutNode::Row(_) => find_first_col(layout_tree, parent_id), + LayoutNode::Col(_) => Some((parent_id, current_id)), + LayoutNode::Widget(_) => None, + }) + } + + /// Descends to a leaf. + fn descend_to_leaf(layout_tree: &Arena, current_id: NodeId) -> NodeId { + if let Some(current_node) = layout_tree.get(current_id) { + match current_node.get() { + LayoutNode::Row(RowLayout { + last_selected_index, + constraint: _, + }) + | LayoutNode::Col(ColLayout { + last_selected_index, + constraint: _, + }) => { + if let Some(next_child) = + current_id.children(layout_tree).nth(*last_selected_index) + { + descend_to_leaf(layout_tree, next_child) + } else { + current_id + } + } + LayoutNode::Widget(_) => { + // Halt! + current_id + } + } + } else { + current_id + } + } + + // If it was NOT handled by the current widget, then move in the correct direction; we can rely + // on the tree layout to help us decide where to go. + // Movement logic is inspired by i3. When we enter a new column/row, we go to the *last* selected + // element; if we can't, go to the nearest one. + match direction { + MovementDirection::Left => { + // When we move "left": + // 1. Look for the parent of the current widget. + // 2. Depending on whether it is a Row or Col: + // a) If we are in a Row, try to move to the child (it can be a Row, Col, or Widget) before it, + // and update the last-selected index. If we can't (i.e. we are the first element), then + // instead move to the parent, and try again to select the element before it. If there is + // no parent (i.e. we hit the root), then just return the original index. + // b) If we are in a Col, then just try to move to the parent. If there is no + // parent (i.e. we hit the root), then just return the original index. + // c) A Widget should be impossible to select. + // 3. Assuming we have now selected a new child, then depending on what the child is: + // a) If we are in a Row or Col, then take the last selected index, and repeat step 3 until you hit + // a Widget. + // b) If we are in a Widget, return the corresponding NodeId. + + fn find_left( + layout_tree: &mut Arena, current_id: NodeId, + ) -> NodeId { + if let Some((parent_id, child_id)) = find_first_row(layout_tree, current_id) + { + if let Some(prev_sibling) = + child_id.preceding_siblings(layout_tree).nth(1) + { + // Subtract one from the currently selected index... + if let Some(parent) = layout_tree.get_mut(parent_id) { + if let LayoutNode::Row(row) = parent.get_mut() { + row.last_selected_index = + row.last_selected_index.saturating_sub(1); + } + } + + // Now descend downwards! + descend_to_leaf(layout_tree, prev_sibling) + } else { + // Darn, we can't go further back! Recurse on this ID. + find_left(layout_tree, child_id) + } + } else { + // Failed, just return the current ID. + current_id + } + } + find_left(layout_tree, current_widget_id) + } + MovementDirection::Right => { + // When we move "right", repeat the steps for "left", but instead try to move to the child *after* + // it in all cases. + + fn find_right( + layout_tree: &mut Arena, current_id: NodeId, + ) -> NodeId { + if let Some((parent_id, child_id)) = find_first_row(layout_tree, current_id) + { + if let Some(prev_sibling) = + child_id.following_siblings(layout_tree).nth(1) + { + // Add one to the currently selected index... + if let Some(parent) = layout_tree.get_mut(parent_id) { + if let LayoutNode::Row(row) = parent.get_mut() { + row.last_selected_index += 1; + } + } + + // Now descend downwards! + descend_to_leaf(layout_tree, prev_sibling) + } else { + // Darn, we can't go further back! Recurse on this ID. + find_right(layout_tree, child_id) + } + } else { + // Failed, just return the current ID. + current_id + } + } + find_right(layout_tree, current_widget_id) + } + MovementDirection::Up => { + // When we move "up", copy the steps for "left", but switch "Row" and "Col". We instead want to move + // vertically, so we want to now avoid Rows and look for Cols! + + fn find_above( + layout_tree: &mut Arena, current_id: NodeId, + ) -> NodeId { + if let Some((parent_id, child_id)) = find_first_col(layout_tree, current_id) + { + if let Some(prev_sibling) = + child_id.preceding_siblings(layout_tree).nth(1) + { + // Subtract one from the currently selected index... + if let Some(parent) = layout_tree.get_mut(parent_id) { + if let LayoutNode::Col(row) = parent.get_mut() { + row.last_selected_index = + row.last_selected_index.saturating_sub(1); + } + } + + // Now descend downwards! + descend_to_leaf(layout_tree, prev_sibling) + } else { + // Darn, we can't go further back! Recurse on this ID. + find_above(layout_tree, child_id) + } + } else { + // Failed, just return the current ID. + current_id + } + } + find_above(layout_tree, current_widget_id) + } + MovementDirection::Down => { + // See "up"'s steps, but now we're going for the child *after* the currently selected one in all + // cases. + + fn find_below( + layout_tree: &mut Arena, current_id: NodeId, + ) -> NodeId { + if let Some((parent_id, child_id)) = find_first_col(layout_tree, current_id) + { + if let Some(prev_sibling) = + child_id.following_siblings(layout_tree).nth(1) + { + // Add one to the currently selected index... + if let Some(parent) = layout_tree.get_mut(parent_id) { + if let LayoutNode::Col(row) = parent.get_mut() { + row.last_selected_index += 1; + } + } + + // Now descend downwards! + descend_to_leaf(layout_tree, prev_sibling) + } else { + // Darn, we can't go further back! Recurse on this ID. + find_below(layout_tree, child_id) + } + } else { + // Failed, just return the current ID. + current_id + } + } + find_below(layout_tree, current_widget_id) + } + } + } + } } diff --git a/src/app/widgets/base/carousel.rs b/src/app/widgets/base/carousel.rs index e69de29b..4454a847 100644 --- a/src/app/widgets/base/carousel.rs +++ b/src/app/widgets/base/carousel.rs @@ -0,0 +1,42 @@ +use indextree::NodeId; +use tui::layout::Rect; + +use crate::app::Component; + +/// A container that "holds"" multiple [`BottomWidget`]s through their [`NodeId`]s. +pub struct Carousel { + index: usize, + children: Vec, + bounds: Rect, +} + +impl Carousel { + /// Creates a new [`Carousel`] with the specified children. + pub fn new(children: Vec) -> Self { + Self { + index: 0, + children, + bounds: Rect::default(), + } + } + + /// Adds a new child to a [`Carousel`]. + pub fn add_child(&mut self, child: NodeId) { + self.children.push(child); + } + + /// Returns the currently selected [`NodeId`] if possible. + pub fn get_currently_selected(&self) -> Option<&NodeId> { + self.children.get(self.index) + } +} + +impl Component for Carousel { + fn bounds(&self) -> tui::layout::Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: tui::layout::Rect) { + self.bounds = new_bounds; + } +} diff --git a/src/app/widgets/base/mod.rs b/src/app/widgets/base/mod.rs deleted file mode 100644 index 7176c1cc..00000000 --- a/src/app/widgets/base/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! 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; diff --git a/src/app/widgets/base/text_table.rs b/src/app/widgets/base/text_table.rs index 6d6088bc..d5ff3d2c 100644 --- a/src/app/widgets/base/text_table.rs +++ b/src/app/widgets/base/text_table.rs @@ -51,9 +51,9 @@ pub struct TextTable { } impl TextTable { - pub fn new(num_items: usize, columns: Vec<(&'static str, Option, bool)>) -> Self { + pub fn new(columns: Vec<(&'static str, Option, bool)>) -> Self { Self { - scrollable: Scrollable::new(num_items), + scrollable: Scrollable::new(0), columns: columns .into_iter() .map(|(name, shortcut, default_descending)| Column { diff --git a/src/app/widgets/base/time_graph.rs b/src/app/widgets/base/time_graph.rs index 3c19bb0a..a0054c38 100644 --- a/src/app/widgets/base/time_graph.rs +++ b/src/app/widgets/base/time_graph.rs @@ -3,7 +3,10 @@ use std::time::{Duration, Instant}; use crossterm::event::{KeyEvent, KeyModifiers, MouseEvent}; use tui::layout::Rect; -use crate::app::{event::EventResult, Component}; +use crate::{ + app::{event::EventResult, AppConfigFields, Component}, + constants::{AUTOHIDE_TIMEOUT_MILLISECONDS, STALE_MAX_MILLISECONDS, STALE_MIN_MILLISECONDS}, +}; #[derive(Clone)] pub enum AutohideTimerState { @@ -13,7 +16,8 @@ pub enum AutohideTimerState { #[derive(Clone)] pub enum AutohideTimer { - Disabled, + AlwaysShow, + AlwaysHide, Enabled { state: AutohideTimerState, show_duration: Duration, @@ -21,10 +25,10 @@ pub enum AutohideTimer { } impl AutohideTimer { - fn trigger_display_timer(&mut self) { + fn start_display_timer(&mut self) { match self { - AutohideTimer::Disabled => { - // Does nothing. + AutohideTimer::AlwaysShow | AutohideTimer::AlwaysHide => { + // Do nothing. } AutohideTimer::Enabled { state, @@ -37,8 +41,8 @@ impl AutohideTimer { pub fn update_display_timer(&mut self) { match self { - AutohideTimer::Disabled => { - // Does nothing. + AutohideTimer::AlwaysShow | AutohideTimer::AlwaysHide => { + // Do nothing. } AutohideTimer::Enabled { state, @@ -70,6 +74,7 @@ pub struct TimeGraph { } impl TimeGraph { + /// Creates a new [`TimeGraph`]. All time values are in milliseconds. pub fn new( start_value: u64, autohide_timer: AutohideTimer, min_duration: u64, max_duration: u64, time_interval: u64, @@ -85,6 +90,26 @@ impl TimeGraph { } } + /// Creates a new [`TimeGraph`] given an [`AppConfigFields`]. + pub fn from_config(app_config_fields: &AppConfigFields) -> Self { + Self::new( + app_config_fields.default_time_value, + if app_config_fields.hide_time { + AutohideTimer::AlwaysHide + } else if app_config_fields.autohide_time { + AutohideTimer::Enabled { + state: AutohideTimerState::Running(Instant::now()), + show_duration: Duration::from_millis(AUTOHIDE_TIMEOUT_MILLISECONDS), + } + } else { + AutohideTimer::AlwaysShow + }, + STALE_MIN_MILLISECONDS, + STALE_MAX_MILLISECONDS, + app_config_fields.time_interval, + ) + } + /// Handles a char `c`. fn handle_char(&mut self, c: char) -> EventResult { match c { @@ -100,12 +125,12 @@ impl TimeGraph { if new_time >= self.min_duration { self.current_display_time = new_time; - self.autohide_timer.trigger_display_timer(); + self.autohide_timer.start_display_timer(); EventResult::Redraw } else if new_time != self.min_duration { self.current_display_time = self.min_duration; - self.autohide_timer.trigger_display_timer(); + self.autohide_timer.start_display_timer(); EventResult::Redraw } else { @@ -118,12 +143,12 @@ impl TimeGraph { if new_time <= self.max_duration { self.current_display_time = new_time; - self.autohide_timer.trigger_display_timer(); + self.autohide_timer.start_display_timer(); EventResult::Redraw } else if new_time != self.max_duration { self.current_display_time = self.max_duration; - self.autohide_timer.trigger_display_timer(); + self.autohide_timer.start_display_timer(); EventResult::Redraw } else { @@ -136,7 +161,7 @@ impl TimeGraph { EventResult::NoRedraw } else { self.current_display_time = self.default_time_value; - self.autohide_timer.trigger_display_timer(); + self.autohide_timer.start_display_timer(); EventResult::Redraw } } diff --git a/src/app/widgets/cpu.rs b/src/app/widgets/cpu.rs index fabeed5c..75f02c4a 100644 --- a/src/app/widgets/cpu.rs +++ b/src/app/widgets/cpu.rs @@ -3,12 +3,9 @@ use std::{collections::HashMap, time::Instant}; use crossterm::event::{KeyEvent, MouseEvent}; use tui::layout::Rect; -use crate::app::event::EventResult; +use crate::app::event::{does_point_intersect_rect, EventResult}; -use super::{ - does_point_intersect_rect, AppScrollWidgetState, CanvasTableWidthState, Component, TextTable, - TimeGraph, Widget, -}; +use super::{AppScrollWidgetState, CanvasTableWidthState, Component, TextTable, TimeGraph, Widget}; pub struct CpuWidgetState { pub current_display_time: u64, diff --git a/src/app/widgets/mod.rs b/src/app/widgets/mod.rs deleted file mode 100644 index 5920aabb..00000000 --- a/src/app/widgets/mod.rs +++ /dev/null @@ -1,492 +0,0 @@ -use std::time::Instant; - -use crossterm::event::{KeyEvent, MouseEvent}; -use enum_dispatch::enum_dispatch; -use indextree::{Arena, NodeId}; -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)] -enum BottomWidget { - MemGraph, - TempTable, - DiskTable, - CpuGraph, - NetGraph, - OldNetGraph, - ProcessManager, - BatteryTable, -} - -/// Checks whether points `(x, y)` intersect a given [`Rect`]. -pub fn does_point_intersect_rect(x: u16, y: u16, rect: Rect) -> bool { - x >= rect.left() && x <= rect.right() && y >= rect.top() && y <= rect.bottom() -} - -/// A [`LayoutNode`] represents a single node in the overall widget hierarchy. Each node is one of: -/// - [`LayoutNode::Row`] (a a non-leaf that distributes its children horizontally) -/// - [`LayoutNode::Col`] (a non-leaf node that distributes its children vertically) -/// - [`LayoutNode::Widget`] (a leaf node that contains the ID of the widget it is associated with) -#[derive(PartialEq, Eq)] -pub enum LayoutNode { - Row(BottomRow), - Col(BottomCol), - Widget, -} - -/// A simple struct representing a row and its state. -#[derive(PartialEq, Eq)] -pub struct BottomRow { - last_selected_index: usize, -} - -/// A simple struct representing a column and its state. -#[derive(PartialEq, Eq)] -pub struct BottomCol { - last_selected_index: usize, -} - -/// Relative movement direction from the currently selected widget. -pub enum MovementDirection { - Left, - Right, - Up, - Down, -} - -/// Attempts to find and return the selected [`BottomWidgetId`] after moving in a direction. -/// -/// Note this function assumes a properly built tree - if not, bad things may happen! We generally assume that: -/// - Only [`LayoutNode::Widget`]s are leaves. -/// - Only [`LayoutNode::Row`]s or [`LayoutNode::Col`]s are non-leaves. -fn move_widget_selection( - layout_tree: &mut Arena, current_widget: &mut BottomWidget, - current_widget_id: NodeId, direction: MovementDirection, -) -> NodeId { - // We first give our currently-selected widget a chance to react to the movement - it may handle it internally! - let handled = match direction { - MovementDirection::Left => current_widget.handle_widget_selection_left(), - MovementDirection::Right => current_widget.handle_widget_selection_right(), - MovementDirection::Up => current_widget.handle_widget_selection_up(), - MovementDirection::Down => current_widget.handle_widget_selection_down(), - }; - - match handled { - SelectionAction::Handled => { - // If it was handled by the widget, then we don't have to do anything - return the current one. - current_widget_id - } - SelectionAction::NotHandled => { - /// Keeps traversing up the `layout_tree` until it hits a parent where `current_id` is a child and parent - /// is a [`LayoutNode::Row`], returning its parent's [`NodeId`] and the child's [`NodeId`] (in that order). - /// If this crawl fails (i.e. hits a root, it is an invalid tree for some reason), it returns [`None`]. - fn find_first_row( - layout_tree: &Arena, current_id: NodeId, - ) -> Option<(NodeId, NodeId)> { - layout_tree - .get(current_id) - .and_then(|current_node| current_node.parent()) - .and_then(|parent_id| { - layout_tree - .get(parent_id) - .map(|parent_node| (parent_id, parent_node)) - }) - .and_then(|(parent_id, parent_node)| match parent_node.get() { - LayoutNode::Row(_) => Some((parent_id, current_id)), - LayoutNode::Col(_) => find_first_row(layout_tree, parent_id), - LayoutNode::Widget => None, - }) - } - - /// Keeps traversing up the `layout_tree` until it hits a parent where `current_id` is a child and parent - /// is a [`LayoutNode::Col`], returning its parent's [`NodeId`] and the child's [`NodeId`] (in that order). - /// If this crawl fails (i.e. hits a root, it is an invalid tree for some reason), it returns [`None`]. - fn find_first_col( - layout_tree: &Arena, current_id: NodeId, - ) -> Option<(NodeId, NodeId)> { - layout_tree - .get(current_id) - .and_then(|current_node| current_node.parent()) - .and_then(|parent_id| { - layout_tree - .get(parent_id) - .map(|parent_node| (parent_id, parent_node)) - }) - .and_then(|(parent_id, parent_node)| match parent_node.get() { - LayoutNode::Row(_) => find_first_col(layout_tree, parent_id), - LayoutNode::Col(_) => Some((parent_id, current_id)), - LayoutNode::Widget => None, - }) - } - - /// Descends to a leaf. - fn descend_to_leaf(layout_tree: &Arena, current_id: NodeId) -> NodeId { - if let Some(current_node) = layout_tree.get(current_id) { - match current_node.get() { - LayoutNode::Row(BottomRow { - last_selected_index, - }) - | LayoutNode::Col(BottomCol { - last_selected_index, - }) => { - if let Some(next_child) = - current_id.children(layout_tree).nth(*last_selected_index) - { - descend_to_leaf(layout_tree, next_child) - } else { - current_id - } - } - LayoutNode::Widget => { - // Halt! - current_id - } - } - } else { - current_id - } - } - - // If it was NOT handled by the current widget, then move in the correct direction; we can rely - // on the tree layout to help us decide where to go. - // Movement logic is inspired by i3. When we enter a new column/row, we go to the *last* selected - // element; if we can't, go to the nearest one. - match direction { - MovementDirection::Left => { - // When we move "left": - // 1. Look for the parent of the current widget. - // 2. Depending on whether it is a Row or Col: - // a) If we are in a Row, try to move to the child (it can be a Row, Col, or Widget) before it, - // and update the last-selected index. If we can't (i.e. we are the first element), then - // instead move to the parent, and try again to select the element before it. If there is - // no parent (i.e. we hit the root), then just return the original index. - // b) If we are in a Col, then just try to move to the parent. If there is no - // parent (i.e. we hit the root), then just return the original index. - // c) A Widget should be impossible to select. - // 3. Assuming we have now selected a new child, then depending on what the child is: - // a) If we are in a Row or Col, then take the last selected index, and repeat step 3 until you hit - // a Widget. - // b) If we are in a Widget, return the corresponding NodeId. - - fn find_left( - layout_tree: &mut Arena, current_id: NodeId, - ) -> NodeId { - if let Some((parent_id, child_id)) = find_first_row(layout_tree, current_id) - { - if let Some(prev_sibling) = - child_id.preceding_siblings(layout_tree).nth(1) - { - // Subtract one from the currently selected index... - if let Some(parent) = layout_tree.get_mut(parent_id) { - if let LayoutNode::Row(row) = parent.get_mut() { - row.last_selected_index = - row.last_selected_index.saturating_sub(1); - } - } - - // Now descend downwards! - descend_to_leaf(layout_tree, prev_sibling) - } else { - // Darn, we can't go further back! Recurse on this ID. - find_left(layout_tree, child_id) - } - } else { - // Failed, just return the current ID. - current_id - } - } - find_left(layout_tree, current_widget_id) - } - MovementDirection::Right => { - // When we move "right", repeat the steps for "left", but instead try to move to the child *after* - // it in all cases. - - fn find_right( - layout_tree: &mut Arena, current_id: NodeId, - ) -> NodeId { - if let Some((parent_id, child_id)) = find_first_row(layout_tree, current_id) - { - if let Some(prev_sibling) = - child_id.following_siblings(layout_tree).nth(1) - { - // Add one to the currently selected index... - if let Some(parent) = layout_tree.get_mut(parent_id) { - if let LayoutNode::Row(row) = parent.get_mut() { - row.last_selected_index += 1; - } - } - - // Now descend downwards! - descend_to_leaf(layout_tree, prev_sibling) - } else { - // Darn, we can't go further back! Recurse on this ID. - find_right(layout_tree, child_id) - } - } else { - // Failed, just return the current ID. - current_id - } - } - find_right(layout_tree, current_widget_id) - } - MovementDirection::Up => { - // When we move "up", copy the steps for "left", but switch "Row" and "Col". We instead want to move - // vertically, so we want to now avoid Rows and look for Cols! - - fn find_above( - layout_tree: &mut Arena, current_id: NodeId, - ) -> NodeId { - if let Some((parent_id, child_id)) = find_first_col(layout_tree, current_id) - { - if let Some(prev_sibling) = - child_id.preceding_siblings(layout_tree).nth(1) - { - // Subtract one from the currently selected index... - if let Some(parent) = layout_tree.get_mut(parent_id) { - if let LayoutNode::Col(row) = parent.get_mut() { - row.last_selected_index = - row.last_selected_index.saturating_sub(1); - } - } - - // Now descend downwards! - descend_to_leaf(layout_tree, prev_sibling) - } else { - // Darn, we can't go further back! Recurse on this ID. - find_above(layout_tree, child_id) - } - } else { - // Failed, just return the current ID. - current_id - } - } - find_above(layout_tree, current_widget_id) - } - MovementDirection::Down => { - // See "up"'s steps, but now we're going for the child *after* the currently selected one in all - // cases. - - fn find_below( - layout_tree: &mut Arena, current_id: NodeId, - ) -> NodeId { - if let Some((parent_id, child_id)) = find_first_col(layout_tree, current_id) - { - if let Some(prev_sibling) = - child_id.following_siblings(layout_tree).nth(1) - { - // Add one to the currently selected index... - if let Some(parent) = layout_tree.get_mut(parent_id) { - if let LayoutNode::Col(row) = parent.get_mut() { - row.last_selected_index += 1; - } - } - - // Now descend downwards! - descend_to_leaf(layout_tree, prev_sibling) - } else { - // Darn, we can't go further back! Recurse on this ID. - find_below(layout_tree, child_id) - } - } else { - // Failed, just return the current ID. - current_id - } - } - find_below(layout_tree, current_widget_id) - } - } - } - } -} - -// ----- 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/net.rs b/src/app/widgets/net.rs index 8be2f106..5d90f9cc 100644 --- a/src/app/widgets/net.rs +++ b/src/app/widgets/net.rs @@ -154,15 +154,15 @@ impl Widget for NetGraph { /// A widget denoting network usage via a graph and a separate, single row table. This is built on [`NetGraph`], /// and the main difference is that it also contains a bounding box for the graph + text. pub struct OldNetGraph { - graph: NetGraph, + net_graph: NetGraph, bounds: Rect, } impl OldNetGraph { /// Creates a new [`OldNetGraph`]. - pub fn new(graph: NetGraph) -> Self { + pub fn new(graph: TimeGraph) -> Self { Self { - graph, + net_graph: NetGraph::new(graph), bounds: Rect::default(), } } @@ -180,13 +180,13 @@ impl Component for OldNetGraph { fn handle_key_event( &mut self, event: crossterm::event::KeyEvent, ) -> crate::app::event::EventResult { - self.graph.handle_key_event(event) + self.net_graph.handle_key_event(event) } fn handle_mouse_event( &mut self, event: crossterm::event::MouseEvent, ) -> crate::app::event::EventResult { - self.graph.handle_mouse_event(event) + self.net_graph.handle_mouse_event(event) } } diff --git a/src/app/widgets/process.rs b/src/app/widgets/process.rs index b5357ddd..dfdfaa19 100644 --- a/src/app/widgets/process.rs +++ b/src/app/widgets/process.rs @@ -7,7 +7,7 @@ use tui::{layout::Rect, widgets::TableState}; use crate::{ app::{ - event::{EventResult, MultiKey, MultiKeyResult}, + event::{does_point_intersect_rect, EventResult, MultiKey, MultiKeyResult}, query::*, }, data_harvester::processes::{self, ProcessSorting}, @@ -15,8 +15,8 @@ use crate::{ use ProcessSorting::*; use super::{ - does_point_intersect_rect, AppScrollWidgetState, CanvasTableWidthState, Component, - CursorDirection, ScrollDirection, TextInput, TextTable, Widget, + AppScrollWidgetState, CanvasTableWidthState, Component, CursorDirection, ScrollDirection, + TextInput, TextTable, Widget, }; /// AppSearchState deals with generic searching (I might do this in the future). @@ -652,8 +652,8 @@ impl ProcessManager { pub fn new(default_in_tree_mode: bool) -> Self { Self { bounds: Rect::default(), - process_table: TextTable::new(0, vec![]), // TODO: Do this - sort_table: TextTable::new(0, vec![]), // TODO: Do this too + process_table: TextTable::new(vec![]), // TODO: Do this + sort_table: TextTable::new(vec![]), // TODO: Do this too search_input: TextInput::new(), dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: Use a static arrayvec selected: ProcessManagerSelection::Processes, diff --git a/src/bin/main.rs b/src/bin/main.rs index b258f466..075ea051 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -39,18 +39,12 @@ fn main() -> Result<()> { .context("Unable to properly parse or create the config file.")?; // Get widget layout separately - let (widget_layout, default_widget_id, default_widget_type_option) = + let (widget_layout, _default_widget_id, _default_widget_type_option) = get_widget_layout(&matches, &config) .context("Found an issue while trying to build the widget layout.")?; // Create "app" struct, which will control most of the program and store settings/state - let mut app = build_app( - &matches, - &mut config, - &widget_layout, - default_widget_id, - &default_widget_type_option, - )?; + let mut app = build_app(&matches, &mut config)?; // Create painter and set colours. let mut painter = canvas::Painter::init( @@ -67,10 +61,11 @@ fn main() -> Result<()> { let thread_termination_cvar = Arc::new(Condvar::new()); // Set up input handling - let (sender, receiver) = mpsc::channel(); + let (sender, receiver) = mpsc::channel(); // FIXME: Make this bounded, prevents overloading. let _input_thread = create_input_thread(sender.clone(), thread_termination_lock.clone()); // Cleaning loop + // TODO: Probably worth spinning this off into an async thread or something... let _cleaning_thread = { let lock = thread_termination_lock.clone(); let cvar = thread_termination_cvar.clone(); @@ -140,7 +135,7 @@ fn main() -> Result<()> { force_redraw(&mut app); try_drawing(&mut terminal, &mut app, &mut painter)?; } - EventResult::NoRedraw => {} + _ => {} } } BottomEvent::MouseInput(event) => match handle_mouse_event(event, &mut app) { @@ -152,7 +147,7 @@ fn main() -> Result<()> { force_redraw(&mut app); try_drawing(&mut terminal, &mut app, &mut painter)?; } - EventResult::NoRedraw => {} + _ => {} }, BottomEvent::Update(data) => { app.data_collection.eat_data(data); diff --git a/src/lib.rs b/src/lib.rs index cee739dc..c5c792b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,8 +32,8 @@ use crossterm::{ use app::{ data_harvester::{self, processes::ProcessSorting}, event::EventResult, - layout_manager::{UsedWidgets, WidgetDirection}, - AppState, + layout_manager::WidgetDirection, + AppState, UsedWidgets, }; use constants::*; use data_conversion::*; @@ -60,9 +60,9 @@ pub type Pid = usize; pub type Pid = libc::pid_t; #[derive(Debug)] -pub enum BottomEvent { - KeyInput(I), - MouseInput(J), +pub enum BottomEvent { + KeyInput(KeyEvent), + MouseInput(MouseEvent), Update(Box), Clean, } @@ -598,10 +598,7 @@ fn sort_process_data( } pub fn create_input_thread( - sender: std::sync::mpsc::Sender< - BottomEvent, - >, - termination_ctrl_lock: Arc>, + sender: std::sync::mpsc::Sender, termination_ctrl_lock: Arc>, ) -> std::thread::JoinHandle<()> { thread::spawn(move || { let mut mouse_timer = Instant::now(); @@ -646,9 +643,7 @@ pub fn create_input_thread( } pub fn create_collection_thread( - sender: std::sync::mpsc::Sender< - BottomEvent, - >, + sender: std::sync::mpsc::Sender, control_receiver: std::sync::mpsc::Receiver, termination_ctrl_lock: Arc>, termination_ctrl_cvar: Arc, app_config_fields: &app::AppConfigFields, filters: app::DataFilters, diff --git a/src/options.rs b/src/options.rs index 941b890d..da3102b1 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,10 +1,6 @@ use regex::Regex; use serde::{Deserialize, Serialize}; -use std::{ - collections::{HashMap, HashSet}, - str::FromStr, - time::Instant, -}; +use std::{collections::HashMap, str::FromStr}; use crate::{ app::{layout_manager::*, *}, @@ -178,169 +174,39 @@ pub struct IgnoreList { pub whole_word: bool, } -pub fn build_app( - matches: &clap::ArgMatches<'static>, config: &mut Config, widget_layout: &BottomLayout, - default_widget_id: u64, default_widget_type_option: &Option, -) -> Result { - use BottomWidgetType::*; +/// Represents the default states of all process widgets. +pub struct ProcessDefaults { + pub is_grouped: bool, + pub is_case_sensitive: bool, + pub is_match_whole_word: bool, + pub is_use_regex: bool, + pub is_show_mem_as_values: bool, + pub is_tree: bool, + pub is_command: bool, +} + +pub fn build_app(matches: &clap::ArgMatches<'static>, config: &mut Config) -> Result { + // Process defaults + let process_defaults = ProcessDefaults { + is_grouped: get_process_grouping(matches, config), + is_case_sensitive: get_case_sensitive(matches, config), + is_match_whole_word: get_match_whole_word(matches, config), + is_use_regex: get_use_regex(matches, config), + is_show_mem_as_values: get_mem_as_value(matches, config), + is_tree: get_is_default_tree(matches, config), + is_command: get_is_default_process_command(matches, config), + }; + + // App config fields let autohide_time = get_autohide_time(matches, config); let default_time_value = get_default_time_value(matches, config) .context("Update 'default_time_value' in your config file.")?; let use_basic_mode = get_use_basic_mode(matches, config); - - // For processes - let is_grouped = get_app_grouping(matches, config); - let is_case_sensitive = get_app_case_sensitive(matches, config); - let is_match_whole_word = get_app_match_whole_word(matches, config); - let is_use_regex = get_app_use_regex(matches, config); - - let mut widget_map = HashMap::new(); - let mut cpu_state_map: HashMap = HashMap::new(); - let mut mem_state_map: HashMap = HashMap::new(); - let mut net_state_map: HashMap = HashMap::new(); - let mut proc_state_map: HashMap = HashMap::new(); - let mut temp_state_map: HashMap = HashMap::new(); - let mut disk_state_map: HashMap = HashMap::new(); - let mut battery_state_map: HashMap = HashMap::new(); - - let autohide_timer = if autohide_time { - Some(Instant::now()) - } else { - None - }; - - let mut initial_widget_id: u64 = default_widget_id; - let mut initial_widget_type = Proc; - let is_custom_layout = config.row.is_some(); - let mut used_widget_set = HashSet::new(); - - let show_memory_as_values = get_mem_as_value(matches, config); - let is_default_tree = get_is_default_tree(matches, config); - let is_default_command = get_is_default_process_command(matches, config); let is_advanced_kill = !get_is_advanced_kill_disabled(matches, config); - let network_unit_type = get_network_unit_type(matches, config); let network_scale_type = get_network_scale_type(matches, config); let network_use_binary_prefix = get_network_use_binary_prefix(matches, config); - for row in &widget_layout.rows { - for col in &row.children { - for col_row in &col.children { - for widget in &col_row.children { - widget_map.insert(widget.widget_id, widget.clone()); - if let Some(default_widget_type) = &default_widget_type_option { - if !is_custom_layout || use_basic_mode { - match widget.widget_type { - BasicCpu => { - if let Cpu = *default_widget_type { - initial_widget_id = widget.widget_id; - initial_widget_type = Cpu; - } - } - BasicMem => { - if let Mem = *default_widget_type { - initial_widget_id = widget.widget_id; - initial_widget_type = Cpu; - } - } - BasicNet => { - if let Net = *default_widget_type { - initial_widget_id = widget.widget_id; - initial_widget_type = Cpu; - } - } - _ => { - if *default_widget_type == widget.widget_type { - initial_widget_id = widget.widget_id; - initial_widget_type = widget.widget_type.clone(); - } - } - } - } - } - - used_widget_set.insert(widget.widget_type.clone()); - - match widget.widget_type { - Cpu => { - cpu_state_map.insert( - widget.widget_id, - CpuWidgetState::init(default_time_value, autohide_timer), - ); - } - Mem => { - mem_state_map.insert( - widget.widget_id, - MemWidgetState::init(default_time_value, autohide_timer), - ); - } - Net => { - net_state_map.insert( - widget.widget_id, - NetWidgetState::init( - default_time_value, - autohide_timer, - // network_unit_type.clone(), - // network_scale_type.clone(), - ), - ); - } - Proc => { - proc_state_map.insert( - widget.widget_id, - ProcWidgetState::init( - is_case_sensitive, - is_match_whole_word, - is_use_regex, - is_grouped, - show_memory_as_values, - is_default_tree, - is_default_command, - ), - ); - } - Disk => { - disk_state_map.insert(widget.widget_id, DiskWidgetState::init()); - } - Temp => { - temp_state_map.insert(widget.widget_id, TempWidgetState::init()); - } - Battery => { - battery_state_map - .insert(widget.widget_id, BatteryWidgetState::default()); - } - _ => {} - } - } - } - } - } - - let basic_table_widget_state = if use_basic_mode { - Some(match initial_widget_type { - Proc | Disk | Temp => BasicTableWidgetState { - currently_displayed_widget_type: initial_widget_type, - currently_displayed_widget_id: initial_widget_id, - widget_id: 100, - left_tlc: None, - left_brc: None, - right_tlc: None, - right_brc: None, - }, - _ => BasicTableWidgetState { - currently_displayed_widget_type: Proc, - currently_displayed_widget_id: DEFAULT_WIDGET_ID, - widget_id: 100, - left_tlc: None, - left_brc: None, - right_tlc: None, - right_brc: None, - }, - }) - } else { - None - }; - let app_config_fields = AppConfigFields { update_rate_in_milliseconds: get_update_rate_in_milliseconds(matches, config) .context("Update 'rate' in your config file.")?, @@ -372,14 +238,20 @@ pub fn build_app( network_use_binary_prefix, }; - let used_widgets = UsedWidgets { - use_cpu: used_widget_set.get(&Cpu).is_some() || used_widget_set.get(&BasicCpu).is_some(), - use_mem: used_widget_set.get(&Mem).is_some() || used_widget_set.get(&BasicMem).is_some(), - use_net: used_widget_set.get(&Net).is_some() || used_widget_set.get(&BasicNet).is_some(), - use_proc: used_widget_set.get(&Proc).is_some(), - use_disk: used_widget_set.get(&Disk).is_some(), - use_temp: used_widget_set.get(&Temp).is_some(), - use_battery: used_widget_set.get(&Battery).is_some(), + let layout_tree_output = if get_use_basic_mode(matches, config) { + todo!() + } else if let Some(row) = &config.row { + create_layout_tree(row, process_defaults, &app_config_fields)? + } else { + if get_use_battery(matches, config) { + let rows = toml::from_str::(DEFAULT_BATTERY_LAYOUT)? + .row + .unwrap(); + create_layout_tree(&rows, process_defaults, &app_config_fields)? + } else { + let rows = toml::from_str::(DEFAULT_LAYOUT)?.row.unwrap(); + create_layout_tree(&rows, process_defaults, &app_config_fields)? + } }; let disk_filter = @@ -390,63 +262,39 @@ pub fn build_app( get_ignore_list(&config.temp_filter).context("Update 'temp_filter' in your config file")?; let net_filter = get_ignore_list(&config.net_filter).context("Update 'net_filter' in your config file")?; + let data_filter = DataFilters { + disk_filter, + mount_filter, + temp_filter, + net_filter, + }; - // One more thing - we have to update the search settings of our proc_state_map, and create the hashmaps if needed! - // Note that if you change your layout, this might not actually match properly... not sure if/where we should deal with that... - if let Some(flags) = &mut config.flags { - if flags.case_sensitive.is_none() && !matches.is_present("case_sensitive") { - if let Some(search_case_enabled_widgets) = &flags.search_case_enabled_widgets { - for widget in search_case_enabled_widgets { - if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) { - proc_widget.process_search_state.is_ignoring_case = !widget.enabled; - } - } - } - } + // Ok(AppState::builder() + // .app_config_fields(app_config_fields) + // .cpu_state(CpuState::init(cpu_state_map)) + // .mem_state(MemState::init(mem_state_map)) + // .net_state(NetState::init(net_state_map)) + // .proc_state(ProcState::init(proc_state_map)) + // .disk_state(DiskState::init(disk_state_map)) + // .temp_state(TempState::init(temp_state_map)) + // .battery_state(BatteryState::init(battery_state_map)) + // .basic_table_widget_state(basic_table_widget_state) + // .current_widget(widget_map.get(&initial_widget_id).unwrap().clone()) // TODO: [UNWRAP] - many of the unwraps are fine (like this one) but do a once-over and/or switch to expect? + // .widget_map(widget_map) + // .used_widgets(used_widgets) + // .filters(DataFilters { + // disk_filter, + // mount_filter, + // temp_filter, + // net_filter, + // }) + // .build()) - if flags.whole_word.is_none() && !matches.is_present("whole_word") { - if let Some(search_whole_word_enabled_widgets) = - &flags.search_whole_word_enabled_widgets - { - for widget in search_whole_word_enabled_widgets { - if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) { - proc_widget.process_search_state.is_searching_whole_word = widget.enabled; - } - } - } - } - - if flags.regex.is_none() && !matches.is_present("regex") { - if let Some(search_regex_enabled_widgets) = &flags.search_regex_enabled_widgets { - for widget in search_regex_enabled_widgets { - if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) { - proc_widget.process_search_state.is_searching_with_regex = widget.enabled; - } - } - } - } - } - - Ok(AppState::builder() - .app_config_fields(app_config_fields) - .cpu_state(CpuState::init(cpu_state_map)) - .mem_state(MemState::init(mem_state_map)) - .net_state(NetState::init(net_state_map)) - .proc_state(ProcState::init(proc_state_map)) - .disk_state(DiskState::init(disk_state_map)) - .temp_state(TempState::init(temp_state_map)) - .battery_state(BatteryState::init(battery_state_map)) - .basic_table_widget_state(basic_table_widget_state) - .current_widget(widget_map.get(&initial_widget_id).unwrap().clone()) // TODO: [UNWRAP] - many of the unwraps are fine (like this one) but do a once-over and/or switch to expect? - .widget_map(widget_map) - .used_widgets(used_widgets) - .filters(DataFilters { - disk_filter, - mount_filter, - temp_filter, - net_filter, - }) - .build()) + Ok(AppState::new( + app_config_fields, + data_filter, + layout_tree_output, + )) } pub fn get_widget_layout( @@ -684,7 +532,7 @@ fn get_time_interval(matches: &clap::ArgMatches<'static>, config: &Config) -> er Ok(time_interval as u64) } -pub fn get_app_grouping(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { +pub fn get_process_grouping(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { if matches.is_present("group") { return true; } else if let Some(flags) = &config.flags { @@ -695,7 +543,7 @@ pub fn get_app_grouping(matches: &clap::ArgMatches<'static>, config: &Config) -> false } -pub fn get_app_case_sensitive(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { +pub fn get_case_sensitive(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { if matches.is_present("case_sensitive") { return true; } else if let Some(flags) = &config.flags { @@ -706,7 +554,7 @@ pub fn get_app_case_sensitive(matches: &clap::ArgMatches<'static>, config: &Conf false } -pub fn get_app_match_whole_word(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { +pub fn get_match_whole_word(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { if matches.is_present("whole_word") { return true; } else if let Some(flags) = &config.flags { @@ -717,7 +565,7 @@ pub fn get_app_match_whole_word(matches: &clap::ArgMatches<'static>, config: &Co false } -pub fn get_app_use_regex(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { +pub fn get_use_regex(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { if matches.is_present("regex") { return true; } else if let Some(flags) = &config.flags { diff --git a/src/options/layout_options.rs b/src/options/layout_options.rs index 025af157..7e5949bf 100644 --- a/src/options/layout_options.rs +++ b/src/options/layout_options.rs @@ -16,7 +16,7 @@ impl Row { &self, iter_id: &mut u64, total_height_ratio: &mut u32, default_widget_id: &mut u64, default_widget_type: &Option, default_widget_count: &mut u64, left_legend: bool, - ) -> Result { + ) -> Result { // TODO: In the future we want to also add percentages. // But for MVP, we aren't going to bother. let row_ratio = self.ratio.unwrap_or(1); @@ -55,7 +55,7 @@ impl Row { BottomWidgetType::Cpu => { let cpu_id = *iter_id; *iter_id += 1; - BottomCol::builder() + OldBottomCol::builder() .col_width_ratio(width_ratio) .children(if left_legend { vec![BottomColRow::builder() @@ -108,7 +108,7 @@ impl Row { let proc_id = *iter_id; let proc_search_id = *iter_id + 1; *iter_id += 2; - BottomCol::builder() + OldBottomCol::builder() .total_col_row_ratio(2) .col_width_ratio(width_ratio) .children(vec![ @@ -144,7 +144,7 @@ impl Row { ]) .build() } - _ => BottomCol::builder() + _ => OldBottomCol::builder() .col_width_ratio(width_ratio) .children(vec![BottomColRow::builder() .children(vec![BottomWidget::builder() @@ -310,7 +310,7 @@ impl Row { } children.push( - BottomCol::builder() + OldBottomCol::builder() .total_col_row_ratio(total_col_row_ratio) .col_width_ratio(col_width_ratio) .children(col_row_children) @@ -321,7 +321,7 @@ impl Row { } } - Ok(BottomRow::builder() + Ok(OldBottomRow::builder() .total_col_ratio(total_col_ratio) .row_height_ratio(row_ratio) .children(children)