diff --git a/src/app.rs b/src/app.rs index 5c1e9b90..764ff92c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,7 +13,7 @@ use std::{ time::Instant, }; -use crossterm::event::{KeyEvent, KeyModifiers}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEventKind}; use fxhash::FxHashMap; use indextree::{Arena, NodeId}; use unicode_segmentation::GraphemeCursor; @@ -28,12 +28,14 @@ pub use widgets::*; use crate::{ canvas, constants::{self, MAX_SIGNAL}, + data_conversion::*, units::data_units::DataUnit, + update_final_process_list, utils::error::{BottomError, Result}, BottomEvent, Pid, }; -use self::event::{does_point_intersect_rect, EventResult, ReturnSignal}; +use self::event::{EventResult, ReturnSignal, ReturnSignalResult}; const MAX_SEARCH_LENGTH: usize = 200; @@ -269,9 +271,38 @@ impl AppState { // TODO: Write this. if event.modifiers.is_empty() { - todo!() - } else if let KeyModifiers::ALT = event.modifiers { - todo!() + match event.code { + KeyCode::Esc => { + if self.is_expanded { + self.is_expanded = false; + Some(EventResult::Redraw) + } else if self.help_dialog_state.is_showing_help { + self.help_dialog_state.is_showing_help = false; + self.help_dialog_state.scroll_state.current_scroll_index = 0; + Some(EventResult::Redraw) + } else if self.delete_dialog_state.is_showing_dd { + self.close_dd(); + Some(EventResult::Redraw) + } else { + None + } + } + KeyCode::Char('q') => Some(EventResult::Quit), + KeyCode::Char('e') => { + self.is_expanded = !self.is_expanded; + Some(EventResult::Redraw) + } + KeyCode::Char('?') => { + self.help_dialog_state.is_showing_help = true; + Some(EventResult::Redraw) + } + _ => None, + } + } else if let KeyModifiers::CONTROL = event.modifiers { + match event.code { + KeyCode::Char('c') => Some(EventResult::Quit), + _ => None, + } } else { None } @@ -296,30 +327,49 @@ impl AppState { // 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()) { - let is_id_selected = self.selected_widget == *id; - self.selected_widget = *id; - - if is_id_selected { - return widget.handle_mouse_event(event); + match &event.kind { + MouseEventKind::Down(MouseButton::Left) => { + if self.is_expanded { + if let Some(widget) = + self.widget_lookup_map.get_mut(&self.selected_widget) + { + return widget.handle_mouse_event(event); + } } else { - // If the aren't equal, *force* a redraw. - widget.handle_mouse_event(event); - return EventResult::Redraw; + for (id, widget) in self.widget_lookup_map.iter_mut() { + if widget.does_intersect_mouse(&event) { + let is_id_selected = self.selected_widget == *id; + self.selected_widget = *id; + + if is_id_selected { + return widget.handle_mouse_event(event); + } else { + // If the aren't equal, *force* a redraw. + widget.handle_mouse_event(event); + return EventResult::Redraw; + } + } + } } } + MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => { + if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) + { + return widget.handle_mouse_event(event); + } + } + _ => {} } EventResult::NoRedraw } - BottomEvent::Update(_new_data) => { + BottomEvent::Update(new_data) => { + self.data_collection.eat_data(new_data); + if !self.is_frozen { - // TODO: Update all data, and redraw. - todo!() + self.convert_data(); + + EventResult::Redraw } else { EventResult::NoRedraw } @@ -336,16 +386,98 @@ impl AppState { } } - /// Handles a [`ReturnSignal`], and returns an [`EventResult`]. - pub fn handle_return_signal(&mut self, return_signal: ReturnSignal) -> EventResult { + /// Handles a [`ReturnSignal`], and returns an [`ReturnSignalResult`]. + pub fn handle_return_signal(&mut self, return_signal: ReturnSignal) -> ReturnSignalResult { match return_signal { - ReturnSignal::Nothing => EventResult::NoRedraw, ReturnSignal::KillProcess => { todo!() } } } + fn convert_data(&mut self) { + // TODO: Probably refactor this. + + // Network + if self.used_widgets.use_net { + let network_data = convert_network_data_points( + &self.data_collection, + false, + self.app_config_fields.use_basic_mode + || self.app_config_fields.use_old_network_legend, + &self.app_config_fields.network_scale_type, + &self.app_config_fields.network_unit_type, + self.app_config_fields.network_use_binary_prefix, + ); + self.canvas_data.network_data_rx = network_data.rx; + self.canvas_data.network_data_tx = network_data.tx; + self.canvas_data.rx_display = network_data.rx_display; + self.canvas_data.tx_display = network_data.tx_display; + if let Some(total_rx_display) = network_data.total_rx_display { + self.canvas_data.total_rx_display = total_rx_display; + } + if let Some(total_tx_display) = network_data.total_tx_display { + self.canvas_data.total_tx_display = total_tx_display; + } + } + + // Disk + if self.used_widgets.use_disk { + self.canvas_data.disk_data = convert_disk_row(&self.data_collection); + } + + // Temperatures + if self.used_widgets.use_temp { + self.canvas_data.temp_sensor_data = convert_temp_row(&self); + } + + // Memory + if self.used_widgets.use_mem { + self.canvas_data.mem_data = convert_mem_data_points(&self.data_collection, false); + self.canvas_data.swap_data = convert_swap_data_points(&self.data_collection, false); + let (memory_labels, swap_labels) = convert_mem_labels(&self.data_collection); + + self.canvas_data.mem_labels = memory_labels; + self.canvas_data.swap_labels = swap_labels; + } + + if self.used_widgets.use_cpu { + // CPU + convert_cpu_data_points(&self.data_collection, &mut self.canvas_data.cpu_data, false); + self.canvas_data.load_avg_data = self.data_collection.load_avg_harvest; + } + + // Processes + if self.used_widgets.use_proc { + self.update_all_process_lists(); + } + + // Battery + if self.used_widgets.use_battery { + self.canvas_data.battery_data = convert_battery_harvest(&self.data_collection); + } + } + + #[allow(clippy::needless_collect)] + fn update_all_process_lists(&mut self) { + // TODO: Probably refactor this. + + // According to clippy, I can avoid a collect... but if I follow it, + // I end up conflicting with the borrow checker since app is used within the closure... hm. + if !self.is_frozen { + let widget_ids = self + .proc_state + .widget_states + .keys() + .cloned() + .collect::>(); + + widget_ids.into_iter().for_each(|widget_id| { + update_final_process_list(self, widget_id); + }); + } + } + pub fn on_esc(&mut self) { self.reset_multi_tap_keys(); if self.is_in_dialog() { @@ -1316,41 +1448,41 @@ impl AppState { pub fn start_killing_process(&mut self) { self.reset_multi_tap_keys(); - if let Some(proc_widget_state) = self - .proc_state - .widget_states - .get(&self.current_widget.widget_id) - { - if let Some(corresponding_filtered_process_list) = self - .canvas_data - .finalized_process_data_map - .get(&self.current_widget.widget_id) - { - if proc_widget_state.scroll_state.current_scroll_position - < corresponding_filtered_process_list.len() - { - let current_process: (String, Vec); - if self.is_grouped(self.current_widget.widget_id) { - if let Some(process) = &corresponding_filtered_process_list - .get(proc_widget_state.scroll_state.current_scroll_position) - { - current_process = (process.name.to_string(), process.group_pids.clone()) - } else { - return; - } - } else { - let process = corresponding_filtered_process_list - [proc_widget_state.scroll_state.current_scroll_position] - .clone(); - current_process = (process.name.clone(), vec![process.pid]) - }; + // if let Some(proc_widget_state) = self + // .proc_state + // .widget_states + // .get(&self.current_widget.widget_id) + // { + // if let Some(corresponding_filtered_process_list) = self + // .canvas_data + // .finalized_process_data_map + // .get(&self.current_widget.widget_id) + // { + // if proc_widget_state.scroll_state.current_scroll_position + // < corresponding_filtered_process_list.len() + // { + // let current_process: (String, Vec); + // if self.is_grouped(self.current_widget.widget_id) { + // if let Some(process) = &corresponding_filtered_process_list + // .get(proc_widget_state.scroll_state.current_scroll_position) + // { + // current_process = (process.name.to_string(), process.group_pids.clone()) + // } else { + // return; + // } + // } else { + // let process = corresponding_filtered_process_list + // [proc_widget_state.scroll_state.current_scroll_position] + // .clone(); + // current_process = (process.name.clone(), vec![process.pid]) + // }; - self.to_delete_process_list = Some(current_process); - self.delete_dialog_state.is_showing_dd = true; - self.is_determining_widget_boundary = true; - } - } - } + // self.to_delete_process_list = Some(current_process); + // self.delete_dialog_state.is_showing_dd = true; + // self.is_determining_widget_boundary = true; + // } + // } + // } } pub fn on_char_key(&mut self, caught_char: char) { @@ -2222,85 +2354,85 @@ impl AppState { } pub fn skip_to_last(&mut self) { - if !self.ignore_normal_keybinds() { - match self.current_widget.widget_type { - BottomWidgetType::Proc => { - if let Some(proc_widget_state) = self - .proc_state - .get_mut_widget_state(self.current_widget.widget_id) - { - if let Some(finalized_process_data) = self - .canvas_data - .finalized_process_data_map - .get(&self.current_widget.widget_id) - { - if !self.canvas_data.finalized_process_data_map.is_empty() { - proc_widget_state.scroll_state.current_scroll_position = - finalized_process_data.len() - 1; - proc_widget_state.scroll_state.scroll_direction = - ScrollDirection::Down; - } - } - } - } - BottomWidgetType::ProcSort => { - if let Some(proc_widget_state) = self - .proc_state - .get_mut_widget_state(self.current_widget.widget_id - 2) - { - proc_widget_state.columns.current_scroll_position = - proc_widget_state.columns.get_enabled_columns_len() - 1; - proc_widget_state.columns.scroll_direction = ScrollDirection::Down; - } - } - BottomWidgetType::Temp => { - if let Some(temp_widget_state) = self - .temp_state - .get_mut_widget_state(self.current_widget.widget_id) - { - if !self.canvas_data.temp_sensor_data.is_empty() { - temp_widget_state.scroll_state.current_scroll_position = - self.canvas_data.temp_sensor_data.len() - 1; - temp_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; - } - } - } - BottomWidgetType::Disk => { - if let Some(disk_widget_state) = self - .disk_state - .get_mut_widget_state(self.current_widget.widget_id) - { - if !self.canvas_data.disk_data.is_empty() { - disk_widget_state.scroll_state.current_scroll_position = - self.canvas_data.disk_data.len() - 1; - disk_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; - } - } - } - BottomWidgetType::CpuLegend => { - if let Some(cpu_widget_state) = self - .cpu_state - .get_mut_widget_state(self.current_widget.widget_id - 1) - { - let cap = self.canvas_data.cpu_data.len(); - if cap > 0 { - cpu_widget_state.scroll_state.current_scroll_position = cap - 1; - cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; - } - } - } - _ => {} - } - self.reset_multi_tap_keys(); - } else if self.help_dialog_state.is_showing_help { - self.help_dialog_state.scroll_state.current_scroll_index = self - .help_dialog_state - .scroll_state - .max_scroll_index - .saturating_sub(1); - } else if self.delete_dialog_state.is_showing_dd { - self.delete_dialog_state.selected_signal = KillSignal::Kill(MAX_SIGNAL); - } + // if !self.ignore_normal_keybinds() { + // match self.current_widget.widget_type { + // BottomWidgetType::Proc => { + // if let Some(proc_widget_state) = self + // .proc_state + // .get_mut_widget_state(self.current_widget.widget_id) + // { + // if let Some(finalized_process_data) = self + // .canvas_data + // .finalized_process_data_map + // .get(&self.current_widget.widget_id) + // { + // if !self.canvas_data.finalized_process_data_map.is_empty() { + // proc_widget_state.scroll_state.current_scroll_position = + // finalized_process_data.len() - 1; + // proc_widget_state.scroll_state.scroll_direction = + // ScrollDirection::Down; + // } + // } + // } + // } + // BottomWidgetType::ProcSort => { + // if let Some(proc_widget_state) = self + // .proc_state + // .get_mut_widget_state(self.current_widget.widget_id - 2) + // { + // proc_widget_state.columns.current_scroll_position = + // proc_widget_state.columns.get_enabled_columns_len() - 1; + // proc_widget_state.columns.scroll_direction = ScrollDirection::Down; + // } + // } + // BottomWidgetType::Temp => { + // if let Some(temp_widget_state) = self + // .temp_state + // .get_mut_widget_state(self.current_widget.widget_id) + // { + // if !self.canvas_data.temp_sensor_data.is_empty() { + // temp_widget_state.scroll_state.current_scroll_position = + // self.canvas_data.temp_sensor_data.len() - 1; + // temp_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; + // } + // } + // } + // BottomWidgetType::Disk => { + // if let Some(disk_widget_state) = self + // .disk_state + // .get_mut_widget_state(self.current_widget.widget_id) + // { + // if !self.canvas_data.disk_data.is_empty() { + // disk_widget_state.scroll_state.current_scroll_position = + // self.canvas_data.disk_data.len() - 1; + // disk_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; + // } + // } + // } + // BottomWidgetType::CpuLegend => { + // if let Some(cpu_widget_state) = self + // .cpu_state + // .get_mut_widget_state(self.current_widget.widget_id - 1) + // { + // let cap = self.canvas_data.cpu_data.len(); + // if cap > 0 { + // cpu_widget_state.scroll_state.current_scroll_position = cap - 1; + // cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; + // } + // } + // } + // _ => {} + // } + // self.reset_multi_tap_keys(); + // } else if self.help_dialog_state.is_showing_help { + // self.help_dialog_state.scroll_state.current_scroll_index = self + // .help_dialog_state + // .scroll_state + // .max_scroll_index + // .saturating_sub(1); + // } else if self.delete_dialog_state.is_showing_dd { + // self.delete_dialog_state.selected_signal = KillSignal::Kill(MAX_SIGNAL); + // } } pub fn decrement_position_count(&mut self) { @@ -2381,35 +2513,35 @@ impl AppState { } /// Returns the new position. - fn increment_process_position(&mut self, num_to_change_by: i64) -> Option { - if let Some(proc_widget_state) = self - .proc_state - .get_mut_widget_state(self.current_widget.widget_id) - { - let current_posn = proc_widget_state.scroll_state.current_scroll_position; - if let Some(finalized_process_data) = self - .canvas_data - .finalized_process_data_map - .get(&self.current_widget.widget_id) - { - if current_posn as i64 + num_to_change_by >= 0 - && current_posn as i64 + num_to_change_by < finalized_process_data.len() as i64 - { - proc_widget_state.scroll_state.current_scroll_position = - (current_posn as i64 + num_to_change_by) as usize; - } else { - return None; - } - } + fn increment_process_position(&mut self, _num_to_change_by: i64) -> Option { + // if let Some(proc_widget_state) = self + // .proc_state + // .get_mut_widget_state(self.current_widget.widget_id) + // { + // let current_posn = proc_widget_state.scroll_state.current_scroll_position; + // if let Some(finalized_process_data) = self + // .canvas_data + // .finalized_process_data_map + // .get(&self.current_widget.widget_id) + // { + // if current_posn as i64 + num_to_change_by >= 0 + // && current_posn as i64 + num_to_change_by < finalized_process_data.len() as i64 + // { + // proc_widget_state.scroll_state.current_scroll_position = + // (current_posn as i64 + num_to_change_by) as usize; + // } else { + // return None; + // } + // } - if num_to_change_by < 0 { - proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; - } else { - proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; - } + // if num_to_change_by < 0 { + // proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; + // } else { + // proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; + // } - return Some(proc_widget_state.scroll_state.current_scroll_position); - } + // return Some(proc_widget_state.scroll_state.current_scroll_position); + // } None } @@ -2537,32 +2669,32 @@ impl AppState { } fn toggle_collapsing_process_branch(&mut self) { - if let Some(proc_widget_state) = self - .proc_state - .widget_states - .get_mut(&self.current_widget.widget_id) - { - let current_posn = proc_widget_state.scroll_state.current_scroll_position; + // if let Some(proc_widget_state) = self + // .proc_state + // .widget_states + // .get_mut(&self.current_widget.widget_id) + // { + // let current_posn = proc_widget_state.scroll_state.current_scroll_position; - if let Some(displayed_process_list) = self - .canvas_data - .finalized_process_data_map - .get(&self.current_widget.widget_id) - { - if let Some(corresponding_process) = displayed_process_list.get(current_posn) { - let corresponding_pid = corresponding_process.pid; + // if let Some(displayed_process_list) = self + // .canvas_data + // .finalized_process_data_map + // .get(&self.current_widget.widget_id) + // { + // if let Some(corresponding_process) = displayed_process_list.get(current_posn) { + // let corresponding_pid = corresponding_process.pid; - if let Some(process_data) = self - .canvas_data - .single_process_data - .get_mut(&corresponding_pid) - { - process_data.is_collapsed_entry = !process_data.is_collapsed_entry; - self.proc_state.force_update = Some(self.current_widget.widget_id); - } - } - } - } + // if let Some(process_data) = self + // .canvas_data + // .single_process_data + // .get_mut(&corresponding_pid) + // { + // process_data.is_collapsed_entry = !process_data.is_collapsed_entry; + // self.proc_state.force_update = Some(self.current_widget.widget_id); + // } + // } + // } + // } } fn zoom_out(&mut self) { diff --git a/src/app/event.rs b/src/app/event.rs index 19d1fb4e..b0e0e39a 100644 --- a/src/app/event.rs +++ b/src/app/event.rs @@ -1,14 +1,10 @@ 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, } @@ -141,8 +137,3 @@ 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 a2a70a42..4dcca70c 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -1082,10 +1082,7 @@ pub fn create_layout_tree( } } BottomWidgetType::Proc => { - widget_lookup_map.insert( - widget_id, - ProcessManager::new(process_defaults.is_tree).into(), - ); + widget_lookup_map.insert(widget_id, ProcessManager::new(process_defaults).into()); } BottomWidgetType::Temp => { widget_lookup_map.insert(widget_id, TempTable::default().into()); diff --git a/src/app/widgets.rs b/src/app/widgets.rs index a99fe7b8..ad24eb00 100644 --- a/src/app/widgets.rs +++ b/src/app/widgets.rs @@ -2,12 +2,7 @@ use std::time::Instant; use crossterm::event::{KeyEvent, MouseEvent}; use enum_dispatch::enum_dispatch; -use tui::{ - backend::Backend, - layout::Rect, - widgets::{Block, TableState}, - Frame, -}; +use tui::{backend::Backend, layout::Rect, widgets::TableState, Frame}; use crate::{ app::{ @@ -66,6 +61,14 @@ pub trait Component { /// Updates a [`Component`]s bounding box to `new_bounds`. fn set_bounds(&mut self, new_bounds: Rect); + + /// Returns whether a [`MouseEvent`] intersects a [`Component`]. + fn does_intersect_mouse(&self, event: &MouseEvent) -> bool { + let x = event.column; + let y = event.row; + let rect = self.bounds(); + x >= rect.left() && x <= rect.right() && y >= rect.top() && y <= rect.bottom() + } } /// A trait for actual fully-fledged widgets to be displayed in bottom. @@ -104,8 +107,8 @@ pub trait Widget { /// Draws a [`Widget`]. Defaults to doing nothing. fn draw( - &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>, - data: &DisplayableData, + &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData, + selected: bool, ) { // TODO: Remove the default implementation in the future! // TODO: Do another pass on ALL of the draw code - currently it's just glue, it should eventually be done properly! diff --git a/src/app/widgets/base/scrollable.rs b/src/app/widgets/base/scrollable.rs index c180192a..4b5f395f 100644 --- a/src/app/widgets/base/scrollable.rs +++ b/src/app/widgets/base/scrollable.rs @@ -1,4 +1,4 @@ -use crossterm::event::{KeyEvent, KeyModifiers, MouseButton, MouseEvent}; +use crossterm::event::{KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use tui::{layout::Rect, widgets::TableState}; use crate::app::{ @@ -6,68 +6,125 @@ use crate::app::{ Component, }; +#[derive(Debug)] pub enum ScrollDirection { Up, Down, } -/// A "scrollable" [`Widget`] component. Intended for use as part of another [`Widget`] - as such, it does -/// not have any bounds or the like. +/// We save the previous window index for future reference, but we must invalidate if the area changes. +#[derive(Default)] +struct WindowIndex { + index: usize, + cached_area: Rect, +} + +/// A "scrollable" [`Component`]. Intended for use as part of another [`Component`] to help manage scrolled state. pub struct Scrollable { + /// The currently selected index. Do *NOT* directly update this, use the helper functions! current_index: usize, - previous_index: usize, + + /// The "window index" is the "start" of the displayed data range, used for drawing purposes. See + /// [`Scrollable::get_list_start`] for more details. + window_index: WindowIndex, + + /// The direction we're scrolling in. scroll_direction: ScrollDirection, + + /// How many items to keep track of. num_items: usize, + /// tui-rs' internal table state; used to keep track of the *visually* selected index. tui_state: TableState, + + /// Manages the `gg` double-tap shortcut. gg_manager: MultiKey, + /// The bounds of the [`Scrollable`] component. bounds: Rect, } impl Scrollable { /// Creates a new [`Scrollable`]. pub fn new(num_items: usize) -> Self { + let mut tui_state = TableState::default(); + tui_state.select(Some(0)); Self { current_index: 0, - previous_index: 0, + window_index: WindowIndex::default(), scroll_direction: ScrollDirection::Down, num_items, - tui_state: TableState::default(), + tui_state, gg_manager: MultiKey::register(vec!['g', 'g']), // TODO: Use a static arrayvec bounds: Rect::default(), } } - /// Creates a new [`Scrollable`]. Note this will set the associated [`TableState`] to select the first entry. - pub fn new_selected(num_items: usize) -> Self { - let mut scrollable = Scrollable::new(num_items); - scrollable.tui_state.select(Some(0)); - - scrollable - } - + /// Returns the currently selected index of the [`Scrollable`]. pub fn index(&self) -> usize { self.current_index } - /// Update the index with this! This will automatically update the previous index and scroll direction! + /// Returns the start of the [`Scrollable`] when displayed. + pub fn get_list_start(&mut self, num_visible_rows: usize) -> usize { + // So it's probably confusing - what is the "window index"? + // The idea is that we display a "window" of data in tables that *contains* the currently selected index. + if self.window_index.cached_area != self.bounds { + self.window_index.index = 0; + self.window_index.cached_area = self.bounds; + } + + let list_start = match self.scroll_direction { + ScrollDirection::Down => { + if self.current_index < self.window_index.index + num_visible_rows { + // If, using the current window index, we can see the element + // (so within that and + num_visible_rows) just reuse the current previously scrolled position + self.window_index.index + } else if self.current_index >= num_visible_rows { + // Else if the current position past the last element visible in the list, omit + // until we can see that element. The +1 is of how indexes start at 0. + + self.window_index.index = self.current_index - num_visible_rows + 1; + self.window_index.index + } else { + // Else, if it is not past the last element visible, do not omit anything + 0 + } + } + ScrollDirection::Up => { + if self.current_index <= self.window_index.index { + // If it's past the first element, then show from that element downwards + self.window_index.index = self.current_index; + } else if self.current_index >= self.window_index.index + num_visible_rows { + self.window_index.index = self.current_index - num_visible_rows + 1; + } + // Else, don't change what our start position is from whatever it is set to! + self.window_index.index + } + }; + + self.tui_state + .select(Some(self.current_index.saturating_sub(list_start))); + + list_start + } + + /// Update the index with this! This will automatically update the scroll direction as well! fn update_index(&mut self, new_index: usize) { use std::cmp::Ordering; match new_index.cmp(&self.current_index) { Ordering::Greater => { - self.previous_index = self.current_index; self.current_index = new_index; self.scroll_direction = ScrollDirection::Down; } Ordering::Less => { - self.previous_index = self.current_index; self.current_index = new_index; self.scroll_direction = ScrollDirection::Up; } - - Ordering::Equal => {} + Ordering::Equal => { + // Do nothing. + } } } @@ -94,33 +151,32 @@ impl Scrollable { /// Moves *downward* by *incrementing* the current index. fn move_down(&mut self, change_by: usize) -> EventResult { + if self.num_items == 0 { + return EventResult::NoRedraw; + } + let new_index = self.current_index + change_by; if new_index >= self.num_items { - let last_index = self.num_items - 1; - if self.current_index != last_index { - self.update_index(last_index); - - EventResult::Redraw - } else { - EventResult::NoRedraw - } + EventResult::NoRedraw } else { - self.update_index(new_index); - EventResult::Redraw + if self.current_index == new_index { + EventResult::NoRedraw + } else { + self.update_index(new_index); + EventResult::Redraw + } } } /// Moves *upward* by *decrementing* the current index. fn move_up(&mut self, change_by: usize) -> EventResult { - let new_index = self.current_index.saturating_sub(change_by); - if new_index == 0 { - if self.current_index != 0 { - self.update_index(0); + if self.num_items == 0 { + return EventResult::NoRedraw; + } - EventResult::Redraw - } else { - EventResult::NoRedraw - } + let new_index = self.current_index.saturating_sub(change_by); + if self.current_index == new_index { + EventResult::NoRedraw } else { self.update_index(new_index); EventResult::Redraw @@ -133,15 +189,15 @@ impl Scrollable { if num_items <= self.current_index { self.current_index = num_items.saturating_sub(1); } - - if num_items <= self.previous_index { - self.previous_index = num_items.saturating_sub(1); - } } pub fn num_items(&self) -> usize { self.num_items } + + pub fn tui_state(&self) -> TableState { + self.tui_state.clone() + } } impl Component for Scrollable { @@ -169,29 +225,31 @@ impl Component for Scrollable { fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { match event.kind { - crossterm::event::MouseEventKind::Down(MouseButton::Left) => { - // This requires a bit of fancy calculation. The main trick is remembering that - // we are using a *visual* index here - not what is the actual index! Luckily, we keep track of that - // inside our linked copy of TableState! + MouseEventKind::Down(MouseButton::Left) => { + if self.does_intersect_mouse(&event) { + // This requires a bit of fancy calculation. The main trick is remembering that + // we are using a *visual* index here - not what is the actual index! Luckily, we keep track of that + // inside our linked copy of TableState! - // Note that y is assumed to be *relative*; - // we assume that y starts at where the list starts (and there are no gaps or whatever). - let y = usize::from(event.row - self.bounds.top()); + // Note that y is assumed to be *relative*; + // we assume that y starts at where the list starts (and there are no gaps or whatever). + let y = usize::from(event.row - self.bounds.top()); - if let Some(selected) = self.tui_state.selected() { - if y > selected { - let offset = y - selected; - return self.move_down(offset); - } else { - let offset = selected - y; - return self.move_up(offset); + if let Some(selected) = self.tui_state.selected() { + if y > selected { + let offset = y - selected; + return self.move_down(offset); + } else if y < selected { + let offset = selected - y; + return self.move_up(offset); + } } } EventResult::NoRedraw } - crossterm::event::MouseEventKind::ScrollDown => self.move_down(1), - crossterm::event::MouseEventKind::ScrollUp => self.move_up(1), + MouseEventKind::ScrollDown => self.move_down(1), + MouseEventKind::ScrollUp => self.move_up(1), _ => EventResult::NoRedraw, } } diff --git a/src/app/widgets/base/text_table.rs b/src/app/widgets/base/text_table.rs index fef2e31f..59395578 100644 --- a/src/app/widgets/base/text_table.rs +++ b/src/app/widgets/base/text_table.rs @@ -1,9 +1,9 @@ use std::{ borrow::Cow, - cmp::{max, min}, + cmp::{max, min, Ordering}, }; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use tui::{ layout::{Constraint, Rect}, text::Text, @@ -24,6 +24,13 @@ pub enum DesiredColumnWidth { Flex { desired: u16, max_percentage: f64 }, } +/// A [`ColumnType`] is a +pub trait ColumnType { + type DataType; + + fn sort_function(a: Self::DataType, b: Self::DataType) -> Ordering; +} + /// A [`Column`] represents some column in a [`TextTable`]. #[derive(Debug)] pub struct Column { @@ -33,7 +40,7 @@ pub struct Column { // TODO: I would remove these in the future, storing them here feels weird... pub desired_width: DesiredColumnWidth, - pub x_bounds: (u16, u16), + pub x_bounds: Option<(u16, u16)>, } impl Column { @@ -44,7 +51,7 @@ impl Column { ) -> Self { Self { name, - x_bounds: (0, 0), + x_bounds: None, shortcut: shortcut.map(|e| { let modifier = if e.modifiers.is_empty() { "" @@ -90,16 +97,17 @@ impl Column { } /// Creates a new [`Column`] with a hard desired width. If none is specified, - /// it will instead use the name's length. + /// it will instead use the name's length + 1. pub fn new_hard( name: &'static str, shortcut: Option, default_descending: bool, hard_length: Option, ) -> Self { + // TODO: It should really be based on the shortcut name... Column::new( name, shortcut, default_descending, - DesiredColumnWidth::Hard(hard_length.unwrap_or(name.len() as u16)), + DesiredColumnWidth::Hard(hard_length.unwrap_or(name.len() as u16 + 1)), ) } @@ -238,7 +246,7 @@ impl TextTable { } pub fn get_desired_column_widths( - columns: &[Column], data: &[Vec], + columns: &[Column], data: &[Vec<(Cow<'static, str>, Option>)>], ) -> Vec { columns .iter() @@ -248,8 +256,22 @@ impl TextTable { let max_len = data .iter() .filter_map(|c| c.get(column_index)) - .max_by(|x, y| x.len().cmp(&y.len())) - .map(|s| s.len()) + .max_by(|(x, short_x), (y, short_y)| { + let x = if let Some(short_x) = short_x { + short_x + } else { + x + }; + + let y = if let Some(short_y) = short_y { + short_y + } else { + y + }; + + x.len().cmp(&y.len()) + }) + .map(|(s, _)| s.len()) .unwrap_or(0) as u16; DesiredColumnWidth::Hard(max(max_len, width)) @@ -262,16 +284,16 @@ impl TextTable { .collect::>() } - fn get_cache(&mut self, area: Rect, data: &[Vec]) -> Vec { + fn get_cache( + &mut self, area: Rect, data: &[Vec<(Cow<'static, str>, Option>)>], + ) -> Vec { fn calculate_column_widths( left_to_right: bool, mut desired_widths: Vec, total_width: u16, ) -> Vec { - debug!("OG desired widths: {:?}", desired_widths); let mut total_width_left = total_width; if !left_to_right { desired_widths.reverse(); } - debug!("Desired widths: {:?}", desired_widths); let mut column_widths: Vec = Vec::with_capacity(desired_widths.len()); for width in desired_widths { @@ -303,7 +325,6 @@ impl TextTable { } } } - debug!("Initial column widths: {:?}", column_widths); if !column_widths.is_empty() { let amount_per_slot = total_width_left / column_widths.len() as u16; @@ -321,8 +342,6 @@ impl TextTable { } } - debug!("Column widths: {:?}", column_widths); - column_widths } @@ -330,9 +349,11 @@ impl TextTable { if data.is_empty() { vec![0; self.columns.len()] } else { - match &mut self.cached_column_widths { + let was_cached: bool; + let column_widths = match &mut self.cached_column_widths { CachedColumnWidths::Uncached => { // Always recalculate. + was_cached = false; let desired_widths = TextTable::get_desired_column_widths(&self.columns, data); let calculated_widths = calculate_column_widths(self.left_to_right, desired_widths, area.width); @@ -349,6 +370,7 @@ impl TextTable { } => { if *cached_area != area { // Recalculate! + was_cached = false; let desired_widths = TextTable::get_desired_column_widths(&self.columns, data); let calculated_widths = @@ -358,10 +380,22 @@ impl TextTable { calculated_widths } else { + was_cached = true; cached_data.clone() } } + }; + + if !was_cached { + let mut column_start = 0; + for (column, width) in self.columns.iter_mut().zip(&column_widths) { + let column_end = column_start + *width; + column.x_bounds = Some((column_start, column_end)); + column_start = column_end + 1; + } } + + column_widths } } @@ -372,9 +406,9 @@ impl TextTable { /// Note if the number of columns don't match in the [`TextTable`] and data, /// it will only create as many columns as it can grab data from both sources from. pub fn create_draw_table( - &mut self, painter: &Painter, data: &[Vec], area: Rect, + &mut self, painter: &Painter, data: &[Vec<(Cow<'static, str>, Option>)>], + area: Rect, ) -> (Table<'_>, Vec, TableState) { - // TODO: Change data: &[Vec] to &[Vec>] use tui::widgets::Row; let table_gap = if !self.show_gap || area.height < TABLE_GAP_HEIGHT_LIMIT { @@ -383,15 +417,16 @@ impl TextTable { 1 }; + self.update_num_items(data.len()); self.set_bounds(area); - let scrollable_height = area.height.saturating_sub(1 + table_gap); + let table_extras = 1 + table_gap; + let scrollable_height = area.height.saturating_sub(table_extras); self.scrollable.set_bounds(Rect::new( area.x, - area.y + 1 + table_gap, + area.y + table_extras, area.width, scrollable_height, )); - self.update_num_items(data.len()); // Calculate widths first, since we need them later. let calculated_widths = self.get_cache(area, data); @@ -403,7 +438,7 @@ impl TextTable { // Then calculate rows. We truncate the amount of data read based on height, // as well as truncating some entries based on available width. let data_slice = { - let start = self.scrollable.index(); + let start = self.scrollable.get_list_start(scrollable_height as usize); let end = std::cmp::min( self.scrollable.num_items(), start + scrollable_height as usize, @@ -411,17 +446,25 @@ impl TextTable { &data[start..end] }; let rows = data_slice.iter().map(|row| { - Row::new(row.iter().zip(&calculated_widths).map(|(cell, width)| { - let width = *width as usize; - let graphemes = - UnicodeSegmentation::graphemes(cell.as_str(), true).collect::>(); - let grapheme_width = graphemes.len(); - if width < grapheme_width && width > 1 { - Text::raw(format!("{}…", graphemes[..(width - 1)].concat())) - } else { - Text::raw(cell.to_owned()) - } - })) + Row::new( + row.iter() + .zip(&calculated_widths) + .map(|((text, shrunk_text), width)| { + let width = *width as usize; + let graphemes = UnicodeSegmentation::graphemes(text.as_ref(), true) + .collect::>(); + let grapheme_width = graphemes.len(); + if width < grapheme_width && width > 1 { + if let Some(shrunk_text) = shrunk_text { + Text::raw(shrunk_text.clone()) + } else { + Text::raw(format!("{}…", graphemes[..(width - 1)].concat())) + } + } else { + Text::raw(text.to_owned()) + } + }), + ) }); // Now build up our headers... @@ -430,8 +473,7 @@ impl TextTable { .bottom_margin(table_gap); // And return tui-rs's [`TableState`]. - let mut tui_state = TableState::default(); - tui_state.select(Some(self.scrollable.index())); + let tui_state = self.scrollable.tui_state(); ( Table::new(rows) @@ -464,25 +506,33 @@ impl Component for TextTable { } fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { - // Note these are representing RELATIVE coordinates! - let x = event.column - self.bounds.left(); - let y = event.row - self.bounds.top(); + if let MouseEventKind::Down(MouseButton::Left) = event.kind { + if !self.does_intersect_mouse(&event) { + return EventResult::NoRedraw; + } - if y == 0 { - for (index, column) in self.columns.iter().enumerate() { - let (start, end) = column.x_bounds; - if start >= x && end <= y { - if self.sort_index == index { - // Just flip the sort if we're already sorting by this. - self.sort_ascending = !self.sort_ascending; - } else { - self.sort_index = index; - self.sort_ascending = !column.default_descending; + // Note these are representing RELATIVE coordinates! They *need* the above intersection check for validity! + let x = event.column - self.bounds.left(); + let y = event.row - self.bounds.top(); + + if y == 0 { + for (index, column) in self.columns.iter().enumerate() { + if let Some((start, end)) = column.x_bounds { + if x >= start && x <= end { + if self.sort_index == index { + // Just flip the sort if we're already sorting by this. + self.sort_ascending = !self.sort_ascending; + } else { + self.sort_index = index; + self.sort_ascending = !column.default_descending; + } + return EventResult::Redraw; + } } } } - EventResult::NoRedraw + self.scrollable.handle_mouse_event(event) } else { self.scrollable.handle_mouse_event(event) } diff --git a/src/app/widgets/cpu.rs b/src/app/widgets/cpu.rs index 9009bfe9..50994c02 100644 --- a/src/app/widgets/cpu.rs +++ b/src/app/widgets/cpu.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, time::Instant}; use crossterm::event::{KeyEvent, MouseEvent}; use tui::layout::Rect; -use crate::app::event::{does_point_intersect_rect, EventResult}; +use crate::app::event::EventResult; use super::{AppScrollWidgetState, CanvasTableWidthState, Component, TextTable, TimeGraph, Widget}; @@ -99,13 +99,10 @@ impl Component for CpuGraph { } fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { - let global_x = event.column; - let global_y = event.row; - - if does_point_intersect_rect(global_x, global_y, self.graph.bounds()) { + if self.graph.does_intersect_mouse(&event) { self.selected = CpuGraphSelection::Graph; self.graph.handle_mouse_event(event) - } else if does_point_intersect_rect(global_x, global_y, self.legend.bounds()) { + } else if self.legend.does_intersect_mouse(&event) { self.selected = CpuGraphSelection::Legend; self.legend.handle_mouse_event(event) } else { diff --git a/src/app/widgets/disk.rs b/src/app/widgets/disk.rs index 119c071d..ebf3ca47 100644 --- a/src/app/widgets/disk.rs +++ b/src/app/widgets/disk.rs @@ -1,7 +1,12 @@ use std::collections::HashMap; use crossterm::event::{KeyEvent, MouseEvent}; -use tui::{backend::Backend, layout::Rect, widgets::Block, Frame}; +use tui::{ + backend::Backend, + layout::Rect, + widgets::{Block, Borders}, + Frame, +}; use crate::{ app::{event::EventResult, text_table::Column}, @@ -92,14 +97,29 @@ impl Widget for DiskTable { } fn draw( - &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>, - data: &DisplayableData, + &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData, + selected: bool, ) { + let block = Block::default() + .border_style(if selected { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }) + .borders(Borders::ALL); + + self.set_bounds(area); let draw_area = block.inner(area); let (table, widths, mut tui_state) = self.table .create_draw_table(painter, &data.disk_data, draw_area); + let table = table.highlight_style(if selected { + painter.colours.currently_selected_text_style + } else { + painter.colours.text_style + }); + f.render_stateful_widget(table.block(block).widths(&widths), area, &mut tui_state); } } diff --git a/src/app/widgets/process.rs b/src/app/widgets/process.rs index 0addb543..a4abcd9a 100644 --- a/src/app/widgets/process.rs +++ b/src/app/widgets/process.rs @@ -1,22 +1,23 @@ use std::collections::HashMap; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use unicode_segmentation::GraphemeCursor; use tui::{ backend::Backend, layout::Rect, - widgets::{Block, TableState}, + widgets::{Block, Borders, TableState}, Frame, }; use crate::{ app::{ - event::{does_point_intersect_rect, EventResult, MultiKey, MultiKeyResult}, + event::{EventResult, MultiKey, MultiKeyResult}, query::*, }, canvas::{DisplayableData, Painter}, data_harvester::processes::{self, ProcessSorting}, + options::ProcessDefaults, }; use ProcessSorting::*; @@ -648,7 +649,7 @@ pub struct ProcessManager { selected: ProcessManagerSelection, in_tree_mode: bool, - show_sort: bool, + show_sort: bool, // TODO: Add this for temp and disk??? show_search: bool, search_modifiers: SearchModifiers, @@ -656,19 +657,28 @@ pub struct ProcessManager { impl ProcessManager { /// Creates a new [`ProcessManager`]. - pub fn new(default_in_tree_mode: bool) -> Self { - Self { + pub fn new(process_defaults: &ProcessDefaults) -> Self { + let process_table_columns = vec![]; + + let mut manager = Self { bounds: Rect::default(), - process_table: TextTable::new(vec![]), // TODO: Do this - sort_table: TextTable::new(vec![]), // TODO: Do this too + process_table: TextTable::new(process_table_columns), // 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 + dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: Maybe use something static... selected: ProcessManagerSelection::Processes, - in_tree_mode: default_in_tree_mode, + in_tree_mode: false, show_sort: false, show_search: false, search_modifiers: SearchModifiers::default(), - } + }; + + manager.set_tree_mode(process_defaults.is_tree); + manager + } + + fn set_tree_mode(&mut self, in_tree_mode: bool) { + self.in_tree_mode = in_tree_mode; } fn open_search(&mut self) -> EventResult { @@ -800,20 +810,27 @@ impl Component for ProcessManager { } fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { - let global_x = event.column; - let global_y = event.row; - - if does_point_intersect_rect(global_x, global_y, self.process_table.bounds()) { - self.selected = ProcessManagerSelection::Processes; - self.process_table.handle_mouse_event(event) - } else if does_point_intersect_rect(global_x, global_y, self.sort_table.bounds()) { - self.selected = ProcessManagerSelection::Sort; - self.sort_table.handle_mouse_event(event) - } else if does_point_intersect_rect(global_x, global_y, self.search_input.bounds()) { - self.selected = ProcessManagerSelection::Search; - self.search_input.handle_mouse_event(event) - } else { - EventResult::NoRedraw + match &event.kind { + MouseEventKind::Down(MouseButton::Left) => { + if self.process_table.does_intersect_mouse(&event) { + self.selected = ProcessManagerSelection::Processes; + self.process_table.handle_mouse_event(event) + } else if self.sort_table.does_intersect_mouse(&event) { + self.selected = ProcessManagerSelection::Sort; + self.sort_table.handle_mouse_event(event) + } else if self.search_input.does_intersect_mouse(&event) { + self.selected = ProcessManagerSelection::Search; + self.search_input.handle_mouse_event(event) + } else { + EventResult::NoRedraw + } + } + MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => match self.selected { + ProcessManagerSelection::Processes => self.process_table.handle_mouse_event(event), + ProcessManagerSelection::Sort => self.sort_table.handle_mouse_event(event), + ProcessManagerSelection::Search => self.search_input.handle_mouse_event(event), + }, + _ => EventResult::NoRedraw, } } } @@ -824,9 +841,18 @@ impl Widget for ProcessManager { } fn draw( - &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>, - data: &DisplayableData, + &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData, + selected: bool, ) { + let block = Block::default() + .border_style(if selected { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }) + .borders(Borders::ALL); + + self.set_bounds(area); let draw_area = block.inner(area); let (process_table, widths, mut tui_state) = self.process_table.create_draw_table( painter, @@ -834,6 +860,12 @@ impl Widget for ProcessManager { draw_area, ); + let process_table = process_table.highlight_style(if selected { + painter.colours.currently_selected_text_style + } else { + painter.colours.text_style + }); + f.render_stateful_widget( process_table.block(block).widths(&widths), area, diff --git a/src/app/widgets/temp.rs b/src/app/widgets/temp.rs index 18a02584..9b1e2214 100644 --- a/src/app/widgets/temp.rs +++ b/src/app/widgets/temp.rs @@ -1,7 +1,12 @@ use std::collections::HashMap; use crossterm::event::{KeyEvent, MouseEvent}; -use tui::{backend::Backend, layout::Rect, widgets::Block, Frame}; +use tui::{ + backend::Backend, + layout::Rect, + widgets::{Block, Borders}, + Frame, +}; use crate::{ app::{event::EventResult, text_table::Column}, @@ -52,7 +57,7 @@ pub struct TempTable { impl Default for TempTable { fn default() -> Self { let table = TextTable::new(vec![ - Column::new_flex("Sensor", None, false, 1.0), + Column::new_flex("Sensor", None, false, 0.8), Column::new_hard("Temp", None, false, Some(4)), ]) .left_to_right(false); @@ -88,14 +93,29 @@ impl Widget for TempTable { } fn draw( - &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>, - data: &DisplayableData, + &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData, + selected: bool, ) { + let block = Block::default() + .border_style(if selected { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }) + .borders(Borders::ALL); // TODO: Also do the scrolling indicator! + + self.set_bounds(area); let draw_area = block.inner(area); let (table, widths, mut tui_state) = self.table .create_draw_table(painter, &data.temp_sensor_data, draw_area); + let table = table.highlight_style(if selected { + painter.colours.currently_selected_text_style + } else { + painter.colours.text_style + }); + f.render_stateful_widget(table.block(block).widths(&widths), area, &mut tui_state); } } diff --git a/src/bin/main.rs b/src/bin/main.rs index f6517167..9c360fca 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -4,7 +4,13 @@ #[macro_use] extern crate log; -use bottom::{app::event::EventResult, canvas, constants::*, data_conversion::*, options::*, *}; +use bottom::{ + app::event::{EventResult, ReturnSignalResult}, + canvas, + constants::*, + options::*, + *, +}; use std::{ boxed::Box, @@ -85,7 +91,8 @@ fn main() -> Result<()> { }; // Event loop - let (collection_sender, collection_thread_ctrl_receiver) = mpsc::channel(); + // TODO: Add back collection sender + let (_collection_sender, collection_thread_ctrl_receiver) = mpsc::channel(); let _collection_thread = create_collection_thread( sender, collection_thread_ctrl_receiver, @@ -114,129 +121,30 @@ fn main() -> Result<()> { ctrlc::set_handler(move || { ist_clone.store(true, Ordering::SeqCst); })?; - let mut first_run = true; while !is_terminated.load(Ordering::SeqCst) { if let Ok(recv) = receiver.recv_timeout(Duration::from_millis(TICK_RATE_IN_MILLISECONDS)) { - match recv { - BottomEvent::KeyInput(event) => { - match handle_key_event(event, &mut app, &collection_sender) { - EventResult::Quit => { - break; - } - EventResult::Redraw => { - // TODO: Be even more granular! Maybe the event triggered no change, then we shouldn't redraw. - force_redraw(&mut app); - try_drawing(&mut terminal, &mut app, &mut painter)?; - } - _ => {} - } + match app.handle_event(recv) { + EventResult::Quit => { + break; } - BottomEvent::MouseInput(event) => match handle_mouse_event(event, &mut app) { - EventResult::Quit => { - break; - } - EventResult::Redraw => { - // TODO: Be even more granular! Maybe the event triggered no change, then we shouldn't redraw. - force_redraw(&mut app); - try_drawing(&mut terminal, &mut app, &mut painter)?; - } - _ => {} - }, - BottomEvent::Update(data) => { - app.data_collection.eat_data(data); - - // This thing is required as otherwise, some widgets can't draw correctly w/o - // some data (or they need to be re-drawn). - if first_run { - first_run = false; - app.is_force_redraw = true; - } - - if !app.is_frozen { - // Convert all data into tui-compliant components - - // Network - if app.used_widgets.use_net { - let network_data = convert_network_data_points( - &app.data_collection, - false, - app.app_config_fields.use_basic_mode - || app.app_config_fields.use_old_network_legend, - &app.app_config_fields.network_scale_type, - &app.app_config_fields.network_unit_type, - app.app_config_fields.network_use_binary_prefix, - ); - app.canvas_data.network_data_rx = network_data.rx; - app.canvas_data.network_data_tx = network_data.tx; - app.canvas_data.rx_display = network_data.rx_display; - app.canvas_data.tx_display = network_data.tx_display; - if let Some(total_rx_display) = network_data.total_rx_display { - app.canvas_data.total_rx_display = total_rx_display; - } - if let Some(total_tx_display) = network_data.total_tx_display { - app.canvas_data.total_tx_display = total_tx_display; - } - } - - // Disk - if app.used_widgets.use_disk { - app.canvas_data.disk_data = convert_disk_row(&app.data_collection); - } - - // Temperatures - if app.used_widgets.use_temp { - app.canvas_data.temp_sensor_data = convert_temp_row(&app); - } - - // Memory - if app.used_widgets.use_mem { - app.canvas_data.mem_data = - convert_mem_data_points(&app.data_collection, false); - app.canvas_data.swap_data = - convert_swap_data_points(&app.data_collection, false); - let (memory_labels, swap_labels) = - convert_mem_labels(&app.data_collection); - - app.canvas_data.mem_labels = memory_labels; - app.canvas_data.swap_labels = swap_labels; - } - - if app.used_widgets.use_cpu { - // CPU - - convert_cpu_data_points( - &app.data_collection, - &mut app.canvas_data.cpu_data, - false, - ); - app.canvas_data.load_avg_data = app.data_collection.load_avg_harvest; - } - - // Processes - if app.used_widgets.use_proc { - update_all_process_lists(&mut app); - } - - // Battery - if app.used_widgets.use_battery { - app.canvas_data.battery_data = - convert_battery_harvest(&app.data_collection); - } - - try_drawing(&mut terminal, &mut app, &mut painter)?; - } - } - BottomEvent::Resize { - width: _, - height: _, - } => { + EventResult::Redraw => { try_drawing(&mut terminal, &mut app, &mut painter)?; } - BottomEvent::Clean => { - app.data_collection - .clean_data(constants::STALE_MAX_MILLISECONDS); + EventResult::NoRedraw => { + continue; } + EventResult::Signal(signal) => match app.handle_return_signal(signal) { + ReturnSignalResult::Quit => { + break; + } + ReturnSignalResult::Redraw => { + try_drawing(&mut terminal, &mut app, &mut painter)?; + } + ReturnSignalResult::NoRedraw => { + continue; + } + }, } } } diff --git a/src/canvas.rs b/src/canvas.rs index 0f98829a..4820a6c0 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, str::FromStr}; +use std::{borrow::Cow, collections::HashMap, str::FromStr}; use fxhash::FxHashMap; use indextree::{Arena, NodeId}; @@ -6,7 +6,7 @@ use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, text::{Span, Spans}, - widgets::{Block, Borders, Paragraph}, + widgets::Paragraph, Frame, Terminal, }; @@ -41,11 +41,10 @@ pub struct DisplayableData { pub total_tx_display: String, pub network_data_rx: Vec, pub network_data_tx: Vec, - pub disk_data: Vec>, - pub temp_sensor_data: Vec>, + pub disk_data: Vec, Option>)>>, + pub temp_sensor_data: Vec, Option>)>>, pub single_process_data: HashMap, // Contains single process data, key is PID - pub finalized_process_data_map: HashMap>, // What's actually displayed, key is the widget ID. - pub stringified_process_data_map: HashMap)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID + pub stringified_process_data_map: HashMap)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID pub mem_labels: Option<(String, String)>, pub swap_labels: Option<(String, String)>, @@ -342,10 +341,7 @@ impl Painter { .widget_lookup_map .get_mut(&app_state.selected_widget) { - let block = Block::default() - .border_style(self.colours.highlighted_border_style) - .borders(Borders::ALL); - current_widget.draw(self, f, draw_area, block, canvas_data); + current_widget.draw(self, f, draw_area, canvas_data, true); } } else { /// A simple traversal through the `arena`. @@ -396,14 +392,7 @@ impl Painter { } LayoutNode::Widget => { if let Some(widget) = lookup_map.get_mut(&node) { - let block = Block::default() - .border_style(if selected_id == node { - painter.colours.highlighted_border_style - } else { - painter.colours.border_style - }) - .borders(Borders::ALL); - widget.draw(painter, f, area, block, canvas_data); + widget.draw(painter, f, area, canvas_data, selected_id == node); } } } diff --git a/src/canvas/drawing.rs b/src/canvas/drawing.rs index c2f5f91e..c527e7ed 100644 --- a/src/canvas/drawing.rs +++ b/src/canvas/drawing.rs @@ -2,22 +2,18 @@ pub mod basic_table_arrows; pub mod battery_display; pub mod cpu_basic; pub mod cpu_graph; -pub mod disk_table; pub mod mem_basic; pub mod mem_graph; pub mod network_basic; pub mod network_graph; pub mod process_table; -pub mod temp_table; pub use basic_table_arrows::*; pub use battery_display::*; pub use cpu_basic::*; pub use cpu_graph::*; -pub use disk_table::*; pub use mem_basic::*; pub use mem_graph::*; pub use network_basic::*; pub use network_graph::*; pub use process_table::*; -pub use temp_table::*; diff --git a/src/canvas/drawing/process_table.rs b/src/canvas/drawing/process_table.rs index ab639a37..2104a5bd 100644 --- a/src/canvas/drawing/process_table.rs +++ b/src/canvas/drawing/process_table.rs @@ -7,6 +7,7 @@ use crate::{ constants::*, }; +use indextree::NodeId; use tui::{ backend::Backend, layout::{Alignment, Constraint, Direction, Layout, Rect}, @@ -101,9 +102,9 @@ static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: Lazy>> = La pub fn draw_process_features( painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, - draw_border: bool, widget_id: u64, + draw_border: bool, widget_id: NodeId, ) { - if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) { + if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&1) { let search_height = if draw_border { 5 } else { 3 }; let is_sort_open = process_widget_state.is_sort_open; let header_len = process_widget_state.columns.longest_header_len; @@ -122,7 +123,7 @@ pub fn draw_process_features( app_state, processes_chunk[1], draw_border, - widget_id + 1, + widget_id, ); } @@ -133,14 +134,7 @@ pub fn draw_process_features( .split(proc_draw_loc); proc_draw_loc = processes_chunk[1]; - draw_process_sort( - painter, - f, - app_state, - processes_chunk[0], - draw_border, - widget_id + 2, - ); + draw_process_sort(painter, f, app_state, processes_chunk[0], draw_border, 1); } draw_processes_table(painter, f, app_state, proc_draw_loc, draw_border, widget_id); @@ -149,17 +143,17 @@ pub fn draw_process_features( fn draw_processes_table( painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, - draw_border: bool, widget_id: u64, + draw_border: bool, widget_id: NodeId, ) { let should_get_widget_bounds = app_state.should_get_widget_bounds(); - if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) { + if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&1) { let recalculate_column_widths = should_get_widget_bounds || proc_widget_state.requires_redraw; if proc_widget_state.requires_redraw { proc_widget_state.requires_redraw = false; } - let is_on_widget = widget_id == app_state.current_widget.widget_id; + let is_on_widget = false; let margined_draw_loc = Layout::default() .constraints([Constraint::Percentage(100)]) .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 }) @@ -178,7 +172,7 @@ fn draw_processes_table( let title_base = if app_state.app_config_fields.show_table_scroll_position { if let Some(finalized_process_data) = app_state .canvas_data - .finalized_process_data_map + .stringified_process_data_map .get(&widget_id) { let title = format!( @@ -511,7 +505,7 @@ fn draw_processes_table( if app_state.should_get_widget_bounds() { // Update draw loc in widget map - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + if let Some(widget) = app_state.widget_map.get_mut(&1) { widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); widget.bottom_right_corner = Some(( margined_draw_loc.x + margined_draw_loc.width, @@ -524,7 +518,7 @@ fn draw_processes_table( fn draw_search_field( painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, - draw_border: bool, widget_id: u64, + draw_border: bool, _widget_id: NodeId, ) { fn build_query<'a>( is_on_widget: bool, grapheme_indices: GraphemeIndices<'a>, start_position: usize, @@ -565,8 +559,8 @@ fn draw_search_field( } // TODO: Make the cursor scroll back if there's space! - if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&(widget_id - 1)) { - let is_on_widget = widget_id == app_state.current_widget.widget_id; + if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&1) { + let is_on_widget = false; let num_columns = usize::from(draw_loc.width); let search_title = "> "; @@ -728,7 +722,7 @@ fn draw_search_field( if app_state.should_get_widget_bounds() { // Update draw loc in widget map - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + if let Some(widget) = app_state.widget_map.get_mut(&1) { widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); widget.bottom_right_corner = Some(( margined_draw_loc.x + margined_draw_loc.width, diff --git a/src/constants.rs b/src/constants.rs index bcd992eb..63953d1c 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -19,7 +19,7 @@ pub const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u64 = 1000; pub const MAX_KEY_TIMEOUT_IN_MILLISECONDS: u64 = 1000; // Limits for when we should stop showing table gaps/labels (anything less means not shown) -pub const TABLE_GAP_HEIGHT_LIMIT: u16 = 7; +pub const TABLE_GAP_HEIGHT_LIMIT: u16 = 5; pub const TIME_LABEL_HEIGHT_LIMIT: u16 = 7; // For kill signals diff --git a/src/data_conversion.rs b/src/data_conversion.rs index af5e11ba..68e07d5c 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -8,6 +8,7 @@ use crate::{ use data_harvester::processes::ProcessSorting; use fxhash::FxBuildHasher; use indexmap::IndexSet; +use std::borrow::Cow; use std::collections::{HashMap, VecDeque}; /// Point is of time, data @@ -83,81 +84,97 @@ pub struct ConvertedCpuData { pub legend_value: String, } -pub fn convert_temp_row(app: &AppState) -> Vec> { +pub fn convert_temp_row( + app: &AppState, +) -> Vec, Option>)>> { let current_data = &app.data_collection; let temp_type = &app.app_config_fields.temperature_type; - let mut sensor_vector: Vec> = current_data - .temp_harvest - .iter() - .map(|temp_harvest| { - vec![ - temp_harvest.name.clone(), - (temp_harvest.temperature.ceil() as u64).to_string() - + match temp_type { - data_harvester::temperature::TemperatureType::Celsius => "°C", - data_harvester::temperature::TemperatureType::Kelvin => "K", - data_harvester::temperature::TemperatureType::Fahrenheit => "°F", - }, - ] - }) - .collect(); + if current_data.temp_harvest.is_empty() { + vec![vec![ + ("No Sensors Found".into(), Some("N/A".into())), + ("".into(), None), + ]] + } else { + let (unit_long, unit_short) = match temp_type { + data_harvester::temperature::TemperatureType::Celsius => ("°C", "C"), + data_harvester::temperature::TemperatureType::Kelvin => ("K", "K"), + data_harvester::temperature::TemperatureType::Fahrenheit => ("°F", "F"), + }; - if sensor_vector.is_empty() { - sensor_vector.push(vec!["No Sensors Found".to_string(), "".to_string()]); + current_data + .temp_harvest + .iter() + .map(|temp_harvest| { + let val = temp_harvest.temperature.ceil().to_string(); + vec![ + (temp_harvest.name.clone().into(), None), + ( + format!("{}{}", val, unit_long).into(), + Some(format!("{}{}", val, unit_short).into()), + ), + ] + }) + .collect() } - - sensor_vector } -pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec> { - let mut disk_vector: Vec> = Vec::new(); +pub fn convert_disk_row( + current_data: &data_farmer::DataCollection, +) -> Vec, Option>)>> { + if current_data.disk_harvest.is_empty() { + vec![vec![ + ("No Disks Found".into(), Some("N/A".into())), + ("".into(), None), + ]] + } else { + current_data + .disk_harvest + .iter() + .zip(¤t_data.io_labels) + .map(|(disk, (io_read, io_write))| { + let free_space_fmt = if let Some(free_space) = disk.free_space { + let converted_free_space = get_decimal_bytes(free_space); + Cow::Owned(format!( + "{:.*}{}", + 0, converted_free_space.0, converted_free_space.1 + )) + } else { + "N/A".into() + }; + let total_space_fmt = if let Some(total_space) = disk.total_space { + let converted_total_space = get_decimal_bytes(total_space); + Cow::Owned(format!( + "{:.*}{}", + 0, converted_total_space.0, converted_total_space.1 + )) + } else { + "N/A".into() + }; - current_data - .disk_harvest - .iter() - .zip(¤t_data.io_labels) - .for_each(|(disk, (io_read, io_write))| { - let free_space_fmt = if let Some(free_space) = disk.free_space { - let converted_free_space = get_decimal_bytes(free_space); - format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1) - } else { - "N/A".to_string() - }; - let total_space_fmt = if let Some(total_space) = disk.total_space { - let converted_total_space = get_decimal_bytes(total_space); - format!( - "{:.*}{}", - 0, converted_total_space.0, converted_total_space.1 - ) - } else { - "N/A".to_string() - }; + let usage_fmt = if let (Some(used_space), Some(total_space)) = + (disk.used_space, disk.total_space) + { + Cow::Owned(format!( + "{:.0}%", + used_space as f64 / total_space as f64 * 100_f64 + )) + } else { + "N/A".into() + }; - let usage_fmt = if let (Some(used_space), Some(total_space)) = - (disk.used_space, disk.total_space) - { - format!("{:.0}%", used_space as f64 / total_space as f64 * 100_f64) - } else { - "N/A".to_string() - }; - - disk_vector.push(vec![ - disk.name.to_string(), - disk.mount_point.to_string(), - usage_fmt, - free_space_fmt, - total_space_fmt, - io_read.to_string(), - io_write.to_string(), - ]); - }); - - if disk_vector.is_empty() { - disk_vector.push(vec!["No Disks Found".to_string(), "".to_string()]); + vec![ + (disk.name.clone().into(), None), + (disk.mount_point.clone().into(), None), + (usage_fmt, None), + (free_space_fmt, None), + (total_space_fmt, None), + (io_read.clone().into(), None), + (io_write.clone().into(), None), + ] + }) + .collect::>() } - - disk_vector } pub fn convert_cpu_data_points( diff --git a/src/lib.rs b/src/lib.rs index f11a0f63..b438204e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -369,108 +369,109 @@ pub fn update_all_process_lists(app: &mut AppState) { } } -fn update_final_process_list(app: &mut AppState, widget_id: u64) { - let process_states = app - .proc_state - .widget_states - .get(&widget_id) - .map(|process_state| { - ( - process_state - .process_search_state - .search_state - .is_invalid_or_blank_search(), - process_state.is_using_command, - process_state.is_grouped, - process_state.is_tree_mode, - ) - }); +fn update_final_process_list(_app: &mut AppState, _widget_id: u64) { + // TODO: [STATE] FINISH THIS + // let process_states = app + // .proc_state + // .widget_states + // .get(&widget_id) + // .map(|process_state| { + // ( + // process_state + // .process_search_state + // .search_state + // .is_invalid_or_blank_search(), + // process_state.is_using_command, + // process_state.is_grouped, + // process_state.is_tree_mode, + // ) + // }); - if let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states { - if !app.is_frozen { - convert_process_data( - &app.data_collection, - &mut app.canvas_data.single_process_data, - #[cfg(target_family = "unix")] - &mut app.user_table, - ); - } - let process_filter = app.get_process_filter(widget_id); - let filtered_process_data: Vec = if is_tree { - app.canvas_data - .single_process_data - .iter() - .map(|(_pid, process)| { - let mut process_clone = process.clone(); - if !is_invalid_or_blank { - if let Some(process_filter) = process_filter { - process_clone.is_disabled_entry = - !process_filter.check(&process_clone, is_using_command); - } - } - process_clone - }) - .collect::>() - } else { - app.canvas_data - .single_process_data - .iter() - .filter_map(|(_pid, process)| { - if !is_invalid_or_blank { - if let Some(process_filter) = process_filter { - if process_filter.check(process, is_using_command) { - Some(process) - } else { - None - } - } else { - Some(process) - } - } else { - Some(process) - } - }) - .cloned() - .collect::>() - }; + // if let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states { + // if !app.is_frozen { + // convert_process_data( + // &app.data_collection, + // &mut app.canvas_data.single_process_data, + // #[cfg(target_family = "unix")] + // &mut app.user_table, + // ); + // } + // let process_filter = app.get_process_filter(widget_id); + // let filtered_process_data: Vec = if is_tree { + // app.canvas_data + // .single_process_data + // .iter() + // .map(|(_pid, process)| { + // let mut process_clone = process.clone(); + // if !is_invalid_or_blank { + // if let Some(process_filter) = process_filter { + // process_clone.is_disabled_entry = + // !process_filter.check(&process_clone, is_using_command); + // } + // } + // process_clone + // }) + // .collect::>() + // } else { + // app.canvas_data + // .single_process_data + // .iter() + // .filter_map(|(_pid, process)| { + // if !is_invalid_or_blank { + // if let Some(process_filter) = process_filter { + // if process_filter.check(process, is_using_command) { + // Some(process) + // } else { + // None + // } + // } else { + // Some(process) + // } + // } else { + // Some(process) + // } + // }) + // .cloned() + // .collect::>() + // }; - if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) { - let mut finalized_process_data = if is_tree { - tree_process_data( - &filtered_process_data, - is_using_command, - &proc_widget_state.process_sorting_type, - proc_widget_state.is_process_sort_descending, - ) - } else if is_grouped { - group_process_data(&filtered_process_data, is_using_command) - } else { - filtered_process_data - }; + // if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) { + // let mut finalized_process_data = if is_tree { + // tree_process_data( + // &filtered_process_data, + // is_using_command, + // &proc_widget_state.process_sorting_type, + // proc_widget_state.is_process_sort_descending, + // ) + // } else if is_grouped { + // group_process_data(&filtered_process_data, is_using_command) + // } else { + // filtered_process_data + // }; - // Note tree mode is sorted well before this, as it's special. - if !is_tree { - sort_process_data(&mut finalized_process_data, proc_widget_state); - } + // // Note tree mode is sorted well before this, as it's special. + // if !is_tree { + // sort_process_data(&mut finalized_process_data, proc_widget_state); + // } - if proc_widget_state.scroll_state.current_scroll_position - >= finalized_process_data.len() - { - proc_widget_state.scroll_state.current_scroll_position = - finalized_process_data.len().saturating_sub(1); - proc_widget_state.scroll_state.previous_scroll_position = 0; - proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down; - } + // if proc_widget_state.scroll_state.current_scroll_position + // >= finalized_process_data.len() + // { + // proc_widget_state.scroll_state.current_scroll_position = + // finalized_process_data.len().saturating_sub(1); + // proc_widget_state.scroll_state.previous_scroll_position = 0; + // proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down; + // } - app.canvas_data.stringified_process_data_map.insert( - widget_id, - stringify_process_data(proc_widget_state, &finalized_process_data), - ); - app.canvas_data - .finalized_process_data_map - .insert(widget_id, finalized_process_data); - } - } + // app.canvas_data.stringified_process_data_map.insert( + // widget_id, + // stringify_process_data(proc_widget_state, &finalized_process_data), + // ); + // app.canvas_data + // .finalized_process_data_map + // .insert(widget_id, finalized_process_data); + // } + // } } fn sort_process_data(