mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-28 16:14:16 +02:00
refactor: start moving over the event system
This commit is contained in:
parent
0afc371eaa
commit
6b69e373de
490
src/app.rs
490
src/app.rs
@ -13,7 +13,7 @@ use std::{
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crossterm::event::{KeyEvent, KeyModifiers};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEventKind};
|
||||
use fxhash::FxHashMap;
|
||||
use indextree::{Arena, NodeId};
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
@ -28,12 +28,14 @@ pub use widgets::*;
|
||||
use crate::{
|
||||
canvas,
|
||||
constants::{self, MAX_SIGNAL},
|
||||
data_conversion::*,
|
||||
units::data_units::DataUnit,
|
||||
update_final_process_list,
|
||||
utils::error::{BottomError, Result},
|
||||
BottomEvent, Pid,
|
||||
};
|
||||
|
||||
use self::event::{does_point_intersect_rect, EventResult, ReturnSignal};
|
||||
use self::event::{EventResult, ReturnSignal, ReturnSignalResult};
|
||||
|
||||
const MAX_SEARCH_LENGTH: usize = 200;
|
||||
|
||||
@ -269,9 +271,38 @@ impl AppState {
|
||||
// TODO: Write this.
|
||||
|
||||
if event.modifiers.is_empty() {
|
||||
todo!()
|
||||
} else if let KeyModifiers::ALT = event.modifiers {
|
||||
todo!()
|
||||
match event.code {
|
||||
KeyCode::Esc => {
|
||||
if self.is_expanded {
|
||||
self.is_expanded = false;
|
||||
Some(EventResult::Redraw)
|
||||
} else if self.help_dialog_state.is_showing_help {
|
||||
self.help_dialog_state.is_showing_help = false;
|
||||
self.help_dialog_state.scroll_state.current_scroll_index = 0;
|
||||
Some(EventResult::Redraw)
|
||||
} else if self.delete_dialog_state.is_showing_dd {
|
||||
self.close_dd();
|
||||
Some(EventResult::Redraw)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
KeyCode::Char('q') => Some(EventResult::Quit),
|
||||
KeyCode::Char('e') => {
|
||||
self.is_expanded = !self.is_expanded;
|
||||
Some(EventResult::Redraw)
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
self.help_dialog_state.is_showing_help = true;
|
||||
Some(EventResult::Redraw)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else if let KeyModifiers::CONTROL = event.modifiers {
|
||||
match event.code {
|
||||
KeyCode::Char('c') => Some(EventResult::Quit),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -296,11 +327,17 @@ 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;
|
||||
|
||||
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 {
|
||||
for (id, widget) in self.widget_lookup_map.iter_mut() {
|
||||
if does_point_intersect_rect(x, y, widget.bounds()) {
|
||||
if widget.does_intersect_mouse(&event) {
|
||||
let is_id_selected = self.selected_widget == *id;
|
||||
self.selected_widget = *id;
|
||||
|
||||
@ -313,13 +350,26 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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!
|
||||
|
@ -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
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
if self.num_items == 0 {
|
||||
return 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,7 +225,8 @@ impl Component for Scrollable {
|
||||
|
||||
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
||||
match event.kind {
|
||||
crossterm::event::MouseEventKind::Down(MouseButton::Left) => {
|
||||
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!
|
||||
@ -182,16 +239,17 @@ impl Component for Scrollable {
|
||||
if y > selected {
|
||||
let offset = y - selected;
|
||||
return self.move_down(offset);
|
||||
} else {
|
||||
} 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,
|
||||
}
|
||||
}
|
||||
|
@ -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,11 +380,23 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`Table`] given the [`TextTable`] and the given data, along with its
|
||||
@ -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)| {
|
||||
Row::new(
|
||||
row.iter()
|
||||
.zip(&calculated_widths)
|
||||
.map(|((text, shrunk_text), width)| {
|
||||
let width = *width as usize;
|
||||
let graphemes =
|
||||
UnicodeSegmentation::graphemes(cell.as_str(), true).collect::<Vec<&str>>();
|
||||
let graphemes = UnicodeSegmentation::graphemes(text.as_ref(), true)
|
||||
.collect::<Vec<&str>>();
|
||||
let grapheme_width = graphemes.len();
|
||||
if width < grapheme_width && width > 1 {
|
||||
Text::raw(format!("{}…", graphemes[..(width - 1)].concat()))
|
||||
if let Some(shrunk_text) = shrunk_text {
|
||||
Text::raw(shrunk_text.clone())
|
||||
} else {
|
||||
Text::raw(cell.to_owned())
|
||||
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,14 +506,19 @@ impl Component for TextTable {
|
||||
}
|
||||
|
||||
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
||||
// Note these are representing RELATIVE coordinates!
|
||||
if let MouseEventKind::Down(MouseButton::Left) = event.kind {
|
||||
if !self.does_intersect_mouse(&event) {
|
||||
return EventResult::NoRedraw;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
let (start, end) = column.x_bounds;
|
||||
if start >= x && end <= y {
|
||||
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;
|
||||
@ -479,10 +526,13 @@ impl Component for TextTable {
|
||||
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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
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,22 +810,29 @@ 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()) {
|
||||
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 does_point_intersect_rect(global_x, global_y, self.sort_table.bounds()) {
|
||||
} else if self.sort_table.does_intersect_mouse(&event) {
|
||||
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()) {
|
||||
} 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for ProcessManager {
|
||||
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
128
src/bin/main.rs
128
src/bin/main.rs
@ -4,7 +4,13 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use bottom::{app::event::EventResult, canvas, constants::*, data_conversion::*, options::*, *};
|
||||
use bottom::{
|
||||
app::event::{EventResult, ReturnSignalResult},
|
||||
canvas,
|
||||
constants::*,
|
||||
options::*,
|
||||
*,
|
||||
};
|
||||
|
||||
use std::{
|
||||
boxed::Box,
|
||||
@ -85,7 +91,8 @@ fn main() -> Result<()> {
|
||||
};
|
||||
|
||||
// Event loop
|
||||
let (collection_sender, collection_thread_ctrl_receiver) = mpsc::channel();
|
||||
// TODO: Add back collection sender
|
||||
let (_collection_sender, collection_thread_ctrl_receiver) = mpsc::channel();
|
||||
let _collection_thread = create_collection_thread(
|
||||
sender,
|
||||
collection_thread_ctrl_receiver,
|
||||
@ -114,129 +121,30 @@ fn main() -> Result<()> {
|
||||
ctrlc::set_handler(move || {
|
||||
ist_clone.store(true, Ordering::SeqCst);
|
||||
})?;
|
||||
let mut first_run = true;
|
||||
|
||||
while !is_terminated.load(Ordering::SeqCst) {
|
||||
if let Ok(recv) = receiver.recv_timeout(Duration::from_millis(TICK_RATE_IN_MILLISECONDS)) {
|
||||
match recv {
|
||||
BottomEvent::KeyInput(event) => {
|
||||
match handle_key_event(event, &mut app, &collection_sender) {
|
||||
match app.handle_event(recv) {
|
||||
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)?;
|
||||
}
|
||||
_ => {}
|
||||
EventResult::NoRedraw => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
BottomEvent::MouseInput(event) => match handle_mouse_event(event, &mut app) {
|
||||
EventResult::Quit => {
|
||||
EventResult::Signal(signal) => match app.handle_return_signal(signal) {
|
||||
ReturnSignalResult::Quit => {
|
||||
break;
|
||||
}
|
||||
EventResult::Redraw => {
|
||||
// TODO: Be even more granular! Maybe the event triggered no change, then we shouldn't redraw.
|
||||
force_redraw(&mut app);
|
||||
ReturnSignalResult::Redraw => {
|
||||
try_drawing(&mut terminal, &mut app, &mut painter)?;
|
||||
}
|
||||
_ => {}
|
||||
ReturnSignalResult::NoRedraw => {
|
||||
continue;
|
||||
}
|
||||
},
|
||||
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)?;
|
||||
}
|
||||
BottomEvent::Clean => {
|
||||
app.data_collection
|
||||
.clean_data(constants::STALE_MAX_MILLISECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
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"),
|
||||
};
|
||||
|
||||
current_data
|
||||
.temp_harvest
|
||||
.iter()
|
||||
.map(|temp_harvest| {
|
||||
let val = temp_harvest.temperature.ceil().to_string();
|
||||
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",
|
||||
},
|
||||
(temp_harvest.name.clone().into(), None),
|
||||
(
|
||||
format!("{}{}", val, unit_long).into(),
|
||||
Some(format!("{}{}", val, unit_short).into()),
|
||||
),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
if sensor_vector.is_empty() {
|
||||
sensor_vector.push(vec!["No Sensors Found".to_string(), "".to_string()]);
|
||||
.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(¤t_data.io_labels)
|
||||
.for_each(|(disk, (io_read, io_write))| {
|
||||
.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);
|
||||
format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1)
|
||||
Cow::Owned(format!(
|
||||
"{:.*}{}",
|
||||
0, converted_free_space.0, converted_free_space.1
|
||||
))
|
||||
} else {
|
||||
"N/A".to_string()
|
||||
"N/A".into()
|
||||
};
|
||||
let total_space_fmt = if let Some(total_space) = disk.total_space {
|
||||
let converted_total_space = get_decimal_bytes(total_space);
|
||||
format!(
|
||||
Cow::Owned(format!(
|
||||
"{:.*}{}",
|
||||
0, converted_total_space.0, converted_total_space.1
|
||||
)
|
||||
))
|
||||
} else {
|
||||
"N/A".to_string()
|
||||
"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)
|
||||
Cow::Owned(format!(
|
||||
"{:.0}%",
|
||||
used_space as f64 / total_space as f64 * 100_f64
|
||||
))
|
||||
} else {
|
||||
"N/A".to_string()
|
||||
"N/A".into()
|
||||
};
|
||||
|
||||
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(
|
||||
|
195
src/lib.rs
195
src/lib.rs
@ -369,108 +369,109 @@ pub fn update_all_process_lists(app: &mut AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_final_process_list(app: &mut AppState, widget_id: u64) {
|
||||
let process_states = app
|
||||
.proc_state
|
||||
.widget_states
|
||||
.get(&widget_id)
|
||||
.map(|process_state| {
|
||||
(
|
||||
process_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.is_invalid_or_blank_search(),
|
||||
process_state.is_using_command,
|
||||
process_state.is_grouped,
|
||||
process_state.is_tree_mode,
|
||||
)
|
||||
});
|
||||
fn update_final_process_list(_app: &mut AppState, _widget_id: u64) {
|
||||
// TODO: [STATE] FINISH THIS
|
||||
// let process_states = app
|
||||
// .proc_state
|
||||
// .widget_states
|
||||
// .get(&widget_id)
|
||||
// .map(|process_state| {
|
||||
// (
|
||||
// process_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .is_invalid_or_blank_search(),
|
||||
// process_state.is_using_command,
|
||||
// process_state.is_grouped,
|
||||
// process_state.is_tree_mode,
|
||||
// )
|
||||
// });
|
||||
|
||||
if let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states {
|
||||
if !app.is_frozen {
|
||||
convert_process_data(
|
||||
&app.data_collection,
|
||||
&mut app.canvas_data.single_process_data,
|
||||
#[cfg(target_family = "unix")]
|
||||
&mut app.user_table,
|
||||
);
|
||||
}
|
||||
let process_filter = app.get_process_filter(widget_id);
|
||||
let filtered_process_data: Vec<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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user