Merge pull request #730 from ClementTsang/consolidate_tables

This serves as somewhat of an intermediary refactor to unify some scrollable table code - in particular, in regards to drawing. This is almost a parallel refactor as #710, which did something similar for time graphs. However, this one has a bit more work in regards to the concepts of component state, in particular, for width calculation caching and scroll position management.
This commit is contained in:
Clement Tsang 2022-05-16 21:03:20 -04:00 committed by GitHub
commit de765fc364
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 4057 additions and 4730 deletions

View File

@ -25,10 +25,10 @@ doc = false
[profile.release] [profile.release]
debug = 0 debug = 0
strip = "symbols"
lto = true lto = true
opt-level = 3 opt-level = 3
codegen-units = 1 codegen-units = 1
strip = "symbols"
[features] [features]
default = ["fern", "log", "battery", "gpu"] default = ["fern", "log", "battery", "gpu"]
@ -40,11 +40,12 @@ nvidia = ["nvml-wrapper"]
[dependencies] [dependencies]
anyhow = "1.0.57" anyhow = "1.0.57"
backtrace = "0.3.65" backtrace = "0.3.65"
cfg-if = "1.0.0"
crossterm = "0.18.2" crossterm = "0.18.2"
ctrlc = { version = "3.1.9", features = ["termination"] } ctrlc = { version = "3.1.9", features = ["termination"] }
clap = { version = "3.1.12", features = ["default", "cargo", "wrap_help"] } clap = { version = "3.1.12", features = ["default", "cargo", "wrap_help"] }
cfg-if = "1.0.0"
concat-string = "1.0.1" concat-string = "1.0.1"
# const_format = "0.2.23"
dirs = "4.0.0" dirs = "4.0.0"
futures = "0.3.21" futures = "0.3.21"
futures-timer = "3.0.2" futures-timer = "3.0.2"

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,32 @@
/// In charge of cleaning, processing, and managing data. I couldn't think of //! In charge of cleaning, processing, and managing data. I couldn't think of
/// a better name for the file. Since I called data collection "harvesting", //! a better name for the file. Since I called data collection "harvesting",
/// then this is the farmer I guess. //! then this is the farmer I guess.
/// //!
/// Essentially the main goal is to shift the initial calculation and distribution //! Essentially the main goal is to shift the initial calculation and distribution
/// of joiner points and data to one central location that will only do it //! of joiner points and data to one central location that will only do it
/// *once* upon receiving the data --- as opposed to doing it on canvas draw, //! *once* upon receiving the data --- as opposed to doing it on canvas draw,
/// which will be a costly process. //! which will be a costly process.
/// //!
/// This will also handle the *cleaning* of stale data. That should be done //! This will also handle the *cleaning* of stale data. That should be done
/// in some manner (timer on another thread, some loop) that will occasionally //! in some manner (timer on another thread, some loop) that will occasionally
/// call the purging function. Failure to do so *will* result in a growing //! call the purging function. Failure to do so *will* result in a growing
/// memory usage and higher CPU usage - you will be trying to process more and //! memory usage and higher CPU usage - you will be trying to process more and
/// more points as this is used! //! more points as this is used!
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use fxhash::FxHashMap;
use itertools::Itertools;
use std::{time::Instant, vec::Vec}; use std::{time::Instant, vec::Vec};
#[cfg(feature = "battery")] #[cfg(feature = "battery")]
use crate::data_harvester::batteries; use crate::data_harvester::batteries;
use crate::{ use crate::{
data_harvester::{cpu, disks, memory, network, processes, temperature, Data}, data_harvester::{cpu, disks, memory, network, processes::ProcessHarvest, temperature, Data},
utils::gen_util::{get_decimal_bytes, GIGA_LIMIT}, utils::gen_util::{get_decimal_bytes, GIGA_LIMIT},
Pid,
}; };
use regex::Regex; use regex::Regex;
@ -38,6 +43,97 @@ pub struct TimedData {
pub swap_data: Option<Value>, pub swap_data: Option<Value>,
} }
pub type StringPidMap = FxHashMap<String, Vec<Pid>>;
#[derive(Clone, Debug, Default)]
pub struct ProcessData {
/// A PID to process data map.
pub process_harvest: FxHashMap<Pid, ProcessHarvest>,
/// A mapping from a process name to any PID with that name.
pub name_pid_map: StringPidMap,
/// A mapping from a process command to any PID with that name.
pub cmd_pid_map: StringPidMap,
/// A mapping between a process PID to any children process PIDs.
pub process_parent_mapping: FxHashMap<Pid, Vec<Pid>>,
/// PIDs corresponding to processes that have no parents.
pub orphan_pids: Vec<Pid>,
}
impl ProcessData {
fn ingest(&mut self, list_of_processes: Vec<ProcessHarvest>) {
// TODO: [Optimization] Probably more efficient to all of this in the data collection step, but it's fine for now.
self.name_pid_map.clear();
self.cmd_pid_map.clear();
self.process_parent_mapping.clear();
// Reverse as otherwise the pid mappings are in the wrong order.
list_of_processes.iter().rev().for_each(|process_harvest| {
if let Some(entry) = self.name_pid_map.get_mut(&process_harvest.name) {
entry.push(process_harvest.pid);
} else {
self.name_pid_map
.insert(process_harvest.name.to_string(), vec![process_harvest.pid]);
}
if let Some(entry) = self.cmd_pid_map.get_mut(&process_harvest.command) {
entry.push(process_harvest.pid);
} else {
self.cmd_pid_map.insert(
process_harvest.command.to_string(),
vec![process_harvest.pid],
);
}
if let Some(parent_pid) = process_harvest.parent_pid {
if let Some(entry) = self.process_parent_mapping.get_mut(&parent_pid) {
entry.push(process_harvest.pid);
} else {
self.process_parent_mapping
.insert(parent_pid, vec![process_harvest.pid]);
}
}
});
self.name_pid_map.shrink_to_fit();
self.cmd_pid_map.shrink_to_fit();
self.process_parent_mapping.shrink_to_fit();
let process_pid_map = list_of_processes
.into_iter()
.map(|process| (process.pid, process))
.collect();
self.process_harvest = process_pid_map;
// This also needs a quick sort + reverse to be in the correct order.
self.orphan_pids = {
let mut res: Vec<Pid> = self
.process_harvest
.iter()
.filter_map(|(pid, process_harvest)| {
if let Some(parent_pid) = process_harvest.parent_pid {
if self.process_harvest.contains_key(&parent_pid) {
None
} else {
Some(*pid)
}
} else {
Some(*pid)
}
})
.sorted()
.collect();
res.reverse();
res
}
}
}
/// AppCollection represents the pooled data stored within the main app /// AppCollection represents the pooled data stored within the main app
/// thread. Basically stores a (occasionally cleaned) record of the data /// thread. Basically stores a (occasionally cleaned) record of the data
/// collected, and what is needed to convert into a displayable form. /// collected, and what is needed to convert into a displayable form.
@ -57,7 +153,7 @@ pub struct DataCollection {
pub swap_harvest: memory::MemHarvest, pub swap_harvest: memory::MemHarvest,
pub cpu_harvest: cpu::CpuHarvest, pub cpu_harvest: cpu::CpuHarvest,
pub load_avg_harvest: cpu::LoadAvgHarvest, pub load_avg_harvest: cpu::LoadAvgHarvest,
pub process_harvest: Vec<processes::ProcessHarvest>, pub process_data: ProcessData,
pub disk_harvest: Vec<disks::DiskHarvest>, pub disk_harvest: Vec<disks::DiskHarvest>,
pub io_harvest: disks::IoHarvest, pub io_harvest: disks::IoHarvest,
pub io_labels_and_prev: Vec<((u64, u64), (u64, u64))>, pub io_labels_and_prev: Vec<((u64, u64), (u64, u64))>,
@ -78,7 +174,7 @@ impl Default for DataCollection {
swap_harvest: memory::MemHarvest::default(), swap_harvest: memory::MemHarvest::default(),
cpu_harvest: cpu::CpuHarvest::default(), cpu_harvest: cpu::CpuHarvest::default(),
load_avg_harvest: cpu::LoadAvgHarvest::default(), load_avg_harvest: cpu::LoadAvgHarvest::default(),
process_harvest: Vec::default(), process_data: Default::default(),
disk_harvest: Vec::default(), disk_harvest: Vec::default(),
io_harvest: disks::IoHarvest::default(), io_harvest: disks::IoHarvest::default(),
io_labels_and_prev: Vec::default(), io_labels_and_prev: Vec::default(),
@ -97,7 +193,7 @@ impl DataCollection {
self.memory_harvest = memory::MemHarvest::default(); self.memory_harvest = memory::MemHarvest::default();
self.swap_harvest = memory::MemHarvest::default(); self.swap_harvest = memory::MemHarvest::default();
self.cpu_harvest = cpu::CpuHarvest::default(); self.cpu_harvest = cpu::CpuHarvest::default();
self.process_harvest = Vec::default(); self.process_data = Default::default();
self.disk_harvest = Vec::default(); self.disk_harvest = Vec::default();
self.io_harvest = disks::IoHarvest::default(); self.io_harvest = disks::IoHarvest::default();
self.io_labels_and_prev = Vec::default(); self.io_labels_and_prev = Vec::default();
@ -108,10 +204,14 @@ impl DataCollection {
} }
} }
pub fn set_frozen_time(&mut self) { pub fn freeze(&mut self) {
self.frozen_instant = Some(self.current_instant); self.frozen_instant = Some(self.current_instant);
} }
pub fn thaw(&mut self) {
self.frozen_instant = None;
}
pub fn clean_data(&mut self, max_time_millis: u64) { pub fn clean_data(&mut self, max_time_millis: u64) {
let current_time = Instant::now(); let current_time = Instant::now();
@ -319,8 +419,8 @@ impl DataCollection {
self.io_harvest = io; self.io_harvest = io;
} }
fn eat_proc(&mut self, list_of_processes: Vec<processes::ProcessHarvest>) { fn eat_proc(&mut self, list_of_processes: Vec<ProcessHarvest>) {
self.process_harvest = list_of_processes; self.process_data.ingest(list_of_processes);
} }
#[cfg(feature = "battery")] #[cfg(feature = "battery")]

View File

@ -104,6 +104,9 @@ pub struct DataCollector {
#[cfg(feature = "battery")] #[cfg(feature = "battery")]
battery_list: Option<Vec<Battery>>, battery_list: Option<Vec<Battery>>,
filters: DataFilters, filters: DataFilters,
#[cfg(target_family = "unix")]
user_table: self::processes::UserTable,
} }
impl DataCollector { impl DataCollector {
@ -133,6 +136,8 @@ impl DataCollector {
#[cfg(feature = "battery")] #[cfg(feature = "battery")]
battery_list: None, battery_list: None,
filters, filters,
#[cfg(target_family = "unix")]
user_table: Default::default(),
} }
} }
@ -191,7 +196,7 @@ impl DataCollector {
}; };
} }
pub fn set_collected_data(&mut self, used_widgets: UsedWidgets) { pub fn set_data_collection(&mut self, used_widgets: UsedWidgets) {
self.widgets_to_harvest = used_widgets; self.widgets_to_harvest = used_widgets;
} }
@ -270,9 +275,21 @@ impl DataCollector {
.duration_since(self.last_collection_time) .duration_since(self.last_collection_time)
.as_secs(), .as_secs(),
self.mem_total_kb, self.mem_total_kb,
&mut self.user_table,
) )
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
{
#[cfg(target_family = "unix")]
{
processes::get_process_data(
&self.sys,
self.use_current_cpu_total,
self.mem_total_kb,
&mut self.user_table,
)
}
#[cfg(not(target_family = "unix"))]
{ {
processes::get_process_data( processes::get_process_data(
&self.sys, &self.sys,
@ -280,6 +297,7 @@ impl DataCollector {
self.mem_total_kb, self.mem_total_kb,
) )
} }
}
} { } {
self.data.list_of_processes = Some(process_list); self.data.list_of_processes = Some(process_list);
} }

View File

