refactor: start moving over the event system

This commit is contained in:
ClementTsang 2021-08-27 00:00:12 -04:00
parent 0afc371eaa
commit 6b69e373de
17 changed files with 883 additions and 678 deletions

View File

@ -13,7 +13,7 @@ use std::{
time::Instant, time::Instant,
}; };
use crossterm::event::{KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEventKind};
use fxhash::FxHashMap; use fxhash::FxHashMap;
use indextree::{Arena, NodeId}; use indextree::{Arena, NodeId};
use unicode_segmentation::GraphemeCursor; use unicode_segmentation::GraphemeCursor;
@ -28,12 +28,14 @@ pub use widgets::*;
use crate::{ use crate::{
canvas, canvas,
constants::{self, MAX_SIGNAL}, constants::{self, MAX_SIGNAL},
data_conversion::*,
units::data_units::DataUnit, units::data_units::DataUnit,
update_final_process_list,
utils::error::{BottomError, Result}, utils::error::{BottomError, Result},
BottomEvent, Pid, BottomEvent, Pid,
}; };
use self::event::{does_point_intersect_rect, EventResult, ReturnSignal}; use self::event::{EventResult, ReturnSignal, ReturnSignalResult};
const MAX_SEARCH_LENGTH: usize = 200; const MAX_SEARCH_LENGTH: usize = 200;
@ -269,9 +271,38 @@ impl AppState {
// TODO: Write this. // TODO: Write this.
if event.modifiers.is_empty() { if event.modifiers.is_empty() {
todo!() match event.code {
} else if let KeyModifiers::ALT = event.modifiers { KeyCode::Esc => {
todo!() 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 { } else {
None None
} }
@ -296,30 +327,49 @@ impl AppState {
// Not great, but basically a blind lookup through the table for anything that clips the click location. // 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. // TODO: Would be cool to use a kd-tree or something like that in the future.
let x = event.column; match &event.kind {
let y = event.row; MouseEventKind::Down(MouseButton::Left) => {
if self.is_expanded {
for (id, widget) in self.widget_lookup_map.iter_mut() { if let Some(widget) =
if does_point_intersect_rect(x, y, widget.bounds()) { self.widget_lookup_map.get_mut(&self.selected_widget)
let is_id_selected = self.selected_widget == *id; {
self.selected_widget = *id; return widget.handle_mouse_event(event);
}
if is_id_selected {
return widget.handle_mouse_event(event);
} else { } else {
// If the aren't equal, *force* a redraw. for (id, widget) in self.widget_lookup_map.iter_mut() {
widget.handle_mouse_event(event); if widget.does_intersect_mouse(&event) {
return EventResult::Redraw; 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 EventResult::NoRedraw
} }
BottomEvent::Update(_new_data) => { BottomEvent::Update(new_data) => {
self.data_collection.eat_data(new_data);
if !self.is_frozen { if !self.is_frozen {
// TODO: Update all data, and redraw. self.convert_data();
todo!()
EventResult::Redraw
} else { } else {
EventResult::NoRedraw EventResult::NoRedraw
} }
@ -336,16 +386,98 @@ impl AppState {
} }
} }
/// Handles a [`ReturnSignal`], and returns an [`EventResult`]. /// Handles a [`ReturnSignal`], and returns an [`ReturnSignalResult`].
pub fn handle_return_signal(&mut self, return_signal: ReturnSignal) -> EventResult { pub fn handle_return_signal(&mut self, return_signal: ReturnSignal) -> ReturnSignalResult {
match return_signal { match return_signal {
ReturnSignal::Nothing => EventResult::NoRedraw,
ReturnSignal::KillProcess => { ReturnSignal::KillProcess => {
todo!() 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::<Vec<_>>();
widget_ids.into_iter().for_each(|widget_id| {
update_final_process_list(self, widget_id);
});
}
}
pub fn on_esc(&mut self) { pub fn on_esc(&mut self) {
self.reset_multi_tap_keys(); self.reset_multi_tap_keys();
if self.is_in_dialog() { if self.is_in_dialog() {
@ -1316,41 +1448,41 @@ impl AppState {
pub fn start_killing_process(&mut self) { pub fn start_killing_process(&mut self) {
self.reset_multi_tap_keys(); self.reset_multi_tap_keys();
if let Some(proc_widget_state) = self // if let Some(proc_widget_state) = self
.proc_state // .proc_state
.widget_states // .widget_states
.get(&self.current_widget.widget_id) // .get(&self.current_widget.widget_id)
{ // {
if let Some(corresponding_filtered_process_list) = self // if let Some(corresponding_filtered_process_list) = self
.canvas_data // .canvas_data
.finalized_process_data_map // .finalized_process_data_map
.get(&self.current_widget.widget_id) // .get(&self.current_widget.widget_id)
{ // {
if proc_widget_state.scroll_state.current_scroll_position // if proc_widget_state.scroll_state.current_scroll_position
< corresponding_filtered_process_list.len() // < corresponding_filtered_process_list.len()
{ // {
let current_process: (String, Vec<Pid>); // let current_process: (String, Vec<Pid>);
if self.is_grouped(self.current_widget.widget_id) { // if self.is_grouped(self.current_widget.widget_id) {
if let Some(process) = &corresponding_filtered_process_list // if let Some(process) = &corresponding_filtered_process_list
.get(proc_widget_state.scroll_state.current_scroll_position) // .get(proc_widget_state.scroll_state.current_scroll_position)
{ // {
current_process = (process.name.to_string(), process.group_pids.clone()) // current_process = (process.name.to_string(), process.group_pids.clone())
} else { // } else {
return; // return;
} // }
} else { // } else {
let process = corresponding_filtered_process_list // let process = corresponding_filtered_process_list
[proc_widget_state.scroll_state.current_scroll_position] // [proc_widget_state.scroll_state.current_scroll_position]
.clone(); // .clone();
current_process = (process.name.clone(), vec![process.pid]) // current_process = (process.name.clone(), vec![process.pid])
}; // };
self.to_delete_process_list = Some(current_process); // self.to_delete_process_list = Some(current_process);
self.delete_dialog_state.is_showing_dd = true; // self.delete_dialog_state.is_showing_dd = true;
self.is_determining_widget_boundary = true; // self.is_determining_widget_boundary = true;
} // }
} // }
} // }
} }
pub fn on_char_key(&mut self, caught_char: char) { pub fn on_char_key(&mut self, caught_char: char) {
@ -2222,85 +2354,85 @@ impl AppState {
} }
pub fn skip_to_last(&mut self) { pub fn skip_to_last(&mut self) {
if !self.ignore_normal_keybinds() { // if !self.ignore_normal_keybinds() {
match self.current_widget.widget_type { // match self.current_widget.widget_type {
BottomWidgetType::Proc => { // BottomWidgetType::Proc => {
if let Some(proc_widget_state) = self // if let Some(proc_widget_state) = self
.proc_state // .proc_state
.get_mut_widget_state(self.current_widget.widget_id) // .get_mut_widget_state(self.current_widget.widget_id)
{ // {
if let Some(finalized_process_data) = self // if let Some(finalized_process_data) = self
.canvas_data // .canvas_data
.finalized_process_data_map // .finalized_process_data_map
.get(&self.current_widget.widget_id) // .get(&self.current_widget.widget_id)
{ // {
if !self.canvas_data.finalized_process_data_map.is_empty() { // if !self.canvas_data.finalized_process_data_map.is_empty() {
proc_widget_state.scroll_state.current_scroll_position = // proc_widget_state.scroll_state.current_scroll_position =
finalized_process_data.len() - 1; // finalized_process_data.len() - 1;
proc_widget_state.scroll_state.scroll_direction = // proc_widget_state.scroll_state.scroll_direction =
ScrollDirection::Down; // ScrollDirection::Down;
} // }
} // }
} // }
} // }
BottomWidgetType::ProcSort => { // BottomWidgetType::ProcSort => {
if let Some(proc_widget_state) = self // if let Some(proc_widget_state) = self
.proc_state // .proc_state
.get_mut_widget_state(self.current_widget.widget_id - 2) // .get_mut_widget_state(self.current_widget.widget_id - 2)
{ // {
proc_widget_state.columns.current_scroll_position = // proc_widget_state.columns.current_scroll_position =
proc_widget_state.columns.get_enabled_columns_len() - 1; // proc_widget_state.columns.get_enabled_columns_len() - 1;
proc_widget_state.columns.scroll_direction = ScrollDirection::Down; // proc_widget_state.columns.scroll_direction = ScrollDirection::Down;
} // }
} // }
BottomWidgetType::Temp => { // BottomWidgetType::Temp => {
if let Some(temp_widget_state) = self // if let Some(temp_widget_state) = self
.temp_state // .temp_state
.get_mut_widget_state(self.current_widget.widget_id) // .get_mut_widget_state(self.current_widget.widget_id)
{ // {
if !self.canvas_data.temp_sensor_data.is_empty() { // if !self.canvas_data.temp_sensor_data.is_empty() {
temp_widget_state.scroll_state.current_scroll_position = // temp_widget_state.scroll_state.current_scroll_position =
self.canvas_data.temp_sensor_data.len() - 1; // self.canvas_data.temp_sensor_data.len() - 1;
temp_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; // temp_widget_state.scroll_state.scroll_direction = ScrollDirection::Down;
} // }
} // }
} // }
BottomWidgetType::Disk => { // BottomWidgetType::Disk => {
if let Some(disk_widget_state) = self // if let Some(disk_widget_state) = self
.disk_state // .disk_state
.get_mut_widget_state(self.current_widget.widget_id) // .get_mut_widget_state(self.current_widget.widget_id)
{ // {
if !self.canvas_data.disk_data.is_empty() { // if !self.canvas_data.disk_data.is_empty() {
disk_widget_state.scroll_state.current_scroll_position = // disk_widget_state.scroll_state.current_scroll_position =
self.canvas_data.disk_data.len() - 1; // self.canvas_data.disk_data.len() - 1;
disk_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; // disk_widget_state.scroll_state.scroll_direction = ScrollDirection::Down;
} // }
} // }
} // }
BottomWidgetType::CpuLegend => { // BottomWidgetType::CpuLegend => {
if let Some(cpu_widget_state) = self // if let Some(cpu_widget_state) = self
.cpu_state // .cpu_state
.get_mut_widget_state(self.current_widget.widget_id - 1) // .get_mut_widget_state(self.current_widget.widget_id - 1)
{ // {
let cap = self.canvas_data.cpu_data.len(); // let cap = self.canvas_data.cpu_data.len();
if cap > 0 { // if cap > 0 {
cpu_widget_state.scroll_state.current_scroll_position = cap - 1; // cpu_widget_state.scroll_state.current_scroll_position = cap - 1;
cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; // cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::Down;
} // }
} // }
} // }
_ => {} // _ => {}
} // }
self.reset_multi_tap_keys(); // self.reset_multi_tap_keys();
} else if self.help_dialog_state.is_showing_help { // } else if self.help_dialog_state.is_showing_help {
self.help_dialog_state.scroll_state.current_scroll_index = self // self.help_dialog_state.scroll_state.current_scroll_index = self
.help_dialog_state // .help_dialog_state
.scroll_state // .scroll_state
.max_scroll_index // .max_scroll_index
.saturating_sub(1); // .saturating_sub(1);
} else if self.delete_dialog_state.is_showing_dd { // } else if self.delete_dialog_state.is_showing_dd {
self.delete_dialog_state.selected_signal = KillSignal::Kill(MAX_SIGNAL); // self.delete_dialog_state.selected_signal = KillSignal::Kill(MAX_SIGNAL);
} // }
} }
pub fn decrement_position_count(&mut self) { pub fn decrement_position_count(&mut self) {
@ -2381,35 +2513,35 @@ impl AppState {
} }
/// Returns the new position. /// Returns the new position.
fn increment_process_position(&mut self, num_to_change_by: i64) -> Option<usize> { fn increment_process_position(&mut self, _num_to_change_by: i64) -> Option<usize> {
if let Some(proc_widget_state) = self // if let Some(proc_widget_state) = self
.proc_state // .proc_state
.get_mut_widget_state(self.current_widget.widget_id) // .get_mut_widget_state(self.current_widget.widget_id)
{ // {
let current_posn = proc_widget_state.scroll_state.current_scroll_position; // let current_posn = proc_widget_state.scroll_state.current_scroll_position;
if let Some(finalized_process_data) = self // if let Some(finalized_process_data) = self
.canvas_data // .canvas_data
.finalized_process_data_map // .finalized_process_data_map
.get(&self.current_widget.widget_id) // .get(&self.current_widget.widget_id)
{ // {
if current_posn as i64 + num_to_change_by >= 0 // if current_posn as i64 + num_to_change_by >= 0
&& current_posn as i64 + num_to_change_by < finalized_process_data.len() as i64 // && current_posn as i64 + num_to_change_by < finalized_process_data.len() as i64
{ // {
proc_widget_state.scroll_state.current_scroll_position = // proc_widget_state.scroll_state.current_scroll_position =
(current_posn as i64 + num_to_change_by) as usize; // (current_posn as i64 + num_to_change_by) as usize;
} else { // } else {
return None; // return None;
} // }
} // }
if num_to_change_by < 0 { // if num_to_change_by < 0 {
proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; // proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Up;
} else { // } else {
proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; // 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 None
} }
@ -2537,32 +2669,32 @@ impl AppState {
} }
fn toggle_collapsing_process_branch(&mut self) { fn toggle_collapsing_process_branch(&mut self) {
if let Some(proc_widget_state) = self // if let Some(proc_widget_state) = self
.proc_state // .proc_state
.widget_states // .widget_states
.get_mut(&self.current_widget.widget_id) // .get_mut(&self.current_widget.widget_id)
{ // {
let current_posn = proc_widget_state.scroll_state.current_scroll_position; // let current_posn = proc_widget_state.scroll_state.current_scroll_position;
if let Some(displayed_process_list) = self // if let Some(displayed_process_list) = self
.canvas_data // .canvas_data
.finalized_process_data_map // .finalized_process_data_map
.get(&self.current_widget.widget_id) // .get(&self.current_widget.widget_id)
{ // {
if let Some(corresponding_process) = displayed_process_list.get(current_posn) { // if let Some(corresponding_process) = displayed_process_list.get(current_posn) {
let corresponding_pid = corresponding_process.pid; // let corresponding_pid = corresponding_process.pid;
if let Some(process_data) = self // if let Some(process_data) = self
.canvas_data // .canvas_data
.single_process_data // .single_process_data
.get_mut(&corresponding_pid) // .get_mut(&corresponding_pid)
{ // {
process_data.is_collapsed_entry = !process_data.is_collapsed_entry; // process_data.is_collapsed_entry = !process_data.is_collapsed_entry;
self.proc_state.force_update = Some(self.current_widget.widget_id); // self.proc_state.force_update = Some(self.current_widget.widget_id);
} // }
} // }
} // }
} // }
} }
fn zoom_out(&mut self) { fn zoom_out(&mut self) {

View File

@ -1,14 +1,10 @@
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tui::layout::Rect;
const MAX_TIMEOUT: Duration = Duration::from_millis(400); const MAX_TIMEOUT: Duration = Duration::from_millis(400);
/// These are "signals" that are sent along with an [`EventResult`] to signify a potential additional action /// 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. /// that the caller must do, along with the "core" result of either drawing or redrawing.
pub enum ReturnSignal { 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). /// A signal returned when some process widget was told to try to kill a process (or group of processes).
KillProcess, 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()
}

View File

@ -1082,10 +1082,7 @@ pub fn create_layout_tree(
} }
} }
BottomWidgetType::Proc => { BottomWidgetType::Proc => {
widget_lookup_map.insert( widget_lookup_map.insert(widget_id, ProcessManager::new(process_defaults).into());
widget_id,
ProcessManager::new(process_defaults.is_tree).into(),
);
} }
BottomWidgetType::Temp => { BottomWidgetType::Temp => {
widget_lookup_map.insert(widget_id, TempTable::default().into()); widget_lookup_map.insert(widget_id, TempTable::default().into());

View File

@ -2,12 +2,7 @@ use std::time::Instant;
use crossterm::event::{KeyEvent, MouseEvent}; use crossterm::event::{KeyEvent, MouseEvent};
use enum_dispatch::enum_dispatch; use enum_dispatch::enum_dispatch;
use tui::{ use tui::{backend::Backend, layout::Rect, widgets::TableState, Frame};
backend::Backend,
layout::Rect,
widgets::{Block, TableState},
Frame,
};
use crate::{ use crate::{
app::{ app::{
@ -66,6 +61,14 @@ pub trait Component {
/// Updates a [`Component`]s bounding box to `new_bounds`. /// Updates a [`Component`]s bounding box to `new_bounds`.
fn set_bounds(&mut self, new_bounds: Rect); 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. /// 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. /// Draws a [`Widget`]. Defaults to doing nothing.
fn draw<B: Backend>( fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>, &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData,
data: &DisplayableData, selected: bool,
) { ) {
// TODO: Remove the default implementation in the future! // 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! // TODO: Do another pass on ALL of the draw code - currently it's just glue, it should eventually be done properly!

View File

@ -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 tui::{layout::Rect, widgets::TableState};
use crate::app::{ use crate::app::{
@ -6,68 +6,125 @@ use crate::app::{
Component, Component,
}; };
#[derive(Debug)]
pub enum ScrollDirection { pub enum ScrollDirection {
Up, Up,
Down, Down,
} }
/// A "scrollable" [`Widget`] component. Intended for use as part of another [`Widget`] - as such, it does /// We save the previous window index for future reference, but we must invalidate if the area changes.
/// not have any bounds or the like. #[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 { pub struct Scrollable {
/// The currently selected index. Do *NOT* directly update this, use the helper functions!
current_index: usize, 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, scroll_direction: ScrollDirection,
/// How many items to keep track of.
num_items: usize, num_items: usize,
/// tui-rs' internal table state; used to keep track of the *visually* selected index.
tui_state: TableState, tui_state: TableState,
/// Manages the `gg` double-tap shortcut.
gg_manager: MultiKey, gg_manager: MultiKey,
/// The bounds of the [`Scrollable`] component.
bounds: Rect, bounds: Rect,
} }
impl Scrollable { impl Scrollable {
/// Creates a new [`Scrollable`]. /// Creates a new [`Scrollable`].
pub fn new(num_items: usize) -> Self { pub fn new(num_items: usize) -> Self {
let mut tui_state = TableState::default();
tui_state.select(Some(0));
Self { Self {
current_index: 0, current_index: 0,
previous_index: 0, window_index: WindowIndex::default(),
scroll_direction: ScrollDirection::Down, scroll_direction: ScrollDirection::Down,
num_items, num_items,
tui_state: TableState::default(), tui_state,
gg_manager: MultiKey::register(vec!['g', 'g']), // TODO: Use a static arrayvec gg_manager: MultiKey::register(vec!['g', 'g']), // TODO: Use a static arrayvec
bounds: Rect::default(), bounds: Rect::default(),
} }
} }
/// Creates a new [`Scrollable`]. Note this will set the associated [`TableState`] to select the first entry. /// Returns the currently selected index of the [`Scrollable`].
pub fn new_selected(num_items: usize) -> Self {
let mut scrollable = Scrollable::new(num_items);
scrollable.tui_state.select(Some(0));
scrollable
}
pub fn index(&self) -> usize { pub fn index(&self) -> usize {
self.current_index 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) { fn update_index(&mut self, new_index: usize) {
use std::cmp::Ordering; use std::cmp::Ordering;
match new_index.cmp(&self.current_index) { match new_index.cmp(&self.current_index) {
Ordering::Greater => { Ordering::Greater => {
self.previous_index = self.current_index;
self.current_index = new_index; self.current_index = new_index;
self.scroll_direction = ScrollDirection::Down; self.scroll_direction = ScrollDirection::Down;
} }
Ordering::Less => { Ordering::Less => {
self.previous_index = self.current_index;
self.current_index = new_index; self.current_index = new_index;
self.scroll_direction = ScrollDirection::Up; self.scroll_direction = ScrollDirection::Up;
} }
Ordering::Equal => {
Ordering::Equal => {} // Do nothing.
}
} }
} }
@ -94,33 +151,32 @@ impl Scrollable {
/// Moves *downward* by *incrementing* the current index. /// Moves *downward* by *incrementing* the current index.
fn move_down(&mut self, change_by: usize) -> EventResult { 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; let new_index = self.current_index + change_by;
if new_index >= self.num_items { if new_index >= self.num_items {
let last_index = self.num_items - 1; EventResult::NoRedraw
if self.current_index != last_index {
self.update_index(last_index);
EventResult::Redraw
} else {
EventResult::NoRedraw
}
} else { } else {
self.update_index(new_index); if self.current_index == new_index {
EventResult::Redraw EventResult::NoRedraw
} else {
self.update_index(new_index);
EventResult::Redraw
}
} }
} }
/// Moves *upward* by *decrementing* the current index. /// Moves *upward* by *decrementing* the current index.
fn move_up(&mut self, change_by: usize) -> EventResult { fn move_up(&mut self, change_by: usize) -> EventResult {
let new_index = self.current_index.saturating_sub(change_by); if self.num_items == 0 {
if new_index == 0 { return EventResult::NoRedraw;
if self.current_index != 0 { }
self.update_index(0);
EventResult::Redraw let new_index = self.current_index.saturating_sub(change_by);
} else { if self.current_index == new_index {
EventResult::NoRedraw EventResult::NoRedraw
}
} else { } else {
self.update_index(new_index); self.update_index(new_index);
EventResult::Redraw EventResult::Redraw
@ -133,15 +189,15 @@ impl Scrollable {
if num_items <= self.current_index { if num_items <= self.current_index {
self.current_index = num_items.saturating_sub(1); 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 { pub fn num_items(&self) -> usize {
self.num_items self.num_items
} }
pub fn tui_state(&self) -> TableState {
self.tui_state.clone()
}
} }
impl Component for Scrollable { impl Component for Scrollable {
@ -169,29 +225,31 @@ impl Component for Scrollable {
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
match event.kind { match event.kind {
crossterm::event::MouseEventKind::Down(MouseButton::Left) => { MouseEventKind::Down(MouseButton::Left) => {
// This requires a bit of fancy calculation. The main trick is remembering that if self.does_intersect_mouse(&event) {
// we are using a *visual* index here - not what is the actual index! Luckily, we keep track of that // This requires a bit of fancy calculation. The main trick is remembering that
// inside our linked copy of TableState! // 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*; // 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). // 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()); let y = usize::from(event.row - self.bounds.top());
if let Some(selected) = self.tui_state.selected() { if let Some(selected) = self.tui_state.selected() {
if y > selected { if y > selected {
let offset = y - selected; let offset = y - selected;
return self.move_down(offset); return self.move_down(offset);
} else { } else if y < selected {
let offset = selected - y; let offset = selected - y;
return self.move_up(offset); return self.move_up(offset);
}
} }
} }
EventResult::NoRedraw EventResult::NoRedraw
} }
crossterm::event::MouseEventKind::ScrollDown => self.move_down(1), MouseEventKind::ScrollDown => self.move_down(1),
crossterm::event::MouseEventKind::ScrollUp => self.move_up(1), MouseEventKind::ScrollUp => self.move_up(1),
_ => EventResult::NoRedraw, _ => EventResult::NoRedraw,
} }
} }

View File

@ -1,9 +1,9 @@
use std::{ use std::{
borrow::Cow, 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::{ use tui::{
layout::{Constraint, Rect}, layout::{Constraint, Rect},
text::Text, text::Text,
@ -24,6 +24,13 @@ pub enum DesiredColumnWidth {
Flex { desired: u16, max_percentage: f64 }, 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`]. /// A [`Column`] represents some column in a [`TextTable`].
#[derive(Debug)] #[derive(Debug)]
pub struct Column { pub struct Column {
@ -33,7 +40,7 @@ pub struct Column {
// TODO: I would remove these in the future, storing them here feels weird... // TODO: I would remove these in the future, storing them here feels weird...
pub desired_width: DesiredColumnWidth, pub desired_width: DesiredColumnWidth,
pub x_bounds: (u16, u16), pub x_bounds: Option<(u16, u16)>,
} }
impl Column { impl Column {
@ -44,7 +51,7 @@ impl Column {
) -> Self { ) -> Self {
Self { Self {
name, name,
x_bounds: (0, 0), x_bounds: None,
shortcut: shortcut.map(|e| { shortcut: shortcut.map(|e| {
let modifier = if e.modifiers.is_empty() { 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, /// 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( pub fn new_hard(
name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool, name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool,
hard_length: Option<u16>, hard_length: Option<u16>,
) -> Self { ) -> Self {
// TODO: It should really be based on the shortcut name...
Column::new( Column::new(
name, name,
shortcut, shortcut,
default_descending, 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( pub fn get_desired_column_widths(
columns: &[Column], data: &[Vec<String>], columns: &[Column], data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>],
) -> Vec<DesiredColumnWidth> { ) -> Vec<DesiredColumnWidth> {
columns columns
.iter() .iter()
@ -248,8 +256,22 @@ impl TextTable {
let max_len = data let max_len = data
.iter() .iter()
.filter_map(|c| c.get(column_index)) .filter_map(|c| c.get(column_index))
.max_by(|x, y| x.len().cmp(&y.len())) .max_by(|(x, short_x), (y, short_y)| {
.map(|s| s.len()) 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; .unwrap_or(0) as u16;
DesiredColumnWidth::Hard(max(max_len, width)) DesiredColumnWidth::Hard(max(max_len, width))
@ -262,16 +284,16 @@ impl TextTable {
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
fn get_cache(&mut self, area: Rect, data: &[Vec<String>]) -> Vec<u16> { fn get_cache(
&mut self, area: Rect, data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>],
) -> Vec<u16> {
fn calculate_column_widths( fn calculate_column_widths(
left_to_right: bool, mut desired_widths: Vec<DesiredColumnWidth>, total_width: u16, left_to_right: bool, mut desired_widths: Vec<DesiredColumnWidth>, total_width: u16,
) -> Vec<u16> { ) -> Vec<u16> {
debug!("OG desired widths: {:?}", desired_widths);
let mut total_width_left = total_width; let mut total_width_left = total_width;
if !left_to_right { if !left_to_right {
desired_widths.reverse(); desired_widths.reverse();
} }
debug!("Desired widths: {:?}", desired_widths);
let mut column_widths: Vec<u16> = Vec::with_capacity(desired_widths.len()); let mut column_widths: Vec<u16> = Vec::with_capacity(desired_widths.len());
for width in desired_widths { for width in desired_widths {
@ -303,7 +325,6 @@ impl TextTable {
} }
} }
} }
debug!("Initial column widths: {:?}", column_widths);
if !column_widths.is_empty() { if !column_widths.is_empty() {
let amount_per_slot = total_width_left / column_widths.len() as u16; 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 column_widths
} }
@ -330,9 +349,11 @@ impl TextTable {
if data.is_empty() { if data.is_empty() {
vec![0; self.columns.len()] vec![0; self.columns.len()]
} else { } else {
match &mut self.cached_column_widths { let was_cached: bool;
let column_widths = match &mut self.cached_column_widths {
CachedColumnWidths::Uncached => { CachedColumnWidths::Uncached => {
// Always recalculate. // Always recalculate.
was_cached = false;
let desired_widths = TextTable::get_desired_column_widths(&self.columns, data); let desired_widths = TextTable::get_desired_column_widths(&self.columns, data);
let calculated_widths = let calculated_widths =
calculate_column_widths(self.left_to_right, desired_widths, area.width); calculate_column_widths(self.left_to_right, desired_widths, area.width);
@ -349,6 +370,7 @@ impl TextTable {
} => { } => {
if *cached_area != area { if *cached_area != area {
// Recalculate! // Recalculate!
was_cached = false;
let desired_widths = let desired_widths =
TextTable::get_desired_column_widths(&self.columns, data); TextTable::get_desired_column_widths(&self.columns, data);
let calculated_widths = let calculated_widths =
@ -358,10 +380,22 @@ impl TextTable {
calculated_widths calculated_widths
} else { } else {
was_cached = true;
cached_data.clone() 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, /// 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. /// it will only create as many columns as it can grab data from both sources from.
pub fn create_draw_table( pub fn create_draw_table(
&mut self, painter: &Painter, data: &[Vec<String>], area: Rect, &mut self, painter: &Painter, data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>],
area: Rect,
) -> (Table<'_>, Vec<Constraint>, TableState) { ) -> (Table<'_>, Vec<Constraint>, TableState) {
// TODO: Change data: &[Vec<String>] to &[Vec<Cow<'static, str>>]
use tui::widgets::Row; use tui::widgets::Row;
let table_gap = if !self.show_gap || area.height < TABLE_GAP_HEIGHT_LIMIT { let table_gap = if !self.show_gap || area.height < TABLE_GAP_HEIGHT_LIMIT {
@ -383,15 +417,16 @@ impl TextTable {
1 1
}; };
self.update_num_items(data.len());
self.set_bounds(area); 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( self.scrollable.set_bounds(Rect::new(
area.x, area.x,
area.y + 1 + table_gap, area.y + table_extras,
area.width, area.width,
scrollable_height, scrollable_height,
)); ));
self.update_num_items(data.len());
// Calculate widths first, since we need them later. // Calculate widths first, since we need them later.
let calculated_widths = self.get_cache(area, data); 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, // Then calculate rows. We truncate the amount of data read based on height,
// as well as truncating some entries based on available width. // as well as truncating some entries based on available width.
let data_slice = { let data_slice = {
let start = self.scrollable.index(); let start = self.scrollable.get_list_start(scrollable_height as usize);
let end = std::cmp::min( let end = std::cmp::min(
self.scrollable.num_items(), self.scrollable.num_items(),
start + scrollable_height as usize, start + scrollable_height as usize,
@ -411,17 +446,25 @@ impl TextTable {
&data[start..end] &data[start..end]
}; };
let rows = data_slice.iter().map(|row| { let rows = data_slice.iter().map(|row| {
Row::new(row.iter().zip(&calculated_widths).map(|(cell, width)| { Row::new(
let width = *width as usize; row.iter()
let graphemes = .zip(&calculated_widths)
UnicodeSegmentation::graphemes(cell.as_str(), true).collect::<Vec<&str>>(); .map(|((text, shrunk_text), width)| {
let grapheme_width = graphemes.len(); let width = *width as usize;
if width < grapheme_width && width > 1 { let graphemes = UnicodeSegmentation::graphemes(text.as_ref(), true)
Text::raw(format!("{}", graphemes[..(width - 1)].concat())) .collect::<Vec<&str>>();
} else { let grapheme_width = graphemes.len();
Text::raw(cell.to_owned()) 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... // Now build up our headers...
@ -430,8 +473,7 @@ impl TextTable {
.bottom_margin(table_gap); .bottom_margin(table_gap);
// And return tui-rs's [`TableState`]. // And return tui-rs's [`TableState`].
let mut tui_state = TableState::default(); let tui_state = self.scrollable.tui_state();
tui_state.select(Some(self.scrollable.index()));
( (
Table::new(rows) Table::new(rows)
@ -464,25 +506,33 @@ impl Component for TextTable {
} }
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
// Note these are representing RELATIVE coordinates! if let MouseEventKind::Down(MouseButton::Left) = event.kind {
let x = event.column - self.bounds.left(); if !self.does_intersect_mouse(&event) {
let y = event.row - self.bounds.top(); return EventResult::NoRedraw;
}
if y == 0 { // Note these are representing RELATIVE coordinates! They *need* the above intersection check for validity!
for (index, column) in self.columns.iter().enumerate() { let x = event.column - self.bounds.left();
let (start, end) = column.x_bounds; let y = event.row - self.bounds.top();
if start >= x && end <= y {
if self.sort_index == index { if y == 0 {
// Just flip the sort if we're already sorting by this. for (index, column) in self.columns.iter().enumerate() {
self.sort_ascending = !self.sort_ascending; if let Some((start, end)) = column.x_bounds {
} else { if x >= start && x <= end {
self.sort_index = index; if self.sort_index == index {
self.sort_ascending = !column.default_descending; // 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 { } else {
self.scrollable.handle_mouse_event(event) self.scrollable.handle_mouse_event(event)
} }

View File

@ -3,7 +3,7 @@ use std::{collections::HashMap, time::Instant};
use crossterm::event::{KeyEvent, MouseEvent}; use crossterm::event::{KeyEvent, MouseEvent};
use tui::layout::Rect; 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}; 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 { fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
let global_x = event.column; if self.graph.does_intersect_mouse(&event) {
let global_y = event.row;
if does_point_intersect_rect(global_x, global_y, self.graph.bounds()) {
self.selected = CpuGraphSelection::Graph; self.selected = CpuGraphSelection::Graph;
self.graph.handle_mouse_event(event) 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.selected = CpuGraphSelection::Legend;
self.legend.handle_mouse_event(event) self.legend.handle_mouse_event(event)
} else { } else {

View File

@ -1,7 +1,12 @@
use std::collections::HashMap; use std::collections::HashMap;
use crossterm::event::{KeyEvent, MouseEvent}; 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::{ use crate::{
app::{event::EventResult, text_table::Column}, app::{event::EventResult, text_table::Column},
@ -92,14 +97,29 @@ impl Widget for DiskTable {
} }
fn draw<B: Backend>( fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>, &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData,
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 draw_area = block.inner(area);
let (table, widths, mut tui_state) = let (table, widths, mut tui_state) =
self.table self.table
.create_draw_table(painter, &data.disk_data, draw_area); .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); f.render_stateful_widget(table.block(block).widths(&widths), area, &mut tui_state);
} }
} }

View File

@ -1,22 +1,23 @@
use std::collections::HashMap; 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 unicode_segmentation::GraphemeCursor;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::Rect, layout::Rect,
widgets::{Block, TableState}, widgets::{Block, Borders, TableState},
Frame, Frame,
}; };
use crate::{ use crate::{
app::{ app::{
event::{does_point_intersect_rect, EventResult, MultiKey, MultiKeyResult}, event::{EventResult, MultiKey, MultiKeyResult},
query::*, query::*,
}, },
canvas::{DisplayableData, Painter}, canvas::{DisplayableData, Painter},
data_harvester::processes::{self, ProcessSorting}, data_harvester::processes::{self, ProcessSorting},
options::ProcessDefaults,
}; };
use ProcessSorting::*; use ProcessSorting::*;
@ -648,7 +649,7 @@ pub struct ProcessManager {
selected: ProcessManagerSelection, selected: ProcessManagerSelection,
in_tree_mode: bool, in_tree_mode: bool,
show_sort: bool, show_sort: bool, // TODO: Add this for temp and disk???
show_search: bool, show_search: bool,
search_modifiers: SearchModifiers, search_modifiers: SearchModifiers,
@ -656,19 +657,28 @@ pub struct ProcessManager {
impl ProcessManager { impl ProcessManager {
/// Creates a new [`ProcessManager`]. /// Creates a new [`ProcessManager`].
pub fn new(default_in_tree_mode: bool) -> Self { pub fn new(process_defaults: &ProcessDefaults) -> Self {
Self { let process_table_columns = vec![];
let mut manager = Self {
bounds: Rect::default(), bounds: Rect::default(),
process_table: TextTable::new(vec![]), // TODO: Do this process_table: TextTable::new(process_table_columns), // TODO: Do this
sort_table: TextTable::new(vec![]), // TODO: Do this too sort_table: TextTable::new(vec![]), // TODO: Do this too
search_input: TextInput::new(), 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, selected: ProcessManagerSelection::Processes,
in_tree_mode: default_in_tree_mode, in_tree_mode: false,
show_sort: false, show_sort: false,
show_search: false, show_search: false,
search_modifiers: SearchModifiers::default(), 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 { fn open_search(&mut self) -> EventResult {
@ -800,20 +810,27 @@ impl Component for ProcessManager {
} }
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
let global_x = event.column; match &event.kind {
let global_y = event.row; MouseEventKind::Down(MouseButton::Left) => {
if self.process_table.does_intersect_mouse(&event) {
if does_point_intersect_rect(global_x, global_y, self.process_table.bounds()) { self.selected = ProcessManagerSelection::Processes;
self.selected = ProcessManagerSelection::Processes; self.process_table.handle_mouse_event(event)
self.process_table.handle_mouse_event(event) } else if self.sort_table.does_intersect_mouse(&event) {
} else if does_point_intersect_rect(global_x, global_y, self.sort_table.bounds()) { self.selected = ProcessManagerSelection::Sort;
self.selected = ProcessManagerSelection::Sort; self.sort_table.handle_mouse_event(event)
self.sort_table.handle_mouse_event(event) } else if self.search_input.does_intersect_mouse(&event) {
} else if does_point_intersect_rect(global_x, global_y, self.search_input.bounds()) { self.selected = ProcessManagerSelection::Search;
self.selected = ProcessManagerSelection::Search; self.search_input.handle_mouse_event(event)
self.search_input.handle_mouse_event(event) } else {
} else { EventResult::NoRedraw
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<B: Backend>( fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>, &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData,
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 draw_area = block.inner(area);
let (process_table, widths, mut tui_state) = self.process_table.create_draw_table( let (process_table, widths, mut tui_state) = self.process_table.create_draw_table(
painter, painter,
@ -834,6 +860,12 @@ impl Widget for ProcessManager {
draw_area, 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( f.render_stateful_widget(
process_table.block(block).widths(&widths), process_table.block(block).widths(&widths),
area, area,

View File

@ -1,7 +1,12 @@
use std::collections::HashMap; use std::collections::HashMap;
use crossterm::event::{KeyEvent, MouseEvent}; 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::{ use crate::{
app::{event::EventResult, text_table::Column}, app::{event::EventResult, text_table::Column},
@ -52,7 +57,7 @@ pub struct TempTable {
impl Default for TempTable { impl Default for TempTable {
fn default() -> Self { fn default() -> Self {
let table = TextTable::new(vec![ 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)), Column::new_hard("Temp", None, false, Some(4)),
]) ])
.left_to_right(false); .left_to_right(false);
@ -88,14 +93,29 @@ impl Widget for TempTable {
} }
fn draw<B: Backend>( fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>, &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData,
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 draw_area = block.inner(area);
let (table, widths, mut tui_state) = let (table, widths, mut tui_state) =
self.table self.table
.create_draw_table(painter, &data.temp_sensor_data, draw_area); .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); f.render_stateful_widget(table.block(block).widths(&widths), area, &mut tui_state);
} }
} }

View File

@ -4,7 +4,13 @@
#[macro_use] #[macro_use]
extern crate log; extern crate log;
use bottom::{app::event::EventResult, canvas, constants::*, data_conversion::*, options::*, *}; use bottom::{
app::event::{EventResult, ReturnSignalResult},
canvas,
constants::*,
options::*,
*,
};
use std::{ use std::{
boxed::Box, boxed::Box,
@ -85,7 +91,8 @@ fn main() -> Result<()> {
}; };
// Event loop // 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( let _collection_thread = create_collection_thread(
sender, sender,
collection_thread_ctrl_receiver, collection_thread_ctrl_receiver,
@ -114,129 +121,30 @@ fn main() -> Result<()> {
ctrlc::set_handler(move || { ctrlc::set_handler(move || {
ist_clone.store(true, Ordering::SeqCst); ist_clone.store(true, Ordering::SeqCst);
})?; })?;
let mut first_run = true;
while !is_terminated.load(Ordering::SeqCst) { while !is_terminated.load(Ordering::SeqCst) {
if let Ok(recv) = receiver.recv_timeout(Duration::from_millis(TICK_RATE_IN_MILLISECONDS)) { if let Ok(recv) = receiver.recv_timeout(Duration::from_millis(TICK_RATE_IN_MILLISECONDS)) {
match recv { match app.handle_event(recv) {
BottomEvent::KeyInput(event) => { EventResult::Quit => {
match handle_key_event(event, &mut app, &collection_sender) { break;
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::MouseInput(event) => match handle_mouse_event(event, &mut app) { EventResult::Redraw => {
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: _,
} => {
try_drawing(&mut terminal, &mut app, &mut painter)?; try_drawing(&mut terminal, &mut app, &mut painter)?;
} }
BottomEvent::Clean => { EventResult::NoRedraw => {
app.data_collection continue;
.clean_data(constants::STALE_MAX_MILLISECONDS);
} }
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;
}
},
} }
} }
} }

View File

@ -1,4 +1,4 @@
use std::{collections::HashMap, str::FromStr}; use std::{borrow::Cow, collections::HashMap, str::FromStr};
use fxhash::FxHashMap; use fxhash::FxHashMap;
use indextree::{Arena, NodeId}; use indextree::{Arena, NodeId};
@ -6,7 +6,7 @@ use tui::{
backend::Backend, backend::Backend,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
text::{Span, Spans}, text::{Span, Spans},
widgets::{Block, Borders, Paragraph}, widgets::Paragraph,
Frame, Terminal, Frame, Terminal,
}; };
@ -41,11 +41,10 @@ pub struct DisplayableData {
pub total_tx_display: String, pub total_tx_display: String,
pub network_data_rx: Vec<Point>, pub network_data_rx: Vec<Point>,
pub network_data_tx: Vec<Point>, pub network_data_tx: Vec<Point>,
pub disk_data: Vec<Vec<String>>, pub disk_data: Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>>,
pub temp_sensor_data: Vec<Vec<String>>, pub temp_sensor_data: Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>>,
pub single_process_data: HashMap<Pid, ConvertedProcessData>, // Contains single process data, key is PID pub single_process_data: HashMap<Pid, ConvertedProcessData>, // Contains single process data, key is PID
pub finalized_process_data_map: HashMap<u64, Vec<ConvertedProcessData>>, // What's actually displayed, key is the widget ID. pub stringified_process_data_map: HashMap<NodeId, Vec<(Vec<(String, Option<String>)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID
pub stringified_process_data_map: HashMap<u64, Vec<(Vec<(String, Option<String>)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID
pub mem_labels: Option<(String, String)>, pub mem_labels: Option<(String, String)>,
pub swap_labels: Option<(String, String)>, pub swap_labels: Option<(String, String)>,
@ -342,10 +341,7 @@ impl Painter {
.widget_lookup_map .widget_lookup_map
.get_mut(&app_state.selected_widget) .get_mut(&app_state.selected_widget)
{ {
let block = Block::default() current_widget.draw(self, f, draw_area, canvas_data, true);
.border_style(self.colours.highlighted_border_style)
.borders(Borders::ALL);
current_widget.draw(self, f, draw_area, block, canvas_data);
} }
} else { } else {
/// A simple traversal through the `arena`. /// A simple traversal through the `arena`.
@ -396,14 +392,7 @@ impl Painter {
} }
LayoutNode::Widget => { LayoutNode::Widget => {
if let Some(widget) = lookup_map.get_mut(&node) { if let Some(widget) = lookup_map.get_mut(&node) {
let block = Block::default() widget.draw(painter, f, area, canvas_data, selected_id == node);
.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);
} }
} }
} }

View File

@ -2,22 +2,18 @@ pub mod basic_table_arrows;
pub mod battery_display; pub mod battery_display;
pub mod cpu_basic; pub mod cpu_basic;
pub mod cpu_graph; pub mod cpu_graph;
pub mod disk_table;
pub mod mem_basic; pub mod mem_basic;
pub mod mem_graph; pub mod mem_graph;
pub mod network_basic; pub mod network_basic;
pub mod network_graph; pub mod network_graph;
pub mod process_table; pub mod process_table;
pub mod temp_table;
pub use basic_table_arrows::*; pub use basic_table_arrows::*;
pub use battery_display::*; pub use battery_display::*;
pub use cpu_basic::*; pub use cpu_basic::*;
pub use cpu_graph::*; pub use cpu_graph::*;
pub use disk_table::*;
pub use mem_basic::*; pub use mem_basic::*;
pub use mem_graph::*; pub use mem_graph::*;
pub use network_basic::*; pub use network_basic::*;
pub use network_graph::*; pub use network_graph::*;
pub use process_table::*; pub use process_table::*;
pub use temp_table::*;

View File

@ -7,6 +7,7 @@ use crate::{
constants::*, constants::*,
}; };
use indextree::NodeId;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
@ -101,9 +102,9 @@ static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: Lazy<Vec<Option<f64>>> = La
pub fn draw_process_features<B: Backend>( pub fn draw_process_features<B: Backend>(
painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, 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 search_height = if draw_border { 5 } else { 3 };
let is_sort_open = process_widget_state.is_sort_open; let is_sort_open = process_widget_state.is_sort_open;
let header_len = process_widget_state.columns.longest_header_len; let header_len = process_widget_state.columns.longest_header_len;
@ -122,7 +123,7 @@ pub fn draw_process_features<B: Backend>(
app_state, app_state,
processes_chunk[1], processes_chunk[1],
draw_border, draw_border,
widget_id + 1, widget_id,
); );
} }
@ -133,14 +134,7 @@ pub fn draw_process_features<B: Backend>(
.split(proc_draw_loc); .split(proc_draw_loc);
proc_draw_loc = processes_chunk[1]; proc_draw_loc = processes_chunk[1];
draw_process_sort( draw_process_sort(painter, f, app_state, processes_chunk[0], draw_border, 1);
painter,
f,
app_state,
processes_chunk[0],
draw_border,
widget_id + 2,
);
} }
draw_processes_table(painter, f, app_state, proc_draw_loc, draw_border, widget_id); draw_processes_table(painter, f, app_state, proc_draw_loc, draw_border, widget_id);
@ -149,17 +143,17 @@ pub fn draw_process_features<B: Backend>(
fn draw_processes_table<B: Backend>( fn draw_processes_table<B: Backend>(
painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, 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(); 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 = let recalculate_column_widths =
should_get_widget_bounds || proc_widget_state.requires_redraw; should_get_widget_bounds || proc_widget_state.requires_redraw;
if proc_widget_state.requires_redraw { if proc_widget_state.requires_redraw {
proc_widget_state.requires_redraw = false; 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() let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)]) .constraints([Constraint::Percentage(100)])
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 }) .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
@ -178,7 +172,7 @@ fn draw_processes_table<B: Backend>(
let title_base = if app_state.app_config_fields.show_table_scroll_position { let title_base = if app_state.app_config_fields.show_table_scroll_position {
if let Some(finalized_process_data) = app_state if let Some(finalized_process_data) = app_state
.canvas_data .canvas_data
.finalized_process_data_map .stringified_process_data_map
.get(&widget_id) .get(&widget_id)
{ {
let title = format!( let title = format!(
@ -511,7 +505,7 @@ fn draw_processes_table<B: Backend>(
if app_state.should_get_widget_bounds() { if app_state.should_get_widget_bounds() {
// Update draw loc in widget map // 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.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some(( widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width, margined_draw_loc.x + margined_draw_loc.width,
@ -524,7 +518,7 @@ fn draw_processes_table<B: Backend>(
fn draw_search_field<B: Backend>( fn draw_search_field<B: Backend>(
painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, 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>( fn build_query<'a>(
is_on_widget: bool, grapheme_indices: GraphemeIndices<'a>, start_position: usize, is_on_widget: bool, grapheme_indices: GraphemeIndices<'a>, start_position: usize,
@ -565,8 +559,8 @@ fn draw_search_field<B: Backend>(
} }
// TODO: Make the cursor scroll back if there's space! // 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)) { if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&1) {
let is_on_widget = widget_id == app_state.current_widget.widget_id; let is_on_widget = false;
let num_columns = usize::from(draw_loc.width); let num_columns = usize::from(draw_loc.width);
let search_title = "> "; let search_title = "> ";
@ -728,7 +722,7 @@ fn draw_search_field<B: Backend>(
if app_state.should_get_widget_bounds() { if app_state.should_get_widget_bounds() {
// Update draw loc in widget map // 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.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some(( widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width, margined_draw_loc.x + margined_draw_loc.width,

View File

@ -19,7 +19,7 @@ pub const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u64 = 1000;
pub const MAX_KEY_TIMEOUT_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) // 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; pub const TIME_LABEL_HEIGHT_LIMIT: u16 = 7;
// For kill signals // For kill signals

View File

@ -8,6 +8,7 @@ use crate::{
use data_harvester::processes::ProcessSorting; use data_harvester::processes::ProcessSorting;
use fxhash::FxBuildHasher; use fxhash::FxBuildHasher;
use indexmap::IndexSet; use indexmap::IndexSet;
use std::borrow::Cow;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
/// Point is of time, data /// Point is of time, data
@ -83,81 +84,97 @@ pub struct ConvertedCpuData {
pub legend_value: String, pub legend_value: String,
} }
pub fn convert_temp_row(app: &AppState) -> Vec<Vec<String>> { pub fn convert_temp_row(
app: &AppState,
) -> Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>> {
let current_data = &app.data_collection; let current_data = &app.data_collection;
let temp_type = &app.app_config_fields.temperature_type; let temp_type = &app.app_config_fields.temperature_type;
let mut sensor_vector: Vec<Vec<String>> = current_data if current_data.temp_harvest.is_empty() {
.temp_harvest vec![vec![
.iter() ("No Sensors Found".into(), Some("N/A".into())),
.map(|temp_harvest| { ("".into(), None),
vec![ ]]
temp_harvest.name.clone(), } else {
(temp_harvest.temperature.ceil() as u64).to_string() let (unit_long, unit_short) = match temp_type {
+ match temp_type { data_harvester::temperature::TemperatureType::Celsius => ("°C", "C"),
data_harvester::temperature::TemperatureType::Celsius => "°C", data_harvester::temperature::TemperatureType::Kelvin => ("K", "K"),
data_harvester::temperature::TemperatureType::Kelvin => "K", data_harvester::temperature::TemperatureType::Fahrenheit => ("°F", "F"),
data_harvester::temperature::TemperatureType::Fahrenheit => "°F", };
},
]
})
.collect();
if sensor_vector.is_empty() { current_data
sensor_vector.push(vec!["No Sensors Found".to_string(), "".to_string()]); .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<Vec<String>> { pub fn convert_disk_row(
let mut disk_vector: Vec<Vec<String>> = Vec::new(); current_data: &data_farmer::DataCollection,
) -> Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>> {
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(&current_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 let usage_fmt = if let (Some(used_space), Some(total_space)) =
.disk_harvest (disk.used_space, disk.total_space)
.iter() {
.zip(&current_data.io_labels) Cow::Owned(format!(
.for_each(|(disk, (io_read, io_write))| { "{:.0}%",
let free_space_fmt = if let Some(free_space) = disk.free_space { used_space as f64 / total_space as f64 * 100_f64
let converted_free_space = get_decimal_bytes(free_space); ))
format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1) } else {
} else { "N/A".into()
"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)) = vec![
(disk.used_space, disk.total_space) (disk.name.clone().into(), None),
{ (disk.mount_point.clone().into(), None),
format!("{:.0}%", used_space as f64 / total_space as f64 * 100_f64) (usage_fmt, None),
} else { (free_space_fmt, None),
"N/A".to_string() (total_space_fmt, None),
}; (io_read.clone().into(), None),
(io_write.clone().into(), None),
disk_vector.push(vec![ ]
disk.name.to_string(), })
disk.mount_point.to_string(), .collect::<Vec<_>>()
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()]);
} }
disk_vector
} }
pub fn convert_cpu_data_points( pub fn convert_cpu_data_points(

View File

@ -369,108 +369,109 @@ pub fn update_all_process_lists(app: &mut AppState) {
} }
} }
fn update_final_process_list(app: &mut AppState, widget_id: u64) { fn update_final_process_list(_app: &mut AppState, _widget_id: u64) {
let process_states = app // TODO: [STATE] FINISH THIS
.proc_state // let process_states = app
.widget_states // .proc_state
.get(&widget_id) // .widget_states
.map(|process_state| { // .get(&widget_id)
( // .map(|process_state| {
process_state // (
.process_search_state // process_state
.search_state // .process_search_state
.is_invalid_or_blank_search(), // .search_state
process_state.is_using_command, // .is_invalid_or_blank_search(),
process_state.is_grouped, // process_state.is_using_command,
process_state.is_tree_mode, // 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 let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states {
if !app.is_frozen { // if !app.is_frozen {
convert_process_data( // convert_process_data(
&app.data_collection, // &app.data_collection,
&mut app.canvas_data.single_process_data, // &mut app.canvas_data.single_process_data,
#[cfg(target_family = "unix")] // #[cfg(target_family = "unix")]
&mut app.user_table, // &mut app.user_table,
); // );
} // }
let process_filter = app.get_process_filter(widget_id); // let process_filter = app.get_process_filter(widget_id);
let filtered_process_data: Vec<ConvertedProcessData> = if is_tree { // let filtered_process_data: Vec<ConvertedProcessData> = if is_tree {
app.canvas_data // app.canvas_data
.single_process_data // .single_process_data
.iter() // .iter()
.map(|(_pid, process)| { // .map(|(_pid, process)| {
let mut process_clone = process.clone(); // let mut process_clone = process.clone();
if !is_invalid_or_blank { // if !is_invalid_or_blank {
if let Some(process_filter) = process_filter { // if let Some(process_filter) = process_filter {
process_clone.is_disabled_entry = // process_clone.is_disabled_entry =
!process_filter.check(&process_clone, is_using_command); // !process_filter.check(&process_clone, is_using_command);
} // }
} // }
process_clone // process_clone
}) // })
.collect::<Vec<_>>() // .collect::<Vec<_>>()
} else { // } else {
app.canvas_data // app.canvas_data
.single_process_data // .single_process_data
.iter() // .iter()
.filter_map(|(_pid, process)| { // .filter_map(|(_pid, process)| {
if !is_invalid_or_blank { // if !is_invalid_or_blank {
if let Some(process_filter) = process_filter { // if let Some(process_filter) = process_filter {
if process_filter.check(process, is_using_command) { // if process_filter.check(process, is_using_command) {
Some(process) // Some(process)
} else { // } else {
None // None
} // }
} else { // } else {
Some(process) // Some(process)
} // }
} else { // } else {
Some(process) // Some(process)
} // }
}) // })
.cloned() // .cloned()
.collect::<Vec<_>>() // .collect::<Vec<_>>()
}; // };
if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) { // if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) {
let mut finalized_process_data = if is_tree { // let mut finalized_process_data = if is_tree {
tree_process_data( // tree_process_data(
&filtered_process_data, // &filtered_process_data,
is_using_command, // is_using_command,
&proc_widget_state.process_sorting_type, // &proc_widget_state.process_sorting_type,
proc_widget_state.is_process_sort_descending, // proc_widget_state.is_process_sort_descending,
) // )
} else if is_grouped { // } else if is_grouped {
group_process_data(&filtered_process_data, is_using_command) // group_process_data(&filtered_process_data, is_using_command)
} else { // } else {
filtered_process_data // filtered_process_data
}; // };
// Note tree mode is sorted well before this, as it's special. // // Note tree mode is sorted well before this, as it's special.
if !is_tree { // if !is_tree {
sort_process_data(&mut finalized_process_data, proc_widget_state); // sort_process_data(&mut finalized_process_data, proc_widget_state);
} // }
if proc_widget_state.scroll_state.current_scroll_position // if proc_widget_state.scroll_state.current_scroll_position
>= finalized_process_data.len() // >= finalized_process_data.len()
{ // {
proc_widget_state.scroll_state.current_scroll_position = // proc_widget_state.scroll_state.current_scroll_position =
finalized_process_data.len().saturating_sub(1); // finalized_process_data.len().saturating_sub(1);
proc_widget_state.scroll_state.previous_scroll_position = 0; // proc_widget_state.scroll_state.previous_scroll_position = 0;
proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down; // proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down;
} // }
app.canvas_data.stringified_process_data_map.insert( // app.canvas_data.stringified_process_data_map.insert(
widget_id, // widget_id,
stringify_process_data(proc_widget_state, &finalized_process_data), // stringify_process_data(proc_widget_state, &finalized_process_data),
); // );
app.canvas_data // app.canvas_data
.finalized_process_data_map // .finalized_process_data_map
.insert(widget_id, finalized_process_data); // .insert(widget_id, finalized_process_data);
} // }
} // }
} }
fn sort_process_data( fn sort_process_data(