mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-27 15:44:17 +02:00
refactor: delete more stuff
Mostly previously re-added files during the merge conflict resolution, and a lot of unused code. Still more to delete after I finish rewriting the process kill dialog.
This commit is contained in:
parent
b6ca3e0a22
commit
9089231bc4
450
src/app.rs
450
src/app.rs
@ -7,24 +7,20 @@ mod process_killer;
|
||||
pub mod query;
|
||||
pub mod widgets;
|
||||
|
||||
use std::{collections::HashMap, time::Instant};
|
||||
use std::time::Instant;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
|
||||
use fxhash::FxHashMap;
|
||||
use indextree::{Arena, NodeId};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub use data_farmer::*;
|
||||
use data_harvester::{processes, temperature};
|
||||
use data_harvester::temperature;
|
||||
pub use filter::*;
|
||||
use layout_manager::*;
|
||||
pub use widgets::*;
|
||||
|
||||
use crate::{
|
||||
canvas, constants,
|
||||
units::data_units::DataUnit,
|
||||
utils::error::{BottomError, Result},
|
||||
BottomEvent, Pid,
|
||||
canvas, constants, units::data_units::DataUnit, utils::error::Result, BottomEvent, Pid,
|
||||
};
|
||||
|
||||
use self::event::{ComponentEventResult, EventResult, ReturnSignal};
|
||||
@ -91,7 +87,7 @@ pub struct AppConfigFields {
|
||||
pub hide_time: bool,
|
||||
pub autohide_time: bool,
|
||||
pub use_old_network_legend: bool,
|
||||
pub table_gap: u16, // TODO: Just make this a bool...
|
||||
pub table_gap: u16, // TODO: [Config, Refactor] Just make this a bool...
|
||||
pub disable_click: bool,
|
||||
pub no_write: bool,
|
||||
pub show_table_scroll_position: bool,
|
||||
@ -129,23 +125,8 @@ pub struct AppState {
|
||||
pub filters: DataFilters,
|
||||
pub app_config_fields: AppConfigFields,
|
||||
|
||||
// --- Eventually delete/rewrite ---
|
||||
// --- FIXME: TO DELETE/REWRITE ---
|
||||
pub delete_dialog_state: AppDeleteDialogState,
|
||||
|
||||
// --- TO DELETE ---
|
||||
pub cpu_state: CpuState,
|
||||
pub mem_state: MemState,
|
||||
pub net_state: NetState,
|
||||
pub proc_state: ProcState,
|
||||
pub temp_state: TempState,
|
||||
pub disk_state: DiskState,
|
||||
pub battery_state: BatteryState,
|
||||
pub basic_table_widget_state: Option<BasicTableWidgetState>,
|
||||
pub widget_map: HashMap<u64, BottomWidget>,
|
||||
pub current_widget: BottomWidget,
|
||||
|
||||
pub basic_mode_use_percent: bool,
|
||||
|
||||
pub is_force_redraw: bool,
|
||||
|
||||
pub is_determining_widget_boundary: bool,
|
||||
@ -190,17 +171,6 @@ impl AppState {
|
||||
data_collection: Default::default(),
|
||||
is_expanded: Default::default(),
|
||||
delete_dialog_state: Default::default(),
|
||||
cpu_state: Default::default(),
|
||||
mem_state: Default::default(),
|
||||
net_state: Default::default(),
|
||||
proc_state: Default::default(),
|
||||
temp_state: Default::default(),
|
||||
disk_state: Default::default(),
|
||||
battery_state: Default::default(),
|
||||
basic_table_widget_state: Default::default(),
|
||||
widget_map: Default::default(),
|
||||
current_widget: Default::default(),
|
||||
basic_mode_use_percent: Default::default(),
|
||||
is_force_redraw: Default::default(),
|
||||
is_determining_widget_boundary: Default::default(),
|
||||
frozen_state: Default::default(),
|
||||
@ -484,7 +454,7 @@ impl AppState {
|
||||
BottomEvent::Update(new_data) => {
|
||||
self.data_collection.eat_data(new_data);
|
||||
|
||||
// TODO: Optimization for dialogs; don't redraw here.
|
||||
// TODO: [Optimization] Optimization for dialogs - don't redraw on an update!
|
||||
|
||||
if !self.is_frozen() {
|
||||
let data_collection = &self.data_collection;
|
||||
@ -509,104 +479,6 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_in_search_widget(&self) -> bool {
|
||||
matches!(
|
||||
self.current_widget.widget_type,
|
||||
BottomWidgetType::ProcSearch
|
||||
)
|
||||
}
|
||||
|
||||
fn is_in_dialog(&self) -> bool {
|
||||
self.delete_dialog_state.is_showing_dd
|
||||
}
|
||||
|
||||
fn ignore_normal_keybinds(&self) -> bool {
|
||||
self.is_in_dialog()
|
||||
}
|
||||
|
||||
pub fn on_tab(&mut self) {
|
||||
// Allow usage whilst only in processes
|
||||
|
||||
if !self.ignore_normal_keybinds() {
|
||||
match self.current_widget.widget_type {
|
||||
BottomWidgetType::Cpu => {
|
||||
if let Some(cpu_widget_state) = self
|
||||
.cpu_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
cpu_widget_state.is_multi_graph_mode =
|
||||
!cpu_widget_state.is_multi_graph_mode;
|
||||
}
|
||||
}
|
||||
BottomWidgetType::Proc => {
|
||||
if let Some(proc_widget_state) = self
|
||||
.proc_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
// Do NOT allow when in tree mode!
|
||||
if !proc_widget_state.is_tree_mode {
|
||||
// Toggles process widget grouping state
|
||||
proc_widget_state.is_grouped = !(proc_widget_state.is_grouped);
|
||||
|
||||
// Forcefully switch off column if we were on it...
|
||||
if (proc_widget_state.is_grouped
|
||||
&& (proc_widget_state.process_sorting_type
|
||||
== processes::ProcessSorting::Pid
|
||||
|| proc_widget_state.process_sorting_type
|
||||
== processes::ProcessSorting::User
|
||||
|| proc_widget_state.process_sorting_type
|
||||
== processes::ProcessSorting::State))
|
||||
|| (!proc_widget_state.is_grouped
|
||||
&& proc_widget_state.process_sorting_type
|
||||
== processes::ProcessSorting::Count)
|
||||
{
|
||||
proc_widget_state.process_sorting_type =
|
||||
processes::ProcessSorting::CpuPercent; // Go back to default, negate PID for group
|
||||
proc_widget_state.is_process_sort_descending = true;
|
||||
}
|
||||
|
||||
proc_widget_state.columns.set_to_sorted_index_from_type(
|
||||
&proc_widget_state.process_sorting_type,
|
||||
);
|
||||
|
||||
proc_widget_state.columns.try_set(
|
||||
&processes::ProcessSorting::State,
|
||||
!(proc_widget_state.is_grouped),
|
||||
);
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
proc_widget_state.columns.try_set(
|
||||
&processes::ProcessSorting::User,
|
||||
!(proc_widget_state.is_grouped),
|
||||
);
|
||||
|
||||
proc_widget_state
|
||||
.columns
|
||||
.toggle(&processes::ProcessSorting::Count);
|
||||
proc_widget_state
|
||||
.columns
|
||||
.toggle(&processes::ProcessSorting::Pid);
|
||||
|
||||
proc_widget_state.requires_redraw = true;
|
||||
self.proc_state.force_update = Some(self.current_widget.widget_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// I don't like this, but removing it causes a bunch of breakage.
|
||||
/// Use ``proc_widget_state.is_grouped`` if possible!
|
||||
pub fn is_grouped(&self, widget_id: u64) -> bool {
|
||||
if let Some(proc_widget_state) = self.proc_state.widget_states.get(&widget_id) {
|
||||
proc_widget_state.is_grouped
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
pub fn on_number(&mut self, number_char: char) {
|
||||
if self.delete_dialog_state.is_showing_dd {
|
||||
@ -642,149 +514,122 @@ impl AppState {
|
||||
}
|
||||
|
||||
pub fn on_left_key(&mut self) {
|
||||
if !self.is_in_dialog() {
|
||||
match self.current_widget.widget_type {
|
||||
BottomWidgetType::ProcSearch => {
|
||||
let is_in_search_widget = self.is_in_search_widget();
|
||||
if let Some(proc_widget_state) = self
|
||||
.proc_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id - 1)
|
||||
{
|
||||
if is_in_search_widget {
|
||||
let prev_cursor = proc_widget_state.get_search_cursor_position();
|
||||
proc_widget_state
|
||||
.search_walk_back(proc_widget_state.get_search_cursor_position());
|
||||
if proc_widget_state.get_search_cursor_position() < prev_cursor {
|
||||
let str_slice = &proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.current_search_query
|
||||
[proc_widget_state.get_search_cursor_position()..prev_cursor];
|
||||
proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.char_cursor_position -= UnicodeWidthStr::width(str_slice);
|
||||
proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.cursor_direction = CursorDirection::Left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BottomWidgetType::Battery => {
|
||||
if !self.canvas_data.battery_data.is_empty() {
|
||||
if let Some(battery_widget_state) = self
|
||||
.battery_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
if battery_widget_state.currently_selected_battery_index > 0 {
|
||||
battery_widget_state.currently_selected_battery_index -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if self.delete_dialog_state.is_showing_dd {
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
if self.app_config_fields.is_advanced_kill {
|
||||
match self.delete_dialog_state.selected_signal {
|
||||
KillSignal::Kill(prev_signal) => {
|
||||
self.delete_dialog_state.selected_signal = match prev_signal - 1 {
|
||||
0 => KillSignal::Cancel,
|
||||
// 32+33 are skipped
|
||||
33 => KillSignal::Kill(31),
|
||||
signal => KillSignal::Kill(signal),
|
||||
};
|
||||
}
|
||||
KillSignal::Cancel => {}
|
||||
};
|
||||
} else {
|
||||
self.delete_dialog_state.selected_signal = KillSignal::default();
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.delete_dialog_state.selected_signal = KillSignal::Kill(1);
|
||||
}
|
||||
}
|
||||
// if !self.is_in_dialog() {
|
||||
// match self.current_widget.widget_type {
|
||||
// BottomWidgetType::ProcSearch => {
|
||||
// let is_in_search_widget = self.is_in_search_widget();
|
||||
// if let Some(proc_widget_state) = self
|
||||
// .proc_state
|
||||
// .get_mut_widget_state(self.current_widget.widget_id - 1)
|
||||
// {
|
||||
// if is_in_search_widget {
|
||||
// let prev_cursor = proc_widget_state.get_search_cursor_position();
|
||||
// proc_widget_state
|
||||
// .search_walk_back(proc_widget_state.get_search_cursor_position());
|
||||
// if proc_widget_state.get_search_cursor_position() < prev_cursor {
|
||||
// let str_slice = &proc_widget_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .current_search_query
|
||||
// [proc_widget_state.get_search_cursor_position()..prev_cursor];
|
||||
// proc_widget_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .char_cursor_position -= UnicodeWidthStr::width(str_slice);
|
||||
// proc_widget_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .cursor_direction = CursorDirection::Left;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _ => {}
|
||||
// }
|
||||
// } else if self.delete_dialog_state.is_showing_dd {
|
||||
// #[cfg(target_family = "unix")]
|
||||
// {
|
||||
// if self.app_config_fields.is_advanced_kill {
|
||||
// match self.delete_dialog_state.selected_signal {
|
||||
// KillSignal::Kill(prev_signal) => {
|
||||
// self.delete_dialog_state.selected_signal = match prev_signal - 1 {
|
||||
// 0 => KillSignal::Cancel,
|
||||
// // 32+33 are skipped
|
||||
// 33 => KillSignal::Kill(31),
|
||||
// signal => KillSignal::Kill(signal),
|
||||
// };
|
||||
// }
|
||||
// KillSignal::Cancel => {}
|
||||
// };
|
||||
// } else {
|
||||
// self.delete_dialog_state.selected_signal = KillSignal::default();
|
||||
// }
|
||||
// }
|
||||
// #[cfg(target_os = "windows")]
|
||||
// {
|
||||
// self.delete_dialog_state.selected_signal = KillSignal::Kill(1);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn on_right_key(&mut self) {
|
||||
if !self.is_in_dialog() {
|
||||
match self.current_widget.widget_type {
|
||||
BottomWidgetType::ProcSearch => {
|
||||
let is_in_search_widget = self.is_in_search_widget();
|
||||
if let Some(proc_widget_state) = self
|
||||
.proc_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id - 1)
|
||||
{
|
||||
if is_in_search_widget {
|
||||
let prev_cursor = proc_widget_state.get_search_cursor_position();
|
||||
proc_widget_state.search_walk_forward(
|
||||
proc_widget_state.get_search_cursor_position(),
|
||||
);
|
||||
if proc_widget_state.get_search_cursor_position() > prev_cursor {
|
||||
let str_slice = &proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.current_search_query
|
||||
[prev_cursor..proc_widget_state.get_search_cursor_position()];
|
||||
proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.char_cursor_position += UnicodeWidthStr::width(str_slice);
|
||||
proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.cursor_direction = CursorDirection::Right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BottomWidgetType::Battery => {
|
||||
if !self.canvas_data.battery_data.is_empty() {
|
||||
let battery_count = self.canvas_data.battery_data.len();
|
||||
if let Some(battery_widget_state) = self
|
||||
.battery_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
if battery_widget_state.currently_selected_battery_index
|
||||
< battery_count - 1
|
||||
{
|
||||
battery_widget_state.currently_selected_battery_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if self.delete_dialog_state.is_showing_dd {
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
if self.app_config_fields.is_advanced_kill {
|
||||
let new_signal = match self.delete_dialog_state.selected_signal {
|
||||
KillSignal::Cancel => 1,
|
||||
// 32+33 are skipped
|
||||
#[cfg(target_os = "linux")]
|
||||
KillSignal::Kill(31) => 34,
|
||||
#[cfg(target_os = "macos")]
|
||||
KillSignal::Kill(31) => 31,
|
||||
KillSignal::Kill(64) => 64,
|
||||
KillSignal::Kill(signal) => signal + 1,
|
||||
};
|
||||
self.delete_dialog_state.selected_signal = KillSignal::Kill(new_signal);
|
||||
} else {
|
||||
self.delete_dialog_state.selected_signal = KillSignal::Cancel;
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.delete_dialog_state.selected_signal = KillSignal::Cancel;
|
||||
}
|
||||
}
|
||||
// if !self.is_in_dialog() {
|
||||
// match self.current_widget.widget_type {
|
||||
// BottomWidgetType::ProcSearch => {
|
||||
// let is_in_search_widget = self.is_in_search_widget();
|
||||
// if let Some(proc_widget_state) = self
|
||||
// .proc_state
|
||||
// .get_mut_widget_state(self.current_widget.widget_id - 1)
|
||||
// {
|
||||
// if is_in_search_widget {
|
||||
// let prev_cursor = proc_widget_state.get_search_cursor_position();
|
||||
// proc_widget_state.search_walk_forward(
|
||||
// proc_widget_state.get_search_cursor_position(),
|
||||
// );
|
||||
// if proc_widget_state.get_search_cursor_position() > prev_cursor {
|
||||
// let str_slice = &proc_widget_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .current_search_query
|
||||
// [prev_cursor..proc_widget_state.get_search_cursor_position()];
|
||||
// proc_widget_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .char_cursor_position += UnicodeWidthStr::width(str_slice);
|
||||
// proc_widget_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .cursor_direction = CursorDirection::Right;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _ => {}
|
||||
// }
|
||||
// } else if self.delete_dialog_state.is_showing_dd {
|
||||
// #[cfg(target_family = "unix")]
|
||||
// {
|
||||
// if self.app_config_fields.is_advanced_kill {
|
||||
// let new_signal = match self.delete_dialog_state.selected_signal {
|
||||
// KillSignal::Cancel => 1,
|
||||
// // 32+33 are skipped
|
||||
// #[cfg(target_os = "linux")]
|
||||
// KillSignal::Kill(31) => 34,
|
||||
// #[cfg(target_os = "macos")]
|
||||
// KillSignal::Kill(31) => 31,
|
||||
// KillSignal::Kill(64) => 64,
|
||||
// KillSignal::Kill(signal) => signal + 1,
|
||||
// };
|
||||
// self.delete_dialog_state.selected_signal = KillSignal::Kill(new_signal);
|
||||
// } else {
|
||||
// self.delete_dialog_state.selected_signal = KillSignal::Cancel;
|
||||
// }
|
||||
// }
|
||||
// #[cfg(target_os = "windows")]
|
||||
// {
|
||||
// self.delete_dialog_state.selected_signal = KillSignal::Cancel;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn start_killing_process(&mut self) {
|
||||
@ -828,35 +673,38 @@ impl AppState {
|
||||
}
|
||||
|
||||
pub fn kill_highlighted_process(&mut self) -> Result<()> {
|
||||
if let BottomWidgetType::Proc = self.current_widget.widget_type {
|
||||
if let Some(current_selected_processes) = &self.to_delete_process_list {
|
||||
#[cfg(target_family = "unix")]
|
||||
let signal = match self.delete_dialog_state.selected_signal {
|
||||
KillSignal::Kill(sig) => sig,
|
||||
KillSignal::Cancel => 15, // should never happen, so just TERM
|
||||
};
|
||||
for pid in ¤t_selected_processes.1 {
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
process_killer::kill_process_given_pid(*pid, signal)?;
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
process_killer::kill_process_given_pid(*pid)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.to_delete_process_list = None;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(BottomError::GenericError(
|
||||
"Cannot kill processes if the current widget is not the Process widget!"
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
// if let BottomWidgetType::Proc = self.current_widget.widget_type {
|
||||
// if let Some(current_selected_processes) = &self.to_delete_process_list {
|
||||
// #[cfg(target_family = "unix")]
|
||||
// let signal = match self.delete_dialog_state.selected_signal {
|
||||
// KillSignal::Kill(sig) => sig,
|
||||
// KillSignal::Cancel => 15, // should never happen, so just TERM
|
||||
// };
|
||||
// for pid in ¤t_selected_processes.1 {
|
||||
// #[cfg(target_family = "unix")]
|
||||
// {
|
||||
// process_killer::kill_process_given_pid(*pid, signal)?;
|
||||
// }
|
||||
// #[cfg(target_os = "windows")]
|
||||
// {
|
||||
// process_killer::kill_process_given_pid(*pid)?;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// self.to_delete_process_list = None;
|
||||
// Ok(())
|
||||
// } else {
|
||||
// Err(BottomError::GenericError(
|
||||
// "Cannot kill processes if the current widget is not the Process widget!"
|
||||
// .to_string(),
|
||||
// ))
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_to_delete_processes(&self) -> Option<(String, Vec<Pid>)> {
|
||||
self.to_delete_process_list.clone()
|
||||
// self.to_delete_process_list.clone()
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
@ -319,7 +319,7 @@ impl DataCollection {
|
||||
}
|
||||
|
||||
fn eat_proc(&mut self, list_of_processes: Vec<processes::ProcessHarvest>) {
|
||||
// TODO: Probably more efficient to do this in the data collection step, but it's fine for now.
|
||||
// TODO: [Optimization] Probably more efficient to do this in the data collection step, but it's fine for now.
|
||||
self.process_name_pid_map.clear();
|
||||
self.process_cmd_pid_map.clear();
|
||||
list_of_processes.iter().for_each(|process_harvest| {
|
||||
|
@ -149,7 +149,7 @@ impl DataCollector {
|
||||
self.sys.refresh_memory();
|
||||
self.mem_total_kb = self.sys.get_total_memory();
|
||||
|
||||
// TODO: Would be good to get this and network list running on a timer instead...?
|
||||
// TODO: [Data Collection] Would be good to get this and network list running on a timer instead...?
|
||||
// Refresh components list once...
|
||||
if self.widgets_to_harvest.use_temp {
|
||||
self.sys.refresh_components_list();
|
||||
|
@ -39,7 +39,7 @@ pub async fn get_network_data(
|
||||
};
|
||||
|
||||
if to_keep {
|
||||
// TODO: Use bytes as the default instead, perhaps?
|
||||
// TODO: [Optimization] Optimization (Potential)Use bytes as the default instead, perhaps?
|
||||
// Since you might have to do a double conversion (bytes -> bits -> bytes) in some cases;
|
||||
// but if you stick to bytes, then in the bytes, case, you do no conversion, and in the bits case,
|
||||
// you only do one conversion...
|
||||
|
@ -27,6 +27,7 @@ use std::borrow::Cow;
|
||||
|
||||
use crate::Pid;
|
||||
|
||||
// FIXME: [URGENT] Delete this.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum ProcessSorting {
|
||||
CpuPercent,
|
||||
|
@ -229,8 +229,6 @@ pub fn get_process_data(
|
||||
pid_mapping: &mut FxHashMap<Pid, PrevProcDetails>, use_current_cpu_total: bool,
|
||||
time_difference_in_secs: u64, mem_total_kb: u64, user_table: &mut UserTable,
|
||||
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
|
||||
// TODO: [PROC THREADS] Add threads
|
||||
|
||||
if let Ok((cpu_usage, cpu_fraction)) = cpu_usage_calculation(prev_idle, prev_non_idle) {
|
||||
let mut pids_to_clear: FxHashSet<Pid> = pid_mapping.keys().cloned().collect();
|
||||
|
||||
|
@ -332,7 +332,7 @@ pub struct LayoutCreationOutput {
|
||||
/// Creates a new [`Arena<LayoutNode>`] from the given config and returns it, along with the [`NodeId`] representing
|
||||
/// the root of the newly created [`Arena`], a mapping from [`NodeId`]s to [`BottomWidget`]s, and optionally, a default
|
||||
/// selected [`NodeId`].
|
||||
// FIXME: This is currently jury-rigged "glue" just to work with the existing config system! We are NOT keeping it like this, it's too awful to keep like this!
|
||||
// FIXME: [AFTER REFACTOR] This is currently jury-rigged "glue" just to work with the existing config system! We are NOT keeping it like this, it's too awful to keep like this!
|
||||
pub fn create_layout_tree(
|
||||
rows: &[Row], process_defaults: ProcessDefaults, app_config_fields: &AppConfigFields,
|
||||
) -> Result<LayoutCreationOutput> {
|
||||
@ -924,6 +924,7 @@ pub fn move_widget_selection(
|
||||
if let Some(proposed_widget) = widget_lookup_map.get_mut(&proposed_id) {
|
||||
match proposed_widget.selectable_type() {
|
||||
SelectableType::Unselectable => {
|
||||
// FIXME: [URGENT] Test this; make sure this cannot recurse infinitely! Maybe through a unit test too.
|
||||
// Try to move again recursively.
|
||||
move_widget_selection(
|
||||
layout_tree,
|
||||
@ -960,7 +961,7 @@ pub fn generate_layout(
|
||||
root: NodeId, arena: &mut Arena<LayoutNode>, area: Rect,
|
||||
lookup_map: &FxHashMap<NodeId, TmpBottomWidget>,
|
||||
) {
|
||||
// TODO: [Layout] Add some caching/dirty mechanisms to reduce calls.
|
||||
// TODO: [Optimization, Layout] Add some caching/dirty mechanisms to reduce calls.
|
||||
|
||||
/// A [`Size`] is a set of widths and heights that a node in our layout wants to be.
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
|
@ -253,7 +253,7 @@ pub fn parse_query(
|
||||
if content == "=" {
|
||||
// Check next string if possible
|
||||
if let Some(queue_next) = query.pop_front() {
|
||||
// TODO: Need to consider the following cases:
|
||||
// TODO: [Query, ???] Need to consider the following cases:
|
||||
// - (test)
|
||||
// - (test
|
||||
// - test)
|
||||
|
@ -1,940 +0,0 @@
|
||||
use std::{collections::HashMap, time::Instant};
|
||||
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
|
||||
use tui::widgets::TableState;
|
||||
|
||||
use crate::{
|
||||
app::{layout_manager::BottomWidgetType, query::*},
|
||||
constants,
|
||||
data_harvester::processes::{self, ProcessSorting},
|
||||
};
|
||||
use ProcessSorting::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScrollDirection {
|
||||
// UP means scrolling up --- this usually DECREMENTS
|
||||
Up,
|
||||
// DOWN means scrolling down --- this usually INCREMENTS
|
||||
Down,
|
||||
}
|
||||
|
||||
impl Default for ScrollDirection {
|
||||
fn default() -> Self {
|
||||
ScrollDirection::Down
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CursorDirection {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// AppScrollWidgetState deals with fields for a scrollable app's current state.
|
||||
#[derive(Default)]
|
||||
pub struct AppScrollWidgetState {
|
||||
pub current_scroll_position: usize,
|
||||
pub previous_scroll_position: usize,
|
||||
pub scroll_direction: ScrollDirection,
|
||||
pub table_state: TableState,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum KillSignal {
|
||||
Cancel,
|
||||
Kill(usize),
|
||||
}
|
||||
|
||||
impl Default for KillSignal {
|
||||
#[cfg(target_family = "unix")]
|
||||
fn default() -> Self {
|
||||
KillSignal::Kill(15)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
fn default() -> Self {
|
||||
KillSignal::Kill(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AppDeleteDialogState {
|
||||
pub is_showing_dd: bool,
|
||||
pub selected_signal: KillSignal,
|
||||
/// tl x, tl y, br x, br y, index/signal
|
||||
pub button_positions: Vec<(u16, u16, u16, u16, usize)>,
|
||||
pub keyboard_signal_select: usize,
|
||||
pub last_number_press: Option<Instant>,
|
||||
pub scroll_pos: usize,
|
||||
}
|
||||
|
||||
pub struct AppHelpDialogState {
|
||||
pub is_showing_help: bool,
|
||||
pub scroll_state: ParagraphScrollState,
|
||||
pub index_shortcuts: Vec<u16>,
|
||||
}
|
||||
|
||||
impl Default for AppHelpDialogState {
|
||||
fn default() -> Self {
|
||||
AppHelpDialogState {
|
||||
is_showing_help: false,
|
||||
scroll_state: ParagraphScrollState::default(),
|
||||
index_shortcuts: vec![0; constants::HELP_TEXT.len()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// AppSearchState deals with generic searching (I might do this in the future).
|
||||
pub struct AppSearchState {
|
||||
pub is_enabled: bool,
|
||||
pub current_search_query: String,
|
||||
pub is_blank_search: bool,
|
||||
pub is_invalid_search: bool,
|
||||
pub grapheme_cursor: GraphemeCursor,
|
||||
pub cursor_direction: CursorDirection,
|
||||
pub cursor_bar: usize,
|
||||
/// This represents the position in terms of CHARACTERS, not graphemes
|
||||
pub char_cursor_position: usize,
|
||||
/// The query
|
||||
pub query: Option<Query>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AppSearchState {
|
||||
fn default() -> Self {
|
||||
AppSearchState {
|
||||
is_enabled: false,
|
||||
current_search_query: String::default(),
|
||||
is_invalid_search: false,
|
||||
is_blank_search: true,
|
||||
grapheme_cursor: GraphemeCursor::new(0, 0, true),
|
||||
cursor_direction: CursorDirection::Right,
|
||||
cursor_bar: 0,
|
||||
char_cursor_position: 0,
|
||||
query: None,
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppSearchState {
|
||||
/// Returns a reset but still enabled app search state
|
||||
pub fn reset(&mut self) {
|
||||
*self = AppSearchState {
|
||||
is_enabled: self.is_enabled,
|
||||
..AppSearchState::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_invalid_or_blank_search(&self) -> bool {
|
||||
self.is_blank_search || self.is_invalid_search
|
||||
}
|
||||
}
|
||||
|
||||
/// Meant for canvas operations involving table column widths.
|
||||
#[derive(Default)]
|
||||
pub struct CanvasTableWidthState {
|
||||
pub desired_column_widths: Vec<u16>,
|
||||
pub calculated_column_widths: Vec<u16>,
|
||||
}
|
||||
|
||||
/// ProcessSearchState only deals with process' search's current settings and state.
|
||||
pub struct ProcessSearchState {
|
||||
pub search_state: AppSearchState,
|
||||
pub is_ignoring_case: bool,
|
||||
pub is_searching_whole_word: bool,
|
||||
pub is_searching_with_regex: bool,
|
||||
}
|
||||
|
||||
impl Default for ProcessSearchState {
|
||||
fn default() -> Self {
|
||||
ProcessSearchState {
|
||||
search_state: AppSearchState::default(),
|
||||
is_ignoring_case: true,
|
||||
is_searching_whole_word: false,
|
||||
is_searching_with_regex: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcessSearchState {
|
||||
pub fn search_toggle_ignore_case(&mut self) {
|
||||
self.is_ignoring_case = !self.is_ignoring_case;
|
||||
}
|
||||
|
||||
pub fn search_toggle_whole_word(&mut self) {
|
||||
self.is_searching_whole_word = !self.is_searching_whole_word;
|
||||
}
|
||||
|
||||
pub fn search_toggle_regex(&mut self) {
|
||||
self.is_searching_with_regex = !self.is_searching_with_regex;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ColumnInfo {
|
||||
pub enabled: bool,
|
||||
pub shortcut: Option<&'static str>,
|
||||
// FIXME: Move column width logic here!
|
||||
// pub hard_width: Option<u16>,
|
||||
// pub max_soft_width: Option<f64>,
|
||||
}
|
||||
|
||||
pub struct ProcColumn {
|
||||
pub ordered_columns: Vec<ProcessSorting>,
|
||||
/// The y location of headers. Since they're all aligned, it's just one value.
|
||||
pub column_header_y_loc: Option<u16>,
|
||||
/// The x start and end bounds for each header.
|
||||
pub column_header_x_locs: Option<Vec<(u16, u16)>>,
|
||||
pub column_mapping: HashMap<ProcessSorting, ColumnInfo>,
|
||||
pub longest_header_len: u16,
|
||||
pub column_state: TableState,
|
||||
pub scroll_direction: ScrollDirection,
|
||||
pub current_scroll_position: usize,
|
||||
pub previous_scroll_position: usize,
|
||||
pub backup_prev_scroll_position: usize,
|
||||
}
|
||||
|
||||
impl Default for ProcColumn {
|
||||
fn default() -> Self {
|
||||
let ordered_columns = vec![
|
||||
Count,
|
||||
Pid,
|
||||
ProcessName,
|
||||
Command,
|
||||
CpuPercent,
|
||||
Mem,
|
||||
MemPercent,
|
||||
ReadPerSecond,
|
||||
WritePerSecond,
|
||||
TotalRead,
|
||||
TotalWrite,
|
||||
User,
|
||||
State,
|
||||
];
|
||||
|
||||
let mut column_mapping = HashMap::new();
|
||||
let mut longest_header_len = 0;
|
||||
for column in ordered_columns.clone() {
|
||||
longest_header_len = std::cmp::max(longest_header_len, column.to_string().len());
|
||||
match column {
|
||||
CpuPercent => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: Some("c"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
MemPercent => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: Some("m"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Mem => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: false,
|
||||
shortcut: Some("m"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
ProcessName => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: Some("n"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Command => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: false,
|
||||
shortcut: Some("n"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Pid => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: Some("p"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Count => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: false,
|
||||
shortcut: None,
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
User => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: cfg!(target_family = "unix"),
|
||||
shortcut: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: None,
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let longest_header_len = longest_header_len as u16;
|
||||
|
||||
ProcColumn {
|
||||
ordered_columns,
|
||||
column_mapping,
|
||||
longest_header_len,
|
||||
column_state: TableState::default(),
|
||||
scroll_direction: ScrollDirection::default(),
|
||||
current_scroll_position: 0,
|
||||
previous_scroll_position: 0,
|
||||
backup_prev_scroll_position: 0,
|
||||
column_header_y_loc: None,
|
||||
column_header_x_locs: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcColumn {
|
||||
/// Returns its new status.
|
||||
pub fn toggle(&mut self, column: &ProcessSorting) -> Option<bool> {
|
||||
if let Some(mapping) = self.column_mapping.get_mut(column) {
|
||||
mapping.enabled = !(mapping.enabled);
|
||||
Some(mapping.enabled)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_set(&mut self, column: &ProcessSorting, setting: bool) -> Option<bool> {
|
||||
if let Some(mapping) = self.column_mapping.get_mut(column) {
|
||||
mapping.enabled = setting;
|
||||
Some(mapping.enabled)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_enable(&mut self, column: &ProcessSorting) -> Option<bool> {
|
||||
if let Some(mapping) = self.column_mapping.get_mut(column) {
|
||||
mapping.enabled = true;
|
||||
Some(mapping.enabled)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_disable(&mut self, column: &ProcessSorting) -> Option<bool> {
|
||||
if let Some(mapping) = self.column_mapping.get_mut(column) {
|
||||
mapping.enabled = false;
|
||||
Some(mapping.enabled)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self, column: &ProcessSorting) -> bool {
|
||||
if let Some(mapping) = self.column_mapping.get(column) {
|
||||
mapping.enabled
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_enabled_columns_len(&self) -> usize {
|
||||
self.ordered_columns
|
||||
.iter()
|
||||
.filter_map(|column_type| {
|
||||
if let Some(col_map) = self.column_mapping.get(column_type) {
|
||||
if col_map.enabled {
|
||||
Some(1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// NOTE: ALWAYS call this when opening the sorted window.
|
||||
pub fn set_to_sorted_index_from_type(&mut self, proc_sorting_type: &ProcessSorting) {
|
||||
// TODO [Custom Columns]: If we add custom columns, this may be needed! Since column indices will change, this runs the risk of OOB. So, when you change columns, CALL THIS AND ADAPT!
|
||||
let mut true_index = 0;
|
||||
for column in &self.ordered_columns {
|
||||
if *column == *proc_sorting_type {
|
||||
break;
|
||||
}
|
||||
if self.column_mapping.get(column).unwrap().enabled {
|
||||
true_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
self.current_scroll_position = true_index;
|
||||
self.backup_prev_scroll_position = self.previous_scroll_position;
|
||||
}
|
||||
|
||||
/// This function sets the scroll position based on the index.
|
||||
pub fn set_to_sorted_index_from_visual_index(&mut self, visual_index: usize) {
|
||||
self.current_scroll_position = visual_index;
|
||||
self.backup_prev_scroll_position = self.previous_scroll_position;
|
||||
}
|
||||
|
||||
pub fn get_column_headers(
|
||||
&self, proc_sorting_type: &ProcessSorting, sort_reverse: bool,
|
||||
) -> Vec<String> {
|
||||
const DOWN_ARROW: char = '▼';
|
||||
const UP_ARROW: char = '▲';
|
||||
|
||||
// TODO: Gonna have to figure out how to do left/right GUI notation if we add it.
|
||||
self.ordered_columns
|
||||
.iter()
|
||||
.filter_map(|column_type| {
|
||||
let mapping = self.column_mapping.get(column_type).unwrap();
|
||||
let mut command_str = String::default();
|
||||
if let Some(command) = mapping.shortcut {
|
||||
command_str = format!("({})", command);
|
||||
}
|
||||
|
||||
if mapping.enabled {
|
||||
Some(format!(
|
||||
"{}{}{}",
|
||||
column_type.to_string(),
|
||||
command_str.as_str(),
|
||||
if proc_sorting_type == column_type {
|
||||
if sort_reverse {
|
||||
DOWN_ARROW
|
||||
} else {
|
||||
UP_ARROW
|
||||
}
|
||||
} else {
|
||||
' '
|
||||
}
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProcWidgetState {
|
||||
pub process_search_state: ProcessSearchState,
|
||||
pub is_grouped: bool,
|
||||
pub scroll_state: AppScrollWidgetState,
|
||||
pub process_sorting_type: processes::ProcessSorting,
|
||||
pub is_process_sort_descending: bool,
|
||||
pub is_using_command: bool,
|
||||
pub current_column_index: usize,
|
||||
pub is_sort_open: bool,
|
||||
pub columns: ProcColumn,
|
||||
pub is_tree_mode: bool,
|
||||
pub table_width_state: CanvasTableWidthState,
|
||||
pub requires_redraw: bool,
|
||||
}
|
||||
|
||||
impl ProcWidgetState {
|
||||
pub fn init(
|
||||
is_case_sensitive: bool, is_match_whole_word: bool, is_use_regex: bool, is_grouped: bool,
|
||||
show_memory_as_values: bool, is_tree_mode: bool, is_using_command: bool,
|
||||
) -> Self {
|
||||
let mut process_search_state = ProcessSearchState::default();
|
||||
|
||||
if is_case_sensitive {
|
||||
// By default it's off
|
||||
process_search_state.search_toggle_ignore_case();
|
||||
}
|
||||
if is_match_whole_word {
|
||||
process_search_state.search_toggle_whole_word();
|
||||
}
|
||||
if is_use_regex {
|
||||
process_search_state.search_toggle_regex();
|
||||
}
|
||||
|
||||
let (process_sorting_type, is_process_sort_descending) = if is_tree_mode {
|
||||
(processes::ProcessSorting::Pid, false)
|
||||
} else {
|
||||
(processes::ProcessSorting::CpuPercent, true)
|
||||
};
|
||||
|
||||
// TODO: If we add customizable columns, this should pull from config
|
||||
let mut columns = ProcColumn::default();
|
||||
columns.set_to_sorted_index_from_type(&process_sorting_type);
|
||||
if is_grouped {
|
||||
// Normally defaults to showing by PID, toggle count on instead.
|
||||
columns.toggle(&ProcessSorting::Count);
|
||||
columns.toggle(&ProcessSorting::Pid);
|
||||
}
|
||||
if show_memory_as_values {
|
||||
// Normally defaults to showing by percent, toggle value on instead.
|
||||
columns.toggle(&ProcessSorting::Mem);
|
||||
columns.toggle(&ProcessSorting::MemPercent);
|
||||
}
|
||||
|
||||
ProcWidgetState {
|
||||
process_search_state,
|
||||
is_grouped,
|
||||
scroll_state: AppScrollWidgetState::default(),
|
||||
process_sorting_type,
|
||||
is_process_sort_descending,
|
||||
is_using_command,
|
||||
current_column_index: 0,
|
||||
is_sort_open: false,
|
||||
columns,
|
||||
is_tree_mode,
|
||||
table_width_state: CanvasTableWidthState::default(),
|
||||
requires_redraw: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates sorting when using the column list.
|
||||
/// ...this really should be part of the ProcColumn struct (along with the sorting fields),
|
||||
/// but I'm too lazy.
|
||||
///
|
||||
/// Sorry, future me, you're gonna have to refactor this later. Too busy getting
|
||||
/// the feature to work in the first place! :)
|
||||
pub fn update_sorting_with_columns(&mut self) {
|
||||
let mut true_index = 0;
|
||||
let mut enabled_index = 0;
|
||||
let target_itx = self.columns.current_scroll_position;
|
||||
for column in &self.columns.ordered_columns {
|
||||
let enabled = self.columns.column_mapping.get(column).unwrap().enabled;
|
||||
if enabled_index == target_itx && enabled {
|
||||
break;
|
||||
}
|
||||
if enabled {
|
||||
enabled_index += 1;
|
||||
}
|
||||
true_index += 1;
|
||||
}
|
||||
|
||||
if let Some(new_sort_type) = self.columns.ordered_columns.get(true_index) {
|
||||
if *new_sort_type == self.process_sorting_type {
|
||||
// Just reverse the search if we're reselecting!
|
||||
self.is_process_sort_descending = !(self.is_process_sort_descending);
|
||||
} else {
|
||||
self.process_sorting_type = new_sort_type.clone();
|
||||
match self.process_sorting_type {
|
||||
ProcessSorting::State
|
||||
| ProcessSorting::Pid
|
||||
| ProcessSorting::ProcessName
|
||||
| ProcessSorting::Command => {
|
||||
// Also invert anything that uses alphabetical sorting by default.
|
||||
self.is_process_sort_descending = false;
|
||||
}
|
||||
_ => {
|
||||
self.is_process_sort_descending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_command_and_name(&mut self, is_using_command: bool) {
|
||||
if let Some(pn) = self
|
||||
.columns
|
||||
.column_mapping
|
||||
.get_mut(&ProcessSorting::ProcessName)
|
||||
{
|
||||
pn.enabled = !is_using_command;
|
||||
}
|
||||
if let Some(c) = self
|
||||
.columns
|
||||
.column_mapping
|
||||
.get_mut(&ProcessSorting::Command)
|
||||
{
|
||||
c.enabled = is_using_command;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_search_cursor_position(&self) -> usize {
|
||||
self.process_search_state
|
||||
.search_state
|
||||
.grapheme_cursor
|
||||
.cur_cursor()
|
||||
}
|
||||
|
||||
pub fn get_char_cursor_position(&self) -> usize {
|
||||
self.process_search_state.search_state.char_cursor_position
|
||||
}
|
||||
|
||||
pub fn is_search_enabled(&self) -> bool {
|
||||
self.process_search_state.search_state.is_enabled
|
||||
}
|
||||
|
||||
pub fn get_current_search_query(&self) -> &String {
|
||||
&self.process_search_state.search_state.current_search_query
|
||||
}
|
||||
|
||||
pub fn update_query(&mut self) {
|
||||
if self
|
||||
.process_search_state
|
||||
.search_state
|
||||
.current_search_query
|
||||
.is_empty()
|
||||
{
|
||||
self.process_search_state.search_state.is_blank_search = true;
|
||||
self.process_search_state.search_state.is_invalid_search = false;
|
||||
self.process_search_state.search_state.error_message = None;
|
||||
} else {
|
||||
let parsed_query = self.parse_query();
|
||||
// debug!("Parsed query: {:#?}", parsed_query);
|
||||
|
||||
if let Ok(parsed_query) = parsed_query {
|
||||
self.process_search_state.search_state.query = Some(parsed_query);
|
||||
self.process_search_state.search_state.is_blank_search = false;
|
||||
self.process_search_state.search_state.is_invalid_search = false;
|
||||
self.process_search_state.search_state.error_message = None;
|
||||
} else if let Err(err) = parsed_query {
|
||||
self.process_search_state.search_state.is_blank_search = false;
|
||||
self.process_search_state.search_state.is_invalid_search = true;
|
||||
self.process_search_state.search_state.error_message = Some(err.to_string());
|
||||
}
|
||||
}
|
||||
self.scroll_state.previous_scroll_position = 0;
|
||||
self.scroll_state.current_scroll_position = 0;
|
||||
}
|
||||
|
||||
pub fn clear_search(&mut self) {
|
||||
self.process_search_state.search_state.reset();
|
||||
}
|
||||
|
||||
pub fn search_walk_forward(&mut self, start_position: usize) {
|
||||
self.process_search_state
|
||||
.search_state
|
||||
.grapheme_cursor
|
||||
.next_boundary(
|
||||
&self.process_search_state.search_state.current_search_query[start_position..],
|
||||
start_position,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn search_walk_back(&mut self, start_position: usize) {
|
||||
self.process_search_state
|
||||
.search_state
|
||||
.grapheme_cursor
|
||||
.prev_boundary(
|
||||
&self.process_search_state.search_state.current_search_query[..start_position],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProcState {
|
||||
pub widget_states: HashMap<u64, ProcWidgetState>,
|
||||
pub force_update: Option<u64>,
|
||||
pub force_update_all: bool,
|
||||
}
|
||||
|
||||
impl ProcState {
|
||||
pub fn init(widget_states: HashMap<u64, ProcWidgetState>) -> Self {
|
||||
ProcState {
|
||||
widget_states,
|
||||
force_update: None,
|
||||
force_update_all: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidgetState> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NetWidgetState {
|
||||
pub current_display_time: u64,
|
||||
pub autohide_timer: Option<Instant>,
|
||||
// pub draw_max_range_cache: f64,
|
||||
// pub draw_labels_cache: Vec<String>,
|
||||
// pub draw_time_start_cache: f64,
|
||||
// TODO: Re-enable these when we move net details state-side!
|
||||
// pub unit_type: DataUnitTypes,
|
||||
// pub scale_type: AxisScaling,
|
||||
}
|
||||
|
||||
impl NetWidgetState {
|
||||
pub fn init(
|
||||
current_display_time: u64,
|
||||
autohide_timer: Option<Instant>,
|
||||
// unit_type: DataUnitTypes,
|
||||
// scale_type: AxisScaling,
|
||||
) -> Self {
|
||||
NetWidgetState {
|
||||
current_display_time,
|
||||
autohide_timer,
|
||||
// draw_max_range_cache: 0.0,
|
||||
// draw_labels_cache: vec![],
|
||||
// draw_time_start_cache: 0.0,
|
||||
// unit_type,
|
||||
// scale_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NetState {
|
||||
pub force_update: Option<u64>,
|
||||
pub widget_states: HashMap<u64, NetWidgetState>,
|
||||
}
|
||||
|
||||
impl NetState {
|
||||
pub fn init(widget_states: HashMap<u64, NetWidgetState>) -> Self {
|
||||
NetState {
|
||||
force_update: None,
|
||||
widget_states,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut NetWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&NetWidgetState> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CpuWidgetState {
|
||||
pub current_display_time: u64,
|
||||
pub is_legend_hidden: bool,
|
||||
pub autohide_timer: Option<Instant>,
|
||||
pub scroll_state: AppScrollWidgetState,
|
||||
pub is_multi_graph_mode: bool,
|
||||
pub table_width_state: CanvasTableWidthState,
|
||||
}
|
||||
|
||||
impl CpuWidgetState {
|
||||
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
|
||||
CpuWidgetState {
|
||||
current_display_time,
|
||||
is_legend_hidden: false,
|
||||
autohide_timer,
|
||||
scroll_state: AppScrollWidgetState::default(),
|
||||
is_multi_graph_mode: false,
|
||||
table_width_state: CanvasTableWidthState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CpuState {
|
||||
pub force_update: Option<u64>,
|
||||
pub widget_states: HashMap<u64, CpuWidgetState>,
|
||||
}
|
||||
|
||||
impl CpuState {
|
||||
pub fn init(widget_states: HashMap<u64, CpuWidgetState>) -> Self {
|
||||
CpuState {
|
||||
force_update: None,
|
||||
widget_states,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut CpuWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&CpuWidgetState> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MemWidgetState {
|
||||
pub current_display_time: u64,
|
||||
pub autohide_timer: Option<Instant>,
|
||||
}
|
||||
|
||||
impl MemWidgetState {
|
||||
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
|
||||
MemWidgetState {
|
||||
current_display_time,
|
||||
autohide_timer,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct MemState {
|
||||
pub force_update: Option<u64>,
|
||||
pub widget_states: HashMap<u64, MemWidgetState>,
|
||||
}
|
||||
|
||||
impl MemState {
|
||||
pub fn init(widget_states: HashMap<u64, MemWidgetState>) -> Self {
|
||||
MemState {
|
||||
force_update: None,
|
||||
widget_states,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut MemWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&MemWidgetState> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TempWidgetState {
|
||||
pub scroll_state: AppScrollWidgetState,
|
||||
pub table_width_state: CanvasTableWidthState,
|
||||
}
|
||||
|
||||
impl TempWidgetState {
|
||||
pub fn init() -> Self {
|
||||
TempWidgetState {
|
||||
scroll_state: AppScrollWidgetState::default(),
|
||||
table_width_state: CanvasTableWidthState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TempState {
|
||||
pub widget_states: HashMap<u64, TempWidgetState>,
|
||||
}
|
||||
|
||||
impl TempState {
|
||||
pub fn init(widget_states: HashMap<u64, TempWidgetState>) -> Self {
|
||||
TempState { widget_states }
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut TempWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&TempWidgetState> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiskWidgetState {
|
||||
pub scroll_state: AppScrollWidgetState,
|
||||
pub table_width_state: CanvasTableWidthState,
|
||||
}
|
||||
|
||||
impl DiskWidgetState {
|
||||
pub fn init() -> Self {
|
||||
DiskWidgetState {
|
||||
scroll_state: AppScrollWidgetState::default(),
|
||||
table_width_state: CanvasTableWidthState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiskState {
|
||||
pub widget_states: HashMap<u64, DiskWidgetState>,
|
||||
}
|
||||
|
||||
impl DiskState {
|
||||
pub fn init(widget_states: HashMap<u64, DiskWidgetState>) -> Self {
|
||||
DiskState { widget_states }
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut DiskWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&DiskWidgetState> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
pub struct BasicTableWidgetState {
|
||||
// Since this is intended (currently) to only be used for ONE widget, that's
|
||||
// how it's going to be written. If we want to allow for multiple of these,
|
||||
// then we can expand outwards with a normal BasicTableState and a hashmap
|
||||
pub currently_displayed_widget_type: BottomWidgetType,
|
||||
pub currently_displayed_widget_id: u64,
|
||||
pub widget_id: i64,
|
||||
pub left_tlc: Option<(u16, u16)>,
|
||||
pub left_brc: Option<(u16, u16)>,
|
||||
pub right_tlc: Option<(u16, u16)>,
|
||||
pub right_brc: Option<(u16, u16)>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BatteryWidgetState {
|
||||
pub currently_selected_battery_index: usize,
|
||||
pub tab_click_locs: Option<Vec<((u16, u16), (u16, u16))>>,
|
||||
}
|
||||
|
||||
pub struct BatteryState {
|
||||
pub widget_states: HashMap<u64, BatteryWidgetState>,
|
||||
}
|
||||
|
||||
impl BatteryState {
|
||||
pub fn init(widget_states: HashMap<u64, BatteryWidgetState>) -> Self {
|
||||
BatteryState { widget_states }
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut BatteryWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&BatteryWidgetState> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ParagraphScrollState {
|
||||
pub current_scroll_index: u16,
|
||||
pub max_scroll_index: u16,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ConfigState {
|
||||
pub current_category_index: usize,
|
||||
pub category_list: Vec<ConfigCategory>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ConfigCategory {
|
||||
pub category_name: &'static str,
|
||||
pub options_list: Vec<ConfigOption>,
|
||||
}
|
||||
|
||||
pub struct ConfigOption {
|
||||
pub set_function: Box<dyn Fn() -> anyhow::Result<()>>,
|
||||
}
|
@ -2,13 +2,10 @@ use std::{fmt::Debug, time::Instant};
|
||||
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use enum_dispatch::enum_dispatch;
|
||||
use tui::{backend::Backend, layout::Rect, widgets::TableState, Frame};
|
||||
use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
event::{ComponentEventResult, SelectionAction},
|
||||
layout_manager::BottomWidgetType,
|
||||
},
|
||||
app::event::{ComponentEventResult, SelectionAction},
|
||||
canvas::Painter,
|
||||
options::layout_options::LayoutRule,
|
||||
};
|
||||
@ -26,7 +23,7 @@ pub use bottom_widgets::*;
|
||||
|
||||
use self::tui_stuff::BlockBuilder;
|
||||
|
||||
use super::{data_farmer::DataCollection, event::EventResult};
|
||||
use super::data_farmer::DataCollection;
|
||||
|
||||
/// A trait for things that are drawn with state.
|
||||
#[enum_dispatch]
|
||||
@ -235,36 +232,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Old stuff below -----
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScrollDirection {
|
||||
// UP means scrolling up --- this usually DECREMENTS
|
||||
Up,
|
||||
// DOWN means scrolling down --- this usually INCREMENTS
|
||||
Down,
|
||||
}
|
||||
|
||||
impl Default for ScrollDirection {
|
||||
fn default() -> Self {
|
||||
ScrollDirection::Down
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CursorDirection {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// AppScrollWidgetState deals with fields for a scrollable app's current state.
|
||||
#[derive(Default)]
|
||||
pub struct AppScrollWidgetState {
|
||||
pub current_scroll_position: usize,
|
||||
pub previous_scroll_position: usize,
|
||||
pub scroll_direction: ScrollDirection,
|
||||
pub table_state: TableState,
|
||||
}
|
||||
// ----- FIXME: Delete the old stuff below -----
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum KillSignal {
|
||||
@ -293,65 +261,3 @@ pub struct AppDeleteDialogState {
|
||||
pub last_number_press: Option<Instant>,
|
||||
pub scroll_pos: usize,
|
||||
}
|
||||
|
||||
pub struct AppHelpDialogState {
|
||||
pub is_showing_help: bool,
|
||||
pub scroll_state: ParagraphScrollState,
|
||||
pub index_shortcuts: Vec<u16>,
|
||||
}
|
||||
|
||||
impl Default for AppHelpDialogState {
|
||||
fn default() -> Self {
|
||||
AppHelpDialogState {
|
||||
is_showing_help: false,
|
||||
scroll_state: ParagraphScrollState::default(),
|
||||
index_shortcuts: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppHelpDialogState {
|
||||
pub fn increment(&mut self) -> EventResult {
|
||||
if self.scroll_state.current_scroll_index < self.scroll_state.max_scroll_index {
|
||||
self.scroll_state.current_scroll_index += 1;
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrement(&mut self) -> EventResult {
|
||||
if self.scroll_state.current_scroll_index > 0 {
|
||||
self.scroll_state.current_scroll_index -= 1;
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Meant for canvas operations involving table column widths.
|
||||
#[derive(Default)]
|
||||
pub struct CanvasTableWidthState {
|
||||
pub desired_column_widths: Vec<u16>,
|
||||
pub calculated_column_widths: Vec<u16>,
|
||||
}
|
||||
|
||||
pub struct BasicTableWidgetState {
|
||||
// Since this is intended (currently) to only be used for ONE widget, that's
|
||||
// how it's going to be written. If we want to allow for multiple of these,
|
||||
// then we can expand outwards with a normal BasicTableState and a hashmap
|
||||
pub currently_displayed_widget_type: BottomWidgetType,
|
||||
pub currently_displayed_widget_id: u64,
|
||||
pub widget_id: i64,
|
||||
pub left_tlc: Option<(u16, u16)>,
|
||||
pub left_brc: Option<(u16, u16)>,
|
||||
pub right_tlc: Option<(u16, u16)>,
|
||||
pub right_brc: Option<(u16, u16)>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ParagraphScrollState {
|
||||
pub current_scroll_index: u16,
|
||||
pub max_scroll_index: u16,
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ impl Scrollable {
|
||||
scroll_direction: ScrollDirection::Down,
|
||||
num_items,
|
||||
tui_state,
|
||||
gg_manager: MultiKey::register(vec!['g', 'g']), // TODO: Use a static arrayvec
|
||||
gg_manager: MultiKey::register(vec!['g', 'g']), // TODO: [Optimization] Use a static arrayvec
|
||||
bounds: Rect::default(),
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
event::{ReturnSignal, ComponentEventResult},
|
||||
event::{ComponentEventResult, ReturnSignal},
|
||||
widgets::tui_stuff::BlockBuilder,
|
||||
Component, TextTable,
|
||||
},
|
||||
|
@ -262,7 +262,7 @@ impl TextInput {
|
||||
|
||||
let after_cursor = graphemes.map(|(_, grapheme)| grapheme).collect::<String>();
|
||||
|
||||
// FIXME: This is NOT done! This is an incomplete (but kinda working) implementation, for now.
|
||||
// FIXME: [AFTER REFACTOR] This is NOT done! This is an incomplete (but kinda working) implementation, for now.
|
||||
|
||||
let search_text = vec![Spans::from(vec![
|
||||
Span::styled(
|
||||
@ -365,10 +365,10 @@ impl Component for TextInput {
|
||||
fn handle_mouse_event(&mut self, _event: MouseEvent) -> ComponentEventResult {
|
||||
// We are assuming this is within bounds...
|
||||
|
||||
// TODO: [Feature] Add mouse input for text input cursor
|
||||
// let x = event.column;
|
||||
// let widget_x = self.bounds.x + 2;
|
||||
// if x >= widget_x {
|
||||
// // TODO: Do this at some point after refactor
|
||||
// ComponentEventResult::Redraw
|
||||
// } else {
|
||||
// ComponentEventResult::NoRedraw
|
||||
|
@ -46,8 +46,6 @@ pub type TextTableDataRef = [Vec<(Cow<'static, str>, Option<Cow<'static, str>>,
|
||||
#[derive(Debug)]
|
||||
pub struct SimpleColumn {
|
||||
name: Cow<'static, str>,
|
||||
|
||||
// TODO: I would remove these in the future, storing them here feels weird...
|
||||
desired_width: DesiredColumnWidth,
|
||||
x_bounds: Option<(u16, u16)>,
|
||||
}
|
||||
@ -130,7 +128,7 @@ where
|
||||
pub show_gap: bool,
|
||||
|
||||
/// The bounding box of the [`TextTable`].
|
||||
pub bounds: Rect, // TODO: Consider moving bounds to something else?
|
||||
pub bounds: Rect, // TODO: [Refactor, Drawing] Consider moving bounds to something else?
|
||||
|
||||
/// The bounds including the border, if there is one.
|
||||
pub border_bounds: Rect,
|
||||
|
@ -45,7 +45,7 @@ pub enum AutohideTimer {
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: [AUTOHIDE] Not a fan of how this is done, as this should really "trigger" a draw when it's done.
|
||||
// TODO: [Refactor] Not a fan of how autohide is currently done, as this should really "trigger" a draw when it's done. Maybe use async/threads?
|
||||
impl AutohideTimer {
|
||||
fn start_display_timer(&mut self) {
|
||||
match self {
|
||||
|
@ -128,7 +128,7 @@ impl Widget for BasicMem {
|
||||
fn update_data(&mut self, data_collection: &DataCollection) {
|
||||
let (memory_labels, swap_labels) = convert_mem_labels(data_collection);
|
||||
|
||||
// TODO: [Data update optimization] Probably should just make another function altogether for basic mode.
|
||||
// TODO: [Optimization] Probably should just make another function altogether for just basic mem mode.
|
||||
self.mem_data = if let (Some(data), Some((_, fraction))) = (
|
||||
convert_mem_data_points(data_collection).last(),
|
||||
memory_labels,
|
||||
|
@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
cmp::{max, min},
|
||||
collections::HashMap,
|
||||
};
|
||||
use std::cmp::{max, min};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, MouseEvent};
|
||||
use tui::{
|
||||
@ -23,23 +20,6 @@ use crate::{
|
||||
options::layout_options::LayoutRule,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BatteryWidgetState {
|
||||
pub currently_selected_battery_index: usize,
|
||||
pub tab_click_locs: Option<Vec<((u16, u16), (u16, u16))>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BatteryState {
|
||||
pub widget_states: HashMap<u64, BatteryWidgetState>,
|
||||
}
|
||||
|
||||
impl BatteryState {
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut BatteryWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// A table displaying battery information on a per-battery basis.
|
||||
pub struct BatteryTable {
|
||||
bounds: Rect,
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::{borrow::Cow, collections::HashMap, time::Instant};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use tui::{
|
||||
@ -12,39 +12,13 @@ use crate::{
|
||||
event::{ComponentEventResult, SelectionAction},
|
||||
text_table::SimpleColumn,
|
||||
time_graph::TimeGraphData,
|
||||
AppConfigFields, AppScrollWidgetState, CanvasTableWidthState, Component, DataCollection,
|
||||
TextTable, TimeGraph, Widget,
|
||||
AppConfigFields, Component, DataCollection, TextTable, TimeGraph, Widget,
|
||||
},
|
||||
canvas::Painter,
|
||||
data_conversion::{convert_cpu_data_points, ConvertedCpuData},
|
||||
options::layout_options::LayoutRule,
|
||||
};
|
||||
|
||||
pub struct CpuWidgetState {
|
||||
pub current_display_time: u64,
|
||||
pub is_legend_hidden: bool,
|
||||
pub autohide_timer: Option<Instant>,
|
||||
pub scroll_state: AppScrollWidgetState,
|
||||
pub is_multi_graph_mode: bool,
|
||||
pub table_width_state: CanvasTableWidthState,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CpuState {
|
||||
pub force_update: Option<u64>,
|
||||
pub widget_states: HashMap<u64, CpuWidgetState>,
|
||||
}
|
||||
|
||||
impl CpuState {
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut CpuWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&CpuWidgetState> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Which part of the [`CpuGraph`] is currently selected.
|
||||
enum CpuGraphSelection {
|
||||
Graph,
|
||||
@ -228,7 +202,7 @@ impl Widget for CpuGraph {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// TODO: You MUST draw the table first, otherwise the index may mismatch after a reset. This is a bad gotcha - we should look into auto-updating the table's length!
|
||||
// TODO: [Gotcha, Refactor] You MUST draw the table first, otherwise the index may mismatch after a reset. This is a bad gotcha - we should look into auto-updating the table's length!
|
||||
self.legend.draw_tui_table(
|
||||
painter,
|
||||
f,
|
||||
|
@ -1,43 +1,17 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use tui::{backend::Backend, layout::Rect, widgets::Borders, Frame};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
data_farmer::DataCollection, event::ComponentEventResult,
|
||||
sort_text_table::SimpleSortableColumn, text_table::TextTableData, AppScrollWidgetState,
|
||||
CanvasTableWidthState, Component, TextTable, Widget,
|
||||
sort_text_table::SimpleSortableColumn, text_table::TextTableData, Component, TextTable,
|
||||
Widget,
|
||||
},
|
||||
canvas::Painter,
|
||||
data_conversion::convert_disk_row,
|
||||
options::layout_options::LayoutRule,
|
||||
};
|
||||
|
||||
pub struct DiskWidgetState {
|
||||
pub scroll_state: AppScrollWidgetState,
|
||||
pub table_width_state: CanvasTableWidthState,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DiskState {
|
||||
pub widget_states: HashMap<u64, DiskWidgetState>,
|
||||
}
|
||||
|
||||
impl DiskState {
|
||||
pub fn init(widget_states: HashMap<u64, DiskWidgetState>) -> Self {
|
||||
DiskState { widget_states }
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut DiskWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&DiskWidgetState> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// A table displaying disk data.
|
||||
pub struct DiskTable {
|
||||
table: TextTable<SimpleSortableColumn>,
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::{borrow::Cow, collections::HashMap, time::Instant};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use tui::{backend::Backend, layout::Rect};
|
||||
@ -10,17 +10,6 @@ use crate::{
|
||||
options::layout_options::LayoutRule,
|
||||
};
|
||||
|
||||
pub struct MemWidgetState {
|
||||
pub current_display_time: u64,
|
||||
pub autohide_timer: Option<Instant>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MemState {
|
||||
pub force_update: Option<u64>,
|
||||
pub widget_states: HashMap<u64, MemWidgetState>,
|
||||
}
|
||||
|
||||
/// A widget that deals with displaying memory usage on a [`TimeGraph`]. Basically just a wrapper
|
||||
/// around [`TimeGraph`] as of now.
|
||||
pub struct MemGraph {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::{borrow::Cow, collections::HashMap, time::Instant};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use tui::{
|
||||
@ -20,19 +20,6 @@ use crate::{
|
||||
utils::gen_util::*,
|
||||
};
|
||||
|
||||
pub struct NetWidgetState {
|
||||
pub current_display_time: u64,
|
||||
pub autohide_timer: Option<Instant>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NetState {
|
||||
pub force_update: Option<u64>,
|
||||
pub widget_states: HashMap<u64, NetWidgetState>,
|
||||
}
|
||||
|
||||
// --- NEW STUFF BELOW ---
|
||||
|
||||
/// Returns the max data point and time given a time.
|
||||
fn get_max_entry(
|
||||
rx: &[(f64, f64)], tx: &[(f64, f64)], time_start: f64, network_scale_type: &AxisScaling,
|
||||
|
@ -4,13 +4,12 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent,
|
||||
use float_ord::FloatOrd;
|
||||
use itertools::{Either, Itertools};
|
||||
use once_cell::unsync::Lazy;
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
text::{Span, Spans},
|
||||
widgets::{Borders, Paragraph, TableState},
|
||||
widgets::{Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
@ -25,557 +24,17 @@ use crate::{
|
||||
},
|
||||
canvas::Painter,
|
||||
data_conversion::get_string_with_bytes,
|
||||
data_harvester::processes::{self, ProcessSorting},
|
||||
options::{layout_options::LayoutRule, ProcessDefaults},
|
||||
utils::error::BottomError,
|
||||
};
|
||||
use ProcessSorting::*;
|
||||
|
||||
use crate::app::{
|
||||
does_bound_intersect_coordinate,
|
||||
sort_text_table::{SimpleSortableColumn, SortStatus, SortableColumn},
|
||||
text_table::TextTableData,
|
||||
AppScrollWidgetState, CanvasTableWidthState, Component, CursorDirection, ScrollDirection,
|
||||
SortMenu, SortableTextTable, TextInput, Widget,
|
||||
Component, SortMenu, SortableTextTable, TextInput, Widget,
|
||||
};
|
||||
|
||||
/// AppSearchState deals with generic searching (I might do this in the future).
|
||||
pub struct AppSearchState {
|
||||
pub is_enabled: bool,
|
||||
pub current_search_query: String,
|
||||
pub is_blank_search: bool,
|
||||
pub is_invalid_search: bool,
|
||||
pub grapheme_cursor: GraphemeCursor,
|
||||
pub cursor_direction: CursorDirection,
|
||||
pub cursor_bar: usize,
|
||||
/// This represents the position in terms of CHARACTERS, not graphemes
|
||||
pub char_cursor_position: usize,
|
||||
/// The query
|
||||
pub query: Option<Query>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AppSearchState {
|
||||
fn default() -> Self {
|
||||
AppSearchState {
|
||||
is_enabled: false,
|
||||
current_search_query: String::default(),
|
||||
is_invalid_search: false,
|
||||
is_blank_search: true,
|
||||
grapheme_cursor: GraphemeCursor::new(0, 0, true),
|
||||
cursor_direction: CursorDirection::Right,
|
||||
cursor_bar: 0,
|
||||
char_cursor_position: 0,
|
||||
query: None,
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppSearchState {
|
||||
/// Returns a reset but still enabled app search state
|
||||
pub fn reset(&mut self) {
|
||||
*self = AppSearchState {
|
||||
is_enabled: self.is_enabled,
|
||||
..AppSearchState::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_invalid_or_blank_search(&self) -> bool {
|
||||
self.is_blank_search || self.is_invalid_search
|
||||
}
|
||||
}
|
||||
|
||||
/// ProcessSearchState only deals with process' search's current settings and state.
|
||||
pub struct ProcessSearchState {
|
||||
pub search_state: AppSearchState,
|
||||
pub is_ignoring_case: bool,
|
||||
pub is_searching_whole_word: bool,
|
||||
pub is_searching_with_regex: bool,
|
||||
}
|
||||
|
||||
impl Default for ProcessSearchState {
|
||||
fn default() -> Self {
|
||||
ProcessSearchState {
|
||||
search_state: AppSearchState::default(),
|
||||
is_ignoring_case: true,
|
||||
is_searching_whole_word: false,
|
||||
is_searching_with_regex: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcessSearchState {
|
||||
pub fn search_toggle_ignore_case(&mut self) {
|
||||
self.is_ignoring_case = !self.is_ignoring_case;
|
||||
}
|
||||
|
||||
pub fn search_toggle_whole_word(&mut self) {
|
||||
self.is_searching_whole_word = !self.is_searching_whole_word;
|
||||
}
|
||||
|
||||
pub fn search_toggle_regex(&mut self) {
|
||||
self.is_searching_with_regex = !self.is_searching_with_regex;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ColumnInfo {
|
||||
pub enabled: bool,
|
||||
pub shortcut: Option<&'static str>,
|
||||
}
|
||||
|
||||
pub struct ProcColumn {
|
||||
pub ordered_columns: Vec<ProcessSorting>,
|
||||
/// The y location of headers. Since they're all aligned, it's just one value.
|
||||
pub column_header_y_loc: Option<u16>,
|
||||
/// The x start and end bounds for each header.
|
||||
pub column_header_x_locs: Option<Vec<(u16, u16)>>,
|
||||
pub column_mapping: HashMap<ProcessSorting, ColumnInfo>,
|
||||
pub longest_header_len: u16,
|
||||
pub column_state: TableState,
|
||||
pub scroll_direction: ScrollDirection,
|
||||
pub current_scroll_position: usize,
|
||||
pub previous_scroll_position: usize,
|
||||
pub backup_prev_scroll_position: usize,
|
||||
}
|
||||
|
||||
impl Default for ProcColumn {
|
||||
fn default() -> Self {
|
||||
let ordered_columns = vec![
|
||||
Count,
|
||||
Pid,
|
||||
ProcessName,
|
||||
Command,
|
||||
CpuPercent,
|
||||
Mem,
|
||||
MemPercent,
|
||||
ReadPerSecond,
|
||||
WritePerSecond,
|
||||
TotalRead,
|
||||
TotalWrite,
|
||||
User,
|
||||
State,
|
||||
];
|
||||
|
||||
let mut column_mapping = HashMap::new();
|
||||
let mut longest_header_len = 0;
|
||||
for column in ordered_columns.clone() {
|
||||
longest_header_len = std::cmp::max(longest_header_len, column.to_string().len());
|
||||
match column {
|
||||
CpuPercent => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: Some("c"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
MemPercent => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: Some("m"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Mem => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: false,
|
||||
shortcut: Some("m"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
ProcessName => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: Some("n"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Command => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: false,
|
||||
shortcut: Some("n"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Pid => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: Some("p"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Count => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: false,
|
||||
shortcut: None,
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
User => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: cfg!(target_family = "unix"),
|
||||
shortcut: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: None,
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let longest_header_len = longest_header_len as u16;
|
||||
|
||||
ProcColumn {
|
||||
ordered_columns,
|
||||
column_mapping,
|
||||
longest_header_len,
|
||||
column_state: TableState::default(),
|
||||
scroll_direction: ScrollDirection::default(),
|
||||
current_scroll_position: 0,
|
||||
previous_scroll_position: 0,
|
||||
backup_prev_scroll_position: 0,
|
||||
column_header_y_loc: None,
|
||||
column_header_x_locs: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcColumn {
|
||||
/// Returns its new status.
|
||||
pub fn toggle(&mut self, column: &ProcessSorting) -> Option<bool> {
|
||||
if let Some(mapping) = self.column_mapping.get_mut(column) {
|
||||
mapping.enabled = !(mapping.enabled);
|
||||
Some(mapping.enabled)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_set(&mut self, column: &ProcessSorting, setting: bool) -> Option<bool> {
|
||||
if let Some(mapping) = self.column_mapping.get_mut(column) {
|
||||
mapping.enabled = setting;
|
||||
Some(mapping.enabled)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_enable(&mut self, column: &ProcessSorting) -> Option<bool> {
|
||||
if let Some(mapping) = self.column_mapping.get_mut(column) {
|
||||
mapping.enabled = true;
|
||||
Some(mapping.enabled)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_disable(&mut self, column: &ProcessSorting) -> Option<bool> {
|
||||
if let Some(mapping) = self.column_mapping.get_mut(column) {
|
||||
mapping.enabled = false;
|
||||
Some(mapping.enabled)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self, column: &ProcessSorting) -> bool {
|
||||
if let Some(mapping) = self.column_mapping.get(column) {
|
||||
mapping.enabled
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_enabled_columns_len(&self) -> usize {
|
||||
self.ordered_columns
|
||||
.iter()
|
||||
.filter_map(|column_type| {
|
||||
if let Some(col_map) = self.column_mapping.get(column_type) {
|
||||
if col_map.enabled {
|
||||
Some(1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// NOTE: ALWAYS call this when opening the sorted window.
|
||||
pub fn set_to_sorted_index_from_type(&mut self, proc_sorting_type: &ProcessSorting) {
|
||||
// TODO [Custom Columns]: If we add custom columns, this may be needed! Since column indices will change, this runs the risk of OOB. So, when you change columns, CALL THIS AND ADAPT!
|
||||
let mut true_index = 0;
|
||||
for column in &self.ordered_columns {
|
||||
if *column == *proc_sorting_type {
|
||||
break;
|
||||
}
|
||||
if self.column_mapping.get(column).unwrap().enabled {
|
||||
true_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
self.current_scroll_position = true_index;
|
||||
self.backup_prev_scroll_position = self.previous_scroll_position;
|
||||
}
|
||||
|
||||
/// This function sets the scroll position based on the index.
|
||||
pub fn set_to_sorted_index_from_visual_index(&mut self, visual_index: usize) {
|
||||
self.current_scroll_position = visual_index;
|
||||
self.backup_prev_scroll_position = self.previous_scroll_position;
|
||||
}
|
||||
|
||||
pub fn get_column_headers(
|
||||
&self, proc_sorting_type: &ProcessSorting, sort_reverse: bool,
|
||||
) -> Vec<String> {
|
||||
const DOWN_ARROW: char = '▼';
|
||||
const UP_ARROW: char = '▲';
|
||||
|
||||
// TODO: Gonna have to figure out how to do left/right GUI notation if we add it.
|
||||
self.ordered_columns
|
||||
.iter()
|
||||
.filter_map(|column_type| {
|
||||
let mapping = self.column_mapping.get(column_type).unwrap();
|
||||
let mut command_str = String::default();
|
||||
if let Some(command) = mapping.shortcut {
|
||||
command_str = format!("({})", command);
|
||||
}
|
||||
|
||||
if mapping.enabled {
|
||||
Some(format!(
|
||||
"{}{}{}",
|
||||
column_type.to_string(),
|
||||
command_str.as_str(),
|
||||
if proc_sorting_type == column_type {
|
||||
if sort_reverse {
|
||||
DOWN_ARROW
|
||||
} else {
|
||||
UP_ARROW
|
||||
}
|
||||
} else {
|
||||
' '
|
||||
}
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProcWidgetState {
|
||||
pub process_search_state: ProcessSearchState,
|
||||
pub is_grouped: bool,
|
||||
pub scroll_state: AppScrollWidgetState,
|
||||
pub process_sorting_type: processes::ProcessSorting,
|
||||
pub is_process_sort_descending: bool,
|
||||
pub is_using_command: bool,
|
||||
pub current_column_index: usize,
|
||||
pub is_sort_open: bool,
|
||||
pub columns: ProcColumn,
|
||||
pub is_tree_mode: bool,
|
||||
pub table_width_state: CanvasTableWidthState,
|
||||
pub requires_redraw: bool,
|
||||
}
|
||||
|
||||
impl ProcWidgetState {
|
||||
/// Updates sorting when using the column list.
|
||||
/// ...this really should be part of the ProcColumn struct (along with the sorting fields),
|
||||
/// but I'm too lazy.
|
||||
///
|
||||
/// Sorry, future me, you're gonna have to refactor this later. Too busy getting
|
||||
/// the feature to work in the first place! :)
|
||||
pub fn update_sorting_with_columns(&mut self) {
|
||||
let mut true_index = 0;
|
||||
let mut enabled_index = 0;
|
||||
let target_itx = self.columns.current_scroll_position;
|
||||
for column in &self.columns.ordered_columns {
|
||||
let enabled = self.columns.column_mapping.get(column).unwrap().enabled;
|
||||
if enabled_index == target_itx && enabled {
|
||||
break;
|
||||
}
|
||||
if enabled {
|
||||
enabled_index += 1;
|
||||
}
|
||||
true_index += 1;
|
||||
}
|
||||
|
||||
if let Some(new_sort_type) = self.columns.ordered_columns.get(true_index) {
|
||||
if *new_sort_type == self.process_sorting_type {
|
||||
// Just reverse the search if we're reselecting!
|
||||
self.is_process_sort_descending = !(self.is_process_sort_descending);
|
||||
} else {
|
||||
self.process_sorting_type = new_sort_type.clone();
|
||||
match self.process_sorting_type {
|
||||
ProcessSorting::State
|
||||
| ProcessSorting::Pid
|
||||
| ProcessSorting::ProcessName
|
||||
| ProcessSorting::Command => {
|
||||
// Also invert anything that uses alphabetical sorting by default.
|
||||
self.is_process_sort_descending = false;
|
||||
}
|
||||
_ => {
|
||||
self.is_process_sort_descending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_command_and_name(&mut self, is_using_command: bool) {
|
||||
if let Some(pn) = self
|
||||
.columns
|
||||
.column_mapping
|
||||
.get_mut(&ProcessSorting::ProcessName)
|
||||
{
|
||||
pn.enabled = !is_using_command;
|
||||
}
|
||||
if let Some(c) = self
|
||||
.columns
|
||||
.column_mapping
|
||||
.get_mut(&ProcessSorting::Command)
|
||||
{
|
||||
c.enabled = is_using_command;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_search_cursor_position(&self) -> usize {
|
||||
self.process_search_state
|
||||
.search_state
|
||||
.grapheme_cursor
|
||||
.cur_cursor()
|
||||
}
|
||||
|
||||
pub fn get_char_cursor_position(&self) -> usize {
|
||||
self.process_search_state.search_state.char_cursor_position
|
||||
}
|
||||
|
||||
pub fn is_search_enabled(&self) -> bool {
|
||||
self.process_search_state.search_state.is_enabled
|
||||
}
|
||||
|
||||
pub fn get_current_search_query(&self) -> &String {
|
||||
&self.process_search_state.search_state.current_search_query
|
||||
}
|
||||
|
||||
pub fn update_query(&mut self) {
|
||||
if self
|
||||
.process_search_state
|
||||
.search_state
|
||||
.current_search_query
|
||||
.is_empty()
|
||||
{
|
||||
self.process_search_state.search_state.is_blank_search = true;
|
||||
self.process_search_state.search_state.is_invalid_search = false;
|
||||
self.process_search_state.search_state.error_message = None;
|
||||
} else {
|
||||
let parsed_query = parse_query(
|
||||
self.get_current_search_query(),
|
||||
self.process_search_state.is_searching_whole_word,
|
||||
self.process_search_state.is_ignoring_case,
|
||||
self.process_search_state.is_searching_with_regex,
|
||||
);
|
||||
// debug!("Parsed query: {:#?}", parsed_query);
|
||||
|
||||
if let Ok(parsed_query) = parsed_query {
|
||||
self.process_search_state.search_state.query = Some(parsed_query);
|
||||
self.process_search_state.search_state.is_blank_search = false;
|
||||
self.process_search_state.search_state.is_invalid_search = false;
|
||||
self.process_search_state.search_state.error_message = None;
|
||||
} else if let Err(err) = parsed_query {
|
||||
self.process_search_state.search_state.is_blank_search = false;
|
||||
self.process_search_state.search_state.is_invalid_search = true;
|
||||
self.process_search_state.search_state.error_message = Some(err.to_string());
|
||||
}
|
||||
}
|
||||
self.scroll_state.previous_scroll_position = 0;
|
||||
self.scroll_state.current_scroll_position = 0;
|
||||
}
|
||||
|
||||
pub fn clear_search(&mut self) {
|
||||
self.process_search_state.search_state.reset();
|
||||
}
|
||||
|
||||
pub fn search_walk_forward(&mut self, start_position: usize) {
|
||||
self.process_search_state
|
||||
.search_state
|
||||
.grapheme_cursor
|
||||
.next_boundary(
|
||||
&self.process_search_state.search_state.current_search_query[start_position..],
|
||||
start_position,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn search_walk_back(&mut self, start_position: usize) {
|
||||
self.process_search_state
|
||||
.search_state
|
||||
.grapheme_cursor
|
||||
.prev_boundary(
|
||||
&self.process_search_state.search_state.current_search_query[..start_position],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ProcState {
|
||||
pub widget_states: HashMap<u64, ProcWidgetState>,
|
||||
pub force_update: Option<u64>,
|
||||
pub force_update_all: bool,
|
||||
}
|
||||
|
||||
impl ProcState {
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidgetState> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// The currently selected part of a [`ProcessManager`]
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
enum ProcessManagerSelection {
|
||||
@ -835,7 +294,7 @@ impl ProcessManager {
|
||||
process_table: SortableTextTable::new(process_table_columns).default_sort_index(2),
|
||||
search_input: TextInput::default(),
|
||||
search_block_bounds: Rect::default(),
|
||||
dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: Maybe use something static...
|
||||
dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: [Optimization] Maybe use something static/const/arrayvec?...
|
||||
selected: ProcessManagerSelection::Processes,
|
||||
prev_selected: ProcessManagerSelection::Processes,
|
||||
in_tree_mode: false,
|
||||
@ -997,7 +456,7 @@ impl ProcessManager {
|
||||
}
|
||||
|
||||
// Invalidate row cache.
|
||||
self.process_table.invalidate_cached_columns(); // TODO: This should be automatically called somehow after sets/removes to avoid forgetting it - maybe do a queue system?
|
||||
self.process_table.invalidate_cached_columns(); // TODO: [Gotcha, Refactor] This should be automatically called somehow after sets/removes to avoid forgetting it - maybe do a queue system?
|
||||
|
||||
ComponentEventResult::Signal(ReturnSignal::Update)
|
||||
}
|
||||
@ -1368,7 +827,7 @@ impl Widget for ProcessManager {
|
||||
f.render_widget(
|
||||
Paragraph::new(Spans::from(vec![
|
||||
Span::styled(&*case_text, case_style),
|
||||
Span::raw(" "), // TODO: Smartly space it out in the future...
|
||||
Span::raw(" "), // TODO: [Drawing] Smartly space it out in the future...
|
||||
Span::styled(&*whole_word_text, whole_word_style),
|
||||
Span::raw(" "),
|
||||
Span::styled(&*regex_text, regex_style),
|
||||
|
@ -1,5 +1,3 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use tui::{backend::Backend, layout::Rect, widgets::Borders, Frame};
|
||||
|
||||
@ -7,48 +5,14 @@ use crate::{
|
||||
app::{
|
||||
data_farmer::DataCollection, data_harvester::temperature::TemperatureType,
|
||||
event::ComponentEventResult, sort_text_table::SimpleSortableColumn,
|
||||
text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component,
|
||||
TextTable, Widget,
|
||||
text_table::TextTableData, Component, TextTable, Widget,
|
||||
},
|
||||
canvas::Painter,
|
||||
data_conversion::convert_temp_row,
|
||||
options::layout_options::LayoutRule,
|
||||
};
|
||||
|
||||
pub struct TempWidgetState {
|
||||
pub scroll_state: AppScrollWidgetState,
|
||||
pub table_width_state: CanvasTableWidthState,
|
||||
}
|
||||
|
||||
impl TempWidgetState {
|
||||
pub fn init() -> Self {
|
||||
TempWidgetState {
|
||||
scroll_state: AppScrollWidgetState::default(),
|
||||
table_width_state: CanvasTableWidthState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TempState {
|
||||
pub widget_states: HashMap<u64, TempWidgetState>,
|
||||
}
|
||||
|
||||
impl TempState {
|
||||
pub fn init(widget_states: HashMap<u64, TempWidgetState>) -> Self {
|
||||
TempState { widget_states }
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut TempWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&TempWidgetState> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// A table displaying disk data..
|
||||
/// A table displaying temperature data.
|
||||
pub struct TempTable {
|
||||
table: TextTable<SimpleSortableColumn>,
|
||||
bounds: Rect,
|
||||
|
@ -34,7 +34,7 @@ pub struct HelpDialog {
|
||||
gg_manager: MultiKey,
|
||||
|
||||
/// A jury-rigged solution for shortcut indices.
|
||||
/// TODO: THIS DOES NOT SCALE WELL!
|
||||
/// TODO: [Refactor] Shortcut indices system - THIS DOES NOT SCALE WELL IN THE FUTURE! Write a better system like multikey (but for multiple combos).
|
||||
shortcut_indices: FxHashMap<u32, usize>,
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ fn main() -> Result<()> {
|
||||
let input_thread = create_input_thread(sender.clone(), thread_termination_lock.clone());
|
||||
|
||||
// Cleaning loop
|
||||
// TODO: Probably worth spinning this off into an async thread or something...
|
||||
// TODO: [Refactor, Optimization (Potentially, maybe not)] Probably worth spinning this off into an async thread or something...
|
||||
let _cleaning_thread = {
|
||||
let lock = thread_termination_lock.clone();
|
||||
let cvar = thread_termination_cvar.clone();
|
||||
@ -80,7 +80,7 @@ fn main() -> Result<()> {
|
||||
};
|
||||
|
||||
// Event loop
|
||||
// TODO: Add back collection sender
|
||||
// TODO: [Threads, Refactor, Config] Add back collection sender for config later if we need to change settings on the fly
|
||||
let (_collection_sender, collection_thread_ctrl_receiver) = mpsc::channel();
|
||||
let _collection_thread = create_collection_thread(
|
||||
sender,
|
||||
@ -102,7 +102,7 @@ fn main() -> Result<()> {
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
// Set panic hook
|
||||
// TODO: Make this close all the child threads too!
|
||||
// TODO: [Threads, Panic] Make this close all the child threads too!
|
||||
panic::set_hook(Box::new(|info| panic_hook(info)));
|
||||
|
||||
// Set termination hook
|
||||
|
@ -193,7 +193,7 @@ impl Painter {
|
||||
let middle_dialog_chunk = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(if terminal_width < 100 {
|
||||
// TODO: [REFACTOR] The point we start changing size at currently hard-coded in.
|
||||
// TODO: [Drawing, Hard-coded] The point we start changing size at currently hard-coded in.
|
||||
[
|
||||
Constraint::Percentage(0),
|
||||
Constraint::Percentage(100),
|
||||
@ -210,7 +210,8 @@ impl Painter {
|
||||
|
||||
help_dialog.draw_help(&self, f, middle_dialog_chunk[1]);
|
||||
} else if app_state.delete_dialog_state.is_showing_dd {
|
||||
// TODO: This needs the paragraph wrap feature from tui-rs to be pushed to complete... but for now it's pretty close!
|
||||
// TODO: [Drawing] Better dd sizing needs the paragraph wrap feature from tui-rs to be pushed to
|
||||
// complete... but for now it's pretty close!
|
||||
// The main problem right now is that I cannot properly calculate the height offset since
|
||||
// line-wrapping is NOT the same as taking the width of the text and dividing by width.
|
||||
// So, I need the height AFTER wrapping.
|
||||
|
@ -38,31 +38,31 @@ impl KillDialog for Painter {
|
||||
Spans::from(dd_err.clone()),
|
||||
Spans::from("Please press ENTER or ESC to close this dialog."),
|
||||
]));
|
||||
} else if let Some(to_kill_processes) = app_state.get_to_delete_processes() {
|
||||
if let Some(first_pid) = to_kill_processes.1.first() {
|
||||
return Some(Text::from(vec![
|
||||
Spans::from(""),
|
||||
if app_state.is_grouped(app_state.current_widget.widget_id) {
|
||||
if to_kill_processes.1.len() != 1 {
|
||||
Spans::from(format!(
|
||||
"Kill {} processes with the name \"{}\"? Press ENTER to confirm.",
|
||||
to_kill_processes.1.len(),
|
||||
to_kill_processes.0
|
||||
))
|
||||
} else {
|
||||
Spans::from(format!(
|
||||
"Kill 1 process with the name \"{}\"? Press ENTER to confirm.",
|
||||
to_kill_processes.0
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Spans::from(format!(
|
||||
"Kill process \"{}\" with PID {}? Press ENTER to confirm.",
|
||||
to_kill_processes.0, first_pid
|
||||
))
|
||||
},
|
||||
]));
|
||||
}
|
||||
} else if let Some(_to_kill_processes) = app_state.get_to_delete_processes() {
|
||||
// if let Some(first_pid) = to_kill_processes.1.first() {
|
||||
// return Some(Text::from(vec![
|
||||
// Spans::from(""),
|
||||
// if app_state.is_grouped(app_state.current_widget.widget_id) {
|
||||
// if to_kill_processes.1.len() != 1 {
|
||||
// Spans::from(format!(
|
||||
// "Kill {} processes with the name \"{}\"? Press ENTER to confirm.",
|
||||
// to_kill_processes.1.len(),
|
||||
// to_kill_processes.0
|
||||
// ))
|
||||
// } else {
|
||||
// Spans::from(format!(
|
||||
// "Kill 1 process with the name \"{}\"? Press ENTER to confirm.",
|
||||
// to_kill_processes.0
|
||||
// ))
|
||||
// }
|
||||
// } else {
|
||||
// Spans::from(format!(
|
||||
// "Kill process \"{}\" with PID {}? Press ENTER to confirm.",
|
||||
// to_kill_processes.0, first_pid
|
||||
// ))
|
||||
// },
|
||||
// ]));
|
||||
// }
|
||||
}
|
||||
|
||||
None
|
||||
@ -140,7 +140,7 @@ impl KillDialog for Painter {
|
||||
} else {
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
// TODO: Can probably make this const.
|
||||
// TODO: [Optimization, Const] Can probably make this const.
|
||||
let signal_text;
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
|
@ -1,249 +0,0 @@
|
||||
use crate::{
|
||||
app::App,
|
||||
canvas::{drawing_utils::interpolate_points, Painter},
|
||||
constants::*,
|
||||
};
|
||||
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Rect},
|
||||
symbols::Marker,
|
||||
terminal::Frame,
|
||||
text::Span,
|
||||
text::Spans,
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset},
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
pub trait MemGraphWidget {
|
||||
fn draw_memory_graph<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
);
|
||||
}
|
||||
|
||||
impl MemGraphWidget for Painter {
|
||||
fn draw_memory_graph<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
) {
|
||||
if let Some(mem_widget_state) = app_state.mem_state.widget_states.get_mut(&widget_id) {
|
||||
let mem_data: &mut [(f64, f64)] = &mut app_state.canvas_data.mem_data;
|
||||
let swap_data: &mut [(f64, f64)] = &mut app_state.canvas_data.swap_data;
|
||||
|
||||
let time_start = -(mem_widget_state.current_display_time as f64);
|
||||
|
||||
let display_time_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}s", mem_widget_state.current_display_time / 1000),
|
||||
self.colours.graph_style,
|
||||
),
|
||||
Span::styled("0s".to_string(), self.colours.graph_style),
|
||||
];
|
||||
let y_axis_label = vec![
|
||||
Span::styled(" 0%", self.colours.graph_style),
|
||||
Span::styled("100%", self.colours.graph_style),
|
||||
];
|
||||
|
||||
let x_axis = if app_state.app_config_fields.hide_time
|
||||
|| (app_state.app_config_fields.autohide_time
|
||||
&& mem_widget_state.autohide_timer.is_none())
|
||||
{
|
||||
Axis::default().bounds([time_start, 0.0])
|
||||
} else if let Some(time) = mem_widget_state.autohide_timer {
|
||||
if std::time::Instant::now().duration_since(time).as_millis()
|
||||
< AUTOHIDE_TIMEOUT_MILLISECONDS as u128
|
||||
{
|
||||
Axis::default()
|
||||
.bounds([time_start, 0.0])
|
||||
.style(self.colours.graph_style)
|
||||
.labels(display_time_labels)
|
||||
} else {
|
||||
mem_widget_state.autohide_timer = None;
|
||||
Axis::default().bounds([time_start, 0.0])
|
||||
}
|
||||
} else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT {
|
||||
Axis::default().bounds([time_start, 0.0])
|
||||
} else {
|
||||
Axis::default()
|
||||
.bounds([time_start, 0.0])
|
||||
.style(self.colours.graph_style)
|
||||
.labels(display_time_labels)
|
||||
};
|
||||
|
||||
let y_axis = Axis::default()
|
||||
.style(self.colours.graph_style)
|
||||
.bounds([0.0, 100.5])
|
||||
.labels(y_axis_label);
|
||||
|
||||
// Interpolate values to avoid ugly gaps
|
||||
let interpolated_mem_point = if let Some(end_pos) = mem_data
|
||||
.iter()
|
||||
.position(|(time, _data)| *time >= time_start)
|
||||
{
|
||||
if end_pos > 1 {
|
||||
let start_pos = end_pos - 1;
|
||||
let outside_point = mem_data.get(start_pos);
|
||||
let inside_point = mem_data.get(end_pos);
|
||||
|
||||
if let (Some(outside_point), Some(inside_point)) = (outside_point, inside_point)
|
||||
{
|
||||
let old = *outside_point;
|
||||
|
||||
let new_point = (
|
||||
time_start,
|
||||
interpolate_points(outside_point, inside_point, time_start),
|
||||
);
|
||||
|
||||
if let Some(to_replace) = mem_data.get_mut(start_pos) {
|
||||
*to_replace = new_point;
|
||||
Some((start_pos, old))
|
||||
} else {
|
||||
None // Failed to get mutable reference.
|
||||
}
|
||||
} else {
|
||||
None // Point somehow doesn't exist in our data
|
||||
}
|
||||
} else {
|
||||
None // Point is already "leftmost", no need to interpolate.
|
||||
}
|
||||
} else {
|
||||
None // There is no point.
|
||||
};
|
||||
|
||||
let interpolated_swap_point = if let Some(end_pos) = swap_data
|
||||
.iter()
|
||||
.position(|(time, _data)| *time >= time_start)
|
||||
{
|
||||
if end_pos > 1 {
|
||||
let start_pos = end_pos - 1;
|
||||
let outside_point = swap_data.get(start_pos);
|
||||
let inside_point = swap_data.get(end_pos);
|
||||
|
||||
if let (Some(outside_point), Some(inside_point)) = (outside_point, inside_point)
|
||||
{
|
||||
let old = *outside_point;
|
||||
|
||||
let new_point = (
|
||||
time_start,
|
||||
interpolate_points(outside_point, inside_point, time_start),
|
||||
);
|
||||
|
||||
if let Some(to_replace) = swap_data.get_mut(start_pos) {
|
||||
*to_replace = new_point;
|
||||
Some((start_pos, old))
|
||||
} else {
|
||||
None // Failed to get mutable reference.
|
||||
}
|
||||
} else {
|
||||
None // Point somehow doesn't exist in our data
|
||||
}
|
||||
} else {
|
||||
None // Point is already "leftmost", no need to interpolate.
|
||||
}
|
||||
} else {
|
||||
None // There is no point.
|
||||
};
|
||||
|
||||
let mut mem_canvas_vec: Vec<Dataset<'_>> = vec![];
|
||||
|
||||
if let Some((label_percent, label_frac)) = &app_state.canvas_data.mem_labels {
|
||||
let mem_label = format!("RAM:{}{}", label_percent, label_frac);
|
||||
mem_canvas_vec.push(
|
||||
Dataset::default()
|
||||
.name(mem_label)
|
||||
.marker(if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
})
|
||||
.style(self.colours.ram_style)
|
||||
.data(mem_data)
|
||||
.graph_type(tui::widgets::GraphType::Line),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some((label_percent, label_frac)) = &app_state.canvas_data.swap_labels {
|
||||
let swap_label = format!("SWP:{}{}", label_percent, label_frac);
|
||||
mem_canvas_vec.push(
|
||||
Dataset::default()
|
||||
.name(swap_label)
|
||||
.marker(if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
})
|
||||
.style(self.colours.swap_style)
|
||||
.data(swap_data)
|
||||
.graph_type(tui::widgets::GraphType::Line),
|
||||
);
|
||||
}
|
||||
|
||||
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
||||
let border_style = if is_on_widget {
|
||||
self.colours.highlighted_border_style
|
||||
} else {
|
||||
self.colours.border_style
|
||||
};
|
||||
|
||||
let title = if app_state.is_expanded {
|
||||
const TITLE_BASE: &str = " Memory ── Esc to go back ";
|
||||
Spans::from(vec![
|
||||
Span::styled(" Memory ", self.colours.widget_title_style),
|
||||
Span::styled(
|
||||
format!(
|
||||
"─{}─ Esc to go back ",
|
||||
"─".repeat(usize::from(draw_loc.width).saturating_sub(
|
||||
UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2
|
||||
))
|
||||
),
|
||||
border_style,
|
||||
),
|
||||
])
|
||||
} else {
|
||||
Spans::from(Span::styled(
|
||||
" Memory ".to_string(),
|
||||
self.colours.widget_title_style,
|
||||
))
|
||||
};
|
||||
|
||||
f.render_widget(
|
||||
Chart::new(mem_canvas_vec)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(if app_state.current_widget.widget_id == widget_id {
|
||||
self.colours.highlighted_border_style
|
||||
} else {
|
||||
self.colours.border_style
|
||||
}),
|
||||
)
|
||||
.x_axis(x_axis)
|
||||
.y_axis(y_axis)
|
||||
.hidden_legend_constraints((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))),
|
||||
draw_loc,
|
||||
);
|
||||
|
||||
// Now if you're done, reset any interpolated points!
|
||||
if let Some((index, old_value)) = interpolated_mem_point {
|
||||
if let Some(to_replace) = mem_data.get_mut(index) {
|
||||
*to_replace = old_value;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((index, old_value)) = interpolated_swap_point {
|
||||
if let Some(to_replace) = swap_data.get_mut(index) {
|
||||
*to_replace = old_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
|
||||
widget.bottom_right_corner =
|
||||
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,770 +0,0 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use std::cmp::max;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::{
|
||||
app::{App, AxisScaling},
|
||||
canvas::{
|
||||
drawing_utils::{get_column_widths, interpolate_points},
|
||||
Painter,
|
||||
},
|
||||
constants::*,
|
||||
units::data_units::DataUnit,
|
||||
utils::gen_util::*,
|
||||
};
|
||||
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
symbols::Marker,
|
||||
terminal::Frame,
|
||||
text::Span,
|
||||
text::{Spans, Text},
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset, Row, Table},
|
||||
};
|
||||
|
||||
const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"];
|
||||
|
||||
static NETWORK_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
|
||||
NETWORK_HEADERS
|
||||
.iter()
|
||||
.map(|entry| entry.len() as u16)
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
pub trait NetworkGraphWidget {
|
||||
fn draw_network<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
);
|
||||
|
||||
fn draw_network_graph<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
hide_legend: bool,
|
||||
);
|
||||
|
||||
fn draw_network_labels<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
);
|
||||
}
|
||||
|
||||
impl NetworkGraphWidget for Painter {
|
||||
fn draw_network<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
) {
|
||||
if app_state.app_config_fields.use_old_network_legend {
|
||||
let network_chunk = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints([
|
||||
Constraint::Length(max(draw_loc.height as i64 - 5, 0) as u16),
|
||||
Constraint::Length(5),
|
||||
])
|
||||
.split(draw_loc);
|
||||
|
||||
self.draw_network_graph(f, app_state, network_chunk[0], widget_id, true);
|
||||
self.draw_network_labels(f, app_state, network_chunk[1], widget_id);
|
||||
} else {
|
||||
self.draw_network_graph(f, app_state, draw_loc, widget_id, false);
|
||||
}
|
||||
|
||||
if app_state.should_get_widget_bounds() {
|
||||
// Update draw loc in widget map
|
||||
// Note that in both cases, we always go to the same widget id so it's fine to do it like
|
||||
// this lol.
|
||||
if let Some(network_widget) = app_state.widget_map.get_mut(&widget_id) {
|
||||
network_widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
|
||||
network_widget.bottom_right_corner =
|
||||
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_network_graph<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
hide_legend: bool,
|
||||
) {
|
||||
/// Point is of time, data
|
||||
type Point = (f64, f64);
|
||||
|
||||
/// Returns the max data point and time given a time.
|
||||
fn get_max_entry(
|
||||
rx: &[Point], tx: &[Point], time_start: f64, network_scale_type: &AxisScaling,
|
||||
network_use_binary_prefix: bool,
|
||||
) -> (f64, f64) {
|
||||
/// Determines a "fake" max value in circumstances where we couldn't find one from the data.
|
||||
fn calculate_missing_max(
|
||||
network_scale_type: &AxisScaling, network_use_binary_prefix: bool,
|
||||
) -> f64 {
|
||||
match network_scale_type {
|
||||
AxisScaling::Log => {
|
||||
if network_use_binary_prefix {
|
||||
LOG_KIBI_LIMIT
|
||||
} else {
|
||||
LOG_KILO_LIMIT
|
||||
}
|
||||
}
|
||||
AxisScaling::Linear => {
|
||||
if network_use_binary_prefix {
|
||||
KIBI_LIMIT_F64
|
||||
} else {
|
||||
KILO_LIMIT_F64
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First, let's shorten our ranges to actually look. We can abuse the fact that our rx and tx arrays
|
||||
// are sorted, so we can short-circuit our search to filter out only the relevant data points...
|
||||
let filtered_rx = if let (Some(rx_start), Some(rx_end)) = (
|
||||
rx.iter().position(|(time, _data)| *time >= time_start),
|
||||
rx.iter().rposition(|(time, _data)| *time <= 0.0),
|
||||
) {
|
||||
Some(&rx[rx_start..=rx_end])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let filtered_tx = if let (Some(tx_start), Some(tx_end)) = (
|
||||
tx.iter().position(|(time, _data)| *time >= time_start),
|
||||
tx.iter().rposition(|(time, _data)| *time <= 0.0),
|
||||
) {
|
||||
Some(&tx[tx_start..=tx_end])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Then, find the maximal rx/tx so we know how to scale, and return it.
|
||||
match (filtered_rx, filtered_tx) {
|
||||
(None, None) => (
|
||||
time_start,
|
||||
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||
),
|
||||
(None, Some(filtered_tx)) => {
|
||||
match filtered_tx
|
||||
.iter()
|
||||
.max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false))
|
||||
{
|
||||
Some((best_time, max_val)) => {
|
||||
if *max_val == 0.0 {
|
||||
(
|
||||
time_start,
|
||||
calculate_missing_max(
|
||||
network_scale_type,
|
||||
network_use_binary_prefix,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(*best_time, *max_val)
|
||||
}
|
||||
}
|
||||
None => (
|
||||
time_start,
|
||||
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||
),
|
||||
}
|
||||
}
|
||||
(Some(filtered_rx), None) => {
|
||||
match filtered_rx
|
||||
.iter()
|
||||
.max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false))
|
||||
{
|
||||
Some((best_time, max_val)) => {
|
||||
if *max_val == 0.0 {
|
||||
(
|
||||
time_start,
|
||||
calculate_missing_max(
|
||||
network_scale_type,
|
||||
network_use_binary_prefix,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(*best_time, *max_val)
|
||||
}
|
||||
}
|
||||
None => (
|
||||
time_start,
|
||||
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||
),
|
||||
}
|
||||
}
|
||||
(Some(filtered_rx), Some(filtered_tx)) => {
|
||||
match filtered_rx
|
||||
.iter()
|
||||
.chain(filtered_tx)
|
||||
.max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false))
|
||||
{
|
||||
Some((best_time, max_val)) => {
|
||||
if *max_val == 0.0 {
|
||||
(
|
||||
*best_time,
|
||||
calculate_missing_max(
|
||||
network_scale_type,
|
||||
network_use_binary_prefix,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(*best_time, *max_val)
|
||||
}
|
||||
}
|
||||
None => (
|
||||
time_start,
|
||||
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the required max data point and labels.
|
||||
fn adjust_network_data_point(
|
||||
max_entry: f64, network_scale_type: &AxisScaling, network_unit_type: &DataUnit,
|
||||
network_use_binary_prefix: bool,
|
||||
) -> (f64, Vec<String>) {
|
||||
// So, we're going with an approach like this for linear data:
|
||||
// - Main goal is to maximize the amount of information displayed given a specific height.
|
||||
// We don't want to drown out some data if the ranges are too far though! Nor do we want to filter
|
||||
// out too much data...
|
||||
// - Change the y-axis unit (kilo/kibi, mega/mebi...) dynamically based on max load.
|
||||
//
|
||||
// The idea is we take the top value, build our scale such that each "point" is a scaled version of that.
|
||||
// So for example, let's say I use 390 Mb/s. If I drew 4 segments, it would be 97.5, 195, 292.5, 390, and
|
||||
// probably something like 438.75?
|
||||
//
|
||||
// So, how do we do this in tui-rs? Well, if we are using intervals that tie in perfectly to the max
|
||||
// value we want... then it's actually not that hard. Since tui-rs accepts a vector as labels and will
|
||||
// properly space them all out... we just work with that and space it out properly.
|
||||
//
|
||||
// Dynamic chart idea based off of FreeNAS's chart design.
|
||||
//
|
||||
// ===
|
||||
//
|
||||
// For log data, we just use the old method of log intervals (kilo/mega/giga/etc.). Keep it nice and simple.
|
||||
|
||||
// Now just check the largest unit we correspond to... then proceed to build some entries from there!
|
||||
|
||||
let unit_char = match network_unit_type {
|
||||
DataUnit::Byte => "B",
|
||||
DataUnit::Bit => "b",
|
||||
};
|
||||
|
||||
match network_scale_type {
|
||||
AxisScaling::Linear => {
|
||||
let (k_limit, m_limit, g_limit, t_limit) = if network_use_binary_prefix {
|
||||
(
|
||||
KIBI_LIMIT_F64,
|
||||
MEBI_LIMIT_F64,
|
||||
GIBI_LIMIT_F64,
|
||||
TEBI_LIMIT_F64,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
KILO_LIMIT_F64,
|
||||
MEGA_LIMIT_F64,
|
||||
GIGA_LIMIT_F64,
|
||||
TERA_LIMIT_F64,
|
||||
)
|
||||
};
|
||||
|
||||
let bumped_max_entry = max_entry * 1.5; // We use the bumped up version to calculate our unit type.
|
||||
let (max_value_scaled, unit_prefix, unit_type): (f64, &str, &str) =
|
||||
if bumped_max_entry < k_limit {
|
||||
(max_entry, "", unit_char)
|
||||
} else if bumped_max_entry < m_limit {
|
||||
(
|
||||
max_entry / k_limit,
|
||||
if network_use_binary_prefix { "Ki" } else { "K" },
|
||||
unit_char,
|
||||
)
|
||||
} else if bumped_max_entry < g_limit {
|
||||
(
|
||||
max_entry / m_limit,
|
||||
if network_use_binary_prefix { "Mi" } else { "M" },
|
||||
unit_char,
|
||||
)
|
||||
} else if bumped_max_entry < t_limit {
|
||||
(
|
||||
max_entry / g_limit,
|
||||
if network_use_binary_prefix { "Gi" } else { "G" },
|
||||
unit_char,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
max_entry / t_limit,
|
||||
if network_use_binary_prefix { "Ti" } else { "T" },
|
||||
unit_char,
|
||||
)
|
||||
};
|
||||
|
||||
// Finally, build an acceptable range starting from there, using the given height!
|
||||
// Note we try to put more of a weight on the bottom section vs. the top, since the top has less data.
|
||||
|
||||
let base_unit = max_value_scaled;
|
||||
let labels: Vec<String> = vec![
|
||||
format!("0{}{}", unit_prefix, unit_type),
|
||||
format!("{:.1}", base_unit * 0.5),
|
||||
format!("{:.1}", base_unit),
|
||||
format!("{:.1}", base_unit * 1.5),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|s| format!("{:>5}", s)) // Pull 5 as the longest legend value is generally going to be 5 digits (if they somehow hit over 5 terabits per second)
|
||||
.collect();
|
||||
|
||||
(bumped_max_entry, labels)
|
||||
}
|
||||
AxisScaling::Log => {
|
||||
let (m_limit, g_limit, t_limit) = if network_use_binary_prefix {
|
||||
(LOG_MEBI_LIMIT, LOG_GIBI_LIMIT, LOG_TEBI_LIMIT)
|
||||
} else {
|
||||
(LOG_MEGA_LIMIT, LOG_GIGA_LIMIT, LOG_TERA_LIMIT)
|
||||
};
|
||||
|
||||
fn get_zero(network_use_binary_prefix: bool, unit_char: &str) -> String {
|
||||
format!(
|
||||
"{}0{}",
|
||||
if network_use_binary_prefix { " " } else { " " },
|
||||
unit_char
|
||||
)
|
||||
}
|
||||
|
||||
fn get_k(network_use_binary_prefix: bool, unit_char: &str) -> String {
|
||||
format!(
|
||||
"1{}{}",
|
||||
if network_use_binary_prefix { "Ki" } else { "K" },
|
||||
unit_char
|
||||
)
|
||||
}
|
||||
|
||||
fn get_m(network_use_binary_prefix: bool, unit_char: &str) -> String {
|
||||
format!(
|
||||
"1{}{}",
|
||||
if network_use_binary_prefix { "Mi" } else { "M" },
|
||||
unit_char
|
||||
)
|
||||
}
|
||||
|
||||
fn get_g(network_use_binary_prefix: bool, unit_char: &str) -> String {
|
||||
format!(
|
||||
"1{}{}",
|
||||
if network_use_binary_prefix { "Gi" } else { "G" },
|
||||
unit_char
|
||||
)
|
||||
}
|
||||
|
||||
fn get_t(network_use_binary_prefix: bool, unit_char: &str) -> String {
|
||||
format!(
|
||||
"1{}{}",
|
||||
if network_use_binary_prefix { "Ti" } else { "T" },
|
||||
unit_char
|
||||
)
|
||||
}
|
||||
|
||||
fn get_p(network_use_binary_prefix: bool, unit_char: &str) -> String {
|
||||
format!(
|
||||
"1{}{}",
|
||||
if network_use_binary_prefix { "Pi" } else { "P" },
|
||||
unit_char
|
||||
)
|
||||
}
|
||||
|
||||
if max_entry < m_limit {
|
||||
(
|
||||
m_limit,
|
||||
vec![
|
||||
get_zero(network_use_binary_prefix, unit_char),
|
||||
get_k(network_use_binary_prefix, unit_char),
|
||||
get_m(network_use_binary_prefix, unit_char),
|
||||
],
|
||||
)
|
||||
} else if max_entry < g_limit {
|
||||
(
|
||||
g_limit,
|
||||
vec![
|
||||
get_zero(network_use_binary_prefix, unit_char),
|
||||
get_k(network_use_binary_prefix, unit_char),
|
||||
get_m(network_use_binary_prefix, unit_char),
|
||||
get_g(network_use_binary_prefix, unit_char),
|
||||
],
|
||||
)
|
||||
} else if max_entry < t_limit {
|
||||
(
|
||||
t_limit,
|
||||
vec![
|
||||
get_zero(network_use_binary_prefix, unit_char),
|
||||
get_k(network_use_binary_prefix, unit_char),
|
||||
get_m(network_use_binary_prefix, unit_char),
|
||||
get_g(network_use_binary_prefix, unit_char),
|
||||
get_t(network_use_binary_prefix, unit_char),
|
||||
],
|
||||
)
|
||||
} else {
|
||||
// I really doubt anyone's transferring beyond petabyte speeds...
|
||||
(
|
||||
if network_use_binary_prefix {
|
||||
LOG_PEBI_LIMIT
|
||||
} else {
|
||||
LOG_PETA_LIMIT
|
||||
},
|
||||
vec![
|
||||
get_zero(network_use_binary_prefix, unit_char),
|
||||
get_k(network_use_binary_prefix, unit_char),
|
||||
get_m(network_use_binary_prefix, unit_char),
|
||||
get_g(network_use_binary_prefix, unit_char),
|
||||
get_t(network_use_binary_prefix, unit_char),
|
||||
get_p(network_use_binary_prefix, unit_char),
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(network_widget_state) = app_state.net_state.widget_states.get_mut(&widget_id) {
|
||||
let network_data_rx: &mut [(f64, f64)] = &mut app_state.canvas_data.network_data_rx;
|
||||
let network_data_tx: &mut [(f64, f64)] = &mut app_state.canvas_data.network_data_tx;
|
||||
|
||||
let time_start = -(network_widget_state.current_display_time as f64);
|
||||
|
||||
let display_time_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}s", network_widget_state.current_display_time / 1000),
|
||||
self.colours.graph_style,
|
||||
),
|
||||
Span::styled("0s".to_string(), self.colours.graph_style),
|
||||
];
|
||||
let x_axis = if app_state.app_config_fields.hide_time
|
||||
|| (app_state.app_config_fields.autohide_time
|
||||
&& network_widget_state.autohide_timer.is_none())
|
||||
{
|
||||
Axis::default().bounds([time_start, 0.0])
|
||||
} else if let Some(time) = network_widget_state.autohide_timer {
|
||||
if std::time::Instant::now().duration_since(time).as_millis()
|
||||
< AUTOHIDE_TIMEOUT_MILLISECONDS as u128
|
||||
{
|
||||
Axis::default()
|
||||
.bounds([time_start, 0.0])
|
||||
.style(self.colours.graph_style)
|
||||
.labels(display_time_labels)
|
||||
} else {
|
||||
network_widget_state.autohide_timer = None;
|
||||
Axis::default().bounds([time_start, 0.0])
|
||||
}
|
||||
} else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT {
|
||||
Axis::default().bounds([time_start, 0.0])
|
||||
} else {
|
||||
Axis::default()
|
||||
.bounds([time_start, 0.0])
|
||||
.style(self.colours.graph_style)
|
||||
.labels(display_time_labels)
|
||||
};
|
||||
|
||||
// Interpolate a point for rx and tx between the last value outside of the left bounds and the first value
|
||||
// inside it.
|
||||
// Because we assume it is all in order for... basically all our code, we can't just append it,
|
||||
// and insertion in the middle seems. So instead, we swap *out* the value that is outside with our
|
||||
// interpolated point, draw and do whatever calculations, then swap back in the old value!
|
||||
//
|
||||
// Note there is some re-used work here! For potential optimizations, we could re-use some work here in/from
|
||||
// get_max_entry...
|
||||
let interpolated_rx_point = if let Some(rx_end_pos) = network_data_rx
|
||||
.iter()
|
||||
.position(|(time, _data)| *time >= time_start)
|
||||
{
|
||||
if rx_end_pos > 1 {
|
||||
let rx_start_pos = rx_end_pos - 1;
|
||||
let outside_rx_point = network_data_rx.get(rx_start_pos);
|
||||
let inside_rx_point = network_data_rx.get(rx_end_pos);
|
||||
|
||||
if let (Some(outside_rx_point), Some(inside_rx_point)) =
|
||||
(outside_rx_point, inside_rx_point)
|
||||
{
|
||||
let old = *outside_rx_point;
|
||||
|
||||
let new_point = (
|
||||
time_start,
|
||||
interpolate_points(outside_rx_point, inside_rx_point, time_start),
|
||||
);
|
||||
|
||||
// debug!(
|
||||
// "Interpolated between {:?} and {:?}, got rx for time {:?}: {:?}",
|
||||
// outside_rx_point, inside_rx_point, time_start, new_point
|
||||
// );
|
||||
|
||||
if let Some(to_replace) = network_data_rx.get_mut(rx_start_pos) {
|
||||
*to_replace = new_point;
|
||||
Some((rx_start_pos, old))
|
||||
} else {
|
||||
None // Failed to get mutable reference.
|
||||
}
|
||||
} else {
|
||||
None // Point somehow doesn't exist in our network_data_rx
|
||||
}
|
||||
} else {
|
||||
None // Point is already "leftmost", no need to interpolate.
|
||||
}
|
||||
} else {
|
||||
None // There is no point.
|
||||
};
|
||||
|
||||
let interpolated_tx_point = if let Some(tx_end_pos) = network_data_tx
|
||||
.iter()
|
||||
.position(|(time, _data)| *time >= time_start)
|
||||
{
|
||||
if tx_end_pos > 1 {
|
||||
let tx_start_pos = tx_end_pos - 1;
|
||||
let outside_tx_point = network_data_tx.get(tx_start_pos);
|
||||
let inside_tx_point = network_data_tx.get(tx_end_pos);
|
||||
|
||||
if let (Some(outside_tx_point), Some(inside_tx_point)) =
|
||||
(outside_tx_point, inside_tx_point)
|
||||
{
|
||||
let old = *outside_tx_point;
|
||||
|
||||
let new_point = (
|
||||
time_start,
|
||||
interpolate_points(outside_tx_point, inside_tx_point, time_start),
|
||||
);
|
||||
|
||||
if let Some(to_replace) = network_data_tx.get_mut(tx_start_pos) {
|
||||
*to_replace = new_point;
|
||||
Some((tx_start_pos, old))
|
||||
} else {
|
||||
None // Failed to get mutable reference.
|
||||
}
|
||||
} else {
|
||||
None // Point somehow doesn't exist in our network_data_tx
|
||||
}
|
||||
} else {
|
||||
None // Point is already "leftmost", no need to interpolate.
|
||||
}
|
||||
} else {
|
||||
None // There is no point.
|
||||
};
|
||||
|
||||
// TODO: Cache network results: Only update if:
|
||||
// - Force update (includes time interval change)
|
||||
// - Old max time is off screen
|
||||
// - A new time interval is better and does not fit (check from end of vector to last checked; we only want to update if it is TOO big!)
|
||||
|
||||
// Find the maximal rx/tx so we know how to scale, and return it.
|
||||
|
||||
let (_best_time, max_entry) = get_max_entry(
|
||||
network_data_rx,
|
||||
network_data_tx,
|
||||
time_start,
|
||||
&app_state.app_config_fields.network_scale_type,
|
||||
app_state.app_config_fields.network_use_binary_prefix,
|
||||
);
|
||||
|
||||
let (max_range, labels) = adjust_network_data_point(
|
||||
max_entry,
|
||||
&app_state.app_config_fields.network_scale_type,
|
||||
&app_state.app_config_fields.network_unit_type,
|
||||
app_state.app_config_fields.network_use_binary_prefix,
|
||||
);
|
||||
|
||||
// Cache results.
|
||||
// network_widget_state.draw_max_range_cache = max_range;
|
||||
// network_widget_state.draw_time_start_cache = best_time;
|
||||
// network_widget_state.draw_labels_cache = labels;
|
||||
|
||||
let y_axis_labels = labels
|
||||
.iter()
|
||||
.map(|label| Span::styled(label, self.colours.graph_style))
|
||||
.collect::<Vec<_>>();
|
||||
let y_axis = Axis::default()
|
||||
.style(self.colours.graph_style)
|
||||
.bounds([0.0, max_range])
|
||||
.labels(y_axis_labels);
|
||||
|
||||
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
||||
let border_style = if is_on_widget {
|
||||
self.colours.highlighted_border_style
|
||||
} else {
|
||||
self.colours.border_style
|
||||
};
|
||||
|
||||
let title = if app_state.is_expanded {
|
||||
const TITLE_BASE: &str = " Network ── Esc to go back ";
|
||||
Spans::from(vec![
|
||||
Span::styled(" Network ", self.colours.widget_title_style),
|
||||
Span::styled(
|
||||
format!(
|
||||
"─{}─ Esc to go back ",
|
||||
"─".repeat(usize::from(draw_loc.width).saturating_sub(
|
||||
UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2
|
||||
))
|
||||
),
|
||||
border_style,
|
||||
),
|
||||
])
|
||||
} else {
|
||||
Spans::from(Span::styled(" Network ", self.colours.widget_title_style))
|
||||
};
|
||||
|
||||
let legend_constraints = if hide_legend {
|
||||
(Constraint::Ratio(0, 1), Constraint::Ratio(0, 1))
|
||||
} else {
|
||||
(Constraint::Ratio(1, 1), Constraint::Ratio(3, 4))
|
||||
};
|
||||
|
||||
// TODO: Add support for clicking on legend to only show that value on chart.
|
||||
let dataset = if app_state.app_config_fields.use_old_network_legend && !hide_legend {
|
||||
vec![
|
||||
Dataset::default()
|
||||
.name(format!("RX: {:7}", app_state.canvas_data.rx_display))
|
||||
.marker(if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
})
|
||||
.style(self.colours.rx_style)
|
||||
.data(network_data_rx)
|
||||
.graph_type(tui::widgets::GraphType::Line),
|
||||
Dataset::default()
|
||||
.name(format!("TX: {:7}", app_state.canvas_data.tx_display))
|
||||
.marker(if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
})
|
||||
.style(self.colours.tx_style)
|
||||
.data(network_data_tx)
|
||||
.graph_type(tui::widgets::GraphType::Line),
|
||||
Dataset::default()
|
||||
.name(format!(
|
||||
"Total RX: {:7}",
|
||||
app_state.canvas_data.total_rx_display
|
||||
))
|
||||
.style(self.colours.total_rx_style),
|
||||
Dataset::default()
|
||||
.name(format!(
|
||||
"Total TX: {:7}",
|
||||
app_state.canvas_data.total_tx_display
|
||||
))
|
||||
.style(self.colours.total_tx_style),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
Dataset::default()
|
||||
.name(&app_state.canvas_data.rx_display)
|
||||
.marker(if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
})
|
||||
.style(self.colours.rx_style)
|
||||
.data(network_data_rx)
|
||||
.graph_type(tui::widgets::GraphType::Line),
|
||||
Dataset::default()
|
||||
.name(&app_state.canvas_data.tx_display)
|
||||
.marker(if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
})
|
||||
.style(self.colours.tx_style)
|
||||
.data(network_data_tx)
|
||||
.graph_type(tui::widgets::GraphType::Line),
|
||||
]
|
||||
};
|
||||
|
||||
f.render_widget(
|
||||
Chart::new(dataset)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(if app_state.current_widget.widget_id == widget_id {
|
||||
self.colours.highlighted_border_style
|
||||
} else {
|
||||
self.colours.border_style
|
||||
}),
|
||||
)
|
||||
.x_axis(x_axis)
|
||||
.y_axis(y_axis)
|
||||
.hidden_legend_constraints(legend_constraints),
|
||||
draw_loc,
|
||||
);
|
||||
|
||||
// Now if you're done, reset any interpolated points!
|
||||
if let Some((index, old_value)) = interpolated_rx_point {
|
||||
if let Some(to_replace) = network_data_rx.get_mut(index) {
|
||||
*to_replace = old_value;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((index, old_value)) = interpolated_tx_point {
|
||||
if let Some(to_replace) = network_data_tx.get_mut(index) {
|
||||
*to_replace = old_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_network_labels<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
) {
|
||||
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
|
||||
0
|
||||
} else {
|
||||
app_state.app_config_fields.table_gap
|
||||
};
|
||||
|
||||
let rx_display = &app_state.canvas_data.rx_display;
|
||||
let tx_display = &app_state.canvas_data.tx_display;
|
||||
let total_rx_display = &app_state.canvas_data.total_rx_display;
|
||||
let total_tx_display = &app_state.canvas_data.total_tx_display;
|
||||
|
||||
// Gross but I need it to work...
|
||||
let total_network = vec![vec![
|
||||
Text::raw(rx_display),
|
||||
Text::raw(tx_display),
|
||||
Text::raw(total_rx_display),
|
||||
Text::raw(total_tx_display),
|
||||
]];
|
||||
let mapped_network = total_network
|
||||
.into_iter()
|
||||
.map(|val| Row::new(val).style(self.colours.text_style));
|
||||
|
||||
// Calculate widths
|
||||
let intrinsic_widths = get_column_widths(
|
||||
draw_loc.width,
|
||||
&[None, None, None, None],
|
||||
&(NETWORK_HEADERS_LENS
|
||||
.iter()
|
||||
.map(|s| Some(*s))
|
||||
.collect::<Vec<_>>()),
|
||||
&[Some(0.25); 4],
|
||||
&(NETWORK_HEADERS_LENS
|
||||
.iter()
|
||||
.map(|s| Some(*s))
|
||||
.collect::<Vec<_>>()),
|
||||
true,
|
||||
);
|
||||
|
||||
// Draw
|
||||
f.render_widget(
|
||||
Table::new(mapped_network)
|
||||
.header(
|
||||
Row::new(NETWORK_HEADERS.to_vec())
|
||||
.style(self.colours.table_header_style)
|
||||
.bottom_margin(table_gap),
|
||||
)
|
||||
.block(Block::default().borders(Borders::ALL).border_style(
|
||||
if app_state.current_widget.widget_id == widget_id {
|
||||
self.colours.highlighted_border_style
|
||||
} else {
|
||||
self.colours.border_style
|
||||
},
|
||||
))
|
||||
.style(self.colours.text_style)
|
||||
.widths(
|
||||
&(intrinsic_widths
|
||||
.iter()
|
||||
.map(|calculated_width| Constraint::Length(*calculated_width as u16))
|
||||
.collect::<Vec<_>>()),
|
||||
),
|
||||
draw_loc,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,911 +0,0 @@
|
||||
use crate::{
|
||||
app::App,
|
||||
canvas::{
|
||||
drawing_utils::{get_column_widths, get_search_start_position, get_start_position},
|
||||
Painter,
|
||||
},
|
||||
constants::*,
|
||||
};
|
||||
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
terminal::Frame,
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, Paragraph, Row, Table},
|
||||
};
|
||||
|
||||
use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static PROCESS_HEADERS_HARD_WIDTH_NO_GROUP: Lazy<Vec<Option<u16>>> = Lazy::new(|| {
|
||||
vec![
|
||||
Some(7),
|
||||
None,
|
||||
Some(8),
|
||||
Some(8),
|
||||
Some(8),
|
||||
Some(8),
|
||||
Some(7),
|
||||
Some(8),
|
||||
#[cfg(target_family = "unix")]
|
||||
None,
|
||||
None,
|
||||
]
|
||||
});
|
||||
static PROCESS_HEADERS_HARD_WIDTH_GROUPED: Lazy<Vec<Option<u16>>> = Lazy::new(|| {
|
||||
vec![
|
||||
Some(7),
|
||||
None,
|
||||
Some(8),
|
||||
Some(8),
|
||||
Some(8),
|
||||
Some(8),
|
||||
Some(7),
|
||||
Some(8),
|
||||
]
|
||||
});
|
||||
|
||||
static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND: Lazy<Vec<Option<f64>>> =
|
||||
Lazy::new(|| vec![None, Some(0.7), None, None, None, None, None, None]);
|
||||
static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE: Lazy<Vec<Option<f64>>> =
|
||||
Lazy::new(|| vec![None, Some(0.3), None, None, None, None, None, None]);
|
||||
|
||||
static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND: Lazy<Vec<Option<f64>>> = Lazy::new(|| {
|
||||
vec![
|
||||
None,
|
||||
Some(0.7),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
#[cfg(target_family = "unix")]
|
||||
Some(0.05),
|
||||
Some(0.2),
|
||||
]
|
||||
});
|
||||
static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE: Lazy<Vec<Option<f64>>> = Lazy::new(|| {
|
||||
vec![
|
||||
None,
|
||||
Some(0.5),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
#[cfg(target_family = "unix")]
|
||||
Some(0.05),
|
||||
Some(0.2),
|
||||
]
|
||||
});
|
||||
static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: Lazy<Vec<Option<f64>>> = Lazy::new(|| {
|
||||
vec![
|
||||
None,
|
||||
Some(0.3),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
#[cfg(target_family = "unix")]
|
||||
Some(0.05),
|
||||
Some(0.2),
|
||||
]
|
||||
});
|
||||
|
||||
pub trait ProcessTableWidget {
|
||||
/// Draws and handles all process-related drawing. Use this.
|
||||
/// - `widget_id` here represents the widget ID of the process widget itself!
|
||||
fn draw_process_features<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
);
|
||||
|
||||
/// Draws the process sort box.
|
||||
/// - `widget_id` represents the widget ID of the process widget itself.
|
||||
///
|
||||
/// This should not be directly called.
|
||||
fn draw_processes_table<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
);
|
||||
|
||||
/// Draws the process search field.
|
||||
/// - `widget_id` represents the widget ID of the search box itself --- NOT the process widget
|
||||
/// state that is stored.
|
||||
///
|
||||
/// This should not be directly called.
|
||||
fn draw_search_field<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
);
|
||||
|
||||
/// Draws the process sort box.
|
||||
/// - `widget_id` represents the widget ID of the sort box itself --- NOT the process widget
|
||||
/// state that is stored.
|
||||
///
|
||||
/// This should not be directly called.
|
||||
fn draw_process_sort<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
);
|
||||
}
|
||||
|
||||
impl ProcessTableWidget for Painter {
|
||||
fn draw_process_features<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
) {
|
||||
if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) {
|
||||
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;
|
||||
|
||||
let mut proc_draw_loc = draw_loc;
|
||||
if process_widget_state.is_search_enabled() {
|
||||
let processes_chunk = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(0), Constraint::Length(search_height)])
|
||||
.split(draw_loc);
|
||||
proc_draw_loc = processes_chunk[0];
|
||||
|
||||
self.draw_search_field(
|
||||
f,
|
||||
app_state,
|
||||
processes_chunk[1],
|
||||
draw_border,
|
||||
widget_id + 1,
|
||||
);
|
||||
}
|
||||
|
||||
if is_sort_open {
|
||||
let processes_chunk = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Length(header_len + 4), Constraint::Min(0)])
|
||||
.split(proc_draw_loc);
|
||||
proc_draw_loc = processes_chunk[1];
|
||||
|
||||
self.draw_process_sort(
|
||||
f,
|
||||
app_state,
|
||||
processes_chunk[0],
|
||||
draw_border,
|
||||
widget_id + 2,
|
||||
);
|
||||
}
|
||||
|
||||
self.draw_processes_table(f, app_state, proc_draw_loc, draw_border, widget_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_processes_table<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
) {
|
||||
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) {
|
||||
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 margined_draw_loc = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)])
|
||||
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
|
||||
.direction(Direction::Horizontal)
|
||||
.split(draw_loc)[0];
|
||||
|
||||
let (border_style, highlight_style) = if is_on_widget {
|
||||
(
|
||||
self.colours.highlighted_border_style,
|
||||
self.colours.currently_selected_text_style,
|
||||
)
|
||||
} else {
|
||||
(self.colours.border_style, self.colours.text_style)
|
||||
};
|
||||
|
||||
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
|
||||
.get(&widget_id)
|
||||
{
|
||||
let title = format!(
|
||||
" Processes ({} of {}) ",
|
||||
proc_widget_state
|
||||
.scroll_state
|
||||
.current_scroll_position
|
||||
.saturating_add(1),
|
||||
finalized_process_data.len()
|
||||
);
|
||||
|
||||
if title.len() <= draw_loc.width as usize {
|
||||
title
|
||||
} else {
|
||||
" Processes ".to_string()
|
||||
}
|
||||
} else {
|
||||
" Processes ".to_string()
|
||||
}
|
||||
} else {
|
||||
" Processes ".to_string()
|
||||
};
|
||||
|
||||
let title = if app_state.is_expanded
|
||||
&& !proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.is_enabled
|
||||
&& !proc_widget_state.is_sort_open
|
||||
{
|
||||
const ESCAPE_ENDING: &str = "── Esc to go back ";
|
||||
|
||||
let (chosen_title_base, expanded_title_base) = {
|
||||
let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING);
|
||||
|
||||
if temp_title_base.len() > draw_loc.width as usize {
|
||||
(
|
||||
" Processes ".to_string(),
|
||||
format!("{}{}", " Processes ".to_string(), ESCAPE_ENDING),
|
||||
)
|
||||
} else {
|
||||
(title_base, temp_title_base)
|
||||
}
|
||||
};
|
||||
|
||||
Spans::from(vec![
|
||||
Span::styled(chosen_title_base, self.colours.widget_title_style),
|
||||
Span::styled(
|
||||
format!(
|
||||
"─{}─ Esc to go back ",
|
||||
"─".repeat(
|
||||
usize::from(draw_loc.width).saturating_sub(
|
||||
UnicodeSegmentation::graphemes(
|
||||
expanded_title_base.as_str(),
|
||||
true
|
||||
)
|
||||
.count()
|
||||
+ 2
|
||||
)
|
||||
)
|
||||
),
|
||||
border_style,
|
||||
),
|
||||
])
|
||||
} else {
|
||||
Spans::from(Span::styled(title_base, self.colours.widget_title_style))
|
||||
};
|
||||
|
||||
let process_block = if draw_border {
|
||||
Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(border_style)
|
||||
} else if is_on_widget {
|
||||
Block::default()
|
||||
.borders(*SIDE_BORDERS)
|
||||
.border_style(self.colours.highlighted_border_style)
|
||||
} else {
|
||||
Block::default().borders(Borders::NONE)
|
||||
};
|
||||
|
||||
if let Some(process_data) = &app_state
|
||||
.canvas_data
|
||||
.stringified_process_data_map
|
||||
.get(&widget_id)
|
||||
{
|
||||
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
|
||||
0
|
||||
} else {
|
||||
app_state.app_config_fields.table_gap
|
||||
};
|
||||
let position = get_start_position(
|
||||
usize::from(
|
||||
(draw_loc.height + (1 - table_gap))
|
||||
.saturating_sub(self.table_height_offset),
|
||||
),
|
||||
&proc_widget_state.scroll_state.scroll_direction,
|
||||
&mut proc_widget_state.scroll_state.previous_scroll_position,
|
||||
proc_widget_state.scroll_state.current_scroll_position,
|
||||
app_state.is_force_redraw,
|
||||
);
|
||||
|
||||
// Sanity check
|
||||
let start_position = if position >= process_data.len() {
|
||||
process_data.len().saturating_sub(1)
|
||||
} else {
|
||||
position
|
||||
};
|
||||
|
||||
let sliced_vec = &process_data[start_position..];
|
||||
let processed_sliced_vec = sliced_vec.iter().map(|(data, disabled)| {
|
||||
(
|
||||
data.iter()
|
||||
.map(|(entry, _alternative)| entry)
|
||||
.collect::<Vec<_>>(),
|
||||
disabled,
|
||||
)
|
||||
});
|
||||
|
||||
let proc_table_state = &mut proc_widget_state.scroll_state.table_state;
|
||||
proc_table_state.select(Some(
|
||||
proc_widget_state
|
||||
.scroll_state
|
||||
.current_scroll_position
|
||||
.saturating_sub(start_position),
|
||||
));
|
||||
|
||||
// Draw!
|
||||
let process_headers = proc_widget_state.columns.get_column_headers(
|
||||
&proc_widget_state.process_sorting_type,
|
||||
proc_widget_state.is_process_sort_descending,
|
||||
);
|
||||
|
||||
// Calculate widths
|
||||
// FIXME: See if we can move this into the recalculate block? I want to move column widths into the column widths
|
||||
let hard_widths = if proc_widget_state.is_grouped {
|
||||
&*PROCESS_HEADERS_HARD_WIDTH_GROUPED
|
||||
} else {
|
||||
&*PROCESS_HEADERS_HARD_WIDTH_NO_GROUP
|
||||
};
|
||||
|
||||
if recalculate_column_widths {
|
||||
let mut column_widths = process_headers
|
||||
.iter()
|
||||
.map(|entry| UnicodeWidthStr::width(entry.as_str()) as u16)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let soft_widths_min = column_widths
|
||||
.iter()
|
||||
.map(|width| Some(*width))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
proc_widget_state.table_width_state.desired_column_widths = {
|
||||
for (row, _disabled) in processed_sliced_vec.clone() {
|
||||
for (col, entry) in row.iter().enumerate() {
|
||||
if let Some(col_width) = column_widths.get_mut(col) {
|
||||
let grapheme_len = UnicodeWidthStr::width(entry.as_str());
|
||||
if grapheme_len as u16 > *col_width {
|
||||
*col_width = grapheme_len as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
column_widths
|
||||
};
|
||||
|
||||
proc_widget_state.table_width_state.desired_column_widths = proc_widget_state
|
||||
.table_width_state
|
||||
.desired_column_widths
|
||||
.iter()
|
||||
.zip(hard_widths)
|
||||
.map(|(current, hard)| {
|
||||
if let Some(hard) = hard {
|
||||
if *hard > *current {
|
||||
*hard
|
||||
} else {
|
||||
*current
|
||||
}
|
||||
} else {
|
||||
*current
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let soft_widths_max = if proc_widget_state.is_grouped {
|
||||
// Note grouped trees are not a thing.
|
||||
|
||||
if proc_widget_state.is_using_command {
|
||||
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND
|
||||
} else {
|
||||
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE
|
||||
}
|
||||
} else if proc_widget_state.is_using_command {
|
||||
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND
|
||||
} else if proc_widget_state.is_tree_mode {
|
||||
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE
|
||||
} else {
|
||||
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE
|
||||
};
|
||||
|
||||
proc_widget_state.table_width_state.calculated_column_widths =
|
||||
get_column_widths(
|
||||
draw_loc.width,
|
||||
hard_widths,
|
||||
&soft_widths_min,
|
||||
soft_widths_max,
|
||||
&(proc_widget_state
|
||||
.table_width_state
|
||||
.desired_column_widths
|
||||
.iter()
|
||||
.map(|width| Some(*width))
|
||||
.collect::<Vec<_>>()),
|
||||
true,
|
||||
);
|
||||
|
||||
// debug!(
|
||||
// "DCW: {:?}",
|
||||
// proc_widget_state.table_width_state.desired_column_widths
|
||||
// );
|
||||
// debug!(
|
||||
// "CCW: {:?}",
|
||||
// proc_widget_state.table_width_state.calculated_column_widths
|
||||
// );
|
||||
}
|
||||
|
||||
let dcw = &proc_widget_state.table_width_state.desired_column_widths;
|
||||
let ccw = &proc_widget_state.table_width_state.calculated_column_widths;
|
||||
|
||||
let process_rows = sliced_vec.iter().map(|(data, disabled)| {
|
||||
let truncated_data = data.iter().zip(hard_widths).enumerate().map(
|
||||
|(itx, ((entry, alternative), width))| {
|
||||
if let (Some(desired_col_width), Some(calculated_col_width)) =
|
||||
(dcw.get(itx), ccw.get(itx))
|
||||
{
|
||||
if width.is_none() {
|
||||
if *desired_col_width > *calculated_col_width
|
||||
&& *calculated_col_width > 0
|
||||
{
|
||||
let graphemes =
|
||||
UnicodeSegmentation::graphemes(entry.as_str(), true)
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
if let Some(alternative) = alternative {
|
||||
Text::raw(alternative)
|
||||
} else if graphemes.len() > *calculated_col_width as usize
|
||||
&& *calculated_col_width > 1
|
||||
{
|
||||
// Truncate with ellipsis
|
||||
let first_n = graphemes
|
||||
[..(*calculated_col_width as usize - 1)]
|
||||
.concat();
|
||||
Text::raw(format!("{}…", first_n))
|
||||
} else {
|
||||
Text::raw(entry)
|
||||
}
|
||||
} else {
|
||||
Text::raw(entry)
|
||||
}
|
||||
} else {
|
||||
Text::raw(entry)
|
||||
}
|
||||
} else {
|
||||
Text::raw(entry)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if *disabled {
|
||||
Row::new(truncated_data).style(self.colours.disabled_text_style)
|
||||
} else {
|
||||
Row::new(truncated_data)
|
||||
}
|
||||
});
|
||||
|
||||
f.render_stateful_widget(
|
||||
Table::new(process_rows)
|
||||
.header(
|
||||
Row::new(process_headers)
|
||||
.style(self.colours.table_header_style)
|
||||
.bottom_margin(table_gap),
|
||||
)
|
||||
.block(process_block)
|
||||
.highlight_style(highlight_style)
|
||||
.style(self.colours.text_style)
|
||||
.widths(
|
||||
&(proc_widget_state
|
||||
.table_width_state
|
||||
.calculated_column_widths
|
||||
.iter()
|
||||
.map(|calculated_width| {
|
||||
Constraint::Length(*calculated_width as u16)
|
||||
})
|
||||
.collect::<Vec<_>>()),
|
||||
),
|
||||
margined_draw_loc,
|
||||
proc_table_state,
|
||||
);
|
||||
} else {
|
||||
f.render_widget(process_block, margined_draw_loc);
|
||||
}
|
||||
|
||||
// Check if we need to update columnar bounds...
|
||||
if recalculate_column_widths
|
||||
|| proc_widget_state.columns.column_header_x_locs.is_none()
|
||||
|| proc_widget_state.columns.column_header_y_loc.is_none()
|
||||
{
|
||||
// y location is just the y location of the widget + border size (1 normally, 0 in basic)
|
||||
proc_widget_state.columns.column_header_y_loc =
|
||||
Some(draw_loc.y + if draw_border { 1 } else { 0 });
|
||||
|
||||
// x location is determined using the x locations of the widget; just offset from the left bound
|
||||
// as appropriate, and use the right bound as limiter.
|
||||
|
||||
let mut current_x_left = draw_loc.x + 1;
|
||||
let max_x_right = draw_loc.x + draw_loc.width - 1;
|
||||
|
||||
let mut x_locs = vec![];
|
||||
|
||||
for width in proc_widget_state
|
||||
.table_width_state
|
||||
.calculated_column_widths
|
||||
.iter()
|
||||
{
|
||||
let right_bound = current_x_left + width;
|
||||
|
||||
if right_bound < max_x_right {
|
||||
x_locs.push((current_x_left, right_bound));
|
||||
current_x_left = right_bound + 1;
|
||||
} else {
|
||||
x_locs.push((current_x_left, max_x_right));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
proc_widget_state.columns.column_header_x_locs = Some(x_locs);
|
||||
}
|
||||
|
||||
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) {
|
||||
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,
|
||||
margined_draw_loc.y + margined_draw_loc.height,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_search_field<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
) {
|
||||
fn build_query<'a>(
|
||||
is_on_widget: bool, grapheme_indices: GraphemeIndices<'a>, start_position: usize,
|
||||
cursor_position: usize, query: &str, currently_selected_text_style: tui::style::Style,
|
||||
text_style: tui::style::Style,
|
||||
) -> Vec<Span<'a>> {
|
||||
let mut current_grapheme_posn = 0;
|
||||
|
||||
if is_on_widget {
|
||||
let mut res = grapheme_indices
|
||||
.filter_map(|grapheme| {
|
||||
current_grapheme_posn += UnicodeWidthStr::width(grapheme.1);
|
||||
|
||||
if current_grapheme_posn <= start_position {
|
||||
None
|
||||
} else {
|
||||
let styled = if grapheme.0 == cursor_position {
|
||||
Span::styled(grapheme.1, currently_selected_text_style)
|
||||
} else {
|
||||
Span::styled(grapheme.1, text_style)
|
||||
};
|
||||
Some(styled)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if cursor_position == query.len() {
|
||||
res.push(Span::styled(" ", currently_selected_text_style))
|
||||
}
|
||||
|
||||
res
|
||||
} else {
|
||||
// This is easier - we just need to get a range of graphemes, rather than
|
||||
// dealing with possibly inserting a cursor (as none is shown!)
|
||||
|
||||
vec![Span::styled(query.to_string(), text_style)]
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
let num_columns = usize::from(draw_loc.width);
|
||||
let search_title = "> ";
|
||||
|
||||
let num_chars_for_text = search_title.len();
|
||||
let cursor_position = proc_widget_state.get_search_cursor_position();
|
||||
let current_cursor_position = proc_widget_state.get_char_cursor_position();
|
||||
|
||||
let start_position: usize = get_search_start_position(
|
||||
num_columns - num_chars_for_text - 5,
|
||||
&proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.cursor_direction,
|
||||
&mut proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.cursor_bar,
|
||||
current_cursor_position,
|
||||
app_state.is_force_redraw,
|
||||
);
|
||||
|
||||
let query = proc_widget_state.get_current_search_query().as_str();
|
||||
let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true);
|
||||
|
||||
// TODO: [CURSOR] blank cursor if not selected
|
||||
// TODO: [CURSOR] blinking cursor?
|
||||
let query_with_cursor = build_query(
|
||||
is_on_widget,
|
||||
grapheme_indices,
|
||||
start_position,
|
||||
cursor_position,
|
||||
query,
|
||||
self.colours.currently_selected_text_style,
|
||||
self.colours.text_style,
|
||||
);
|
||||
|
||||
let mut search_text = vec![Spans::from({
|
||||
let mut search_vec = vec![Span::styled(
|
||||
search_title,
|
||||
if is_on_widget {
|
||||
self.colours.table_header_style
|
||||
} else {
|
||||
self.colours.text_style
|
||||
},
|
||||
)];
|
||||
search_vec.extend(query_with_cursor);
|
||||
|
||||
search_vec
|
||||
})];
|
||||
|
||||
// Text options shamelessly stolen from VS Code.
|
||||
let case_style = if !proc_widget_state.process_search_state.is_ignoring_case {
|
||||
self.colours.currently_selected_text_style
|
||||
} else {
|
||||
self.colours.text_style
|
||||
};
|
||||
|
||||
let whole_word_style = if proc_widget_state
|
||||
.process_search_state
|
||||
.is_searching_whole_word
|
||||
{
|
||||
self.colours.currently_selected_text_style
|
||||
} else {
|
||||
self.colours.text_style
|
||||
};
|
||||
|
||||
let regex_style = if proc_widget_state
|
||||
.process_search_state
|
||||
.is_searching_with_regex
|
||||
{
|
||||
self.colours.currently_selected_text_style
|
||||
} else {
|
||||
self.colours.text_style
|
||||
};
|
||||
|
||||
// FIXME: [MOUSE] Mouse support for these in search
|
||||
// FIXME: [MOVEMENT] Movement support for these in search
|
||||
let option_text = Spans::from(vec![
|
||||
Span::styled(
|
||||
format!("Case({})", if self.is_mac_os { "F1" } else { "Alt+C" }),
|
||||
case_style,
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
format!("Whole({})", if self.is_mac_os { "F2" } else { "Alt+W" }),
|
||||
whole_word_style,
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
format!("Regex({})", if self.is_mac_os { "F3" } else { "Alt+R" }),
|
||||
regex_style,
|
||||
),
|
||||
]);
|
||||
|
||||
search_text.push(Spans::from(Span::styled(
|
||||
if let Some(err) = &proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.error_message
|
||||
{
|
||||
err.as_str()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
self.colours.invalid_query_style,
|
||||
)));
|
||||
search_text.push(option_text);
|
||||
|
||||
let current_border_style = if proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.is_invalid_search
|
||||
{
|
||||
self.colours.invalid_query_style
|
||||
} else if is_on_widget {
|
||||
self.colours.highlighted_border_style
|
||||
} else {
|
||||
self.colours.border_style
|
||||
};
|
||||
|
||||
let title = Span::styled(
|
||||
if draw_border {
|
||||
const TITLE_BASE: &str = " Esc to close ";
|
||||
let repeat_num =
|
||||
usize::from(draw_loc.width).saturating_sub(TITLE_BASE.chars().count() + 2);
|
||||
format!("{} Esc to close ", "─".repeat(repeat_num))
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
current_border_style,
|
||||
);
|
||||
|
||||
let process_search_block = if draw_border {
|
||||
Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(current_border_style)
|
||||
} else if is_on_widget {
|
||||
Block::default()
|
||||
.borders(*SIDE_BORDERS)
|
||||
.border_style(current_border_style)
|
||||
} else {
|
||||
Block::default().borders(Borders::NONE)
|
||||
};
|
||||
|
||||
let margined_draw_loc = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)])
|
||||
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
|
||||
.direction(Direction::Horizontal)
|
||||
.split(draw_loc)[0];
|
||||
|
||||
f.render_widget(
|
||||
Paragraph::new(search_text)
|
||||
.block(process_search_block)
|
||||
.style(self.colours.text_style)
|
||||
.alignment(Alignment::Left),
|
||||
margined_draw_loc,
|
||||
);
|
||||
|
||||
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) {
|
||||
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,
|
||||
margined_draw_loc.y + margined_draw_loc.height,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_process_sort<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
) {
|
||||
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(&(widget_id - 2))
|
||||
{
|
||||
let current_scroll_position = proc_widget_state.columns.current_scroll_position;
|
||||
let sort_string = proc_widget_state
|
||||
.columns
|
||||
.ordered_columns
|
||||
.iter()
|
||||
.filter(|column_type| {
|
||||
proc_widget_state
|
||||
.columns
|
||||
.column_mapping
|
||||
.get(column_type)
|
||||
.unwrap()
|
||||
.enabled
|
||||
})
|
||||
.map(|column_type| column_type.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
|
||||
0
|
||||
} else {
|
||||
app_state.app_config_fields.table_gap
|
||||
};
|
||||
let position = get_start_position(
|
||||
usize::from(
|
||||
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
|
||||
),
|
||||
&proc_widget_state.columns.scroll_direction,
|
||||
&mut proc_widget_state.columns.previous_scroll_position,
|
||||
current_scroll_position,
|
||||
app_state.is_force_redraw,
|
||||
);
|
||||
|
||||
// Sanity check
|
||||
let start_position = if position >= sort_string.len() {
|
||||
sort_string.len().saturating_sub(1)
|
||||
} else {
|
||||
position
|
||||
};
|
||||
|
||||
let sliced_vec = &sort_string[start_position..];
|
||||
|
||||
let sort_options = sliced_vec
|
||||
.iter()
|
||||
.map(|column| Row::new(vec![column.as_str()]));
|
||||
|
||||
let column_state = &mut proc_widget_state.columns.column_state;
|
||||
column_state.select(Some(
|
||||
proc_widget_state
|
||||
.columns
|
||||
.current_scroll_position
|
||||
.saturating_sub(start_position),
|
||||
));
|
||||
let current_border_style = if proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.is_invalid_search
|
||||
{
|
||||
self.colours.invalid_query_style
|
||||
} else if is_on_widget {
|
||||
self.colours.highlighted_border_style
|
||||
} else {
|
||||
self.colours.border_style
|
||||
};
|
||||
|
||||
let process_sort_block = if draw_border {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(current_border_style)
|
||||
} else if is_on_widget {
|
||||
Block::default()
|
||||
.borders(*SIDE_BORDERS)
|
||||
.border_style(current_border_style)
|
||||
} else {
|
||||
Block::default().borders(Borders::NONE)
|
||||
};
|
||||
|
||||
let highlight_style = if is_on_widget {
|
||||
self.colours.currently_selected_text_style
|
||||
} else {
|
||||
self.colours.text_style
|
||||
};
|
||||
|
||||
let margined_draw_loc = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)])
|
||||
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
|
||||
.direction(Direction::Horizontal)
|
||||
.split(draw_loc)[0];
|
||||
|
||||
f.render_stateful_widget(
|
||||
Table::new(sort_options)
|
||||
.header(
|
||||
Row::new(vec!["Sort By"])
|
||||
.style(self.colours.table_header_style)
|
||||
.bottom_margin(table_gap),
|
||||
)
|
||||
.block(process_sort_block)
|
||||
.highlight_style(highlight_style)
|
||||
.style(self.colours.text_style)
|
||||
.widths(&[Constraint::Percentage(100)]),
|
||||
margined_draw_loc,
|
||||
column_state,
|
||||
);
|
||||
|
||||
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) {
|
||||
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,
|
||||
margined_draw_loc.y + margined_draw_loc.height,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -84,7 +84,7 @@ pub fn get_matches() -> clap::ArgMatches<'static> {
|
||||
build_app().get_matches()
|
||||
}
|
||||
|
||||
// TODO: Refactor this a bit, it's quite messy atm
|
||||
// TODO: [Refactor] Refactor the clap app creation a bit, it's quite messy atm
|
||||
pub fn build_app() -> App<'static, 'static> {
|
||||
// Temps
|
||||
let kelvin = Arg::with_name("kelvin")
|
||||
@ -149,7 +149,7 @@ When searching for a process, enables case sensitivity by default.\n\n",
|
||||
Sets process CPU% usage to be based on the current system CPU% usage
|
||||
rather than total CPU usage.\n\n",
|
||||
);
|
||||
// TODO: [DEBUG] Add a proper debugging solution.
|
||||
// TODO: [Feature] Add a proper debugging solution. Potentially, add a "diagnose" option to just see if we can gather data.
|
||||
// let debug = Arg::with_name("debug")
|
||||
// .long("debug")
|
||||
// .help("Enables debug logging.")
|
||||
@ -157,7 +157,6 @@ rather than total CPU usage.\n\n",
|
||||
// "\
|
||||
// Enables debug logging. The program will print where it logged to after running.",
|
||||
// );
|
||||
// TODO: [DIAGNOSE] Add a diagnose option to help with debugging.
|
||||
let disable_click = Arg::with_name("disable_click")
|
||||
.long("disable_click")
|
||||
.help("Disables mouse clicks.")
|
||||
@ -176,7 +175,7 @@ Uses a dot marker for graphs as opposed to the default braille
|
||||
marker.\n\n",
|
||||
);
|
||||
|
||||
let group = Arg::with_name("group") // FIXME: Rename this to something like "group_process", would be "breaking" though.
|
||||
let group = Arg::with_name("group") // TODO: [Config, Refactor, Breaking] Rename this to something like "group_process", would be "breaking" though.
|
||||
.short("g")
|
||||
.long("group")
|
||||
.help("Groups processes with the same name by default.")
|
||||
|
@ -508,7 +508,7 @@ pub const DEFAULT_BASIC_BATTERY_LAYOUT: &str = r##"
|
||||
// Config and flags
|
||||
pub const DEFAULT_CONFIG_FILE_PATH: &str = "bottom/bottom.toml";
|
||||
|
||||
// TODO: Eventually deprecate this.
|
||||
// TODO: [Config, Deprecation/Refactor] Eventually deprecate this with better configs.
|
||||
pub const CONFIG_TEXT: &str = r##"# This is a default config file for bottom. All of the settings are commented
|
||||
# out by default; if you wish to change them uncomment and modify as you see
|
||||
# fit.
|
||||
|
@ -3,11 +3,11 @@
|
||||
use crate::app::data_harvester::temperature::TemperatureType;
|
||||
use crate::app::text_table::TextTableData;
|
||||
use crate::app::DataCollection;
|
||||
use crate::{app::AxisScaling, units::data_units::DataUnit, Pid};
|
||||
use crate::{
|
||||
app::{data_harvester, ProcWidgetState},
|
||||
app::data_harvester,
|
||||
utils::{self, gen_util::*},
|
||||
};
|
||||
use crate::{app::AxisScaling, units::data_units::DataUnit, Pid};
|
||||
use data_harvester::processes::ProcessSorting;
|
||||
use fxhash::FxBuildHasher;
|
||||
use indexmap::IndexSet;
|
||||
@ -47,7 +47,7 @@ pub struct ConvertedNetworkData {
|
||||
pub tx_display: String,
|
||||
pub total_rx_display: Option<String>,
|
||||
pub total_tx_display: Option<String>,
|
||||
// TODO: [NETWORKING] add min/max/mean of each
|
||||
// TODO: [Feature] Networking - add the following stats in the future!
|
||||
// min_rx : f64,
|
||||
// max_rx : f64,
|
||||
// mean_rx: f64,
|
||||
@ -56,7 +56,7 @@ pub struct ConvertedNetworkData {
|
||||
// mean_tx: f64,
|
||||
}
|
||||
|
||||
// TODO: [REFACTOR] Process data... stuff really needs a rewrite. Again.
|
||||
// TODO: [Refactor] Process data might need some refactoring lol
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct ConvertedProcessData {
|
||||
pub pid: Pid,
|
||||
@ -321,7 +321,7 @@ pub fn convert_mem_labels(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Should probably make this only return none if no data is left/visible?
|
||||
// TODO: [Refactor] Should probably make this only return none if no data is left/visible?
|
||||
(
|
||||
if current_data.memory_harvest.mem_total_in_kib > 0 {
|
||||
Some((
|
||||
@ -586,7 +586,7 @@ pub fn convert_process_data(
|
||||
existing_converted_process_data: &mut HashMap<Pid, ConvertedProcessData>,
|
||||
#[cfg(target_family = "unix")] user_table: &mut data_harvester::processes::UserTable,
|
||||
) {
|
||||
// TODO [THREAD]: Thread highlighting and hiding support
|
||||
// TODO: [Feature] Thread highlighting and hiding support; can we also count number of threads per process and display it as a column?
|
||||
// For macOS see https://github.com/hishamhm/htop/pull/848/files
|
||||
|
||||
let mut complete_pid_set: fxhash::FxHashSet<Pid> =
|
||||
@ -715,7 +715,7 @@ fn tree_process_data(
|
||||
filtered_process_data: &[ConvertedProcessData], is_using_command: bool,
|
||||
sorting_type: &ProcessSorting, is_sort_descending: bool,
|
||||
) -> Vec<ConvertedProcessData> {
|
||||
// TODO: [TREE] Option to sort usage by total branch usage or individual value usage?
|
||||
// TODO: [Feature] Option to sort usage by total branch usage or individual value usage?
|
||||
|
||||
// Let's first build up a (really terrible) parent -> child mapping...
|
||||
// At the same time, let's make a mapping of PID -> process data!
|
||||
@ -1178,78 +1178,79 @@ fn tree_process_data(
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
// FIXME: [OPT] This is an easy target for optimization, too many to_strings!
|
||||
fn stringify_process_data(
|
||||
proc_widget_state: &ProcWidgetState, finalized_process_data: &[ConvertedProcessData],
|
||||
) -> Vec<(Vec<(String, Option<String>)>, bool)> {
|
||||
let is_proc_widget_grouped = proc_widget_state.is_grouped;
|
||||
let is_using_command = proc_widget_state.is_using_command;
|
||||
let is_tree = proc_widget_state.is_tree_mode;
|
||||
let mem_enabled = proc_widget_state.columns.is_enabled(&ProcessSorting::Mem);
|
||||
// FIXME: [URGENT] Delete this
|
||||
// // TODO: [Optimization] This is an easy target for optimization, too many to_strings!
|
||||
// fn stringify_process_data(
|
||||
// proc_widget_state: &ProcWidgetState, finalized_process_data: &[ConvertedProcessData],
|
||||
// ) -> Vec<(Vec<(String, Option<String>)>, bool)> {
|
||||
// let is_proc_widget_grouped = proc_widget_state.is_grouped;
|
||||
// let is_using_command = proc_widget_state.is_using_command;
|
||||
// let is_tree = proc_widget_state.is_tree_mode;
|
||||
// let mem_enabled = proc_widget_state.columns.is_enabled(&ProcessSorting::Mem);
|
||||
|
||||
finalized_process_data
|
||||
.iter()
|
||||
.map(|process| {
|
||||
(
|
||||
vec![
|
||||
(
|
||||
if is_proc_widget_grouped {
|
||||
process.group_pids.len().to_string()
|
||||
} else {
|
||||
process.pid.to_string()
|
||||
},
|
||||
None,
|
||||
),
|
||||
(
|
||||
if is_tree {
|
||||
if let Some(prefix) = &process.process_description_prefix {
|
||||
prefix.clone()
|
||||
} else {
|
||||
String::default()
|
||||
}
|
||||
} else if is_using_command {
|
||||
process.command.clone()
|
||||
} else {
|
||||
process.name.clone()
|
||||
},
|
||||
None,
|
||||
),
|
||||
(format!("{:.1}%", process.cpu_percent_usage), None),
|
||||
(
|
||||
if mem_enabled {
|
||||
if process.mem_usage_bytes <= GIBI_LIMIT {
|
||||
format!("{:.0}{}", process.mem_usage_str.0, process.mem_usage_str.1)
|
||||
} else {
|
||||
format!("{:.1}{}", process.mem_usage_str.0, process.mem_usage_str.1)
|
||||
}
|
||||
} else {
|
||||
format!("{:.1}%", process.mem_percent_usage)
|
||||
},
|
||||
None,
|
||||
),
|
||||
(process.read_per_sec.clone(), None),
|
||||
(process.write_per_sec.clone(), None),
|
||||
(process.total_read.clone(), None),
|
||||
(process.total_write.clone(), None),
|
||||
#[cfg(target_family = "unix")]
|
||||
(
|
||||
if let Some(user) = &process.user {
|
||||
user.clone()
|
||||
} else {
|
||||
"N/A".to_string()
|
||||
},
|
||||
None,
|
||||
),
|
||||
(
|
||||
process.process_state.clone(),
|
||||
Some(process.process_char.to_string()),
|
||||
),
|
||||
],
|
||||
process.is_disabled_entry,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
// finalized_process_data
|
||||
// .iter()
|
||||
// .map(|process| {
|
||||
// (
|
||||
// vec![
|
||||
// (
|
||||
// if is_proc_widget_grouped {
|
||||
// process.group_pids.len().to_string()
|
||||
// } else {
|
||||
// process.pid.to_string()
|
||||
// },
|
||||
// None,
|
||||
// ),
|
||||
// (
|
||||
// if is_tree {
|
||||
// if let Some(prefix) = &process.process_description_prefix {
|
||||
// prefix.clone()
|
||||
// } else {
|
||||
// String::default()
|
||||
// }
|
||||
// } else if is_using_command {
|
||||
// process.command.clone()
|
||||
// } else {
|
||||
// process.name.clone()
|
||||
// },
|
||||
// None,
|
||||
// ),
|
||||
// (format!("{:.1}%", process.cpu_percent_usage), None),
|
||||
// (
|
||||
// if mem_enabled {
|
||||
// if process.mem_usage_bytes <= GIBI_LIMIT {
|
||||
// format!("{:.0}{}", process.mem_usage_str.0, process.mem_usage_str.1)
|
||||
// } else {
|
||||
// format!("{:.1}{}", process.mem_usage_str.0, process.mem_usage_str.1)
|
||||
// }
|
||||
// } else {
|
||||
// format!("{:.1}%", process.mem_percent_usage)
|
||||
// },
|
||||
// None,
|
||||
// ),
|
||||
// (process.read_per_sec.clone(), None),
|
||||
// (process.write_per_sec.clone(), None),
|
||||
// (process.total_read.clone(), None),
|
||||
// (process.total_write.clone(), None),
|
||||
// #[cfg(target_family = "unix")]
|
||||
// (
|
||||
// if let Some(user) = &process.user {
|
||||
// user.clone()
|
||||
// } else {
|
||||
// "N/A".to_string()
|
||||
// },
|
||||
// None,
|
||||
// ),
|
||||
// (
|
||||
// process.process_state.clone(),
|
||||
// Some(process.process_char.to_string()),
|
||||
// ),
|
||||
// ],
|
||||
// process.is_disabled_entry,
|
||||
// )
|
||||
// })
|
||||
// .collect()
|
||||
// }
|
||||
|
||||
/// Takes a set of converted process data and groups it together.
|
||||
///
|
||||
@ -1364,7 +1365,7 @@ pub fn convert_battery_harvest(current_data: &DataCollection) -> Vec<ConvertedBa
|
||||
short: format!("{}:{:02}:{:02}", time.num_hours(), num_minutes, num_seconds),
|
||||
}
|
||||
} else if let Some(secs_till_full) = battery_harvest.secs_until_full {
|
||||
let time = chrono::Duration::seconds(secs_till_full); // FIXME [DEP]: Can I get rid of chrono?
|
||||
let time = chrono::Duration::seconds(secs_till_full); // TODO: [Dependencies] Can I get rid of chrono?
|
||||
let num_minutes = time.num_minutes() - time.num_hours() * 60;
|
||||
let num_seconds = time.num_seconds() - time.num_minutes() * 60;
|
||||
BatteryDuration::Charging {
|
||||
|
@ -4,7 +4,7 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
// TODO: Deny unused imports.
|
||||
// TODO: [Style] Deny unused imports.
|
||||
|
||||
use std::{
|
||||
boxed::Box,
|
||||
@ -183,7 +183,7 @@ pub fn create_input_thread(
|
||||
sender: std::sync::mpsc::Sender<BottomEvent>, termination_ctrl_lock: Arc<Mutex<bool>>,
|
||||
) -> std::thread::JoinHandle<()> {
|
||||
thread::spawn(move || {
|
||||
// TODO: Maybe experiment with removing these timers. Look into using buffers instead?
|
||||
// TODO: [Optimization, Input] Maybe experiment with removing these timers. Look into using buffers instead?
|
||||
let mut mouse_timer = Instant::now();
|
||||
let mut keyboard_timer = Instant::now();
|
||||
|
||||
|
@ -150,7 +150,7 @@ fn default_as_true() -> bool {
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct IgnoreList {
|
||||
#[serde(default = "default_as_true")]
|
||||
// TODO: Deprecate and/or rename, current name sounds awful.
|
||||
// TODO: [Config] Deprecate and/or rename, current name sounds awful.
|
||||
// Maybe to something like "deny_entries"? Currently it defaults to a denylist anyways, so maybe "allow_entries"?
|
||||
pub is_list_ignored: bool,
|
||||
pub list: Vec<String>,
|
||||
|
@ -52,7 +52,6 @@ impl From<heim::Error> for BottomError {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl From<std::num::ParseIntError> for BottomError {
|
||||
fn from(err: std::num::ParseIntError) -> Self {
|
||||
BottomError::ConfigError(err.to_string())
|
||||
|
@ -1,3 +1,3 @@
|
||||
//! Mocks layout management, so we can check if we broke anything.
|
||||
//! Mocks layout management.
|
||||
|
||||
// TODO: Redo testing.
|
||||
// FIXME: [URGENT] Redo testing for layout management.
|
||||
|
@ -1 +1,3 @@
|
||||
// TODO: Redo testing
|
||||
//! Mocks layout movement.
|
||||
|
||||
// FIXME: [URGENT] Add testing for layout movement.
|
||||
|
Loading…
x
Reference in New Issue
Block a user