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,
};
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::<Vec<_>>();
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<Pid>);
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<Pid>);
// 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<usize> {
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<usize> {
// 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) {

View File

@ -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()
}

View File

@ -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());

View File

@ -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<B: Backend>(
&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!

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 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,
}
}

View File

@ -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<KeyEvent>, default_descending: bool,
hard_length: Option<u16>,
) -> 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<String>],
columns: &[Column], data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>],
) -> Vec<DesiredColumnWidth> {
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::<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(
left_to_right: bool, mut desired_widths: Vec<DesiredColumnWidth>, total_width: u16,
) -> Vec<u16> {
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<u16> = 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<String>], area: Rect,
&mut self, painter: &Painter, data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>],
area: Rect,
) -> (Table<'_>, Vec<Constraint>, TableState) {
// TODO: Change data: &[Vec<String>] to &[Vec<Cow<'static, str>>]
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::<Vec<&str>>();
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::<Vec<&str>>();
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)
}

View File

@ -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 {

View File

@ -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<B: Backend>(
&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);
}
}

View File

@ -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<B: Backend>(
&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,

View File

@ -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<B: Backend>(
&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);
}
}

View File

@ -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;
}
},
}
}
}

View File

@ -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<Point>,
pub network_data_tx: Vec<Point>,
pub disk_data: Vec<Vec<String>>,
pub temp_sensor_data: Vec<Vec<String>>,
pub disk_data: Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>>,
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 finalized_process_data_map: HashMap<u64, Vec<ConvertedProcessData>>, // What's actually displayed, 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 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 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);
}
}
}

View File

@ -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::*;

View File

@ -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<Vec<Option<f64>>> = La
pub fn draw_process_features<B: Backend>(
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<B: Backend>(
app_state,
processes_chunk[1],
draw_border,
widget_id + 1,
widget_id,
);
}
@ -133,14 +134,7 @@ pub fn draw_process_features<B: Backend>(
.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<B: Backend>(
fn draw_processes_table<B: Backend>(
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<B: Backend>(
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<B: Backend>(
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<B: Backend>(
fn draw_search_field<B: Backend>(
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<B: Backend>(
}
// 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<B: Backend>(
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,

View File

@ -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

View File

@ -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<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 temp_type = &app.app_config_fields.temperature_type;
let mut sensor_vector: Vec<Vec<String>> = 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<Vec<String>> {
let mut disk_vector: Vec<Vec<String>> = Vec::new();
pub fn convert_disk_row(
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
.disk_harvest
.iter()
.zip(&current_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::<Vec<_>>()
}
disk_vector
}
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) {
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<ConvertedProcessData> = 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::<Vec<_>>()
} 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::<Vec<_>>()
};
// 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<ConvertedProcessData> = 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::<Vec<_>>()
// } 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::<Vec<_>>()
// };
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(