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:
ClementTsang 2021-09-26 01:18:03 -04:00
parent b6ca3e0a22
commit 9089231bc4
38 changed files with 308 additions and 4097 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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")]
{

View File

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

View File

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

View File

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

View File

@ -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.")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1,3 @@
// TODO: Redo testing
//! Mocks layout movement.
// FIXME: [URGENT] Add testing for layout movement.