@ -3,7 +3,7 @@
use super::NetworkHarvest; use super::NetworkHarvest;
use std::time::Instant; use std::time::Instant;
// FIXME: Eventually make it so that this thing also takes individual usage into account, so we can allow for showing per-interface! // TODO: Eventually make it so that this thing also takes individual usage into account, so we can show per-interface!
pub async fn get_network_data( pub async fn get_network_data(
prev_net_access_time: Instant, prev_net_rx: &mut u64, prev_net_tx: &mut u64, prev_net_access_time: Instant, prev_net_rx: &mut u64, prev_net_tx: &mut u64,
curr_time: Instant, actually_get: bool, filter: &Option<crate::app::Filter>, curr_time: Instant, actually_get: bool, filter: &Option<crate::app::Filter>,

View File

@ -5,7 +5,7 @@ use std::collections::hash_map::Entry;
use crate::utils::error::{self, BottomError}; use crate::utils::error::{self, BottomError};
use crate::Pid; use crate::Pid;
use super::ProcessHarvest; use super::{ProcessHarvest, UserTable};
use sysinfo::ProcessStatus; use sysinfo::ProcessStatus;
@ -120,6 +120,7 @@ fn get_linux_cpu_usage(
fn read_proc( fn read_proc(
prev_proc: &PrevProcDetails, stat: &Stat, cpu_usage: f64, cpu_fraction: f64, prev_proc: &PrevProcDetails, stat: &Stat, cpu_usage: f64, cpu_fraction: f64,
use_current_cpu_total: bool, time_difference_in_secs: u64, mem_total_kb: u64, use_current_cpu_total: bool, time_difference_in_secs: u64, mem_total_kb: u64,
user_table: &mut UserTable,
) -> error::Result<(ProcessHarvest, u64)> { ) -> error::Result<(ProcessHarvest, u64)> {
use std::convert::TryFrom; use std::convert::TryFrom;
@ -156,7 +157,10 @@ fn read_proc(
}; };
let process_state_char = stat.state; let process_state_char = stat.state;
let process_state = ProcessStatus::from(process_state_char).to_string(); let process_state = (
ProcessStatus::from(process_state_char).to_string(),
process_state_char,
);
let (cpu_usage_percent, new_process_times) = get_linux_cpu_usage( let (cpu_usage_percent, new_process_times) = get_linux_cpu_usage(
stat, stat,
cpu_usage, cpu_usage,
@ -199,7 +203,7 @@ fn read_proc(
(0, 0, 0, 0) (0, 0, 0, 0)
}; };
let uid = Some(process.owner); let uid = process.owner;
Ok(( Ok((
ProcessHarvest { ProcessHarvest {
@ -215,8 +219,11 @@ fn read_proc(
total_read_bytes, total_read_bytes,
total_write_bytes, total_write_bytes,
process_state, process_state,
process_state_char,
uid, uid,
user: user_table
.get_uid_to_username_mapping(uid)
.map(Into::into)
.unwrap_or_else(|_| "N/A".into()),
}, },
new_process_times, new_process_times,
)) ))
@ -225,7 +232,7 @@ fn read_proc(
pub fn get_process_data( pub fn get_process_data(
prev_idle: &mut f64, prev_non_idle: &mut f64, prev_idle: &mut f64, prev_non_idle: &mut f64,
pid_mapping: &mut FxHashMap<Pid, PrevProcDetails>, use_current_cpu_total: bool, pid_mapping: &mut FxHashMap<Pid, PrevProcDetails>, use_current_cpu_total: bool,
time_difference_in_secs: u64, mem_total_kb: u64, time_difference_in_secs: u64, mem_total_kb: u64, user_table: &mut UserTable,
) -> crate::utils::error::Result<Vec<ProcessHarvest>> { ) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
// TODO: [PROC THREADS] Add threads // TODO: [PROC THREADS] Add threads
@ -268,6 +275,7 @@ pub fn get_process_data(
use_current_cpu_total, use_current_cpu_total,
time_difference_in_secs, time_difference_in_secs,
mem_total_kb, mem_total_kb,
user_table,
) { ) {
prev_proc_details.cpu_time = new_process_times; prev_proc_details.cpu_time = new_process_times;
prev_proc_details.total_read_bytes = prev_proc_details.total_read_bytes =

View File

@ -3,6 +3,8 @@
use super::ProcessHarvest; use super::ProcessHarvest;
use sysinfo::{PidExt, ProcessExt, ProcessStatus, ProcessorExt, System, SystemExt}; use sysinfo::{PidExt, ProcessExt, ProcessStatus, ProcessorExt, System, SystemExt};
use crate::data_harvester::processes::UserTable;
fn get_macos_process_cpu_usage( fn get_macos_process_cpu_usage(
pids: &[i32], pids: &[i32],
) -> std::io::Result<std::collections::HashMap<i32, f64>> { ) -> std::io::Result<std::collections::HashMap<i32, f64>> {
@ -35,7 +37,7 @@ fn get_macos_process_cpu_usage(
} }
pub fn get_process_data( pub fn get_process_data(
sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, user_table: &mut UserTable,
) -> crate::utils::error::Result<Vec<ProcessHarvest>> { ) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
let mut process_vector: Vec<ProcessHarvest> = Vec::new(); let mut process_vector: Vec<ProcessHarvest> = Vec::new();
let process_hashmap = sys.processes(); let process_hashmap = sys.processes();
@ -86,6 +88,11 @@ pub fn get_process_data(
}; };
let disk_usage = process_val.disk_usage(); let disk_usage = process_val.disk_usage();
let process_state = {
let ps = process_val.status();
(ps.to_string(), convert_process_status_to_char(ps))
};
let uid = process_val.uid;
process_vector.push(ProcessHarvest { process_vector.push(ProcessHarvest {
pid: process_val.pid().as_u32() as _, pid: process_val.pid().as_u32() as _,
parent_pid: process_val.parent().map(|p| p.as_u32() as _), parent_pid: process_val.parent().map(|p| p.as_u32() as _),
@ -102,16 +109,19 @@ pub fn get_process_data(
write_bytes_per_sec: disk_usage.written_bytes, write_bytes_per_sec: disk_usage.written_bytes,
total_read_bytes: disk_usage.total_read_bytes, total_read_bytes: disk_usage.total_read_bytes,
total_write_bytes: disk_usage.total_written_bytes, total_write_bytes: disk_usage.total_written_bytes,
process_state: process_val.status().to_string(), process_state,
process_state_char: convert_process_status_to_char(process_val.status()), uid,
uid: Some(process_val.uid), user: user_table
.get_uid_to_username_mapping(uid)
.map(Into::into)
.unwrap_or_else(|_| "N/A".into()),
}); });
} }
let unknown_state = ProcessStatus::Unknown(0).to_string(); let unknown_state = ProcessStatus::Unknown(0).to_string();
let cpu_usage_unknown_pids: Vec<i32> = process_vector let cpu_usage_unknown_pids: Vec<i32> = process_vector
.iter() .iter()
.filter(|process| process.process_state == unknown_state) .filter(|process| process.process_state.0 == unknown_state)
.map(|process| process.pid) .map(|process| process.pid)
.collect(); .collect();
let cpu_usages = get_macos_process_cpu_usage(&cpu_usage_unknown_pids)?; let cpu_usages = get_macos_process_cpu_usage(&cpu_usage_unknown_pids)?;

View File

@ -25,73 +25,64 @@ cfg_if::cfg_if! {
use crate::Pid; use crate::Pid;
// TODO: Add value so we know if it's sorted ascending or descending by default?
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum ProcessSorting {
CpuPercent,
Mem,
MemPercent,
Pid,
ProcessName,
Command,
ReadPerSecond,
WritePerSecond,
TotalRead,
TotalWrite,
State,
User,
Count,
}
impl std::fmt::Display for ProcessSorting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match &self {
ProcessSorting::CpuPercent => "CPU%",
ProcessSorting::MemPercent => "Mem%",
ProcessSorting::Mem => "Mem",
ProcessSorting::ReadPerSecond => "R/s",
ProcessSorting::WritePerSecond => "W/s",
ProcessSorting::TotalRead => "T.Read",
ProcessSorting::TotalWrite => "T.Write",
ProcessSorting::State => "State",
ProcessSorting::ProcessName => "Name",
ProcessSorting::Command => "Command",
ProcessSorting::Pid => "PID",
ProcessSorting::Count => "Count",
ProcessSorting::User => "User",
}
)
}
}
impl Default for ProcessSorting {
fn default() -> Self {
ProcessSorting::CpuPercent
}
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ProcessHarvest { pub struct ProcessHarvest {
/// The pid of the process.
pub pid: Pid, pub pid: Pid,
pub parent_pid: Option<Pid>, // Remember, parent_pid 0 is root...
/// The parent PID of the process. Remember, parent_pid 0 is root.
pub parent_pid: Option<Pid>,
/// CPU usage as a percentage.
pub cpu_usage_percent: f64, pub cpu_usage_percent: f64,
/// Memory usage as a percentage.
pub mem_usage_percent: f64, pub mem_usage_percent: f64,
/// Memory usage as bytes.
pub mem_usage_bytes: u64, pub mem_usage_bytes: u64,
/// The name of the process.
pub name: String,
/// The exact command for the process.
pub command: String,
/// Bytes read per second.
pub read_bytes_per_sec: u64,
/// Bytes written per second.
pub write_bytes_per_sec: u64,
/// The total number of bytes read by the process.
pub total_read_bytes: u64,
/// The total number of bytes written by the process.
pub total_write_bytes: u64,
/// The current state of the process (e.g. zombie, asleep)
pub process_state: (String, char),
/// This is the *effective* user ID of the process. This is only used on Unix platforms.
#[cfg(target_family = "unix")]
pub uid: libc::uid_t,
/// This is the process' user. This is only used on Unix platforms.
#[cfg(target_family = "unix")]
pub user: std::borrow::Cow<'static, str>,
// TODO: Additional fields
// pub rss_kb: u64, // pub rss_kb: u64,
// pub virt_kb: u64, // pub virt_kb: u64,
pub name: String, }
pub command: String,
pub read_bytes_per_sec: u64, impl ProcessHarvest {
pub write_bytes_per_sec: u64, pub(crate) fn add(&mut self, rhs: &ProcessHarvest) {
pub total_read_bytes: u64, self.cpu_usage_percent += rhs.cpu_usage_percent;
pub total_write_bytes: u64, self.mem_usage_bytes += rhs.mem_usage_bytes;
pub process_state: String, self.mem_usage_percent += rhs.mem_usage_percent;
pub process_state_char: char, self.read_bytes_per_sec += rhs.read_bytes_per_sec;
self.write_bytes_per_sec += rhs.write_bytes_per_sec;
/// This is the *effective* user ID. self.total_read_bytes += rhs.total_read_bytes;
#[cfg(target_family = "unix")] self.total_write_bytes += rhs.total_write_bytes;
pub uid: Option<libc::uid_t>, }
} }

View File

@ -1,10 +1,12 @@
//! Unix-specific parts of process collection. //! Unix-specific parts of process collection.
use fxhash::FxHashMap;
use crate::utils::error; use crate::utils::error;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct UserTable { pub struct UserTable {
pub uid_user_mapping: std::collections::HashMap<libc::uid_t, String>, pub uid_user_mapping: FxHashMap<libc::uid_t, String>,
} }
impl UserTable { impl UserTable {

View File

@ -55,6 +55,7 @@ pub fn get_process_data(
}; };
let disk_usage = process_val.disk_usage(); let disk_usage = process_val.disk_usage();
let process_state = (process_val.status().to_string(), 'R');
process_vector.push(ProcessHarvest { process_vector.push(ProcessHarvest {
pid: process_val.pid().as_u32() as _, pid: process_val.pid().as_u32() as _,
parent_pid: process_val.parent().map(|p| p.as_u32() as _), parent_pid: process_val.parent().map(|p| p.as_u32() as _),
@ -71,8 +72,7 @@ pub fn get_process_data(
write_bytes_per_sec: disk_usage.written_bytes, write_bytes_per_sec: disk_usage.written_bytes,
total_read_bytes: disk_usage.total_read_bytes, total_read_bytes: disk_usage.total_read_bytes,
total_write_bytes: disk_usage.total_written_bytes, total_write_bytes: disk_usage.total_written_bytes,
process_state: process_val.status().to_string(), process_state,
process_state_char: 'R',
}); });
} }

View File

@ -1,46 +1,40 @@
use super::ProcWidgetState; use crate::utils::error::{
use crate::{
data_conversion::ConvertedProcessData,
utils::error::{
BottomError::{self, QueryError}, BottomError::{self, QueryError},
Result, Result,
},
}; };
use std::fmt::Debug; use std::fmt::Debug;
use std::{borrow::Cow, collections::VecDeque}; use std::{borrow::Cow, collections::VecDeque};
use super::data_harvester::processes::ProcessHarvest;
const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"']; const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"'];
const COMPARISON_LIST: [&str; 3] = [">", "=", "<"]; const COMPARISON_LIST: [&str; 3] = [">", "=", "<"];
const OR_LIST: [&str; 2] = ["or", "||"]; const OR_LIST: [&str; 2] = ["or", "||"];
const AND_LIST: [&str; 2] = ["and", "&&"]; const AND_LIST: [&str; 2] = ["and", "&&"];
/// I only separated this as otherwise, the states.rs file gets huge... and this should /// In charge of parsing the given query.
/// belong in another file anyways, IMO. /// We are defining the following language for a query (case-insensitive prefixes):
pub trait ProcessQuery { ///
/// In charge of parsing the given query. /// - Process names: No prefix required, can use regex, match word, or case.
/// We are defining the following language for a query (case-insensitive prefixes): /// Enclosing anything, including prefixes, in quotes, means we treat it as an entire process
/// /// rather than a prefix.
/// - Process names: No prefix required, can use regex, match word, or case. /// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant).
/// Enclosing anything, including prefixes, in quotes, means we treat it as an entire process /// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can compare.
/// rather than a prefix. /// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare.
/// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant). /// - STATE: Use prefix `state`, can use regex, match word, or case.
/// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can compare. /// - USER: Use prefix `user`, can use regex, match word, or case.
/// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare. /// - Read/s: Use prefix `r`. Can compare.
/// - STATE: Use prefix `state`, can use regex, match word, or case. /// - Write/s: Use prefix `w`. Can compare.
/// - USER: Use prefix `user`, can use regex, match word, or case. /// - Total read: Use prefix `read`. Can compare.
/// - Read/s: Use prefix `r`. Can compare. /// - Total write: Use prefix `write`. Can compare.
/// - Write/s: Use prefix `w`. Can compare. ///
/// - Total read: Use prefix `read`. Can compare. /// For queries, whitespaces are our delimiters. We will merge together any adjacent non-prefixed
/// - Total write: Use prefix `write`. Can compare. /// or quoted elements after splitting to treat as process names.
/// /// Furthermore, we want to support boolean joiners like AND and OR, and brackets.
/// For queries, whitespaces are our delimiters. We will merge together any adjacent non-prefixed pub fn parse_query(
/// or quoted elements after splitting to treat as process names. search_query: &str, is_searching_whole_word: bool, is_ignoring_case: bool,
/// Furthermore, we want to support boolean joiners like AND and OR, and brackets. is_searching_with_regex: bool,
fn parse_query(&self) -> Result<Query>; ) -> Result<Query> {
}
impl ProcessQuery for ProcWidgetState {
fn parse_query(&self) -> Result<Query> {
fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> { fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
let lhs = process_or(query)?; let lhs = process_or(query)?;
let mut list_of_ors = vec![lhs]; let mut list_of_ors = vec![lhs];
@ -155,10 +149,7 @@ impl ProcessQuery for ProcWidgetState {
} }
return Ok(Prefix { return Ok(Prefix {
or: None, or: None,
regex_prefix: Some(( regex_prefix: Some((PrefixType::Name, StringQuery::Value(quoted_string))),
PrefixType::Name,
StringQuery::Value(quoted_string),
)),
compare_prefix: None, compare_prefix: None,
}); });
} }
@ -262,7 +253,7 @@ impl ProcessQuery for ProcWidgetState {
if content == "=" { if content == "=" {
// Check next string if possible // Check next string if possible
if let Some(queue_next) = query.pop_front() { 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 // - (test
// - test) // - test)
@ -283,10 +274,7 @@ impl ProcessQuery for ProcWidgetState {
} else { } else {
return Ok(Prefix { return Ok(Prefix {
or: None, or: None,
regex_prefix: Some(( regex_prefix: Some((prefix_type, StringQuery::Value(content))),
prefix_type,
StringQuery::Value(content),
)),
compare_prefix: None, compare_prefix: None,
}); });
} }
@ -418,9 +406,7 @@ impl ProcessQuery for ProcWidgetState {
let mut split_query = VecDeque::new(); let mut split_query = VecDeque::new();
self.get_current_search_query() search_query.split_whitespace().for_each(|s| {
.split_whitespace()
.for_each(|s| {
// From https://stackoverflow.com/a/56923739 in order to get a split but include the parentheses // From https://stackoverflow.com/a/56923739 in order to get a split but include the parentheses
let mut last = 0; let mut last = 0;
for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) { for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) {
@ -437,13 +423,12 @@ impl ProcessQuery for ProcWidgetState {
let mut process_filter = process_string_to_filter(&mut split_query)?; let mut process_filter = process_string_to_filter(&mut split_query)?;
process_filter.process_regexes( process_filter.process_regexes(
self.process_search_state.is_searching_whole_word, is_searching_whole_word,
self.process_search_state.is_ignoring_case, is_ignoring_case,
self.process_search_state.is_searching_with_regex, is_searching_with_regex,
)?; )?;
Ok(process_filter) Ok(process_filter)
}
} }
pub struct Query { pub struct Query {
@ -467,7 +452,7 @@ impl Query {
Ok(()) Ok(())
} }
pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool { pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
self.query self.query
.iter() .iter()
.all(|ok| ok.check(process, is_using_command)) .all(|ok| ok.check(process, is_using_command))
@ -507,7 +492,7 @@ impl Or {
Ok(()) Ok(())
} }
pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool { pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
if let Some(rhs) = &self.rhs { if let Some(rhs) = &self.rhs {
self.lhs.check(process, is_using_command) || rhs.check(process, is_using_command) self.lhs.check(process, is_using_command) || rhs.check(process, is_using_command)
} else { } else {
@ -552,7 +537,7 @@ impl And {
Ok(()) Ok(())
} }
pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool { pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
if let Some(rhs) = &self.rhs { if let Some(rhs) = &self.rhs {
self.lhs.check(process, is_using_command) && rhs.check(process, is_using_command) self.lhs.check(process, is_using_command) && rhs.check(process, is_using_command)
} else { } else {
@ -662,7 +647,7 @@ impl Prefix {
Ok(()) Ok(())
} }
pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool { pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
fn matches_condition(condition: &QueryComparison, lhs: f64, rhs: f64) -> bool { fn matches_condition(condition: &QueryComparison, lhs: f64, rhs: f64) -> bool {
match condition { match condition {
QueryComparison::Equal => (lhs - rhs).abs() < std::f64::EPSILON, QueryComparison::Equal => (lhs - rhs).abs() < std::f64::EPSILON,
@ -684,11 +669,14 @@ impl Prefix {
process.name.as_str() process.name.as_str()
}), }),
PrefixType::Pid => r.is_match(process.pid.to_string().as_str()), PrefixType::Pid => r.is_match(process.pid.to_string().as_str()),
PrefixType::State => r.is_match(process.process_state.as_str()), PrefixType::State => r.is_match(process.process_state.0.as_str()),
PrefixType::User => { PrefixType::User => {
if let Some(user) = &process.user { #[cfg(target_family = "unix")]
r.is_match(user.as_str()) {
} else { r.is_match(process.user.as_ref())
}
#[cfg(not(target_family = "unix"))]
{
false false
} }
} }
@ -701,12 +689,12 @@ impl Prefix {
match prefix_type { match prefix_type {
PrefixType::PCpu => matches_condition( PrefixType::PCpu => matches_condition(
&numerical_query.condition, &numerical_query.condition,
process.cpu_percent_usage, process.cpu_usage_percent,
numerical_query.value, numerical_query.value,
), ),
PrefixType::PMem => matches_condition( PrefixType::PMem => matches_condition(
&numerical_query.condition, &numerical_query.condition,
process.mem_percent_usage, process.mem_usage_percent,
numerical_query.value, numerical_query.value,
), ),
PrefixType::MemBytes => matches_condition( PrefixType::MemBytes => matches_condition(
@ -716,22 +704,22 @@ impl Prefix {
), ),
PrefixType::Rps => matches_condition( PrefixType::Rps => matches_condition(
&numerical_query.condition, &numerical_query.condition,
process.rps_f64, process.read_bytes_per_sec as f64,
numerical_query.value, numerical_query.value,
), ),
PrefixType::Wps => matches_condition( PrefixType::Wps => matches_condition(
&numerical_query.condition, &numerical_query.condition,
process.wps_f64, process.write_bytes_per_sec as f64,
numerical_query.value, numerical_query.value,
), ),
PrefixType::TRead => matches_condition( PrefixType::TRead => matches_condition(
&numerical_query.condition, &numerical_query.condition,
process.tr_f64, process.total_read_bytes as f64,
numerical_query.value, numerical_query.value,
), ),
PrefixType::TWrite => matches_condition( PrefixType::TWrite => matches_condition(
&numerical_query.condition, &numerical_query.condition,
process.tw_f64, process.total_write_bytes as f64,
numerical_query.value, numerical_query.value,
), ),
_ => true, _ => true,

View File

@ -1,15 +1,16 @@
use std::{collections::HashMap, convert::TryInto, time::Instant}; use std::{collections::HashMap, time::Instant};
use unicode_segmentation::GraphemeCursor; use unicode_segmentation::GraphemeCursor;
use tui::widgets::TableState;
use crate::{ use crate::{
app::{layout_manager::BottomWidgetType, query::*}, app::{layout_manager::BottomWidgetType, query::*},
constants, constants,
data_harvester::processes::{self, ProcessSorting},
}; };
use ProcessSorting::*;
pub mod table_state;
pub use table_state::*;
use super::widgets::ProcWidget;
#[derive(Debug)] #[derive(Debug)]
pub enum ScrollDirection { pub enum ScrollDirection {
@ -31,41 +32,11 @@ pub enum CursorDirection {
Right, Right,
} }
/// AppScrollWidgetState deals with fields for a scrollable app's current state. /// Meant for canvas operations involving table column widths.
#[derive(Default)] #[derive(Default)]
pub struct AppScrollWidgetState { pub struct CanvasTableWidthState {
pub current_scroll_position: usize, pub desired_column_widths: Vec<u16>,
pub scroll_bar: usize, pub calculated_column_widths: Vec<u16>,
pub scroll_direction: ScrollDirection,
pub table_state: TableState,
}
impl AppScrollWidgetState {
/// Updates the position if possible, and if there is a valid change, returns the new position.
pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option<usize> {
if change == 0 {
return None;
}
let csp: Result<i64, _> = self.current_scroll_position.try_into();
if let Ok(csp) = csp {
let proposed: Result<usize, _> = (csp + change).try_into();
if let Ok(proposed) = proposed {
if proposed < num_entries {
self.current_scroll_position = proposed;
if change < 0 {
self.scroll_direction = ScrollDirection::Up;
} else {
self.scroll_direction = ScrollDirection::Down;
}
return Some(self.current_scroll_position);
}
}
}
None
}
} }
#[derive(PartialEq)] #[derive(PartialEq)]
@ -159,561 +130,20 @@ impl AppSearchState {
} }
} }
/// 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,
command_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);
}
if is_using_command {
columns.toggle(&ProcessSorting::ProcessName);
columns.toggle(&ProcessSorting::Command);
}
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.scroll_bar = 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 struct ProcState {
pub widget_states: HashMap<u64, ProcWidgetState>, pub widget_states: HashMap<u64, ProcWidget>,
pub force_update: Option<u64>,
pub force_update_all: bool,
} }
impl ProcState { impl ProcState {
pub fn init(widget_states: HashMap<u64, ProcWidgetState>) -> Self { pub fn init(widget_states: HashMap<u64, ProcWidget>) -> Self {
ProcState { ProcState { widget_states }
widget_states,
force_update: None,
force_update_all: false,
}
} }
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidgetState> { pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidget> {
self.widget_states.get_mut(&widget_id) self.widget_states.get_mut(&widget_id)
} }
pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidgetState> { pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidget> {
self.widget_states.get(&widget_id) self.widget_states.get(&widget_id)
} }
} }
@ -721,29 +151,13 @@ impl ProcState {
pub struct NetWidgetState { pub struct NetWidgetState {
pub current_display_time: u64, pub current_display_time: u64,
pub autohide_timer: Option<Instant>, 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 { impl NetWidgetState {
pub fn init( pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
current_display_time: u64,
autohide_timer: Option<Instant>,
// unit_type: DataUnitTypes,
// scale_type: AxisScaling,
) -> Self {
NetWidgetState { NetWidgetState {
current_display_time, current_display_time,
autohide_timer, autohide_timer,
// draw_max_range_cache: 0.0,
// draw_labels_cache: vec![],
// draw_time_start_cache: 0.0,
// unit_type,
// scale_type,
} }
} }
} }
@ -774,20 +188,34 @@ pub struct CpuWidgetState {
pub current_display_time: u64, pub current_display_time: u64,
pub is_legend_hidden: bool, pub is_legend_hidden: bool,
pub autohide_timer: Option<Instant>, pub autohide_timer: Option<Instant>,
pub scroll_state: AppScrollWidgetState, pub table_state: TableComponentState,
pub is_multi_graph_mode: bool, pub is_multi_graph_mode: bool,
pub table_width_state: CanvasTableWidthState,
} }
impl CpuWidgetState { impl CpuWidgetState {
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self { pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"];
const WIDTHS: [WidthBounds; CPU_LEGEND_HEADER.len()] = [
WidthBounds::soft_from_str("CPU", Some(0.5)),
WidthBounds::soft_from_str("Use%", Some(0.5)),
];
let table_state = TableComponentState::new(
CPU_LEGEND_HEADER
.iter()
.zip(WIDTHS)
.map(|(c, width)| {
TableComponentColumn::new_custom(CellContent::new(*c, None), width)
})
.collect(),
);
CpuWidgetState { CpuWidgetState {
current_display_time, current_display_time,
is_legend_hidden: false, is_legend_hidden: false,
autohide_timer, autohide_timer,
scroll_state: AppScrollWidgetState::default(), table_state,
is_multi_graph_mode: false, is_multi_graph_mode: false,
table_width_state: CanvasTableWidthState::default(),
} }
} }
} }
@ -850,15 +278,27 @@ impl MemState {
} }
pub struct TempWidgetState { pub struct TempWidgetState {
pub scroll_state: AppScrollWidgetState, pub table_state: TableComponentState,
pub table_width_state: CanvasTableWidthState,
} }
impl TempWidgetState { impl Default for TempWidgetState {
pub fn init() -> Self { fn default() -> Self {
const TEMP_HEADERS: [&str; 2] = ["Sensor", "Temp"];
const WIDTHS: [WidthBounds; TEMP_HEADERS.len()] = [
WidthBounds::soft_from_str(TEMP_HEADERS[0], Some(0.8)),
WidthBounds::soft_from_str(TEMP_HEADERS[1], None),
];
TempWidgetState { TempWidgetState {
scroll_state: AppScrollWidgetState::default(), table_state: TableComponentState::new(
table_width_state: CanvasTableWidthState::default(), TEMP_HEADERS
.iter()
.zip(WIDTHS)
.map(|(header, width)| {
TableComponentColumn::new_custom(CellContent::new(*header, None), width)
})
.collect(),
),
} }
} }
} }
@ -882,15 +322,32 @@ impl TempState {
} }
pub struct DiskWidgetState { pub struct DiskWidgetState {
pub scroll_state: AppScrollWidgetState, pub table_state: TableComponentState,
pub table_width_state: CanvasTableWidthState,
} }
impl DiskWidgetState { impl Default for DiskWidgetState {
pub fn init() -> Self { fn default() -> Self {
const DISK_HEADERS: [&str; 7] = ["Disk", "Mount", "Used", "Free", "Total", "R/s", "W/s"];
const WIDTHS: [WidthBounds; DISK_HEADERS.len()] = [
WidthBounds::soft_from_str(DISK_HEADERS[0], Some(0.2)),
WidthBounds::soft_from_str(DISK_HEADERS[1], Some(0.2)),
WidthBounds::Hard(4),
WidthBounds::Hard(6),
WidthBounds::Hard(6),
WidthBounds::Hard(7),
WidthBounds::Hard(7),
];
DiskWidgetState { DiskWidgetState {
scroll_state: AppScrollWidgetState::default(), table_state: TableComponentState::new(
table_width_state: CanvasTableWidthState::default(), DISK_HEADERS
.iter()
.zip(WIDTHS)
.map(|(header, width)| {
TableComponentColumn::new_custom(CellContent::new(*header, None), width)
})
.collect(),
),
} }
} }
} }
@ -954,76 +411,3 @@ pub struct ParagraphScrollState {
pub current_scroll_index: u16, pub current_scroll_index: u16,
pub max_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<()>>,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_scroll_update_position() {
fn check_scroll_update(
scroll: &mut AppScrollWidgetState, change: i64, max: usize, ret: Option<usize>,
new_position: usize,
) {
assert_eq!(scroll.update_position(change, max), ret);
assert_eq!(scroll.current_scroll_position, new_position);
}
let mut scroll = AppScrollWidgetState {
current_scroll_position: 5,
scroll_bar: 0,
scroll_direction: ScrollDirection::Down,
table_state: Default::default(),
};
let s = &mut scroll;
// Update by 0. Should not change.
check_scroll_update(s, 0, 15, None, 5);
// Update by 5. Should increment to index 10.
check_scroll_update(s, 5, 15, Some(10), 10);
// Update by 5. Should not change.
check_scroll_update(s, 5, 15, None, 10);
// Update by 4. Should increment to index 14 (supposed max).
check_scroll_update(s, 4, 15, Some(14), 14);
// Update by 1. Should do nothing.
check_scroll_update(s, 1, 15, None, 14);
// Update by -15. Should do nothing.
check_scroll_update(s, -15, 15, None, 14);
// Update by -14. Should land on position 0.
check_scroll_update(s, -14, 15, Some(0), 0);
// Update by -1. Should do nothing.
check_scroll_update(s, -15, 15, None, 0);
// Update by 0. Should do nothing.
check_scroll_update(s, 0, 15, None, 0);
// Update by 15. Should do nothing.
check_scroll_update(s, 15, 15, None, 0);
// Update by 15 but with a larger bound. Should increment to 15.
check_scroll_update(s, 15, 16, Some(15), 15);
}
}

View File

@ -0,0 +1,681 @@
use std::{borrow::Cow, convert::TryInto, ops::Range};
use itertools::Itertools;
use tui::{layout::Rect, widgets::TableState};
use super::ScrollDirection;
/// A bound on the width of a column.
#[derive(Clone, Copy, Debug)]
pub enum WidthBounds {
/// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point.
Soft {
/// The minimum amount before giving up and hiding.
min_width: u16,
/// The desired, calculated width. Take this if possible as the base starting width.
desired: u16,
/// The max width, as a percentage of the total width available. If [`None`],
/// then it can grow as desired.
max_percentage: Option<f32>,
},
/// A width of this type is either as long as specified, or does not appear at all.
Hard(u16),
/// Always uses the width of the [`CellContent`].
CellWidth,
}
impl WidthBounds {
pub const fn soft_from_str(name: &'static str, max_percentage: Option<f32>) -> WidthBounds {
let len = name.len() as u16;
WidthBounds::Soft {
min_width: len,
desired: len,
max_percentage,
}
}
pub const fn soft_from_str_with_alt(
name: &'static str, alt: &'static str, max_percentage: Option<f32>,
) -> WidthBounds {
WidthBounds::Soft {
min_width: alt.len() as u16,
desired: name.len() as u16,
max_percentage,
}
}
}
/// A [`CellContent`] contains text information for display in a table.
#[derive(Clone, Debug)]
pub enum CellContent {
Simple(Cow<'static, str>),
HasAlt {
alt: Cow<'static, str>,
main: Cow<'static, str>,
},
}
impl CellContent {
/// Creates a new [`CellContent`].
pub fn new<I>(name: I, alt: Option<I>) -> Self
where
I: Into<Cow<'static, str>>,
{
if let Some(alt) = alt {
CellContent::HasAlt {
alt: alt.into(),
main: name.into(),
}
} else {
CellContent::Simple(name.into())
}
}
/// Returns the length of the [`CellContent`]. Note that for a [`CellContent::HasAlt`], it will return
/// the length of the "main" field.
pub fn len(&self) -> usize {
match self {
CellContent::Simple(s) => s.len(),
CellContent::HasAlt { alt: _, main: long } => long.len(),
}
}
/// Whether the [`CellContent`]'s text is empty.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn main_text(&self) -> &Cow<'static, str> {
match self {
CellContent::Simple(main) => main,
CellContent::HasAlt { alt: _, main } => main,
}
}
}
pub trait TableComponentHeader {
fn header_text(&self) -> &CellContent;
}
impl TableComponentHeader for CellContent {
fn header_text(&self) -> &CellContent {
self
}
}
impl From<Cow<'static, str>> for CellContent {
fn from(c: Cow<'static, str>) -> Self {
CellContent::Simple(c)
}
}
impl From<&'static str> for CellContent {
fn from(s: &'static str) -> Self {
CellContent::Simple(s.into())
}
}
impl From<String> for CellContent {
fn from(s: String) -> Self {
CellContent::Simple(s.into())
}
}
pub struct TableComponentColumn<H: TableComponentHeader> {
/// The header of the column.
pub header: H,
/// A restriction on this column's width, if desired.
pub width_bounds: WidthBounds,
/// The calculated width of the column.
pub calculated_width: u16,
/// Marks that this column is currently "hidden", and should *always* be skipped.
pub is_hidden: bool,
}
impl<H: TableComponentHeader> TableComponentColumn<H> {
pub fn new_custom(header: H, width_bounds: WidthBounds) -> Self {
Self {
header,
width_bounds,
calculated_width: 0,
is_hidden: false,
}
}
pub fn new(header: H) -> Self {
Self {
header,
width_bounds: WidthBounds::CellWidth,
calculated_width: 0,
is_hidden: false,
}
}
pub fn new_hard(header: H, width: u16) -> Self {
Self {
header,
width_bounds: WidthBounds::Hard(width),
calculated_width: 0,
is_hidden: false,
}
}
pub fn new_soft(header: H, max_percentage: Option<f32>) -> Self {
let min_width = header.header_text().len() as u16;
Self {
header,
width_bounds: WidthBounds::Soft {
min_width,
desired: min_width,
max_percentage,
},
calculated_width: 0,
is_hidden: false,
}
}
pub fn is_zero_width(&self) -> bool {
self.calculated_width == 0
}
pub fn is_skipped(&self) -> bool {
self.is_zero_width() || self.is_hidden
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum SortOrder {
Ascending,
Descending,
}
impl SortOrder {
pub fn is_descending(&self) -> bool {
matches!(self, SortOrder::Descending)
}
}
/// Represents the current table's sorting state.
#[derive(Debug)]
pub enum SortState {
Unsortable,
Sortable(SortableState),
}
#[derive(Debug)]
pub struct SortableState {
/// The "x locations" of the headers.
visual_mappings: Vec<Range<u16>>,
/// The "y location" of the header row. Since all headers share the same y-location we just set it once here.
y_loc: u16,
/// This is a bit of a lazy hack to handle this for now - ideally the entire [`SortableState`]
/// is instead handled by a separate table struct that also can access the columns and their default sort orderings.
default_sort_orderings: Vec<SortOrder>,
/// The currently selected sort index.
pub current_index: usize,
/// The current sorting order.
pub order: SortOrder,
}
impl SortableState {
/// Creates a new [`SortableState`].
pub fn new(
default_index: usize, default_order: SortOrder, default_sort_orderings: Vec<SortOrder>,
) -> Self {
Self {
visual_mappings: Default::default(),
y_loc: 0,
default_sort_orderings,
current_index: default_index,
order: default_order,
}
}
/// Toggles the current sort order.
pub fn toggle_order(&mut self) {
self.order = match self.order {
SortOrder::Ascending => SortOrder::Descending,
SortOrder::Descending => SortOrder::Ascending,
}
}
/// Updates the visual index.
///
/// This function will create a *sorted* range list - in debug mode,
/// the program will assert this, but it will not do so in release mode!
pub fn update_visual_index(&mut self, draw_loc: Rect, row_widths: &[u16]) {
let mut start = draw_loc.x;
let visual_index = row_widths
.iter()
.map(|width| {
let range_start = start;
let range_end = start + width + 1; // +1 for the gap b/w cols.
start = range_end;
range_start..range_end
})
.collect_vec();
debug_assert!(visual_index.iter().all(|a| { a.start <= a.end }));
debug_assert!(visual_index
.iter()
.tuple_windows()
.all(|(a, b)| { b.start >= a.end }));
self.visual_mappings = visual_index;
self.y_loc = draw_loc.y;
}
/// Given some `x` and `y`, if possible, select the corresponding column or toggle the column if already selected,
/// and otherwise do nothing.
///
/// If there was some update, the corresponding column type will be returned. If nothing happens, [`None`] is
/// returned.
pub fn try_select_location(&mut self, x: u16, y: u16) -> Option<usize> {
if self.y_loc == y {
if let Some(index) = self.get_range(x) {
self.update_sort_index(index);
Some(self.current_index)
} else {
None
}
} else {
None
}
}
/// Updates the sort index, and sets the sort order as appropriate.
///
/// If the index is different from the previous one, it will move to the new index and set the sort order
/// to the prescribed default sort order.
///
/// If the index is the same as the previous one, it will simply toggle the current sort order.
pub fn update_sort_index(&mut self, index: usize) {
if self.current_index == index {
self.toggle_order();
} else {
self.current_index = index;
self.order = self.default_sort_orderings[index];
}
}
/// Given a `needle` coordinate, select the corresponding index and value.
fn get_range(&self, needle: u16) -> Option<usize> {
match self
.visual_mappings
.binary_search_by_key(&needle, |range| range.start)
{
Ok(index) => Some(index),
Err(index) => index.checked_sub(1),
}
.and_then(|index| {
if needle < self.visual_mappings[index].end {
Some(index)
} else {
None
}
})
}
}
/// [`TableComponentState`] deals with fields for a scrollable's current state.
pub struct TableComponentState<H: TableComponentHeader = CellContent> {
pub current_scroll_position: usize,
pub scroll_bar: usize,
pub scroll_direction: ScrollDirection,
pub table_state: TableState,
pub columns: Vec<TableComponentColumn<H>>,
pub sort_state: SortState,
}
impl<H: TableComponentHeader> TableComponentState<H> {
pub fn new(columns: Vec<TableComponentColumn<H>>) -> Self {
Self {
current_scroll_position: 0,
scroll_bar: 0,
scroll_direction: ScrollDirection::Down,
table_state: Default::default(),
columns,
sort_state: SortState::Unsortable,
}
}
pub fn sort_state(mut self, sort_state: SortState) -> Self {
self.sort_state = sort_state;
self
}
/// Calculates widths for the columns for this table.
///
/// * `total_width` is the, well, total width available.
/// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if
/// false.
///
/// **NOTE:** Trailing 0's may break tui-rs, remember to filter them out later!
pub fn calculate_column_widths(&mut self, total_width: u16, left_to_right: bool) {
use itertools::Either;
use std::cmp::{max, min};
let mut total_width_left = total_width;
let columns = if left_to_right {
Either::Left(self.columns.iter_mut())
} else {
Either::Right(self.columns.iter_mut().rev())
};
let arrow_offset = match self.sort_state {
SortState::Unsortable => 0,
SortState::Sortable { .. } => 1,
};
let mut num_columns = 0;
let mut skip_iter = false;
for column in columns {
column.calculated_width = 0;
if column.is_hidden || skip_iter {
continue;
}
match &column.width_bounds {
WidthBounds::Soft {
min_width,
desired,
max_percentage,
} => {
let min_width = *min_width + arrow_offset;
if min_width > total_width_left {
skip_iter = true;
continue;
}
let soft_limit = max(
if let Some(max_percentage) = max_percentage {
// TODO: Rust doesn't have an `into()` or `try_into()` for floats to integers.
((*max_percentage * f32::from(total_width)).ceil()) as u16
} else {
*desired
},
min_width,
);
let space_taken = min(min(soft_limit, *desired), total_width_left);
if min_width > space_taken || min_width == 0 {
skip_iter = true;
} else if space_taken > 0 {
total_width_left = total_width_left.saturating_sub(space_taken + 1);
column.calculated_width = space_taken;
num_columns += 1;
}
}
WidthBounds::CellWidth => {
let width = column.header.header_text().len() as u16;
let min_width = width + arrow_offset;
if min_width > total_width_left || min_width == 0 {
skip_iter = true;
} else if min_width > 0 {
total_width_left = total_width_left.saturating_sub(min_width + 1);
column.calculated_width = min_width;
num_columns += 1;
}
}
WidthBounds::Hard(width) => {
let min_width = *width + arrow_offset;
if min_width > total_width_left || min_width == 0 {
skip_iter = true;
} else if min_width > 0 {
total_width_left = total_width_left.saturating_sub(min_width + 1);
column.calculated_width = min_width;
num_columns += 1;
}
}
}
}
if num_columns > 0 {
// Redistribute remaining.
let mut num_dist = num_columns;
let amount_per_slot = total_width_left / num_dist;
total_width_left %= num_dist;
for column in self.columns.iter_mut() {
if num_dist == 0 {
break;
}
if column.calculated_width > 0 {
if total_width_left > 0 {
column.calculated_width += amount_per_slot + 1;
total_width_left -= 1;
} else {
column.calculated_width += amount_per_slot;
}
num_dist -= 1;
}
}
}
}
/// Updates the position if possible, and if there is a valid change, returns the new position.
pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option<usize> {
if change == 0 {
return None;
}
let csp: Result<i64, _> = self.current_scroll_position.try_into();
if let Ok(csp) = csp {
let proposed: Result<usize, _> = (csp + change).try_into();
if let Ok(proposed) = proposed {
if proposed < num_entries {
self.current_scroll_position = proposed;
if change < 0 {
self.scroll_direction = ScrollDirection::Up;
} else {
self.scroll_direction = ScrollDirection::Down;
}
return Some(self.current_scroll_position);
}
}
}
None
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_scroll_update_position() {
#[track_caller]
fn check_scroll_update(
scroll: &mut TableComponentState, change: i64, max: usize, ret: Option<usize>,
new_position: usize,
) {
assert_eq!(scroll.update_position(change, max), ret);
assert_eq!(scroll.current_scroll_position, new_position);
}
let mut scroll = TableComponentState {
current_scroll_position: 5,
scroll_bar: 0,
scroll_direction: ScrollDirection::Down,
table_state: Default::default(),
columns: vec![],
sort_state: SortState::Unsortable,
};
let s = &mut scroll;
// Update by 0. Should not change.
check_scroll_update(s, 0, 15, None, 5);
// Update by 5. Should increment to index 10.
check_scroll_update(s, 5, 15, Some(10), 10);
// Update by 5. Should not change.
check_scroll_update(s, 5, 15, None, 10);
// Update by 4. Should increment to index 14 (supposed max).
check_scroll_update(s, 4, 15, Some(14), 14);
// Update by 1. Should do nothing.
check_scroll_update(s, 1, 15, None, 14);
// Update by -15. Should do nothing.
check_scroll_update(s, -15, 15, None, 14);
// Update by -14. Should land on position 0.
check_scroll_update(s, -14, 15, Some(0), 0);
// Update by -1. Should do nothing.
check_scroll_update(s, -15, 15, None, 0);
// Update by 0. Should do nothing.
check_scroll_update(s, 0, 15, None, 0);
// Update by 15. Should do nothing.
check_scroll_update(s, 15, 15, None, 0);
// Update by 15 but with a larger bound. Should increment to 15.
check_scroll_update(s, 15, 16, Some(15), 15);
}
#[test]
fn test_table_width_calculation() {
#[track_caller]
fn test_calculation(state: &mut TableComponentState, width: u16, expected: Vec<u16>) {
state.calculate_column_widths(width, true);
assert_eq!(
state
.columns
.iter()
.filter_map(|c| if c.calculated_width == 0 {
None
} else {
Some(c.calculated_width)
})
.collect::<Vec<_>>(),
expected
)
}
let mut state = TableComponentState::new(vec![
TableComponentColumn::new(CellContent::from("a")),
TableComponentColumn::new_custom(
"a".into(),
WidthBounds::Soft {
min_width: 1,
desired: 10,
max_percentage: Some(0.125),
},
),
TableComponentColumn::new_custom(
"a".into(),
WidthBounds::Soft {
min_width: 2,
desired: 10,
max_percentage: Some(0.5),
},
),
]);
test_calculation(&mut state, 0, vec![]);
test_calculation(&mut state, 1, vec![1]);
test_calculation(&mut state, 2, vec![1]);
test_calculation(&mut state, 3, vec![1, 1]);
test_calculation(&mut state, 4, vec![1, 1]);
test_calculation(&mut state, 5, vec![2, 1]);
test_calculation(&mut state, 6, vec![1, 1, 2]);
test_calculation(&mut state, 7, vec![1, 1, 3]);
test_calculation(&mut state, 8, vec![1, 1, 4]);
test_calculation(&mut state, 14, vec![2, 2, 7]);
test_calculation(&mut state, 20, vec![2, 4, 11]);
test_calculation(&mut state, 100, vec![27, 35, 35]);
state.sort_state = SortState::Sortable(SortableState::new(1, SortOrder::Ascending, vec![]));
test_calculation(&mut state, 0, vec![]);
test_calculation(&mut state, 1, vec![]);
test_calculation(&mut state, 2, vec![2]);
test_calculation(&mut state, 3, vec![2]);
test_calculation(&mut state, 4, vec![3]);
test_calculation(&mut state, 5, vec![2, 2]);
test_calculation(&mut state, 6, vec![2, 2]);
test_calculation(&mut state, 7, vec![3, 2]);
test_calculation(&mut state, 8, vec![3, 3]);
test_calculation(&mut state, 14, vec![2, 2, 7]);
test_calculation(&mut state, 20, vec![3, 4, 10]);
test_calculation(&mut state, 100, vec![27, 35, 35]);
}
#[test]
fn test_visual_index_selection() {
let mut state = SortableState::new(
0,
SortOrder::Ascending,
vec![SortOrder::Ascending, SortOrder::Descending],
);
const X_OFFSET: u16 = 10;
const Y_OFFSET: u16 = 15;
state.update_visual_index(Rect::new(X_OFFSET, Y_OFFSET, 20, 15), &[4, 14]);
#[track_caller]
fn test_selection(
state: &mut SortableState, from_x_offset: u16, from_y_offset: u16,
result: (Option<usize>, SortOrder),
) {
assert_eq!(
state.try_select_location(X_OFFSET + from_x_offset, Y_OFFSET + from_y_offset),
result.0
);
assert_eq!(state.order, result.1);
}
use SortOrder::*;
// Clicking on these don't do anything, so don't show any change.
test_selection(&mut state, 5, 1, (None, Ascending));
test_selection(&mut state, 21, 0, (None, Ascending));
// Clicking on the first column should toggle it as it is already selected.
test_selection(&mut state, 3, 0, (Some(0), Descending));
// Clicking on the first column should toggle it again as it is already selected.
test_selection(&mut state, 4, 0, (Some(0), Ascending));
// Clicking on second column should select and switch to the descending ordering as that is its default.
test_selection(&mut state, 5, 0, (Some(1), Descending));
// Clicking on second column should toggle it.
test_selection(&mut state, 19, 0, (Some(1), Ascending));
// Overshoot, should not do anything.
test_selection(&mut state, 20, 0, (None, Ascending));
// Further overshoot, should not do anything.
test_selection(&mut state, 25, 0, (None, Ascending));
// Go back to first column, should be ascending to match default for index 0.
test_selection(&mut state, 3, 0, (Some(0), Ascending));
// Click on first column should then go to descending as it is already selected and ascending.
test_selection(&mut state, 3, 0, (Some(0), Descending));
}
}

2
src/app/widgets.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod process_widget;
pub use process_widget::*;

File diff suppressed because it is too large Load Diff

View File

@ -54,13 +54,8 @@ fn main() -> Result<()> {
)?; )?;
// Create painter and set colours. // Create painter and set colours.
let mut painter = canvas::Painter::init( let mut painter =
widget_layout, canvas::Painter::init(widget_layout, &config, get_color_scheme(&matches, &config)?)?;
app.app_config_fields.table_gap,
app.app_config_fields.use_basic_mode,
&config,
get_color_scheme(&matches, &config)?,
)?;
// Create termination mutex and cvar // Create termination mutex and cvar
#[allow(clippy::mutex_atomic)] #[allow(clippy::mutex_atomic)]
@ -135,11 +130,11 @@ fn main() -> Result<()> {
if handle_key_event_or_break(event, &mut app, &collection_thread_ctrl_sender) { if handle_key_event_or_break(event, &mut app, &collection_thread_ctrl_sender) {
break; break;
} }
handle_force_redraws(&mut app); update_data(&mut app);
} }
BottomEvent::MouseInput(event) => { BottomEvent::MouseInput(event) => {
handle_mouse_event(event, &mut app); handle_mouse_event(event, &mut app);
handle_force_redraws(&mut app); update_data(&mut app);
} }
BottomEvent::Update(data) => { BottomEvent::Update(data) => {
app.data_collection.eat_data(data); app.data_collection.eat_data(data);
@ -158,46 +153,45 @@ fn main() -> Result<()> {
if app.used_widgets.use_net { if app.used_widgets.use_net {
let network_data = convert_network_data_points( let network_data = convert_network_data_points(
&app.data_collection, &app.data_collection,
false,
app.app_config_fields.use_basic_mode app.app_config_fields.use_basic_mode
|| app.app_config_fields.use_old_network_legend, || app.app_config_fields.use_old_network_legend,
&app.app_config_fields.network_scale_type, &app.app_config_fields.network_scale_type,
&app.app_config_fields.network_unit_type, &app.app_config_fields.network_unit_type,
app.app_config_fields.network_use_binary_prefix, app.app_config_fields.network_use_binary_prefix,
); );
app.canvas_data.network_data_rx = network_data.rx; app.converted_data.network_data_rx = network_data.rx;
app.canvas_data.network_data_tx = network_data.tx; app.converted_data.network_data_tx = network_data.tx;
app.canvas_data.rx_display = network_data.rx_display; app.converted_data.rx_display = network_data.rx_display;
app.canvas_data.tx_display = network_data.tx_display; app.converted_data.tx_display = network_data.tx_display;
if let Some(total_rx_display) = network_data.total_rx_display { if let Some(total_rx_display) = network_data.total_rx_display {
app.canvas_data.total_rx_display = total_rx_display; app.converted_data.total_rx_display = total_rx_display;
} }
if let Some(total_tx_display) = network_data.total_tx_display { if let Some(total_tx_display) = network_data.total_tx_display {
app.canvas_data.total_tx_display = total_tx_display; app.converted_data.total_tx_display = total_tx_display;
} }
} }
// Disk // Disk
if app.used_widgets.use_disk { if app.used_widgets.use_disk {
app.canvas_data.disk_data = convert_disk_row(&app.data_collection); app.converted_data.disk_data = convert_disk_row(&app.data_collection);
} }
// Temperatures // Temperatures
if app.used_widgets.use_temp { if app.used_widgets.use_temp {
app.canvas_data.temp_sensor_data = convert_temp_row(&app); app.converted_data.temp_sensor_data = convert_temp_row(&app);
} }
// Memory // Memory
if app.used_widgets.use_mem { if app.used_widgets.use_mem {
app.canvas_data.mem_data = app.converted_data.mem_data =
convert_mem_data_points(&app.data_collection, false); convert_mem_data_points(&app.data_collection);
app.canvas_data.swap_data = app.converted_data.swap_data =
convert_swap_data_points(&app.data_collection, false); convert_swap_data_points(&app.data_collection);
let (memory_labels, swap_labels) = let (memory_labels, swap_labels) =
convert_mem_labels(&app.data_collection); convert_mem_labels(&app.data_collection);
app.canvas_data.mem_labels = memory_labels; app.converted_data.mem_labels = memory_labels;
app.canvas_data.swap_labels = swap_labels; app.converted_data.swap_labels = swap_labels;
} }
if app.used_widgets.use_cpu { if app.used_widgets.use_cpu {
@ -205,25 +199,28 @@ fn main() -> Result<()> {
convert_cpu_data_points( convert_cpu_data_points(
&app.data_collection, &app.data_collection,
&mut app.canvas_data.cpu_data, &mut app.converted_data.cpu_data,
false,
); );
app.canvas_data.load_avg_data = app.data_collection.load_avg_harvest; app.converted_data.load_avg_data = app.data_collection.load_avg_harvest;
} }
// Processes // Processes
if app.used_widgets.use_proc { if app.used_widgets.use_proc {
update_all_process_lists(&mut app); for proc in app.proc_state.widget_states.values_mut() {
proc.force_data_update();
}
} }
// Battery // Battery
#[cfg(feature = "battery")] #[cfg(feature = "battery")]
{ {
if app.used_widgets.use_battery { if app.used_widgets.use_battery {
app.canvas_data.battery_data = app.converted_data.battery_data =
convert_battery_harvest(&app.data_collection); convert_battery_harvest(&app.data_collection);
} }
} }
update_data(&mut app);
} }
} }
BottomEvent::Clean => { BottomEvent::Clean => {

View File

@ -1,5 +1,5 @@
use itertools::izip; use itertools::izip;
use std::{collections::HashMap, str::FromStr}; use std::str::FromStr;
use tui::{ use tui::{
backend::Backend, backend::Backend,
@ -18,11 +18,9 @@ use crate::{
App, App,
}, },
constants::*, constants::*,
data_conversion::{ConvertedBatteryData, ConvertedCpuData, ConvertedProcessData},
options::Config, options::Config,
utils::error, utils::error,
utils::error::BottomError, utils::error::BottomError,
Pid,
}; };
pub use self::components::Point; pub use self::components::Point;
@ -33,30 +31,6 @@ mod dialogs;
mod drawing_utils; mod drawing_utils;
mod widgets; mod widgets;
#[derive(Default)]
pub struct DisplayableData {
pub rx_display: String,
pub tx_display: String,
pub total_rx_display: String,
pub total_tx_display: String,
pub network_data_rx: Vec<Point>,
pub network_data_tx: Vec<Point>,
pub disk_data: Vec<Vec<String>>,
pub temp_sensor_data: Vec<Vec<String>>,
pub single_process_data: HashMap<Pid, ConvertedProcessData>, // Contains single process data, key is PID
pub finalized_process_data_map: HashMap<u64, Vec<ConvertedProcessData>>, // What's actually displayed, key is the widget ID.
pub stringified_process_data_map: HashMap<u64, Vec<(Vec<(String, Option<String>)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID
pub mem_labels: Option<(String, String)>,
pub swap_labels: Option<(String, String)>,
pub mem_data: Vec<Point>, // TODO: Switch this and all data points over to a better data structure...
pub swap_data: Vec<Point>,
pub load_avg_data: [f32; 3],
pub cpu_data: Vec<ConvertedCpuData>,
pub battery_data: Vec<ConvertedBatteryData>,
}
#[derive(Debug)] #[derive(Debug)]
pub enum ColourScheme { pub enum ColourScheme {
Default, Default,
@ -94,20 +68,20 @@ pub struct Painter {
height: u16, height: u16,
width: u16, width: u16,
styled_help_text: Vec<Spans<'static>>, styled_help_text: Vec<Spans<'static>>,
is_mac_os: bool, // FIXME: This feels out of place... is_mac_os: bool, // TODO: This feels out of place...
// TODO: Redo this entire thing.
row_constraints: Vec<Constraint>, row_constraints: Vec<Constraint>,
col_constraints: Vec<Vec<Constraint>>, col_constraints: Vec<Vec<Constraint>>,
col_row_constraints: Vec<Vec<Vec<Constraint>>>, col_row_constraints: Vec<Vec<Vec<Constraint>>>,
layout_constraints: Vec<Vec<Vec<Vec<Constraint>>>>, layout_constraints: Vec<Vec<Vec<Vec<Constraint>>>>,
derived_widget_draw_locs: Vec<Vec<Vec<Vec<Rect>>>>, derived_widget_draw_locs: Vec<Vec<Vec<Vec<Rect>>>>,
widget_layout: BottomLayout, widget_layout: BottomLayout,
table_height_offset: u16,
} }
impl Painter { impl Painter {
pub fn init( pub fn init(
widget_layout: BottomLayout, table_gap: u16, is_basic_mode: bool, config: &Config, widget_layout: BottomLayout, config: &Config, colour_scheme: ColourScheme,
colour_scheme: ColourScheme,
) -> anyhow::Result<Self> { ) -> anyhow::Result<Self> {
// Now for modularity; we have to also initialize the base layouts! // Now for modularity; we have to also initialize the base layouts!
// We want to do this ONCE and reuse; after this we can just construct // We want to do this ONCE and reuse; after this we can just construct
@ -188,7 +162,6 @@ impl Painter {
layout_constraints, layout_constraints,
widget_layout, widget_layout,
derived_widget_draw_locs: Vec::default(), derived_widget_draw_locs: Vec::default(),
table_height_offset: if is_basic_mode { 2 } else { 4 } + table_gap,
}; };
if let ColourScheme::Custom = colour_scheme { if let ColourScheme::Custom = colour_scheme {
@ -338,12 +311,6 @@ impl Painter {
for battery_widget in app_state.battery_state.widget_states.values_mut() { for battery_widget in app_state.battery_state.widget_states.values_mut() {
battery_widget.tab_click_locs = None; battery_widget.tab_click_locs = None;
} }
// Reset column headers for sorting in process widget...
for proc_widget in app_state.proc_state.widget_states.values_mut() {
proc_widget.columns.column_header_y_loc = None;
proc_widget.columns.column_header_x_locs = None;
}
} }
if app_state.help_dialog_state.is_showing_help { if app_state.help_dialog_state.is_showing_help {
@ -506,7 +473,7 @@ impl Painter {
_ => 0, _ => 0,
}; };
self.draw_process_features(f, app_state, rect[0], true, widget_id); self.draw_process_widget(f, app_state, rect[0], true, widget_id);
} }
Battery => self.draw_battery_display( Battery => self.draw_battery_display(
f, f,
@ -524,7 +491,7 @@ impl Painter {
self.draw_frozen_indicator(f, frozen_draw_loc); self.draw_frozen_indicator(f, frozen_draw_loc);
} }
let actual_cpu_data_len = app_state.canvas_data.cpu_data.len().saturating_sub(1); let actual_cpu_data_len = app_state.converted_data.cpu_data.len().saturating_sub(1);
// This fixes #397, apparently if the height is 1, it can't render the CPU bars... // This fixes #397, apparently if the height is 1, it can't render the CPU bars...
let cpu_height = { let cpu_height = {
@ -585,7 +552,7 @@ impl Painter {
ProcSort => 2, ProcSort => 2,
_ => 0, _ => 0,
}; };
self.draw_process_features( self.draw_process_widget(
f, f,
app_state, app_state,
vertical_chunks[3], vertical_chunks[3],
@ -736,7 +703,7 @@ impl Painter {
Disk => { Disk => {
self.draw_disk_table(f, app_state, *widget_draw_loc, true, widget.widget_id) self.draw_disk_table(f, app_state, *widget_draw_loc, true, widget.widget_id)
} }
Proc => self.draw_process_features( Proc => self.draw_process_widget(
f, f,
app_state, app_state,
*widget_draw_loc, *widget_draw_loc,

View File

@ -1 +1,502 @@
use std::{
borrow::Cow,
cmp::{max, min},
};
use concat_string::concat_string;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::Style,
text::{Span, Spans, Text},
widgets::{Block, Borders, Row, Table},
Frame,
};
use unicode_segmentation::UnicodeSegmentation;
use crate::{
app::{
self, layout_manager::BottomWidget, CellContent, SortState, TableComponentColumn,
TableComponentHeader, TableComponentState, WidthBounds,
},
constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
data_conversion::{TableData, TableRow},
};
pub struct TextTableTitle<'a> {
pub title: Cow<'a, str>,
pub is_expanded: bool,
}
pub struct TextTable<'a> {
pub table_gap: u16,
pub is_force_redraw: bool, // TODO: Is this force redraw thing needed? Or is there a better way?
pub recalculate_column_widths: bool,
/// The header style.
pub header_style: Style,
/// The border style.
pub border_style: Style,
/// The highlighted text style.
pub highlighted_text_style: Style,
/// The graph title and whether it is expanded (if there is one).
pub title: Option<TextTableTitle<'a>>,
/// Whether this widget is selected.
pub is_on_widget: bool,
/// Whether to draw all borders.
pub draw_border: bool,
/// Whether to show the scroll position.
pub show_table_scroll_position: bool,
/// The title style.
pub title_style: Style,
/// The text style.
pub text_style: Style,
/// Whether to determine widths from left to right.
pub left_to_right: bool,
}
impl<'a> TextTable<'a> {
/// Generates a title for the [`TextTable`] widget, given the available space.
fn generate_title(&self, draw_loc: Rect, pos: usize, total: usize) -> Option<Spans<'_>> {
self.title
.as_ref()
.map(|TextTableTitle { title, is_expanded }| {
let title = if self.show_table_scroll_position {
let title_string = concat_string!(
title,
"(",
pos.to_string(),
" of ",
total.to_string(),
") "
);
if title_string.len() + 2 <= draw_loc.width.into() {
title_string
} else {
title.to_string()
}
} else {
title.to_string()
};
if *is_expanded {
let title_base = concat_string!(title, "── Esc to go back ");
let esc = concat_string!(
"",
"".repeat(usize::from(draw_loc.width).saturating_sub(
UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2
)),
"─ Esc to go back "
);
Spans::from(vec![
Span::styled(title, self.title_style),
Span::styled(esc, self.border_style),
])
} else {
Spans::from(Span::styled(title, self.title_style))
}
})
}
pub fn draw_text_table<B: Backend, H: TableComponentHeader>(
&self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState<H>,
table_data: &TableData, btm_widget: Option<&mut BottomWidget>,
) {
// TODO: This is a *really* ugly hack to get basic mode to hide the border when not selected, without shifting everything.
let is_not_basic = self.is_on_widget || self.draw_border;
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)])
.horizontal_margin(if is_not_basic { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc)[0];
let block = if self.draw_border {
let block = Block::default()
.borders(Borders::ALL)
.border_style(self.border_style);
if let Some(title) = self.generate_title(
draw_loc,
state.current_scroll_position.saturating_add(1),
table_data.data.len(),
) {
block.title(title)
} else {
block
}
} else if self.is_on_widget {
Block::default()
.borders(SIDE_BORDERS)
.border_style(self.border_style)
} else {
Block::default().borders(Borders::NONE)
};
let inner_rect = block.inner(margined_draw_loc);
let (inner_width, inner_height) = { (inner_rect.width, inner_rect.height) };
if inner_width == 0 || inner_height == 0 {
f.render_widget(block, margined_draw_loc);
} else {
let show_header = inner_height > 1;
let header_height = if show_header { 1 } else { 0 };
let table_gap = if !show_header || draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
self.table_gap
};
let sliced_vec = {
let num_rows = usize::from(inner_height.saturating_sub(table_gap + header_height));
let start = get_start_position(
num_rows,
&state.scroll_direction,
&mut state.scroll_bar,
state.current_scroll_position,
self.is_force_redraw,
);
let end = min(table_data.data.len(), start + num_rows);
state
.table_state
.select(Some(state.current_scroll_position.saturating_sub(start)));
&table_data.data[start..end]
};
// Calculate widths
if self.recalculate_column_widths {
state
.columns
.iter_mut()
.zip(&table_data.col_widths)
.for_each(|(column, data_width)| match &mut column.width_bounds {
WidthBounds::Soft {
min_width: _,
desired,
max_percentage: _,
} => {
*desired = max(
*desired,
max(column.header.header_text().len(), *data_width) as u16,
);
}
WidthBounds::CellWidth => {}
WidthBounds::Hard(_width) => {}
});
state.calculate_column_widths(inner_width, self.left_to_right);
if let SortState::Sortable(st) = &mut state.sort_state {
let row_widths = state
.columns
.iter()
.filter_map(|c| {
if c.calculated_width == 0 {
None
} else {
Some(c.calculated_width)
}
})
.collect::<Vec<_>>();
st.update_visual_index(inner_rect, &row_widths);
}
// Update draw loc in widget map
if let Some(btm_widget) = btm_widget {
btm_widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
btm_widget.bottom_right_corner =
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
}
}
let columns = &state.columns;
let header = build_header(columns, &state.sort_state)
.style(self.header_style)
.bottom_margin(table_gap);
let table_rows = sliced_vec.iter().map(|row| {
let (row, style) = match row {
TableRow::Raw(row) => (row, None),
TableRow::Styled(row, style) => (row, Some(*style)),
};
Row::new(row.iter().zip(columns).filter_map(|(cell, c)| {
if c.calculated_width == 0 {
None
} else {
Some(truncate_text(cell, c.calculated_width.into(), style))
}
}))
});
if !table_data.data.is_empty() {
let widget = {
let mut table = Table::new(table_rows)
.block(block)
.highlight_style(self.highlighted_text_style)
.style(self.text_style);
if show_header {
table = table.header(header);
}
table
};
f.render_stateful_widget(
widget.widths(
&(columns
.iter()
.filter_map(|c| {
if c.calculated_width == 0 {
None
} else {
Some(Constraint::Length(c.calculated_width))
}
})
.collect::<Vec<_>>()),
),
margined_draw_loc,
&mut state.table_state,
);
} else {
f.render_widget(block, margined_draw_loc);
}
}
}
}
/// Constructs the table header.
fn build_header<'a, H: TableComponentHeader>(
columns: &'a [TableComponentColumn<H>], sort_state: &SortState,
) -> Row<'a> {
use itertools::Either;
const UP_ARROW: &str = "";
const DOWN_ARROW: &str = "";
let iter = match sort_state {
SortState::Unsortable => Either::Left(columns.iter().filter_map(|c| {
if c.calculated_width == 0 {
None
} else {
Some(truncate_text(
c.header.header_text(),
c.calculated_width.into(),
None,
))
}
})),
SortState::Sortable(s) => {
let order = &s.order;
let index = s.current_index;
let arrow = match order {
app::SortOrder::Ascending => UP_ARROW,
app::SortOrder::Descending => DOWN_ARROW,
};
Either::Right(columns.iter().enumerate().filter_map(move |(itx, c)| {
if c.calculated_width == 0 {
None
} else if itx == index {
Some(truncate_suffixed_text(
c.header.header_text(),
arrow,
c.calculated_width.into(),
None,
))
} else {
Some(truncate_text(
c.header.header_text(),
c.calculated_width.into(),
None,
))
}
}))
}
};
Row::new(iter)
}
/// Truncates text if it is too long, and adds an ellipsis at the end if needed.
fn truncate_text(content: &CellContent, width: usize, row_style: Option<Style>) -> Text<'_> {
let (main_text, alt_text) = match content {
CellContent::Simple(s) => (s, None),
CellContent::HasAlt {
alt: short,
main: long,
} => (long, Some(short)),
};
let mut text = {
let graphemes: Vec<&str> =
UnicodeSegmentation::graphemes(main_text.as_ref(), true).collect();
if graphemes.len() > width && width > 0 {
if let Some(s) = alt_text {
// If an alternative exists, use that.
Text::raw(s.as_ref())
} else {
// Truncate with ellipsis
let first_n = graphemes[..(width - 1)].concat();
Text::raw(concat_string!(first_n, ""))
}
} else {
Text::raw(main_text.as_ref())
}
};
if let Some(row_style) = row_style {
text.patch_style(row_style);
}
text
}
fn truncate_suffixed_text<'a>(
content: &'a CellContent, suffix: &str, width: usize, row_style: Option<Style>,
) -> Text<'a> {
let (main_text, alt_text) = match content {
CellContent::Simple(s) => (s, None),
CellContent::HasAlt {
alt: short,
main: long,
} => (long, Some(short)),
};
let mut text = {
let suffixed = concat_string!(main_text, suffix);
let graphemes: Vec<&str> =
UnicodeSegmentation::graphemes(suffixed.as_str(), true).collect();
if graphemes.len() > width && width > 1 {
if let Some(alt) = alt_text {
// If an alternative exists, use that + arrow.
Text::raw(concat_string!(alt, suffix))
} else {
// Truncate with ellipsis + arrow.
let first_n = graphemes[..(width - 2)].concat();
Text::raw(concat_string!(first_n, "", suffix))
}
} else {
Text::raw(suffixed)
}
};
if let Some(row_style) = row_style {
text.patch_style(row_style);
}
text
}
/// Gets the starting position of a table.
pub fn get_start_position(
num_rows: usize, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut usize,
currently_selected_position: usize, is_force_redraw: bool,
) -> usize {
if is_force_redraw {
*scroll_position_bar = 0;
}
match scroll_direction {
app::ScrollDirection::Down => {
if currently_selected_position < *scroll_position_bar + num_rows {
// If, using previous_scrolled_position, we can see the element
// (so within that and + num_rows) just reuse the current previously scrolled position
*scroll_position_bar
} else if currently_selected_position >= num_rows {
// Else if the current position past the last element visible in the list, omit
// until we can see that element
*scroll_position_bar = currently_selected_position - num_rows + 1;
*scroll_position_bar
} else {
// Else, if it is not past the last element visible, do not omit anything
0
}
}
app::ScrollDirection::Up => {
if currently_selected_position <= *scroll_position_bar {
// If it's past the first element, then show from that element downwards
*scroll_position_bar = currently_selected_position;
} else if currently_selected_position >= *scroll_position_bar + num_rows {
*scroll_position_bar = currently_selected_position - num_rows + 1;
}
// Else, don't change what our start position is from whatever it is set to!
*scroll_position_bar
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_get_start_position() {
use crate::app::ScrollDirection::{self, Down, Up};
#[track_caller]
fn test_get(
bar: usize, rows: usize, direction: ScrollDirection, selected: usize, force: bool,
expected_posn: usize, expected_bar: usize,
) {
let mut bar = bar;
assert_eq!(
get_start_position(rows, &direction, &mut bar, selected, force),
expected_posn,
"returned start position should match"
);
assert_eq!(bar, expected_bar, "bar positions should match");
}
// Scrolling down from start
test_get(0, 10, Down, 0, false, 0, 0);
// Simple scrolling down
test_get(0, 10, Down, 1, false, 0, 0);
// Scrolling down from the middle high up
test_get(0, 10, Down, 4, false, 0, 0);
// Scrolling down into boundary
test_get(0, 10, Down, 10, false, 1, 1);
test_get(0, 10, Down, 11, false, 2, 2);
// Scrolling down from the with non-zero bar
test_get(5, 10, Down, 14, false, 5, 5);
// Force redraw scrolling down (e.g. resize)
test_get(5, 15, Down, 14, true, 0, 0);
// Test jumping down
test_get(1, 10, Down, 19, true, 10, 10);
// Scrolling up from bottom
test_get(10, 10, Up, 19, false, 10, 10);
// Simple scrolling up
test_get(10, 10, Up, 18, false, 10, 10);
// Scrolling up from the middle
test_get(10, 10, Up, 10, false, 10, 10);
// Scrolling up into boundary
test_get(10, 10, Up, 9, false, 9, 9);
// Force redraw scrolling up (e.g. resize)
test_get(5, 10, Up, 14, true, 5, 5);
// Test jumping up
test_get(10, 10, Up, 0, false, 0, 0);
}
}

View File

@ -1,7 +1,4 @@
use std::{ use std::{borrow::Cow, cmp::max};
borrow::Cow,
cmp::{max, Ordering},
};
use tui::{ use tui::{
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Rect}, layout::{Constraint, Rect},
@ -15,6 +12,8 @@ use tui::{
}; };
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use crate::utils::gen_util::partial_ordering;
/// An X or Y axis for the chart widget /// An X or Y axis for the chart widget
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Axis<'a> { pub struct Axis<'a> {
@ -556,16 +555,11 @@ impl<'a> Widget for TimeChart<'a> {
} }
} }
fn bin_cmp(a: &f64, b: &f64) -> Ordering {
// TODO: Switch to `total_cmp` on 1.62
a.partial_cmp(b).unwrap_or(Ordering::Equal)
}
/// Returns the start index and potential interpolation index given the start time and the dataset. /// Returns the start index and potential interpolation index given the start time and the dataset.
fn get_start(dataset: &Dataset<'_>, start_bound: f64) -> (usize, Option<usize>) { fn get_start(dataset: &Dataset<'_>, start_bound: f64) -> (usize, Option<usize>) {
match dataset match dataset
.data .data
.binary_search_by(|(x, _y)| bin_cmp(x, &start_bound)) .binary_search_by(|(x, _y)| partial_ordering(x, &start_bound))
{ {
Ok(index) => (index, None), Ok(index) => (index, None),
Err(index) => (index, index.checked_sub(1)), Err(index) => (index, index.checked_sub(1)),
@ -576,7 +570,7 @@ fn get_start(dataset: &Dataset<'_>, start_bound: f64) -> (usize, Option<usize>)
fn get_end(dataset: &Dataset<'_>, end_bound: f64) -> (usize, Option<usize>) { fn get_end(dataset: &Dataset<'_>, end_bound: f64) -> (usize, Option<usize>) {
match dataset match dataset
.data .data
.binary_search_by(|(x, _y)| bin_cmp(x, &end_bound)) .binary_search_by(|(x, _y)| partial_ordering(x, &end_bound))
{ {
// In the success case, this means we found an index. Add one since we want to include this index and we // In the success case, this means we found an index. Add one since we want to include this index and we
// expect to use the returned index as part of a (m..n) range. // expect to use the returned index as part of a (m..n) range.
@ -622,9 +616,7 @@ mod test {
} }
#[test] #[test]
fn time_chart_test_data_trimming() { fn time_chart_empty_dataset() {
// Quick test on a completely empty dataset...
{
let data = []; let data = [];
let dataset = Dataset::default().data(&data); let dataset = Dataset::default().data(&data);
@ -635,6 +627,8 @@ mod test {
assert_eq!(get_end(&dataset, 100.0), (0, None)); assert_eq!(get_end(&dataset, 100.0), (0, None));
} }
#[test]
fn time_chart_test_data_trimming() {
let data = [ let data = [
(-3.0, 8.0), (-3.0, 8.0),
(-2.5, 15.0), (-2.5, 15.0),

View File

@ -25,7 +25,6 @@ pub struct GraphData<'a> {
pub name: Option<Cow<'a, str>>, pub name: Option<Cow<'a, str>>,
} }
#[derive(Default)]
pub struct TimeGraph<'a> { pub struct TimeGraph<'a> {
/// Whether to use a dot marker over the default braille markers. /// Whether to use a dot marker over the default braille markers.
pub use_dot: bool, pub use_dot: bool,
@ -144,14 +143,14 @@ impl<'a> TimeGraph<'a> {
.collect() .collect()
}; };
f.render_widget( let block = Block::default()
TimeChart::new(data)
.block(
Block::default()
.title(self.generate_title(draw_loc)) .title(self.generate_title(draw_loc))
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(self.border_style), .border_style(self.border_style);
)
f.render_widget(
TimeChart::new(data)
.block(block)
.x_axis(x_axis) .x_axis(x_axis)
.y_axis(y_axis) .y_axis(y_axis)
.hidden_legend_constraints( .hidden_legend_constraints(

View File

@ -9,7 +9,7 @@ use tui::{
}; };
use crate::{ use crate::{
app::{App, KillSignal}, app::{widgets::ProcWidgetMode, App, KillSignal},
canvas::Painter, canvas::Painter,
}; };
@ -29,7 +29,13 @@ impl Painter {
if let Some(first_pid) = to_kill_processes.1.first() { if let Some(first_pid) = to_kill_processes.1.first() {
return Some(Text::from(vec![ return Some(Text::from(vec![
Spans::from(""), Spans::from(""),
if app_state.is_grouped(app_state.current_widget.widget_id) { if app_state
.proc_state
.widget_states
.get(&app_state.current_widget.widget_id)
.map(|p| matches!(p.mode, ProcWidgetMode::Grouped))
.unwrap_or(false)
{
if to_kill_processes.1.len() != 1 { if to_kill_processes.1.len() != 1 {
Spans::from(format!( Spans::from(format!(
"Kill {} processes with the name \"{}\"? Press ENTER to confirm.", "Kill {} processes with the name \"{}\"? Press ENTER to confirm.",

View File

@ -1,124 +1,10 @@
use tui::layout::Rect; use tui::layout::Rect;
use crate::app; use crate::app::CursorDirection;
use std::{ use std::{cmp::min, time::Instant};
cmp::{max, min},
time::Instant,
};
/// Return a (hard)-width vector for column widths.
///
/// * `total_width` is the, well, total width available. **NOTE:** This function automatically
/// takes away 2 from the width as part of the left/right
/// bounds.
/// * `hard_widths` is inflexible column widths. Use a `None` to represent a soft width.
/// * `soft_widths_min` is the lower limit for a soft width. Use `None` if a hard width goes there.
/// * `soft_widths_max` is the upper limit for a soft width, in percentage of the total width. Use
/// `None` if a hard width goes there.
/// * `soft_widths_desired` is the desired soft width. Use `None` if a hard width goes there.
/// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if
/// false.
///
/// **NOTE:** This function ASSUMES THAT ALL PASSED SLICES ARE OF THE SAME SIZE.
///
/// **NOTE:** The returned vector may not be the same size as the slices, this is because including
/// 0-constraints breaks tui-rs.
pub fn get_column_widths(
total_width: u16, hard_widths: &[Option<u16>], soft_widths_min: &[Option<u16>],
soft_widths_max: &[Option<f64>], soft_widths_desired: &[Option<u16>], left_to_right: bool,
) -> Vec<u16> {
debug_assert!(
hard_widths.len() == soft_widths_min.len(),
"hard width length != soft width min length!"
);
debug_assert!(
soft_widths_min.len() == soft_widths_max.len(),
"soft width min length != soft width max length!"
);
debug_assert!(
soft_widths_max.len() == soft_widths_desired.len(),
"soft width max length != soft width desired length!"
);
if total_width > 2 {
let initial_width = total_width - 2;
let mut total_width_left = initial_width;
let mut column_widths: Vec<u16> = vec![0; hard_widths.len()];
let range: Vec<usize> = if left_to_right {
(0..hard_widths.len()).collect()
} else {
(0..hard_widths.len()).rev().collect()
};
for itx in &range {
if let Some(Some(hard_width)) = hard_widths.get(*itx) {
// Hard width...
let space_taken = min(*hard_width, total_width_left);
// TODO [COLUMN MOVEMENT]: Remove this
if *hard_width > space_taken {
break;
}
column_widths[*itx] = space_taken;
total_width_left -= space_taken;
total_width_left = total_width_left.saturating_sub(1);
} else if let (
Some(Some(soft_width_max)),
Some(Some(soft_width_min)),
Some(Some(soft_width_desired)),
) = (
soft_widths_max.get(*itx),
soft_widths_min.get(*itx),
soft_widths_desired.get(*itx),
) {
// Soft width...
let soft_limit = max(
if soft_width_max.is_sign_negative() {
*soft_width_desired
} else {
(*soft_width_max * initial_width as f64).ceil() as u16
},
*soft_width_min,
);
let space_taken = min(min(soft_limit, *soft_width_desired), total_width_left);
// TODO [COLUMN MOVEMENT]: Remove this
if *soft_width_min > space_taken {
break;
}
column_widths[*itx] = space_taken;
total_width_left -= space_taken;
total_width_left = total_width_left.saturating_sub(1);
}
}
while let Some(0) = column_widths.last() {
column_widths.pop();
}
if !column_widths.is_empty() {
// Redistribute remaining.
let amount_per_slot = total_width_left / column_widths.len() as u16;
total_width_left %= column_widths.len() as u16;
for (index, width) in column_widths.iter_mut().enumerate() {
if index < total_width_left.into() {
*width += amount_per_slot + 1;
} else {
*width += amount_per_slot;
}
}
}
column_widths
} else {
vec![]
}
}
pub fn get_search_start_position( pub fn get_search_start_position(
num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize, num_columns: usize, cursor_direction: &CursorDirection, cursor_bar: &mut usize,
current_cursor_position: usize, is_force_redraw: bool, current_cursor_position: usize, is_force_redraw: bool,
) -> usize { ) -> usize {
if is_force_redraw { if is_force_redraw {
@ -126,24 +12,24 @@ pub fn get_search_start_position(
} }
match cursor_direction { match cursor_direction {
app::CursorDirection::Right => { CursorDirection::Right => {
if current_cursor_position < *cursor_bar + num_columns { if current_cursor_position < *cursor_bar + num_columns {
// If, using previous_scrolled_position, we can see the element // If, using previous_scrolled_position, we can see the element
// (so within that and + num_rows) just reuse the current previously scrolled position // (so within that and + num_rows) just reuse the current previously scrolled position.
*cursor_bar *cursor_bar
} else if current_cursor_position >= num_columns { } else if current_cursor_position >= num_columns {
// Else if the current position past the last element visible in the list, omit // Else if the current position past the last element visible in the list, omit
// until we can see that element // until we can see that element.
*cursor_bar = current_cursor_position - num_columns; *cursor_bar = current_cursor_position - num_columns;
*cursor_bar *cursor_bar
} else { } else {
// Else, if it is not past the last element visible, do not omit anything // Else, if it is not past the last element visible, do not omit anything.
0 0
} }
} }
app::CursorDirection::Left => { CursorDirection::Left => {
if current_cursor_position <= *cursor_bar { if current_cursor_position <= *cursor_bar {
// If it's past the first element, then show from that element downwards // If it's past the first element, then show from that element downwards.
*cursor_bar = current_cursor_position; *cursor_bar = current_cursor_position;
} else if current_cursor_position >= *cursor_bar + num_columns { } else if current_cursor_position >= *cursor_bar + num_columns {
*cursor_bar = current_cursor_position - num_columns; *cursor_bar = current_cursor_position - num_columns;
@ -154,46 +40,9 @@ pub fn get_search_start_position(
} }
} }
pub fn get_start_position(
num_rows: usize, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut usize,
currently_selected_position: usize, is_force_redraw: bool,
) -> usize {
if is_force_redraw {
*scroll_position_bar = 0;
}
match scroll_direction {
app::ScrollDirection::Down => {
if currently_selected_position < *scroll_position_bar + num_rows {
// If, using previous_scrolled_position, we can see the element
// (so within that and + num_rows) just reuse the current previously scrolled position
*scroll_position_bar
} else if currently_selected_position >= num_rows {
// Else if the current position past the last element visible in the list, omit
// until we can see that element
*scroll_position_bar = currently_selected_position - num_rows;
*scroll_position_bar
} else {
// Else, if it is not past the last element visible, do not omit anything
0
}
}
app::ScrollDirection::Up => {
if currently_selected_position <= *scroll_position_bar {
// If it's past the first element, then show from that element downwards
*scroll_position_bar = currently_selected_position;
} else if currently_selected_position >= *scroll_position_bar + num_rows {
*scroll_position_bar = currently_selected_position - num_rows;
}
// Else, don't change what our start position is from whatever it is set to!
*scroll_position_bar
}
}
}
/// Calculate how many bars are to be drawn within basic mode's components. /// Calculate how many bars are to be drawn within basic mode's components.
pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize { pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
std::cmp::min( min(
(num_bars_available as f64 * use_percentage / 100.0).round() as usize, (num_bars_available as f64 * use_percentage / 100.0).round() as usize,
num_bars_available, num_bars_available,
) )
@ -224,62 +73,6 @@ mod test {
use super::*; use super::*;
#[test]
fn test_get_start_position() {
use crate::app::ScrollDirection::{self, Down, Up};
fn test(
bar: usize, num: usize, direction: ScrollDirection, selected: usize, force: bool,
expected_posn: usize, expected_bar: usize,
) {
let mut bar = bar;
assert_eq!(
get_start_position(num, &direction, &mut bar, selected, force),
expected_posn
);
assert_eq!(bar, expected_bar);
}
// Scrolling down from start
test(0, 10, Down, 0, false, 0, 0);
// Simple scrolling down
test(0, 10, Down, 1, false, 0, 0);
// Scrolling down from the middle high up
test(0, 10, Down, 5, false, 0, 0);
// Scrolling down into boundary
test(0, 10, Down, 11, false, 1, 1);
// Scrolling down from the with non-zero bar
test(5, 10, Down, 15, false, 5, 5);
// Force redraw scrolling down (e.g. resize)
test(5, 15, Down, 15, true, 0, 0);
// Test jumping down
test(1, 10, Down, 20, true, 10, 10);
// Scrolling up from bottom
test(10, 10, Up, 20, false, 10, 10);
// Simple scrolling up
test(10, 10, Up, 19, false, 10, 10);
// Scrolling up from the middle
test(10, 10, Up, 10, false, 10, 10);
// Scrolling up into boundary
test(10, 10, Up, 9, false, 9, 9);
// Force redraw scrolling up (e.g. resize)
test(5, 10, Up, 15, true, 5, 5);
// Test jumping up
test(10, 10, Up, 0, false, 0, 0);
}
#[test] #[test]
fn test_calculate_basic_use_bars() { fn test_calculate_basic_use_bars() {
// Testing various breakpoints and edge cases. // Testing various breakpoints and edge cases.
@ -327,49 +120,4 @@ mod test {
)); ));
assert!(over_timer.is_none()); assert!(over_timer.is_none());
} }
#[test]
fn test_zero_width() {
assert_eq!(
get_column_widths(
0,
&[Some(1), None, None],
&[None, Some(1), Some(2)],
&[None, Some(0.125), Some(0.5)],
&[None, Some(10), Some(10)],
true
),
vec![],
);
}
#[test]
fn test_two_width() {
assert_eq!(
get_column_widths(
2,
&[Some(1), None, None],
&[None, Some(1), Some(2)],
&[None, Some(0.125), Some(0.5)],
&[None, Some(10), Some(10)],
true
),
vec![],
);
}
#[test]
fn test_non_zero_width() {
assert_eq!(
get_column_widths(
16,
&[Some(1), None, None],
&[None, Some(1), Some(2)],
&[None, Some(0.125), Some(0.5)],
&[None, Some(10), Some(10)],
true
),
vec![2, 2, 7],
);
}
} }

View File

@ -69,7 +69,7 @@ impl Painter {
}; };
let battery_names = app_state let battery_names = app_state
.canvas_data .converted_data
.battery_data .battery_data
.iter() .iter()
.map(|battery| &battery.battery_name) .map(|battery| &battery.battery_name)
@ -106,7 +106,7 @@ impl Painter {
.split(draw_loc)[0]; .split(draw_loc)[0];
if let Some(battery_details) = app_state if let Some(battery_details) = app_state
.canvas_data .converted_data
.battery_data .battery_data
.get(battery_widget_state.currently_selected_battery_index) .get(battery_widget_state.currently_selected_battery_index)
{ {

View File

@ -20,8 +20,8 @@ impl Painter {
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) { ) {
// Skip the first element, it's the "all" element // Skip the first element, it's the "all" element
if app_state.canvas_data.cpu_data.len() > 1 { if app_state.converted_data.cpu_data.len() > 1 {
let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data[1..]; let cpu_data: &[ConvertedCpuData] = &app_state.converted_data.cpu_data[1..];
// This is a bit complicated, but basically, we want to draw SOME number // This is a bit complicated, but basically, we want to draw SOME number
// of columns to draw all CPUs. Ideally, as well, we want to not have // of columns to draw all CPUs. Ideally, as well, we want to not have

View File

@ -1,38 +1,34 @@
use std::borrow::Cow; use std::{borrow::Cow, iter};
use crate::{ use crate::{
app::{layout_manager::WidgetDirection, App}, app::{layout_manager::WidgetDirection, App, CellContent, CpuWidgetState},
canvas::{ canvas::{
components::{GraphData, TimeGraph}, components::{GraphData, TextTable, TimeGraph},
drawing_utils::{get_column_widths, get_start_position, should_hide_x_label}, drawing_utils::should_hide_x_label,
Painter, Painter,
}, },
constants::*, data_conversion::{ConvertedCpuData, TableData, TableRow},
data_conversion::ConvertedCpuData,
}; };
use concat_string::concat_string; use concat_string::concat_string;
use itertools::Either;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
terminal::Frame, terminal::Frame,
text::Text,
widgets::{Block, Borders, Row, Table},
}; };
const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"];
const AVG_POSITION: usize = 1; const AVG_POSITION: usize = 1;
const ALL_POSITION: usize = 0; const ALL_POSITION: usize = 0;
static CPU_LEGEND_HEADER_LENS: [usize; 2] =
[CPU_LEGEND_HEADER[0].len(), CPU_LEGEND_HEADER[1].len()];
impl Painter { impl Painter {
pub fn draw_cpu<B: Backend>( pub fn draw_cpu<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) { ) {
if draw_loc.width as f64 * 0.15 <= 6.0 { let legend_width = (draw_loc.width as f64 * 0.15) as u16;
if legend_width < 6 {
// Skip drawing legend // Skip drawing legend
if app_state.current_widget.widget_id == (widget_id + 1) { if app_state.current_widget.widget_id == (widget_id + 1) {
if app_state.app_config_fields.left_legend { if app_state.app_config_fields.left_legend {
@ -55,18 +51,25 @@ impl Painter {
} }
} }
} else { } else {
let graph_width = draw_loc.width - legend_width;
let (graph_index, legend_index, constraints) = let (graph_index, legend_index, constraints) =
if app_state.app_config_fields.left_legend { if app_state.app_config_fields.left_legend {
( (
1, 1,
0, 0,
[Constraint::Percentage(15), Constraint::Percentage(85)], [
Constraint::Length(legend_width),
Constraint::Length(graph_width),
],
) )
} else { } else {
( (
0, 0,
1, 1,
[Constraint::Percentage(85), Constraint::Percentage(15)], [
Constraint::Length(graph_width),
Constraint::Length(legend_width),
],
) )
}; };
@ -115,26 +118,13 @@ impl Painter {
} }
} }
fn draw_cpu_graph<B: Backend>( fn generate_points<'a>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, &self, cpu_widget_state: &CpuWidgetState, cpu_data: &'a [ConvertedCpuData],
) { show_avg_cpu: bool,
const Y_BOUNDS: [f64; 2] = [0.0, 100.5]; ) -> Vec<GraphData<'a>> {
const Y_LABELS: [Cow<'static, str>; 2] = [Cow::Borrowed(" 0%"), Cow::Borrowed("100%")];
if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&widget_id) {
let cpu_data = &app_state.canvas_data.cpu_data;
let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
let x_bounds = [0, cpu_widget_state.current_display_time];
let hide_x_labels = should_hide_x_label(
app_state.app_config_fields.hide_time,
app_state.app_config_fields.autohide_time,
&mut cpu_widget_state.autohide_timer,
draw_loc,
);
let show_avg_cpu = app_state.app_config_fields.show_average_cpu;
let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 }; let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 };
let points = {
let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position; let current_scroll_position = cpu_widget_state.table_state.current_scroll_position;
if current_scroll_position == ALL_POSITION { if current_scroll_position == ALL_POSITION {
// This case ensures the other cases cannot have the position be equal to 0. // This case ensures the other cases cannot have the position be equal to 0.
cpu_data cpu_data
@ -164,8 +154,8 @@ impl Painter {
self.colours.avg_colour_style self.colours.avg_colour_style
} else { } else {
let offset_position = current_scroll_position - 1; // Because of the all position let offset_position = current_scroll_position - 1; // Because of the all position
self.colours.cpu_colour_styles[(offset_position - show_avg_offset) self.colours.cpu_colour_styles
% self.colours.cpu_colour_styles.len()] [(offset_position - show_avg_offset) % self.colours.cpu_colour_styles.len()]
}; };
vec![GraphData { vec![GraphData {
@ -176,11 +166,34 @@ impl Painter {
} else { } else {
vec![] vec![]
} }
}; }
fn draw_cpu_graph<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) {
const Y_BOUNDS: [f64; 2] = [0.0, 100.5];
const Y_LABELS: [Cow<'static, str>; 2] = [Cow::Borrowed(" 0%"), Cow::Borrowed("100%")];
if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&widget_id) {
let cpu_data = &app_state.converted_data.cpu_data;
let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
let x_bounds = [0, cpu_widget_state.current_display_time];
let hide_x_labels = should_hide_x_label(
app_state.app_config_fields.hide_time,
app_state.app_config_fields.autohide_time,
&mut cpu_widget_state.autohide_timer,
draw_loc,
);
let points = self.generate_points(
cpu_widget_state,
cpu_data,
app_state.app_config_fields.show_average_cpu,
);
// TODO: Maybe hide load avg if too long? Or maybe the CPU part. // TODO: Maybe hide load avg if too long? Or maybe the CPU part.
let title = if cfg!(target_family = "unix") { let title = if cfg!(target_family = "unix") {
let load_avg = app_state.canvas_data.load_avg_data; let load_avg = app_state.converted_data.load_avg_data;
let load_avg_str = format!( let load_avg_str = format!(
"─ {:.2} {:.2} {:.2} ", "─ {:.2} {:.2} {:.2} ",
load_avg[0], load_avg[1], load_avg[2] load_avg[0], load_avg[1], load_avg[2]
@ -214,148 +227,86 @@ impl Painter {
let recalculate_column_widths = app_state.should_get_widget_bounds(); let recalculate_column_widths = app_state.should_get_widget_bounds();
if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&(widget_id - 1)) if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&(widget_id - 1))
{ {
// TODO: This line (and the one above, see caller) is pretty dumb but I guess needed.
cpu_widget_state.is_legend_hidden = false; cpu_widget_state.is_legend_hidden = false;
let cpu_data: &mut [ConvertedCpuData] = &mut app_state.canvas_data.cpu_data;
let cpu_table_state = &mut cpu_widget_state.scroll_state.table_state;
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let start_position = get_start_position(
usize::from(
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
),
&cpu_widget_state.scroll_state.scroll_direction,
&mut cpu_widget_state.scroll_state.scroll_bar,
cpu_widget_state.scroll_state.current_scroll_position,
app_state.is_force_redraw,
);
cpu_table_state.select(Some(
cpu_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position),
));
let sliced_cpu_data = &cpu_data[start_position..];
let offset_scroll_index = cpu_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position);
let show_avg_cpu = app_state.app_config_fields.show_average_cpu; let show_avg_cpu = app_state.app_config_fields.show_average_cpu;
let cpu_data = {
// Calculate widths let col_widths = vec![1, 3]; // TODO: Should change this to take const generics (usize) and an array.
if recalculate_column_widths { let colour_iter = if show_avg_cpu {
cpu_widget_state.table_width_state.desired_column_widths = vec![6, 4]; Either::Left(
cpu_widget_state.table_width_state.calculated_column_widths = get_column_widths( iter::once(&self.colours.all_colour_style)
draw_loc.width, .chain(iter::once(&self.colours.avg_colour_style))
&[None, None], .chain(self.colours.cpu_colour_styles.iter().cycle()),
&(CPU_LEGEND_HEADER_LENS )
.iter()
.map(|width| Some(*width as u16))
.collect::<Vec<_>>()),
&[Some(0.5), Some(0.5)],
&(cpu_widget_state
.table_width_state
.desired_column_widths
.iter()
.map(|width| Some(*width))
.collect::<Vec<_>>()),
false,
);
}
let dcw = &cpu_widget_state.table_width_state.desired_column_widths;
let ccw = &cpu_widget_state.table_width_state.calculated_column_widths;
let cpu_rows = sliced_cpu_data.iter().enumerate().map(|(itx, cpu)| {
let mut truncated_name =
if let (Some(desired_column_width), Some(calculated_column_width)) =
(dcw.get(0), ccw.get(0))
{
if *desired_column_width > *calculated_column_width {
Text::raw(&cpu.short_cpu_name)
} else { } else {
Text::raw(&cpu.cpu_name) Either::Right(
} iter::once(&self.colours.all_colour_style)
} else { .chain(self.colours.cpu_colour_styles.iter().cycle()),
Text::raw(&cpu.cpu_name) )
}; };
let is_first_column_hidden = if let Some(calculated_column_width) = ccw.get(0) { let data = {
*calculated_column_width == 0 let iter = app_state.converted_data.cpu_data.iter().zip(colour_iter);
const CPU_WIDTH_CHECK: u16 = 10; // This is hard-coded, it's terrible.
if draw_loc.width < CPU_WIDTH_CHECK {
Either::Left(iter.map(|(cpu, style)| {
let row = vec![
CellContent::Simple("".into()),
CellContent::Simple(if cpu.legend_value.is_empty() {
cpu.cpu_name.clone().into()
} else { } else {
false cpu.legend_value.clone().into()
}),
];
TableRow::Styled(row, *style)
}))
} else {
Either::Right(iter.map(|(cpu, style)| {
let row = vec![
CellContent::HasAlt {
alt: cpu.short_cpu_name.clone().into(),
main: cpu.cpu_name.clone().into(),
},
CellContent::Simple(cpu.legend_value.clone().into()),
];
TableRow::Styled(row, *style)
}))
}
}
.collect();
TableData { data, col_widths }
}; };
let truncated_legend = if is_first_column_hidden && cpu.legend_value.is_empty() { let is_on_widget = widget_id == app_state.current_widget.widget_id;
// For the case where we only have room for one column, display "All" in the normally blank area. let border_style = if is_on_widget {
Text::raw("All")
} else {
Text::raw(&cpu.legend_value)
};
if !is_first_column_hidden
&& itx == offset_scroll_index
&& itx + start_position == ALL_POSITION
{
truncated_name.patch_style(self.colours.currently_selected_text_style);
Row::new(vec![truncated_name, truncated_legend])
} else {
let cpu_string_row = vec![truncated_name, truncated_legend];
Row::new(cpu_string_row).style(if itx == offset_scroll_index {
self.colours.currently_selected_text_style
} else if itx + start_position == ALL_POSITION {
self.colours.all_colour_style
} else if show_avg_cpu {
if itx + start_position == AVG_POSITION {
self.colours.avg_colour_style
} else {
self.colours.cpu_colour_styles[(itx + start_position
- AVG_POSITION
- 1)
% self.colours.cpu_colour_styles.len()]
}
} else {
self.colours.cpu_colour_styles[(itx + start_position - ALL_POSITION - 1)
% self.colours.cpu_colour_styles.len()]
})
}
});
// Note we don't set highlight_style, as it should always be shown for this widget.
let border_and_title_style = if is_on_widget {
self.colours.highlighted_border_style self.colours.highlighted_border_style
} else { } else {
self.colours.border_style self.colours.border_style
}; };
// Draw TextTable {
f.render_stateful_widget( table_gap: app_state.app_config_fields.table_gap,
Table::new(cpu_rows) is_force_redraw: app_state.is_force_redraw,
.block( recalculate_column_widths,
Block::default() header_style: self.colours.table_header_style,
.borders(Borders::ALL) border_style,
.border_style(border_and_title_style), highlighted_text_style: self.colours.currently_selected_text_style, // We always highlight the selected CPU entry... not sure if I like this though.
) title: None,
.header( is_on_widget,
Row::new(CPU_LEGEND_HEADER.to_vec()) draw_border: true,
.style(self.colours.table_header_style) show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
.bottom_margin(table_gap), title_style: self.colours.widget_title_style,
) text_style: self.colours.text_style,
.widths( left_to_right: false,
&(cpu_widget_state }
.table_width_state .draw_text_table(
.calculated_column_widths f,
.iter()
.map(|calculated_width| Constraint::Length(*calculated_width as u16))
.collect::<Vec<_>>()),
),
draw_loc, draw_loc,
cpu_table_state, &mut cpu_widget_state.table_state,
&cpu_data,
None,
); );
} }
} }

View File

@ -1,31 +1,12 @@
use once_cell::sync::Lazy; use tui::{backend::Backend, layout::Rect, terminal::Frame};
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
terminal::Frame,
text::Span,
text::{Spans, Text},
widgets::{Block, Borders, Row, Table},
};
use crate::{ use crate::{
app, app,
canvas::{ canvas::{
drawing_utils::{get_column_widths, get_start_position}, components::{TextTable, TextTableTitle},
Painter, Painter,
}, },
constants::*,
}; };
use unicode_segmentation::UnicodeSegmentation;
const DISK_HEADERS: [&str; 7] = ["Disk", "Mount", "Used", "Free", "Total", "R/s", "W/s"];
static DISK_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
DISK_HEADERS
.iter()
.map(|entry| entry.len() as u16)
.collect::<Vec<_>>()
});
impl Painter { impl Painter {
pub fn draw_disk_table<B: Backend>( pub fn draw_disk_table<B: Backend>(
@ -34,120 +15,8 @@ impl Painter {
) { ) {
let recalculate_column_widths = app_state.should_get_widget_bounds(); let recalculate_column_widths = app_state.should_get_widget_bounds();
if let Some(disk_widget_state) = app_state.disk_state.widget_states.get_mut(&widget_id) { if let Some(disk_widget_state) = app_state.disk_state.widget_states.get_mut(&widget_id) {
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let start_position = get_start_position(
usize::from(
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
),
&disk_widget_state.scroll_state.scroll_direction,
&mut disk_widget_state.scroll_state.scroll_bar,
disk_widget_state.scroll_state.current_scroll_position,
app_state.is_force_redraw,
);
let is_on_widget = app_state.current_widget.widget_id == widget_id; let is_on_widget = app_state.current_widget.widget_id == widget_id;
let disk_table_state = &mut disk_widget_state.scroll_state.table_state; let (border_style, highlighted_text_style) = if is_on_widget {
disk_table_state.select(Some(
disk_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position),
));
let sliced_vec = &app_state.canvas_data.disk_data[start_position..];
// Calculate widths
let hard_widths = [None, None, Some(4), Some(6), Some(6), Some(7), Some(7)];
if recalculate_column_widths {
disk_widget_state.table_width_state.desired_column_widths = {
let mut column_widths = DISK_HEADERS_LENS.clone();
for row in sliced_vec {
for (col, entry) in row.iter().enumerate() {
if entry.len() as u16 > column_widths[col] {
column_widths[col] = entry.len() as u16;
}
}
}
column_widths
};
disk_widget_state.table_width_state.desired_column_widths = disk_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<_>>();
disk_widget_state.table_width_state.calculated_column_widths = get_column_widths(
draw_loc.width,
&hard_widths,
&(DISK_HEADERS_LENS
.iter()
.map(|w| Some(*w))
.collect::<Vec<_>>()),
&[Some(0.2), Some(0.2), None, None, None, None, None],
&(disk_widget_state
.table_width_state
.desired_column_widths
.iter()
.map(|w| Some(*w))
.collect::<Vec<_>>()),
true,
);
}
let dcw = &disk_widget_state.table_width_state.desired_column_widths;
let ccw = &disk_widget_state.table_width_state.calculated_column_widths;
let disk_rows =
sliced_vec.iter().map(|disk_row| {
let truncated_data = disk_row.iter().zip(&hard_widths).enumerate().map(
|(itx, (entry, width))| {
if width.is_none() {
if let (Some(desired_col_width), Some(calculated_col_width)) =
(dcw.get(itx), ccw.get(itx))
{
if *desired_col_width > *calculated_col_width
&& *calculated_col_width > 0
{
let calculated_col_width: usize =
(*calculated_col_width).into();
let graphemes =
UnicodeSegmentation::graphemes(entry.as_str(), true)
.collect::<Vec<&str>>();
if graphemes.len() > calculated_col_width
&& calculated_col_width > 1
{
// Truncate with ellipsis
let first_n =
graphemes[..(calculated_col_width - 1)].concat();
return Text::raw(format!("{}", first_n));
}
}
}
}
Text::raw(entry)
},
);
Row::new(truncated_data)
});
let (border_style, highlight_style) = if is_on_widget {
( (
self.colours.highlighted_border_style, self.colours.highlighted_border_style,
self.colours.currently_selected_text_style, self.colours.currently_selected_text_style,
@ -155,117 +24,31 @@ impl Painter {
} else { } else {
(self.colours.border_style, self.colours.text_style) (self.colours.border_style, self.colours.text_style)
}; };
TextTable {
let title_base = if app_state.app_config_fields.show_table_scroll_position { table_gap: app_state.app_config_fields.table_gap,
let title_string = format!( is_force_redraw: app_state.is_force_redraw,
" Disk ({} of {}) ", recalculate_column_widths,
disk_widget_state header_style: self.colours.table_header_style,
.scroll_state
.current_scroll_position
.saturating_add(1),
app_state.canvas_data.disk_data.len()
);
if title_string.len() <= draw_loc.width.into() {
title_string
} else {
" Disk ".to_string()
}
} else {
" Disk ".to_string()
};
let title = if app_state.is_expanded {
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.into() {
(
" Disk ".to_string(),
format!("{}{}", " Disk ", 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, border_style,
), highlighted_text_style,
]) title: Some(TextTableTitle {
} else { title: " Disks ".into(),
Spans::from(Span::styled(title_base, self.colours.widget_title_style)) is_expanded: app_state.is_expanded,
}; }),
is_on_widget,
let disk_block = if draw_border { draw_border,
Block::default() show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
.title(title) title_style: self.colours.widget_title_style,
.borders(Borders::ALL) text_style: self.colours.text_style,
.border_style(border_style) left_to_right: true,
} else if is_on_widget { }
Block::default() .draw_text_table(
.borders(SIDE_BORDERS) f,
.border_style(self.colours.highlighted_border_style) draw_loc,
} else { &mut disk_widget_state.table_state,
Block::default().borders(Borders::NONE) &app_state.converted_data.disk_data,
}; app_state.widget_map.get_mut(&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];
// Draw!
f.render_stateful_widget(
Table::new(disk_rows)
.block(disk_block)
.header(
Row::new(DISK_HEADERS.to_vec())
.style(self.colours.table_header_style)
.bottom_margin(table_gap),
)
.highlight_style(highlight_style)
.style(self.colours.text_style)
.widths(
&(disk_widget_state
.table_width_state
.calculated_column_widths
.iter()
.map(|calculated_width| Constraint::Length(*calculated_width))
.collect::<Vec<_>>()),
),
margined_draw_loc,
disk_table_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

@ -17,8 +17,8 @@ impl Painter {
pub fn draw_basic_memory<B: Backend>( pub fn draw_basic_memory<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) { ) {
let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data; let mem_data: &[(f64, f64)] = &app_state.converted_data.mem_data;
let swap_data: &[(f64, f64)] = &app_state.canvas_data.swap_data; let swap_data: &[(f64, f64)] = &app_state.converted_data.swap_data;
let margined_loc = Layout::default() let margined_loc = Layout::default()
.constraints([Constraint::Percentage(100)]) .constraints([Constraint::Percentage(100)])
@ -48,14 +48,14 @@ impl Painter {
const EMPTY_MEMORY_FRAC_STRING: &str = "0.0B/0.0B"; const EMPTY_MEMORY_FRAC_STRING: &str = "0.0B/0.0B";
let trimmed_memory_frac = let trimmed_memory_frac =
if let Some((_label_percent, label_frac)) = &app_state.canvas_data.mem_labels { if let Some((_label_percent, label_frac)) = &app_state.converted_data.mem_labels {
label_frac.trim() label_frac.trim()
} else { } else {
EMPTY_MEMORY_FRAC_STRING EMPTY_MEMORY_FRAC_STRING
}; };
let trimmed_swap_frac = let trimmed_swap_frac =
if let Some((_label_percent, label_frac)) = &app_state.canvas_data.swap_labels { if let Some((_label_percent, label_frac)) = &app_state.converted_data.swap_labels {
label_frac.trim() label_frac.trim()
} else { } else {
EMPTY_MEMORY_FRAC_STRING EMPTY_MEMORY_FRAC_STRING

View File

@ -33,18 +33,18 @@ impl Painter {
); );
let points = { let points = {
let mut points = Vec::with_capacity(2); let mut points = Vec::with_capacity(2);
if let Some((label_percent, label_frac)) = &app_state.canvas_data.mem_labels { if let Some((label_percent, label_frac)) = &app_state.converted_data.mem_labels {
let mem_label = format!("RAM:{}{}", label_percent, label_frac); let mem_label = format!("RAM:{}{}", label_percent, label_frac);
points.push(GraphData { points.push(GraphData {
points: &app_state.canvas_data.mem_data, points: &app_state.converted_data.mem_data,
style: self.colours.ram_style, style: self.colours.ram_style,
name: Some(mem_label.into()), name: Some(mem_label.into()),
}); });
} }
if let Some((label_percent, label_frac)) = &app_state.canvas_data.swap_labels { if let Some((label_percent, label_frac)) = &app_state.converted_data.swap_labels {
let swap_label = format!("SWP:{}{}", label_percent, label_frac); let swap_label = format!("SWP:{}{}", label_percent, label_frac);
points.push(GraphData { points.push(GraphData {
points: &app_state.canvas_data.swap_data, points: &app_state.converted_data.swap_data,
style: self.colours.swap_style, style: self.colours.swap_style,
name: Some(swap_label.into()), name: Some(swap_label.into()),
}); });

View File

@ -38,10 +38,10 @@ impl Painter {
); );
} }
let rx_label = format!("RX: {}", &app_state.canvas_data.rx_display); let rx_label = format!("RX: {}", &app_state.converted_data.rx_display);
let tx_label = format!("TX: {}", &app_state.canvas_data.tx_display); let tx_label = format!("TX: {}", &app_state.converted_data.tx_display);
let total_rx_label = format!("Total RX: {}", &app_state.canvas_data.total_rx_display); let total_rx_label = format!("Total RX: {}", &app_state.converted_data.total_rx_display);
let total_tx_label = format!("Total TX: {}", &app_state.canvas_data.total_tx_display); let total_tx_label = format!("Total TX: {}", &app_state.converted_data.total_tx_display);
let net_text = vec![ let net_text = vec![
Spans::from(Span::styled(rx_label, self.colours.rx_style)), Spans::from(Span::styled(rx_label, self.colours.rx_style)),

View File

@ -1,14 +1,10 @@
use once_cell::sync::Lazy;
use std::cmp::max;
use crate::{ use crate::{
app::{App, AxisScaling}, app::{App, AxisScaling},
canvas::{ canvas::{
components::{GraphData, TimeGraph}, components::{GraphData, TimeGraph},
drawing_utils::{get_column_widths, should_hide_x_label}, drawing_utils::should_hide_x_label,
Painter, Point, Painter, Point,
}, },
constants::*,
units::data_units::DataUnit, units::data_units::DataUnit,
utils::gen_util::*, utils::gen_util::*,
}; };
@ -21,26 +17,18 @@ use tui::{
widgets::{Block, Borders, Row, Table}, widgets::{Block, Borders, 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<_>>()
});
impl Painter { impl Painter {
pub fn draw_network<B: Backend>( pub fn draw_network<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, &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 { if app_state.app_config_fields.use_old_network_legend {
const LEGEND_HEIGHT: u16 = 4;
let network_chunk = Layout::default() let network_chunk = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(0) .margin(0)
.constraints([ .constraints([
Constraint::Length(max(draw_loc.height as i64 - 5, 0) as u16), Constraint::Length(draw_loc.height.saturating_sub(LEGEND_HEIGHT)),
Constraint::Length(5), Constraint::Length(LEGEND_HEIGHT),
]) ])
.split(draw_loc); .split(draw_loc);
@ -67,8 +55,8 @@ impl Painter {
hide_legend: bool, hide_legend: bool,
) { ) {
if let Some(network_widget_state) = app_state.net_state.widget_states.get_mut(&widget_id) { if let Some(network_widget_state) = app_state.net_state.widget_states.get_mut(&widget_id) {
let network_data_rx: &[(f64, f64)] = &app_state.canvas_data.network_data_rx; let network_data_rx: &[(f64, f64)] = &app_state.converted_data.network_data_rx;
let network_data_tx: &[(f64, f64)] = &app_state.canvas_data.network_data_tx; let network_data_tx: &[(f64, f64)] = &app_state.converted_data.network_data_tx;
let time_start = -(network_widget_state.current_display_time as f64); let time_start = -(network_widget_state.current_display_time as f64);
let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id); let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
let x_bounds = [0, network_widget_state.current_display_time]; let x_bounds = [0, network_widget_state.current_display_time];
@ -115,18 +103,18 @@ impl Painter {
GraphData { GraphData {
points: network_data_rx, points: network_data_rx,
style: self.colours.rx_style, style: self.colours.rx_style,
name: Some(format!("RX: {:7}", app_state.canvas_data.rx_display).into()), name: Some(format!("RX: {:7}", app_state.converted_data.rx_display).into()),
}, },
GraphData { GraphData {
points: network_data_tx, points: network_data_tx,
style: self.colours.tx_style, style: self.colours.tx_style,
name: Some(format!("TX: {:7}", app_state.canvas_data.tx_display).into()), name: Some(format!("TX: {:7}", app_state.converted_data.tx_display).into()),
}, },
GraphData { GraphData {
points: &[], points: &[],
style: self.colours.total_rx_style, style: self.colours.total_rx_style,
name: Some( name: Some(
format!("Total RX: {:7}", app_state.canvas_data.total_rx_display) format!("Total RX: {:7}", app_state.converted_data.total_rx_display)
.into(), .into(),
), ),
}, },
@ -134,7 +122,7 @@ impl Painter {
points: &[], points: &[],
style: self.colours.total_tx_style, style: self.colours.total_tx_style,
name: Some( name: Some(
format!("Total TX: {:7}", app_state.canvas_data.total_tx_display) format!("Total TX: {:7}", app_state.converted_data.total_tx_display)
.into(), .into(),
), ),
}, },
@ -144,12 +132,12 @@ impl Painter {
GraphData { GraphData {
points: network_data_rx, points: network_data_rx,
style: self.colours.rx_style, style: self.colours.rx_style,
name: Some((&app_state.canvas_data.rx_display).into()), name: Some((&app_state.converted_data.rx_display).into()),
}, },
GraphData { GraphData {
points: network_data_tx, points: network_data_tx,
style: self.colours.tx_style, style: self.colours.tx_style,
name: Some((&app_state.canvas_data.tx_display).into()), name: Some((&app_state.converted_data.tx_display).into()),
}, },
] ]
}; };
@ -174,52 +162,25 @@ impl Painter {
fn draw_network_labels<B: Backend>( fn draw_network_labels<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, &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 { const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"];
0
} else {
app_state.app_config_fields.table_gap
};
let rx_display = &app_state.canvas_data.rx_display; let rx_display = &app_state.converted_data.rx_display;
let tx_display = &app_state.canvas_data.tx_display; let tx_display = &app_state.converted_data.tx_display;
let total_rx_display = &app_state.canvas_data.total_rx_display; let total_rx_display = &app_state.converted_data.total_rx_display;
let total_tx_display = &app_state.canvas_data.total_tx_display; let total_tx_display = &app_state.converted_data.total_tx_display;
// Gross but I need it to work... // Gross but I need it to work...
let total_network = vec![vec![ let total_network = vec![Row::new(vec![
Text::raw(rx_display), Text::styled(rx_display, self.colours.rx_style),
Text::raw(tx_display), Text::styled(tx_display, self.colours.tx_style),
Text::raw(total_rx_display), Text::styled(total_rx_display, self.colours.total_rx_style),
Text::raw(total_tx_display), Text::styled(total_tx_display, self.colours.total_tx_style),
]]; ])];
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 // Draw
f.render_widget( f.render_widget(
Table::new(mapped_network) Table::new(total_network)
.header( .header(Row::new(NETWORK_HEADERS.to_vec()).style(self.colours.table_header_style))
Row::new(NETWORK_HEADERS.to_vec())
.style(self.colours.table_header_style)
.bottom_margin(table_gap),
)
.block(Block::default().borders(Borders::ALL).border_style( .block(Block::default().borders(Borders::ALL).border_style(
if app_state.current_widget.widget_id == widget_id { if app_state.current_widget.widget_id == widget_id {
self.colours.highlighted_border_style self.colours.highlighted_border_style
@ -229,9 +190,9 @@ impl Painter {
)) ))
.style(self.colours.text_style) .style(self.colours.text_style)
.widths( .widths(
&(intrinsic_widths &((std::iter::repeat(draw_loc.width.saturating_sub(2) / 4))
.iter() .take(4)
.map(|calculated_width| Constraint::Length(*calculated_width)) .map(Constraint::Length)
.collect::<Vec<_>>()), .collect::<Vec<_>>()),
), ),
draw_loc, draw_loc,
@ -295,7 +256,7 @@ fn get_max_entry(
(None, Some(filtered_tx)) => { (None, Some(filtered_tx)) => {
match filtered_tx match filtered_tx
.iter() .iter()
.max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) .max_by(|(_, data_a), (_, data_b)| partial_ordering(data_a, data_b))
{ {
Some((best_time, max_val)) => { Some((best_time, max_val)) => {
if *max_val == 0.0 { if *max_val == 0.0 {
@ -316,7 +277,7 @@ fn get_max_entry(
(Some(filtered_rx), None) => { (Some(filtered_rx), None) => {
match filtered_rx match filtered_rx
.iter() .iter()
.max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) .max_by(|(_, data_a), (_, data_b)| partial_ordering(data_a, data_b))
{ {
Some((best_time, max_val)) => { Some((best_time, max_val)) => {
if *max_val == 0.0 { if *max_val == 0.0 {
@ -338,7 +299,7 @@ fn get_max_entry(
match filtered_rx match filtered_rx
.iter() .iter()
.chain(filtered_tx) .chain(filtered_tx)
.max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) .max_by(|(_, data_a), (_, data_b)| partial_ordering(data_a, data_b))
{ {
Some((best_time, max_val)) => { Some((best_time, max_val)) => {
if *max_val == 0.0 { if *max_val == 0.0 {

View File

@ -1,106 +1,40 @@
use crate::{ use crate::{
app::App, app::App,
canvas::{ canvas::{
drawing_utils::{get_column_widths, get_search_start_position, get_start_position}, components::{TextTable, TextTableTitle},
drawing_utils::get_search_start_position,
Painter, Painter,
}, },
constants::*, constants::*,
data_conversion::{TableData, TableRow},
}; };
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
terminal::Frame, terminal::Frame,
text::{Span, Spans, Text}, text::{Span, Spans},
widgets::{Block, Borders, Paragraph, Row, Table}, widgets::{Block, Borders, Paragraph},
}; };
use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation}; use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
const PROCESS_HEADERS_HARD_WIDTH_NO_GROUP: &[Option<u16>] = &[ const SORT_MENU_WIDTH: u16 = 7;
Some(7),
None,
Some(8),
Some(8),
Some(8),
Some(8),
Some(7),
Some(8),
#[cfg(target_family = "unix")]
None,
None,
];
const PROCESS_HEADERS_HARD_WIDTH_GROUPED: &[Option<u16>] = &[
Some(7),
None,
Some(8),
Some(8),
Some(8),
Some(8),
Some(7),
Some(8),
];
const PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND: &[Option<f64>] =
&[None, Some(0.7), None, None, None, None, None, None];
const PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE: &[Option<f64>] =
&[None, Some(0.3), None, None, None, None, None, None];
const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND: &[Option<f64>] = &[
None,
Some(0.7),
None,
None,
None,
None,
None,
None,
#[cfg(target_family = "unix")]
Some(0.05),
Some(0.2),
];
const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE: &[Option<f64>] = &[
None,
Some(0.5),
None,
None,
None,
None,
None,
None,
#[cfg(target_family = "unix")]
Some(0.05),
Some(0.2),
];
const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: &[Option<f64>] = &[
None,
Some(0.3),
None,
None,
None,
None,
None,
None,
#[cfg(target_family = "unix")]
Some(0.05),
Some(0.2),
];
impl Painter { impl Painter {
/// Draws and handles all process-related drawing. Use this. /// Draws and handles all process-related drawing. Use this.
/// - `widget_id` here represents the widget ID of the process widget itself! /// - `widget_id` here represents the widget ID of the process widget itself!
pub fn draw_process_features<B: Backend>( pub fn draw_process_widget<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool, &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64, widget_id: u64,
) { ) {
if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) { if let Some(proc_widget_state) = app_state.proc_state.widget_states.get(&widget_id) {
let search_height = if draw_border { 5 } else { 3 }; let search_height = if draw_border { 5 } else { 3 };
let is_sort_open = process_widget_state.is_sort_open; let is_sort_open = proc_widget_state.is_sort_open;
let header_len = process_widget_state.columns.longest_header_len;
let mut proc_draw_loc = draw_loc; let mut proc_draw_loc = draw_loc;
if process_widget_state.is_search_enabled() { if proc_widget_state.is_search_enabled() {
let processes_chunk = Layout::default() let processes_chunk = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(search_height)]) .constraints([Constraint::Min(0), Constraint::Length(search_height)])
@ -119,25 +53,26 @@ impl Painter {
if is_sort_open { if is_sort_open {
let processes_chunk = Layout::default() let processes_chunk = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Length(header_len + 4), Constraint::Min(0)]) .constraints([Constraint::Length(SORT_MENU_WIDTH + 4), Constraint::Min(0)])
.split(proc_draw_loc); .split(proc_draw_loc);
proc_draw_loc = processes_chunk[1]; proc_draw_loc = processes_chunk[1];
self.draw_process_sort( self.draw_sort_table(f, app_state, processes_chunk[0], draw_border, widget_id + 2);
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); self.draw_processes_table(f, app_state, proc_draw_loc, draw_border, widget_id);
} }
if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) {
// Reset redraw marker.
if proc_widget_state.force_rerender {
proc_widget_state.force_rerender = false;
}
}
} }
/// Draws the process sort box. /// Draws the process sort box.
/// - `widget_id` represents the widget ID of the process widget itself. /// - `widget_id` represents the widget ID of the process widget itself.an
/// ///
/// This should not be directly called. /// This should not be directly called.
fn draw_processes_table<B: Backend>( fn draw_processes_table<B: Backend>(
@ -147,19 +82,10 @@ impl Painter {
let should_get_widget_bounds = app_state.should_get_widget_bounds(); let should_get_widget_bounds = app_state.should_get_widget_bounds();
if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) { if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) {
let recalculate_column_widths = let recalculate_column_widths =
should_get_widget_bounds || proc_widget_state.requires_redraw; should_get_widget_bounds || proc_widget_state.force_rerender;
if proc_widget_state.requires_redraw {
proc_widget_state.requires_redraw = false;
}
let is_on_widget = widget_id == app_state.current_widget.widget_id; let is_on_widget = widget_id == app_state.current_widget.widget_id;
let margined_draw_loc = Layout::default() let (border_style, highlighted_text_style) = if is_on_widget {
.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.highlighted_border_style,
self.colours.currently_selected_text_style, self.colours.currently_selected_text_style,
@ -168,357 +94,39 @@ impl Painter {
(self.colours.border_style, self.colours.text_style) (self.colours.border_style, self.colours.text_style)
}; };
let title_base = if app_state.app_config_fields.show_table_scroll_position { // TODO: [Refactor] This is an ugly hack to add the disabled style...
if let Some(finalized_process_data) = app_state // this could be solved by storing style locally to the widget.
.canvas_data for row in &mut proc_widget_state.table_data.data {
.finalized_process_data_map if let TableRow::Styled(_, style) = row {
.get(&widget_id) *style = style.patch(self.colours.disabled_text_style);
{
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.into() {
title
} else {
" Processes ".to_string()
} }
} else {
" Processes ".to_string()
} }
} else {
" Processes ".to_string()
};
let title = if app_state.is_expanded TextTable {
&& !proc_widget_state table_gap: app_state.app_config_fields.table_gap,
.process_search_state is_force_redraw: app_state.is_force_redraw,
.search_state recalculate_column_widths,
.is_enabled header_style: self.colours.table_header_style,
&& !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.into() {
(
" Processes ".to_string(),
format!("{}{}", " Processes ", 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, border_style,
), highlighted_text_style,
]) title: Some(TextTableTitle {
} else { title: " Processes ".into(),
Spans::from(Span::styled(title_base, self.colours.widget_title_style)) is_expanded: app_state.is_expanded,
}; }),
is_on_widget,
let process_block = if draw_border { draw_border,
Block::default() show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
.title(title) title_style: self.colours.widget_title_style,
.borders(Borders::ALL) text_style: self.colours.text_style,
.border_style(border_style) left_to_right: true,
} else if is_on_widget { }
Block::default() .draw_text_table(
.borders(SIDE_BORDERS) f,
.border_style(self.colours.highlighted_border_style) draw_loc,
} else { &mut proc_widget_state.table_state,
Block::default().borders(Borders::NONE) &proc_widget_state.table_data,
}; app_state.widget_map.get_mut(&widget_id),
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.scroll_bar,
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 calculated_col_width: usize =
(*calculated_col_width).into();
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
&& calculated_col_width > 1
{
// Truncate with ellipsis
let first_n =
graphemes[..(calculated_col_width - 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))
.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,
));
}
}
} }
} }
@ -583,14 +191,8 @@ impl Painter {
let start_position: usize = get_search_start_position( let start_position: usize = get_search_start_position(
num_columns - num_chars_for_text - 5, num_columns - num_chars_for_text - 5,
&proc_widget_state &proc_widget_state.proc_search.search_state.cursor_direction,
.process_search_state &mut proc_widget_state.proc_search.search_state.cursor_bar,
.search_state
.cursor_direction,
&mut proc_widget_state
.process_search_state
.search_state
.cursor_bar,
current_cursor_position, current_cursor_position,
app_state.is_force_redraw, app_state.is_force_redraw,
); );
@ -625,32 +227,26 @@ impl Painter {
})]; })];
// Text options shamelessly stolen from VS Code. // Text options shamelessly stolen from VS Code.
let case_style = if !proc_widget_state.process_search_state.is_ignoring_case { let case_style = if !proc_widget_state.proc_search.is_ignoring_case {
self.colours.currently_selected_text_style self.colours.currently_selected_text_style
} else { } else {
self.colours.text_style self.colours.text_style
}; };
let whole_word_style = if proc_widget_state let whole_word_style = if proc_widget_state.proc_search.is_searching_whole_word {
.process_search_state
.is_searching_whole_word
{
self.colours.currently_selected_text_style self.colours.currently_selected_text_style
} else { } else {
self.colours.text_style self.colours.text_style
}; };
let regex_style = if proc_widget_state let regex_style = if proc_widget_state.proc_search.is_searching_with_regex {
.process_search_state
.is_searching_with_regex
{
self.colours.currently_selected_text_style self.colours.currently_selected_text_style
} else { } else {
self.colours.text_style self.colours.text_style
}; };
// FIXME: [MOUSE] Mouse support for these in search // TODO: [MOUSE] Mouse support for these in search
// FIXME: [MOVEMENT] Movement support for these in search // TODO: [MOVEMENT] Movement support for these in search
let option_text = Spans::from(vec![ let option_text = Spans::from(vec![
Span::styled( Span::styled(
format!("Case({})", if self.is_mac_os { "F1" } else { "Alt+C" }), format!("Case({})", if self.is_mac_os { "F1" } else { "Alt+C" }),
@ -669,11 +265,7 @@ impl Painter {
]); ]);
search_text.push(Spans::from(Span::styled( search_text.push(Spans::from(Span::styled(
if let Some(err) = &proc_widget_state if let Some(err) = &proc_widget_state.proc_search.search_state.error_message {
.process_search_state
.search_state
.error_message
{
err.as_str() err.as_str()
} else { } else {
"" ""
@ -682,11 +274,8 @@ impl Painter {
))); )));
search_text.push(option_text); search_text.push(option_text);
let current_border_style = if proc_widget_state let current_border_style =
.process_search_state if proc_widget_state.proc_search.search_state.is_invalid_search {
.search_state
.is_invalid_search
{
self.colours.invalid_query_style self.colours.invalid_query_style
} else if is_on_widget { } else if is_on_widget {
self.colours.highlighted_border_style self.colours.highlighted_border_style
@ -751,127 +340,70 @@ impl Painter {
/// state that is stored. /// state that is stored.
/// ///
/// This should not be directly called. /// This should not be directly called.
fn draw_process_sort<B: Backend>( fn draw_sort_table<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool, &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64, widget_id: u64,
) { ) {
let is_on_widget = widget_id == app_state.current_widget.widget_id; let should_get_widget_bounds = app_state.should_get_widget_bounds();
if let Some(proc_widget_state) = if let Some(proc_widget_state) =
app_state.proc_state.widget_states.get_mut(&(widget_id - 2)) app_state.proc_state.widget_states.get_mut(&(widget_id - 2))
{ {
let current_scroll_position = proc_widget_state.columns.current_scroll_position; let recalculate_column_widths =
let sort_string = proc_widget_state should_get_widget_bounds || proc_widget_state.force_rerender;
.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 { let is_on_widget = widget_id == app_state.current_widget.widget_id;
0 let (border_style, highlighted_text_style) = if is_on_widget {
} else { (
app_state.app_config_fields.table_gap self.colours.highlighted_border_style,
}; self.colours.currently_selected_text_style,
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) } else {
.highlight_style(highlight_style) (self.colours.border_style, self.colours.text_style)
.style(self.colours.text_style) };
.widths(&[Constraint::Percentage(100)]),
margined_draw_loc,
column_state,
);
if app_state.should_get_widget_bounds() { // TODO: [PROC] Perhaps move this generation elsewhere... or leave it as is but look at partial rendering?
// Update draw loc in widget map let table_data = {
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { let data = proc_widget_state
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); .table_state
widget.bottom_right_corner = Some(( .columns
margined_draw_loc.x + margined_draw_loc.width, .iter()
margined_draw_loc.y + margined_draw_loc.height, .filter_map(|col| {
)); if col.is_hidden {
None
} else {
Some(TableRow::Raw(vec![col.header.text().clone()]))
} }
})
.collect();
TableData {
data,
col_widths: vec![usize::from(SORT_MENU_WIDTH)],
} }
};
TextTable {
table_gap: app_state.app_config_fields.table_gap,
is_force_redraw: app_state.is_force_redraw,
recalculate_column_widths,
header_style: self.colours.table_header_style,
border_style,
highlighted_text_style,
title: None,
is_on_widget,
draw_border,
show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
title_style: self.colours.widget_title_style,
text_style: self.colours.text_style,
left_to_right: true,
}
.draw_text_table(
f,
draw_loc,
&mut proc_widget_state.sort_table_state,
&table_data,
app_state.widget_map.get_mut(&widget_id),
);
} }
} }
} }

View File

@ -1,31 +1,12 @@
use once_cell::sync::Lazy; use tui::{backend::Backend, layout::Rect, terminal::Frame};
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
terminal::Frame,
text::Span,
text::{Spans, Text},
widgets::{Block, Borders, Row, Table},
};
use crate::{ use crate::{
app, app,
canvas::{ canvas::{
drawing_utils::{get_column_widths, get_start_position}, components::{TextTable, TextTableTitle},
Painter, Painter,
}, },
constants::*,
}; };
use unicode_segmentation::UnicodeSegmentation;
const TEMP_HEADERS: [&str; 2] = ["Sensor", "Temp"];
static TEMP_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
TEMP_HEADERS
.iter()
.map(|entry| entry.len() as u16)
.collect::<Vec<_>>()
});
impl Painter { impl Painter {
pub fn draw_temp_table<B: Backend>( pub fn draw_temp_table<B: Backend>(
@ -34,109 +15,9 @@ impl Painter {
) { ) {
let recalculate_column_widths = app_state.should_get_widget_bounds(); let recalculate_column_widths = app_state.should_get_widget_bounds();
if let Some(temp_widget_state) = app_state.temp_state.widget_states.get_mut(&widget_id) { if let Some(temp_widget_state) = app_state.temp_state.widget_states.get_mut(&widget_id) {
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { let is_on_widget = app_state.current_widget.widget_id == widget_id;
0
} else {
app_state.app_config_fields.table_gap
};
let start_position = get_start_position(
usize::from(
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
),
&temp_widget_state.scroll_state.scroll_direction,
&mut temp_widget_state.scroll_state.scroll_bar,
temp_widget_state.scroll_state.current_scroll_position,
app_state.is_force_redraw,
);
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let temp_table_state = &mut temp_widget_state.scroll_state.table_state;
temp_table_state.select(Some(
temp_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position),
));
let sliced_vec = &app_state.canvas_data.temp_sensor_data[start_position..];
// Calculate widths let (border_style, highlighted_text_style) = if is_on_widget {
let hard_widths = [None, None];
if recalculate_column_widths {
temp_widget_state.table_width_state.desired_column_widths = {
let mut column_widths = TEMP_HEADERS_LENS.clone();
for row in sliced_vec {
for (col, entry) in row.iter().enumerate() {
if entry.len() as u16 > column_widths[col] {
column_widths[col] = entry.len() as u16;
}
}
}
column_widths
};
temp_widget_state.table_width_state.calculated_column_widths = get_column_widths(
draw_loc.width,
&hard_widths,
&(TEMP_HEADERS_LENS
.iter()
.map(|width| Some(*width))
.collect::<Vec<_>>()),
&[Some(0.80), Some(-1.0)],
&temp_widget_state
.table_width_state
.desired_column_widths
.iter()
.map(|width| Some(*width))
.collect::<Vec<_>>(),
false,
);
}
let dcw = &temp_widget_state.table_width_state.desired_column_widths;
let ccw = &temp_widget_state.table_width_state.calculated_column_widths;
let temperature_rows =
sliced_vec.iter().map(|temp_row| {
let truncated_data = temp_row.iter().zip(&hard_widths).enumerate().map(
|(itx, (entry, width))| {
if width.is_none() {
if let (Some(desired_col_width), Some(calculated_col_width)) =
(dcw.get(itx), ccw.get(itx))
{
if *desired_col_width > *calculated_col_width
&& *calculated_col_width > 0
{
let calculated_col_width: usize =
(*calculated_col_width).into();
let graphemes =
UnicodeSegmentation::graphemes(entry.as_str(), true)
.collect::<Vec<&str>>();
if graphemes.len() > calculated_col_width
&& calculated_col_width > 1
{
// Truncate with ellipsis
let first_n =
graphemes[..(calculated_col_width - 1)].concat();
Text::raw(format!("{}", first_n))
} else {
Text::raw(entry)
}
} else {
Text::raw(entry)
}
} else {
Text::raw(entry)
}
} else {
Text::raw(entry)
}
},
);
Row::new(truncated_data)
});
let (border_style, highlight_style) = if is_on_widget {
( (
self.colours.highlighted_border_style, self.colours.highlighted_border_style,
self.colours.currently_selected_text_style, self.colours.currently_selected_text_style,
@ -144,118 +25,31 @@ impl Painter {
} else { } else {
(self.colours.border_style, self.colours.text_style) (self.colours.border_style, self.colours.text_style)
}; };
TextTable {
let title_base = if app_state.app_config_fields.show_table_scroll_position { table_gap: app_state.app_config_fields.table_gap,
let title_string = format!( is_force_redraw: app_state.is_force_redraw,
" Temperatures ({} of {}) ", recalculate_column_widths,
temp_widget_state header_style: self.colours.table_header_style,
.scroll_state
.current_scroll_position
.saturating_add(1),
app_state.canvas_data.temp_sensor_data.len()
);
if title_string.len() <= draw_loc.width.into() {
title_string
} else {
" Temperatures ".to_string()
}
} else {
" Temperatures ".to_string()
};
let title = if app_state.is_expanded {
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.into() {
(
" Temperatures ".to_string(),
format!("{}{}", " Temperatures ", 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, border_style,
), highlighted_text_style,
]) title: Some(TextTableTitle {
} else { title: " Temperatures ".into(),
Spans::from(Span::styled(title_base, self.colours.widget_title_style)) is_expanded: app_state.is_expanded,
}; }),
is_on_widget,
let temp_block = if draw_border { draw_border,
Block::default() show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
.title(title) title_style: self.colours.widget_title_style,
.borders(Borders::ALL) text_style: self.colours.text_style,
.border_style(border_style) left_to_right: false,
} else if is_on_widget { }
Block::default() .draw_text_table(
.borders(SIDE_BORDERS) f,
.border_style(self.colours.highlighted_border_style) draw_loc,
} else { &mut temp_widget_state.table_state,
Block::default().borders(Borders::NONE) &app_state.converted_data.temp_sensor_data,
}; app_state.widget_map.get_mut(&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];
// Draw
f.render_stateful_widget(
Table::new(temperature_rows)
.header(
Row::new(TEMP_HEADERS.to_vec())
.style(self.colours.table_header_style)
.bottom_margin(table_gap),
)
.block(temp_block)
.highlight_style(highlight_style)
.style(self.colours.text_style)
.widths(
&(temp_widget_state
.table_width_state
.calculated_column_widths
.iter()
.map(|calculated_width| Constraint::Length(*calculated_width))
.collect::<Vec<_>>()),
),
margined_draw_loc,
temp_table_state,
); );
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
// Note there is no difference between this and using draw_loc, but I'm too lazy to fix it.
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

@ -148,7 +148,7 @@ pub fn build_app() -> Command<'static> {
.help("Uses a dot marker for graphs.") .help("Uses a dot marker for graphs.")
.long_help("Uses a dot marker for graphs as opposed to the default braille marker."); .long_help("Uses a dot marker for graphs as opposed to the default braille marker.");
let group = Arg::new("group") // FIXME: Rename this to something like "group_process", would be "breaking" though. let group = Arg::new("group") // TODO: Rename this to something like "group_process", would be "breaking" though.
.short('g') .short('g')
.long("group") .long("group")
.help("Groups processes with the same name by default.") .help("Groups processes with the same name by default.")

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,7 @@ use crossterm::{
}; };
use app::{ use app::{
data_harvester::{self, processes::ProcessSorting}, data_harvester,
layout_manager::{UsedWidgets, WidgetDirection}, layout_manager::{UsedWidgets, WidgetDirection},
App, App,
}; };
@ -302,295 +302,40 @@ pub fn panic_hook(panic_info: &PanicInfo<'_>) {
.unwrap(); .unwrap();
} }
pub fn handle_force_redraws(app: &mut App) { pub fn update_data(app: &mut App) {
// Currently we use an Option... because we might want to future-proof this for proc in app.proc_state.widget_states.values_mut() {
// if we eventually get widget-specific redrawing! if proc.force_update_data {
if app.proc_state.force_update_all { proc.update_displayed_process_data(&app.data_collection);
update_all_process_lists(app); proc.force_update_data = false;
app.proc_state.force_update_all = false; }
} else if let Some(widget_id) = app.proc_state.force_update {
update_final_process_list(app, widget_id);
app.proc_state.force_update = None;
} }
if app.cpu_state.force_update.is_some() { if app.cpu_state.force_update.is_some() {
convert_cpu_data_points( convert_cpu_data_points(&app.data_collection, &mut app.converted_data.cpu_data);
&app.data_collection, app.converted_data.load_avg_data = app.data_collection.load_avg_harvest;
&mut app.canvas_data.cpu_data,
app.is_frozen,
);
app.canvas_data.load_avg_data = app.data_collection.load_avg_harvest;
app.cpu_state.force_update = None; app.cpu_state.force_update = None;
} }
// FIXME: [OPT] Prefer reassignment over new vectors? // TODO: [OPT] Prefer reassignment over new vectors?
if app.mem_state.force_update.is_some() { if app.mem_state.force_update.is_some() {
app.canvas_data.mem_data = convert_mem_data_points(&app.data_collection, app.is_frozen); app.converted_data.mem_data = convert_mem_data_points(&app.data_collection);
app.canvas_data.swap_data = convert_swap_data_points(&app.data_collection, app.is_frozen); app.converted_data.swap_data = convert_swap_data_points(&app.data_collection);
app.mem_state.force_update = None; app.mem_state.force_update = None;
} }
if app.net_state.force_update.is_some() { if app.net_state.force_update.is_some() {
let (rx, tx) = get_rx_tx_data_points( let (rx, tx) = get_rx_tx_data_points(
&app.data_collection, &app.data_collection,
app.is_frozen,
&app.app_config_fields.network_scale_type, &app.app_config_fields.network_scale_type,
&app.app_config_fields.network_unit_type, &app.app_config_fields.network_unit_type,
app.app_config_fields.network_use_binary_prefix, app.app_config_fields.network_use_binary_prefix,
); );
app.canvas_data.network_data_rx = rx; app.converted_data.network_data_rx = rx;
app.canvas_data.network_data_tx = tx; app.converted_data.network_data_tx = tx;
app.net_state.force_update = None; app.net_state.force_update = None;
} }
} }
#[allow(clippy::needless_collect)]
pub fn update_all_process_lists(app: &mut App) {
// According to clippy, I can avoid a collect... but if I follow it,
// I end up conflicting with the borrow checker since app is used within the closure... hm.
if !app.is_frozen {
let widget_ids = app
.proc_state
.widget_states
.keys()
.cloned()
.collect::<Vec<_>>();
widget_ids.into_iter().for_each(|widget_id| {
update_final_process_list(app, widget_id);
});
}
}
fn update_final_process_list(app: &mut App, widget_id: u64) {
let process_states = app
.proc_state
.widget_states
.get(&widget_id)
.map(|process_state| {
(
process_state
.process_search_state
.search_state
.is_invalid_or_blank_search(),
process_state.is_using_command,
process_state.is_grouped,
process_state.is_tree_mode,
)
});
if let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states {
if !app.is_frozen {
convert_process_data(
&app.data_collection,
&mut app.canvas_data.single_process_data,
#[cfg(target_family = "unix")]
&mut app.user_table,
);
}
let process_filter = app.get_process_filter(widget_id);
let filtered_process_data: Vec<ConvertedProcessData> = if is_tree {
app.canvas_data
.single_process_data
.iter()
.map(|(_pid, process)| {
let mut process_clone = process.clone();
if !is_invalid_or_blank {
if let Some(process_filter) = process_filter {
process_clone.is_disabled_entry =
!process_filter.check(&process_clone, is_using_command);
}
}
process_clone
})
.collect::<Vec<_>>()
} else {
app.canvas_data
.single_process_data
.iter()
.filter_map(|(_pid, process)| {
if !is_invalid_or_blank {
if let Some(process_filter) = process_filter {
if process_filter.check(process, is_using_command) {
Some(process)
} else {
None
}
} else {
Some(process)
}
} else {
Some(process)
}
})
.cloned()
.collect::<Vec<_>>()
};
if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) {
let mut finalized_process_data = if is_tree {
tree_process_data(
&filtered_process_data,
is_using_command,
&proc_widget_state.process_sorting_type,
proc_widget_state.is_process_sort_descending,
)
} else if is_grouped {
group_process_data(&filtered_process_data, is_using_command)
} else {
filtered_process_data
};
// Note tree mode is sorted well before this, as it's special.
if !is_tree {
sort_process_data(&mut finalized_process_data, proc_widget_state);
}
if proc_widget_state.scroll_state.current_scroll_position
>= finalized_process_data.len()
{
proc_widget_state.scroll_state.current_scroll_position =
finalized_process_data.len().saturating_sub(1);
proc_widget_state.scroll_state.scroll_bar = 0;
proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down;
}
app.canvas_data.stringified_process_data_map.insert(
widget_id,
stringify_process_data(proc_widget_state, &finalized_process_data),
);
app.canvas_data
.finalized_process_data_map
.insert(widget_id, finalized_process_data);
}
}
}
fn sort_process_data(
to_sort_vec: &mut [ConvertedProcessData], proc_widget_state: &app::ProcWidgetState,
) {
to_sort_vec.sort_by_cached_key(|c| c.name.to_lowercase());
match &proc_widget_state.process_sorting_type {
ProcessSorting::CpuPercent => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.cpu_percent_usage,
b.cpu_percent_usage,
proc_widget_state.is_process_sort_descending,
)
});
}
ProcessSorting::Mem => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.mem_usage_bytes,
b.mem_usage_bytes,
proc_widget_state.is_process_sort_descending,
)
});
}
ProcessSorting::MemPercent => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.mem_percent_usage,
b.mem_percent_usage,
proc_widget_state.is_process_sort_descending,
)
});
}
ProcessSorting::ProcessName => {
// Don't repeat if false... it sorts by name by default anyways.
if proc_widget_state.is_process_sort_descending {
to_sort_vec.sort_by_cached_key(|c| c.name.to_lowercase());
if proc_widget_state.is_process_sort_descending {
to_sort_vec.reverse();
}
}
}
ProcessSorting::Command => {
to_sort_vec.sort_by_cached_key(|c| c.command.to_lowercase());
if proc_widget_state.is_process_sort_descending {
to_sort_vec.reverse();
}
}
ProcessSorting::Pid => {
if !proc_widget_state.is_grouped {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.pid,
b.pid,
proc_widget_state.is_process_sort_descending,
)
});
}
}
ProcessSorting::ReadPerSecond => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.rps_f64,
b.rps_f64,
proc_widget_state.is_process_sort_descending,
)
});
}
ProcessSorting::WritePerSecond => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.wps_f64,
b.wps_f64,
proc_widget_state.is_process_sort_descending,
)
});
}
ProcessSorting::TotalRead => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.tr_f64,
b.tr_f64,
proc_widget_state.is_process_sort_descending,
)
});
}
ProcessSorting::TotalWrite => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.tw_f64,
b.tw_f64,
proc_widget_state.is_process_sort_descending,
)
});
}
ProcessSorting::State => {
to_sort_vec.sort_by_cached_key(|c| c.process_state.to_lowercase());
if proc_widget_state.is_process_sort_descending {
to_sort_vec.reverse();
}
}
ProcessSorting::User => to_sort_vec.sort_by(|a, b| match (&a.user, &b.user) {
(Some(user_a), Some(user_b)) => utils::gen_util::get_ordering(
user_a.to_lowercase(),
user_b.to_lowercase(),
proc_widget_state.is_process_sort_descending,
),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Less,
}),
ProcessSorting::Count => {
if proc_widget_state.is_grouped {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.group_pids.len(),
b.group_pids.len(),
proc_widget_state.is_process_sort_descending,
)
});
}
}
}
}
pub fn create_input_thread( pub fn create_input_thread(
sender: std::sync::mpsc::Sender< sender: std::sync::mpsc::Sender<
BottomEvent<crossterm::event::KeyEvent, crossterm::event::MouseEvent>, BottomEvent<crossterm::event::KeyEvent, crossterm::event::MouseEvent>,
@ -651,7 +396,7 @@ pub fn create_collection_thread(
thread::spawn(move || { thread::spawn(move || {
let mut data_state = data_harvester::DataCollector::new(filters); let mut data_state = data_harvester::DataCollector::new(filters);
data_state.set_collected_data(used_widget_set); data_state.set_data_collection(used_widget_set);
data_state.set_temperature_type(temp_type); data_state.set_temperature_type(temp_type);
data_state.set_use_current_cpu_total(use_current_cpu_total); data_state.set_use_current_cpu_total(use_current_cpu_total);
data_state.set_show_average_cpu(show_average_cpu); data_state.set_show_average_cpu(show_average_cpu);
@ -682,7 +427,7 @@ pub fn create_collection_thread(
data_state.set_show_average_cpu(app_config_fields.show_average_cpu); data_state.set_show_average_cpu(app_config_fields.show_average_cpu);
} }
ThreadControlEvent::UpdateUsedWidgets(used_widget_set) => { ThreadControlEvent::UpdateUsedWidgets(used_widget_set) => {
data_state.set_collected_data(*used_widget_set); data_state.set_data_collection(*used_widget_set);
} }
ThreadControlEvent::UpdateUpdateTime(new_time) => { ThreadControlEvent::UpdateUpdateTime(new_time) => {
update_time = new_time; update_time = new_time;

View File

@ -10,7 +10,11 @@ use std::{
}; };
use crate::{ use crate::{
app::{layout_manager::*, *}, app::{
layout_manager::*,
widgets::{ProcWidget, ProcWidgetMode},
*,
},
canvas::ColourScheme, canvas::ColourScheme,
constants::*, constants::*,
units::data_units::DataUnit, units::data_units::DataUnit,
@ -265,7 +269,7 @@ pub fn build_app(
let mut cpu_state_map: HashMap<u64, CpuWidgetState> = HashMap::new(); let mut cpu_state_map: HashMap<u64, CpuWidgetState> = HashMap::new();
let mut mem_state_map: HashMap<u64, MemWidgetState> = HashMap::new(); let mut mem_state_map: HashMap<u64, MemWidgetState> = HashMap::new();
let mut net_state_map: HashMap<u64, NetWidgetState> = HashMap::new(); let mut net_state_map: HashMap<u64, NetWidgetState> = HashMap::new();
let mut proc_state_map: HashMap<u64, ProcWidgetState> = HashMap::new(); let mut proc_state_map: HashMap<u64, ProcWidget> = HashMap::new();
let mut temp_state_map: HashMap<u64, TempWidgetState> = HashMap::new(); let mut temp_state_map: HashMap<u64, TempWidgetState> = HashMap::new();
let mut disk_state_map: HashMap<u64, DiskWidgetState> = HashMap::new(); let mut disk_state_map: HashMap<u64, DiskWidgetState> = HashMap::new();
let mut battery_state_map: HashMap<u64, BatteryWidgetState> = HashMap::new(); let mut battery_state_map: HashMap<u64, BatteryWidgetState> = HashMap::new();
@ -344,33 +348,37 @@ pub fn build_app(
Net => { Net => {
net_state_map.insert( net_state_map.insert(
widget.widget_id, widget.widget_id,
NetWidgetState::init( NetWidgetState::init(default_time_value, autohide_timer),
default_time_value,
autohide_timer,
// network_unit_type.clone(),
// network_scale_type.clone(),
),
); );
} }
Proc => { Proc => {
let mode = if is_grouped {
ProcWidgetMode::Grouped
} else if is_default_tree {
ProcWidgetMode::Tree {
collapsed_pids: Default::default(),
}
} else {
ProcWidgetMode::Normal
};
proc_state_map.insert( proc_state_map.insert(
widget.widget_id, widget.widget_id,
ProcWidgetState::init( ProcWidget::init(
mode,
is_case_sensitive, is_case_sensitive,
is_match_whole_word, is_match_whole_word,
is_use_regex, is_use_regex,
is_grouped,
show_memory_as_values, show_memory_as_values,
is_default_tree,
is_default_command, is_default_command,
), ),
); );
} }
Disk => { Disk => {
disk_state_map.insert(widget.widget_id, DiskWidgetState::init()); disk_state_map.insert(widget.widget_id, DiskWidgetState::default());
} }
Temp => { Temp => {
temp_state_map.insert(widget.widget_id, TempWidgetState::init()); temp_state_map.insert(widget.widget_id, TempWidgetState::default());
} }
Battery => { Battery => {
battery_state_map battery_state_map
@ -466,7 +474,7 @@ pub fn build_app(
let mapping = HashMap::new(); let mapping = HashMap::new();
for widget in search_case_enabled_widgets { for widget in search_case_enabled_widgets {
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) { if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
proc_widget.process_search_state.is_ignoring_case = !widget.enabled; proc_widget.proc_search.is_ignoring_case = !widget.enabled;
} }
} }
flags.search_case_enabled_widgets_map = Some(mapping); flags.search_case_enabled_widgets_map = Some(mapping);
@ -480,7 +488,7 @@ pub fn build_app(
let mapping = HashMap::new(); let mapping = HashMap::new();
for widget in search_whole_word_enabled_widgets { for widget in search_whole_word_enabled_widgets {
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) { if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
proc_widget.process_search_state.is_searching_whole_word = widget.enabled; proc_widget.proc_search.is_searching_whole_word = widget.enabled;
} }
} }
flags.search_whole_word_enabled_widgets_map = Some(mapping); flags.search_whole_word_enabled_widgets_map = Some(mapping);
@ -492,7 +500,7 @@ pub fn build_app(
let mapping = HashMap::new(); let mapping = HashMap::new();
for widget in search_regex_enabled_widgets { for widget in search_regex_enabled_widgets {
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) { if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
proc_widget.process_search_state.is_searching_with_regex = widget.enabled; proc_widget.proc_search.is_searching_with_regex = widget.enabled;
} }
} }
flags.search_regex_enabled_widgets_map = Some(mapping); flags.search_regex_enabled_widgets_map = Some(mapping);

View File

@ -92,30 +92,50 @@ pub fn get_decimal_prefix(quantity: u64, unit: &str) -> (f64, String) {
} }
} }
/// Gotta get partial ordering? No problem, here's something to deal with it~ #[inline]
/// pub fn sort_partial_fn<T: std::cmp::PartialOrd>(is_reverse: bool) -> fn(T, T) -> Ordering {
/// Note that https://github.com/reem/rust-ordered-float exists, maybe move to it one day? IDK. if is_reverse {
pub fn get_ordering<T: std::cmp::PartialOrd>( partial_ordering_rev
a_val: T, b_val: T, reverse_order: bool,
) -> std::cmp::Ordering {
match a_val.partial_cmp(&b_val) {
Some(x) => match x {
Ordering::Greater => {
if reverse_order {
std::cmp::Ordering::Less
} else { } else {
std::cmp::Ordering::Greater partial_ordering
} }
} }
Ordering::Less => {
if reverse_order { /// Returns an [`Ordering`] between two [`PartialOrd`]s.
std::cmp::Ordering::Greater #[inline]
} else { pub fn partial_ordering<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering {
std::cmp::Ordering::Less // TODO: Switch to `total_cmp` on 1.62
} a.partial_cmp(&b).unwrap_or(Ordering::Equal)
} }
Ordering::Equal => Ordering::Equal,
}, /// Returns a reversed [`Ordering`] between two [`PartialOrd`]s.
None => Ordering::Equal, ///
/// This is simply a wrapper function around [`partial_ordering`] that reverses
/// the result.
#[inline]
pub fn partial_ordering_rev<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering {
partial_ordering(a, b).reverse()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_sort_partial_fn() {
let mut x = vec![9, 5, 20, 15, 10, 5];
let mut y = vec![1.0, 15.0, -1.0, -100.0, -100.1, 16.15, -100.0];
x.sort_by(|a, b| sort_partial_fn(false)(a, b));
assert_eq!(x, vec![5, 5, 9, 10, 15, 20]);
x.sort_by(|a, b| sort_partial_fn(true)(a, b));
assert_eq!(x, vec![20, 15, 10, 9, 5, 5]);
y.sort_by(|a, b| sort_partial_fn(false)(a, b));
assert_eq!(y, vec![-100.1, -100.0, -100.0, -1.0, 1.0, 15.0, 16.15]);
y.sort_by(|a, b| sort_partial_fn(true)(a, b));
assert_eq!(y, vec![16.15, 15.0, 1.0, -1.0, -100.0, -100.0, -100.1]);
} }
} }