mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-09-25 18:49:06 +02:00
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:
commit
de765fc364
@ -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"
|
||||||
|
891
src/app.rs
891
src/app.rs
File diff suppressed because it is too large
Load Diff
@ -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")]
|
||||||
|
@ -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,15 +275,28 @@ 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"))]
|
||||||
{
|
{
|
||||||
processes::get_process_data(
|
#[cfg(target_family = "unix")]
|
||||||
&self.sys,
|
{
|
||||||
self.use_current_cpu_total,
|
processes::get_process_data(
|
||||||
self.mem_total_kb,
|
&self.sys,
|
||||||
)
|
self.use_current_cpu_total,
|
||||||
|
self.mem_total_kb,
|
||||||
|
&mut self.user_table,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#[cfg(not(target_family = "unix"))]
|
||||||
|
{
|
||||||
|
processes::get_process_data(
|
||||||
|
&self.sys,
|
||||||
|
self.use_current_cpu_total,
|
||||||
|
self.mem_total_kb,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} {
|
} {
|
||||||
self.data.list_of_processes = Some(process_list);
|
self.data.list_of_processes = Some(process_list);
|
||||||
|
@ -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>,
|
||||||
|
@ -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 =
|
||||||
|
@ -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)?;
|
||||||
|
@ -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>,
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
778
src/app/query.rs
778
src/app/query.rs
@ -1,449 +1,434 @@
|
|||||||
use super::ProcWidgetState;
|
use crate::utils::error::{
|
||||||
use crate::{
|
BottomError::{self, QueryError},
|
||||||
data_conversion::ConvertedProcessData,
|
Result,
|
||||||
utils::error::{
|
|
||||||
BottomError::{self, QueryError},
|
|
||||||
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> {
|
||||||
}
|
fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
|
||||||
|
let lhs = process_or(query)?;
|
||||||
|
let mut list_of_ors = vec![lhs];
|
||||||
|
|
||||||
impl ProcessQuery for ProcWidgetState {
|
while query.front().is_some() {
|
||||||
fn parse_query(&self) -> Result<Query> {
|
list_of_ors.push(process_or(query)?);
|
||||||
fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
|
|
||||||
let lhs = process_or(query)?;
|
|
||||||
let mut list_of_ors = vec![lhs];
|
|
||||||
|
|
||||||
while query.front().is_some() {
|
|
||||||
list_of_ors.push(process_or(query)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Query { query: list_of_ors })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_or(query: &mut VecDeque<String>) -> Result<Or> {
|
Ok(Query { query: list_of_ors })
|
||||||
let mut lhs = process_and(query)?;
|
}
|
||||||
let mut rhs: Option<Box<And>> = None;
|
|
||||||
|
|
||||||
while let Some(queue_top) = query.front() {
|
fn process_or(query: &mut VecDeque<String>) -> Result<Or> {
|
||||||
// debug!("OR QT: {:?}", queue_top);
|
let mut lhs = process_and(query)?;
|
||||||
if OR_LIST.contains(&queue_top.to_lowercase().as_str()) {
|
let mut rhs: Option<Box<And>> = None;
|
||||||
query.pop_front();
|
|
||||||
rhs = Some(Box::new(process_and(query)?));
|
|
||||||
|
|
||||||
if let Some(queue_next) = query.front() {
|
while let Some(queue_top) = query.front() {
|
||||||
if OR_LIST.contains(&queue_next.to_lowercase().as_str()) {
|
// debug!("OR QT: {:?}", queue_top);
|
||||||
// Must merge LHS and RHS
|
if OR_LIST.contains(&queue_top.to_lowercase().as_str()) {
|
||||||
lhs = And {
|
query.pop_front();
|
||||||
lhs: Prefix {
|
rhs = Some(Box::new(process_and(query)?));
|
||||||
or: Some(Box::new(Or { lhs, rhs })),
|
|
||||||
regex_prefix: None,
|
|
||||||
compare_prefix: None,
|
|
||||||
},
|
|
||||||
rhs: None,
|
|
||||||
};
|
|
||||||
rhs = None;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
|
|
||||||
return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Or { lhs, rhs })
|
if let Some(queue_next) = query.front() {
|
||||||
}
|
if OR_LIST.contains(&queue_next.to_lowercase().as_str()) {
|
||||||
|
// Must merge LHS and RHS
|
||||||
fn process_and(query: &mut VecDeque<String>) -> Result<And> {
|
lhs = And {
|
||||||
let mut lhs = process_prefix(query, false)?;
|
|
||||||
let mut rhs: Option<Box<Prefix>> = None;
|
|
||||||
|
|
||||||
while let Some(queue_top) = query.front() {
|
|
||||||
// debug!("AND QT: {:?}", queue_top);
|
|
||||||
if AND_LIST.contains(&queue_top.to_lowercase().as_str()) {
|
|
||||||
query.pop_front();
|
|
||||||
|
|
||||||
rhs = Some(Box::new(process_prefix(query, false)?));
|
|
||||||
|
|
||||||
if let Some(next_queue_top) = query.front() {
|
|
||||||
if AND_LIST.contains(&next_queue_top.to_lowercase().as_str()) {
|
|
||||||
// Must merge LHS and RHS
|
|
||||||
lhs = Prefix {
|
|
||||||
or: Some(Box::new(Or {
|
|
||||||
lhs: And { lhs, rhs },
|
|
||||||
rhs: None,
|
|
||||||
})),
|
|
||||||
regex_prefix: None,
|
|
||||||
compare_prefix: None,
|
|
||||||
};
|
|
||||||
rhs = None;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
|
|
||||||
return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(And { lhs, rhs })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_prefix(query: &mut VecDeque<String>, inside_quotation: bool) -> Result<Prefix> {
|
|
||||||
if let Some(queue_top) = query.pop_front() {
|
|
||||||
if inside_quotation {
|
|
||||||
if queue_top == "\"" {
|
|
||||||
// This means we hit something like "". Return an empty prefix, and to deal with
|
|
||||||
// the close quote checker, add one to the top of the stack. Ugly fix but whatever.
|
|
||||||
query.push_front("\"".to_string());
|
|
||||||
return Ok(Prefix {
|
|
||||||
or: None,
|
|
||||||
regex_prefix: Some((
|
|
||||||
PrefixType::Name,
|
|
||||||
StringQuery::Value(String::default()),
|
|
||||||
)),
|
|
||||||
compare_prefix: None,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
let mut quoted_string = queue_top;
|
|
||||||
while let Some(next_str) = query.front() {
|
|
||||||
if next_str == "\"" {
|
|
||||||
// Stop!
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
quoted_string.push_str(next_str);
|
|
||||||
query.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(Prefix {
|
|
||||||
or: None,
|
|
||||||
regex_prefix: Some((
|
|
||||||
PrefixType::Name,
|
|
||||||
StringQuery::Value(quoted_string),
|
|
||||||
)),
|
|
||||||
compare_prefix: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if queue_top == "(" {
|
|
||||||
if query.is_empty() {
|
|
||||||
return Err(QueryError(Cow::Borrowed("Missing closing parentheses")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut list_of_ors = VecDeque::new();
|
|
||||||
|
|
||||||
while let Some(in_paren_query_top) = query.front() {
|
|
||||||
if in_paren_query_top != ")" {
|
|
||||||
list_of_ors.push_back(process_or(query)?);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure not empty
|
|
||||||
if list_of_ors.is_empty() {
|
|
||||||
return Err(QueryError("No values within parentheses group".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now convert this back to a OR...
|
|
||||||
let initial_or = Or {
|
|
||||||
lhs: And {
|
|
||||||
lhs: Prefix {
|
lhs: Prefix {
|
||||||
or: list_of_ors.pop_front().map(Box::new),
|
or: Some(Box::new(Or { lhs, rhs })),
|
||||||
compare_prefix: None,
|
|
||||||
regex_prefix: None,
|
regex_prefix: None,
|
||||||
|
compare_prefix: None,
|
||||||
},
|
},
|
||||||
rhs: None,
|
rhs: None,
|
||||||
},
|
};
|
||||||
rhs: None,
|
rhs = None;
|
||||||
};
|
}
|
||||||
let returned_or = list_of_ors.into_iter().fold(initial_or, |lhs, rhs| Or {
|
} else {
|
||||||
lhs: And {
|
break;
|
||||||
lhs: Prefix {
|
}
|
||||||
or: Some(Box::new(lhs)),
|
} else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
|
||||||
compare_prefix: None,
|
return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
|
||||||
regex_prefix: None,
|
} else {
|
||||||
},
|
break;
|
||||||
rhs: Some(Box::new(Prefix {
|
}
|
||||||
or: Some(Box::new(rhs)),
|
}
|
||||||
compare_prefix: None,
|
|
||||||
regex_prefix: None,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
rhs: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(close_paren) = query.pop_front() {
|
Ok(Or { lhs, rhs })
|
||||||
if close_paren == ")" {
|
}
|
||||||
return Ok(Prefix {
|
|
||||||
or: Some(Box::new(returned_or)),
|
fn process_and(query: &mut VecDeque<String>) -> Result<And> {
|
||||||
regex_prefix: None,
|
let mut lhs = process_prefix(query, false)?;
|
||||||
compare_prefix: None,
|
let mut rhs: Option<Box<Prefix>> = None;
|
||||||
});
|
|
||||||
|
while let Some(queue_top) = query.front() {
|
||||||
|
// debug!("AND QT: {:?}", queue_top);
|
||||||
|
if AND_LIST.contains(&queue_top.to_lowercase().as_str()) {
|
||||||
|
query.pop_front();
|
||||||
|
|
||||||
|
rhs = Some(Box::new(process_prefix(query, false)?));
|
||||||
|
|
||||||
|
if let Some(next_queue_top) = query.front() {
|
||||||
|
if AND_LIST.contains(&next_queue_top.to_lowercase().as_str()) {
|
||||||
|
// Must merge LHS and RHS
|
||||||
|
lhs = Prefix {
|
||||||
|
or: Some(Box::new(Or {
|
||||||
|
lhs: And { lhs, rhs },
|
||||||
|
rhs: None,
|
||||||
|
})),
|
||||||
|
regex_prefix: None,
|
||||||
|
compare_prefix: None,
|
||||||
|
};
|
||||||
|
rhs = None;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
|
||||||
|
return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(And { lhs, rhs })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_prefix(query: &mut VecDeque<String>, inside_quotation: bool) -> Result<Prefix> {
|
||||||
|
if let Some(queue_top) = query.pop_front() {
|
||||||
|
if inside_quotation {
|
||||||
|
if queue_top == "\"" {
|
||||||
|
// This means we hit something like "". Return an empty prefix, and to deal with
|
||||||
|
// the close quote checker, add one to the top of the stack. Ugly fix but whatever.
|
||||||
|
query.push_front("\"".to_string());
|
||||||
|
return Ok(Prefix {
|
||||||
|
or: None,
|
||||||
|
regex_prefix: Some((
|
||||||
|
PrefixType::Name,
|
||||||
|
StringQuery::Value(String::default()),
|
||||||
|
)),
|
||||||
|
compare_prefix: None,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let mut quoted_string = queue_top;
|
||||||
|
while let Some(next_str) = query.front() {
|
||||||
|
if next_str == "\"" {
|
||||||
|
// Stop!
|
||||||
|
break;
|
||||||
} else {
|
} else {
|
||||||
return Err(QueryError("Missing closing parentheses".into()));
|
quoted_string.push_str(next_str);
|
||||||
|
query.pop_front();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return Ok(Prefix {
|
||||||
|
or: None,
|
||||||
|
regex_prefix: Some((PrefixType::Name, StringQuery::Value(quoted_string))),
|
||||||
|
compare_prefix: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if queue_top == "(" {
|
||||||
|
if query.is_empty() {
|
||||||
|
return Err(QueryError(Cow::Borrowed("Missing closing parentheses")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut list_of_ors = VecDeque::new();
|
||||||
|
|
||||||
|
while let Some(in_paren_query_top) = query.front() {
|
||||||
|
if in_paren_query_top != ")" {
|
||||||
|
list_of_ors.push_back(process_or(query)?);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure not empty
|
||||||
|
if list_of_ors.is_empty() {
|
||||||
|
return Err(QueryError("No values within parentheses group".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now convert this back to a OR...
|
||||||
|
let initial_or = Or {
|
||||||
|
lhs: And {
|
||||||
|
lhs: Prefix {
|
||||||
|
or: list_of_ors.pop_front().map(Box::new),
|
||||||
|
compare_prefix: None,
|
||||||
|
regex_prefix: None,
|
||||||
|
},
|
||||||
|
rhs: None,
|
||||||
|
},
|
||||||
|
rhs: None,
|
||||||
|
};
|
||||||
|
let returned_or = list_of_ors.into_iter().fold(initial_or, |lhs, rhs| Or {
|
||||||
|
lhs: And {
|
||||||
|
lhs: Prefix {
|
||||||
|
or: Some(Box::new(lhs)),
|
||||||
|
compare_prefix: None,
|
||||||
|
regex_prefix: None,
|
||||||
|
},
|
||||||
|
rhs: Some(Box::new(Prefix {
|
||||||
|
or: Some(Box::new(rhs)),
|
||||||
|
compare_prefix: None,
|
||||||
|
regex_prefix: None,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
rhs: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(close_paren) = query.pop_front() {
|
||||||
|
if close_paren == ")" {
|
||||||
|
return Ok(Prefix {
|
||||||
|
or: Some(Box::new(returned_or)),
|
||||||
|
regex_prefix: None,
|
||||||
|
compare_prefix: None,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return Err(QueryError("Missing closing parentheses".into()));
|
return Err(QueryError("Missing closing parentheses".into()));
|
||||||
}
|
}
|
||||||
} else if queue_top == ")" {
|
} else {
|
||||||
return Err(QueryError("Missing opening parentheses".into()));
|
return Err(QueryError("Missing closing parentheses".into()));
|
||||||
} else if queue_top == "\"" {
|
}
|
||||||
// Similar to parentheses, trap and check for missing closing quotes. Note, however, that we
|
} else if queue_top == ")" {
|
||||||
// will DIRECTLY call another process_prefix call...
|
return Err(QueryError("Missing opening parentheses".into()));
|
||||||
|
} else if queue_top == "\"" {
|
||||||
|
// Similar to parentheses, trap and check for missing closing quotes. Note, however, that we
|
||||||
|
// will DIRECTLY call another process_prefix call...
|
||||||
|
|
||||||
let prefix = process_prefix(query, true)?;
|
let prefix = process_prefix(query, true)?;
|
||||||
if let Some(close_paren) = query.pop_front() {
|
if let Some(close_paren) = query.pop_front() {
|
||||||
if close_paren == "\"" {
|
if close_paren == "\"" {
|
||||||
return Ok(prefix);
|
return Ok(prefix);
|
||||||
} else {
|
|
||||||
return Err(QueryError("Missing closing quotation".into()));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return Err(QueryError("Missing closing quotation".into()));
|
return Err(QueryError("Missing closing quotation".into()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Get prefix type...
|
return Err(QueryError("Missing closing quotation".into()));
|
||||||
let prefix_type = queue_top.parse::<PrefixType>()?;
|
}
|
||||||
let content = if let PrefixType::Name = prefix_type {
|
} else {
|
||||||
Some(queue_top)
|
// Get prefix type...
|
||||||
} else {
|
let prefix_type = queue_top.parse::<PrefixType>()?;
|
||||||
query.pop_front()
|
let content = if let PrefixType::Name = prefix_type {
|
||||||
};
|
Some(queue_top)
|
||||||
|
} else {
|
||||||
|
query.pop_front()
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(content) = content {
|
if let Some(content) = content {
|
||||||
match &prefix_type {
|
match &prefix_type {
|
||||||
PrefixType::Name => {
|
PrefixType::Name => {
|
||||||
return Ok(Prefix {
|
return Ok(Prefix {
|
||||||
or: None,
|
or: None,
|
||||||
regex_prefix: Some((prefix_type, StringQuery::Value(content))),
|
regex_prefix: Some((prefix_type, StringQuery::Value(content))),
|
||||||
compare_prefix: None,
|
compare_prefix: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
PrefixType::Pid | PrefixType::State | PrefixType::User => {
|
PrefixType::Pid | PrefixType::State | PrefixType::User => {
|
||||||
// We have to check if someone put an "="...
|
// We have to check if someone put an "="...
|
||||||
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)
|
||||||
// These are split into 2 to 3 different strings due to parentheses being
|
// These are split into 2 to 3 different strings due to parentheses being
|
||||||
// delimiters in our query system.
|
// delimiters in our query system.
|
||||||
//
|
//
|
||||||
// Do we want these to be valid? They should, as a string, right?
|
// Do we want these to be valid? They should, as a string, right?
|
||||||
|
|
||||||
return Ok(Prefix {
|
|
||||||
or: None,
|
|
||||||
regex_prefix: Some((
|
|
||||||
prefix_type,
|
|
||||||
StringQuery::Value(queue_next),
|
|
||||||
)),
|
|
||||||
compare_prefix: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Ok(Prefix {
|
return Ok(Prefix {
|
||||||
or: None,
|
or: None,
|
||||||
regex_prefix: Some((
|
regex_prefix: Some((
|
||||||
prefix_type,
|
prefix_type,
|
||||||
StringQuery::Value(content),
|
StringQuery::Value(queue_next),
|
||||||
)),
|
)),
|
||||||
compare_prefix: None,
|
compare_prefix: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return Ok(Prefix {
|
||||||
|
or: None,
|
||||||
|
regex_prefix: Some((prefix_type, StringQuery::Value(content))),
|
||||||
|
compare_prefix: None,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
_ => {
|
}
|
||||||
// Now we gotta parse the content... yay.
|
_ => {
|
||||||
|
// Now we gotta parse the content... yay.
|
||||||
|
|
||||||
let mut condition: Option<QueryComparison> = None;
|
let mut condition: Option<QueryComparison> = None;
|
||||||
let mut value: Option<f64> = None;
|
let mut value: Option<f64> = None;
|
||||||
|
|
||||||
if content == "=" {
|
if content == "=" {
|
||||||
condition = Some(QueryComparison::Equal);
|
condition = Some(QueryComparison::Equal);
|
||||||
if let Some(queue_next) = query.pop_front() {
|
if let Some(queue_next) = query.pop_front() {
|
||||||
value = queue_next.parse::<f64>().ok();
|
value = queue_next.parse::<f64>().ok();
|
||||||
} else {
|
} else {
|
||||||
return Err(QueryError("Missing value".into()));
|
return Err(QueryError("Missing value".into()));
|
||||||
}
|
}
|
||||||
} else if content == ">" || content == "<" {
|
} else if content == ">" || content == "<" {
|
||||||
// We also have to check if the next string is an "="...
|
// We also have to check if the next string is an "="...
|
||||||
if let Some(queue_next) = query.pop_front() {
|
if let Some(queue_next) = query.pop_front() {
|
||||||
if queue_next == "=" {
|
if queue_next == "=" {
|
||||||
condition = Some(if content == ">" {
|
condition = Some(if content == ">" {
|
||||||
QueryComparison::GreaterOrEqual
|
QueryComparison::GreaterOrEqual
|
||||||
} else {
|
|
||||||
QueryComparison::LessOrEqual
|
|
||||||
});
|
|
||||||
if let Some(queue_next_next) = query.pop_front() {
|
|
||||||
value = queue_next_next.parse::<f64>().ok();
|
|
||||||
} else {
|
|
||||||
return Err(QueryError("Missing value".into()));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
condition = Some(if content == ">" {
|
QueryComparison::LessOrEqual
|
||||||
QueryComparison::Greater
|
});
|
||||||
} else {
|
if let Some(queue_next_next) = query.pop_front() {
|
||||||
QueryComparison::Less
|
value = queue_next_next.parse::<f64>().ok();
|
||||||
});
|
} else {
|
||||||
value = queue_next.parse::<f64>().ok();
|
return Err(QueryError("Missing value".into()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(QueryError("Missing value".into()));
|
condition = Some(if content == ">" {
|
||||||
|
QueryComparison::Greater
|
||||||
|
} else {
|
||||||
|
QueryComparison::Less
|
||||||
|
});
|
||||||
|
value = queue_next.parse::<f64>().ok();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return Err(QueryError("Missing value".into()));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(condition) = condition {
|
if let Some(condition) = condition {
|
||||||
if let Some(read_value) = value {
|
if let Some(read_value) = value {
|
||||||
// Now we want to check one last thing - is there a unit?
|
// Now we want to check one last thing - is there a unit?
|
||||||
// If no unit, assume base.
|
// If no unit, assume base.
|
||||||
// Furthermore, base must be PEEKED at initially, and will
|
// Furthermore, base must be PEEKED at initially, and will
|
||||||
// require (likely) prefix_type specific checks
|
// require (likely) prefix_type specific checks
|
||||||
// Lastly, if it *is* a unit, remember to POP!
|
// Lastly, if it *is* a unit, remember to POP!
|
||||||
|
|
||||||
let mut value = read_value;
|
let mut value = read_value;
|
||||||
|
|
||||||
match prefix_type {
|
match prefix_type {
|
||||||
PrefixType::MemBytes
|
PrefixType::MemBytes
|
||||||
| PrefixType::Rps
|
| PrefixType::Rps
|
||||||
| PrefixType::Wps
|
| PrefixType::Wps
|
||||||
| PrefixType::TRead
|
| PrefixType::TRead
|
||||||
| PrefixType::TWrite => {
|
| PrefixType::TWrite => {
|
||||||
if let Some(potential_unit) = query.front() {
|
if let Some(potential_unit) = query.front() {
|
||||||
match potential_unit.to_lowercase().as_str() {
|
match potential_unit.to_lowercase().as_str() {
|
||||||
"tb" => {
|
"tb" => {
|
||||||
value *= 1_000_000_000_000.0;
|
value *= 1_000_000_000_000.0;
|
||||||
query.pop_front();
|
query.pop_front();
|
||||||
}
|
|
||||||
"tib" => {
|
|
||||||
value *= 1_099_511_627_776.0;
|
|
||||||
query.pop_front();
|
|
||||||
}
|
|
||||||
"gb" => {
|
|
||||||
value *= 1_000_000_000.0;
|
|
||||||
query.pop_front();
|
|
||||||
}
|
|
||||||
"gib" => {
|
|
||||||
value *= 1_073_741_824.0;
|
|
||||||
query.pop_front();
|
|
||||||
}
|
|
||||||
"mb" => {
|
|
||||||
value *= 1_000_000.0;
|
|
||||||
query.pop_front();
|
|
||||||
}
|
|
||||||
"mib" => {
|
|
||||||
value *= 1_048_576.0;
|
|
||||||
query.pop_front();
|
|
||||||
}
|
|
||||||
"kb" => {
|
|
||||||
value *= 1000.0;
|
|
||||||
query.pop_front();
|
|
||||||
}
|
|
||||||
"kib" => {
|
|
||||||
value *= 1024.0;
|
|
||||||
query.pop_front();
|
|
||||||
}
|
|
||||||
"b" => {
|
|
||||||
// Just gotta pop.
|
|
||||||
query.pop_front();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
"tib" => {
|
||||||
|
value *= 1_099_511_627_776.0;
|
||||||
|
query.pop_front();
|
||||||
|
}
|
||||||
|
"gb" => {
|
||||||
|
value *= 1_000_000_000.0;
|
||||||
|
query.pop_front();
|
||||||
|
}
|
||||||
|
"gib" => {
|
||||||
|
value *= 1_073_741_824.0;
|
||||||
|
query.pop_front();
|
||||||
|
}
|
||||||
|
"mb" => {
|
||||||
|
value *= 1_000_000.0;
|
||||||
|
query.pop_front();
|
||||||
|
}
|
||||||
|
"mib" => {
|
||||||
|
value *= 1_048_576.0;
|
||||||
|
query.pop_front();
|
||||||
|
}
|
||||||
|
"kb" => {
|
||||||
|
value *= 1000.0;
|
||||||
|
query.pop_front();
|
||||||
|
}
|
||||||
|
"kib" => {
|
||||||
|
value *= 1024.0;
|
||||||
|
query.pop_front();
|
||||||
|
}
|
||||||
|
"b" => {
|
||||||
|
// Just gotta pop.
|
||||||
|
query.pop_front();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
return Ok(Prefix {
|
|
||||||
or: None,
|
|
||||||
regex_prefix: None,
|
|
||||||
compare_prefix: Some((
|
|
||||||
prefix_type,
|
|
||||||
NumericalQuery { condition, value },
|
|
||||||
)),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Ok(Prefix {
|
||||||
|
or: None,
|
||||||
|
regex_prefix: None,
|
||||||
|
compare_prefix: Some((
|
||||||
|
prefix_type,
|
||||||
|
NumericalQuery { condition, value },
|
||||||
|
)),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return Err(QueryError("Missing argument for search prefix".into()));
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return Err(QueryError("Missing argument for search prefix".into()));
|
||||||
}
|
}
|
||||||
} else if inside_quotation {
|
|
||||||
// Uh oh, it's empty with quotes!
|
|
||||||
return Err(QueryError("Missing closing quotation".into()));
|
|
||||||
}
|
}
|
||||||
|
} else if inside_quotation {
|
||||||
Err(QueryError("Invalid query".into()))
|
// Uh oh, it's empty with quotes!
|
||||||
|
return Err(QueryError("Missing closing quotation".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut split_query = VecDeque::new();
|
Err(QueryError("Invalid query".into()))
|
||||||
|
|
||||||
self.get_current_search_query()
|
|
||||||
.split_whitespace()
|
|
||||||
.for_each(|s| {
|
|
||||||
// From https://stackoverflow.com/a/56923739 in order to get a split but include the parentheses
|
|
||||||
let mut last = 0;
|
|
||||||
for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) {
|
|
||||||
if last != index {
|
|
||||||
split_query.push_back(s[last..index].to_owned());
|
|
||||||
}
|
|
||||||
split_query.push_back(matched.to_owned());
|
|
||||||
last = index + matched.len();
|
|
||||||
}
|
|
||||||
if last < s.len() {
|
|
||||||
split_query.push_back(s[last..].to_owned());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut process_filter = process_string_to_filter(&mut split_query)?;
|
|
||||||
process_filter.process_regexes(
|
|
||||||
self.process_search_state.is_searching_whole_word,
|
|
||||||
self.process_search_state.is_ignoring_case,
|
|
||||||
self.process_search_state.is_searching_with_regex,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(process_filter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut split_query = VecDeque::new();
|
||||||
|
|
||||||
|
search_query.split_whitespace().for_each(|s| {
|
||||||
|
// From https://stackoverflow.com/a/56923739 in order to get a split but include the parentheses
|
||||||
|
let mut last = 0;
|
||||||
|
for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) {
|
||||||
|
if last != index {
|
||||||
|
split_query.push_back(s[last..index].to_owned());
|
||||||
|
}
|
||||||
|
split_query.push_back(matched.to_owned());
|
||||||
|
last = index + matched.len();
|
||||||
|
}
|
||||||
|
if last < s.len() {
|
||||||
|
split_query.push_back(s[last..].to_owned());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut process_filter = process_string_to_filter(&mut split_query)?;
|
||||||
|
process_filter.process_regexes(
|
||||||
|
is_searching_whole_word,
|
||||||
|
is_ignoring_case,
|
||||||
|
is_searching_with_regex,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
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,
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
681
src/app/states/table_state.rs
Normal file
681
src/app/states/table_state.rs
Normal 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
2
src/app/widgets.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod process_widget;
|
||||||
|
pub use process_widget::*;
|
1164
src/app/widgets/process_widget.rs
Normal file
1164
src/app/widgets/process_widget.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -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 => {
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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.
|
||||||
@ -621,20 +615,20 @@ mod test {
|
|||||||
assert_eq!(interpolate_point(&data[0], &data[1], -3.0), 8.0);
|
assert_eq!(interpolate_point(&data[0], &data[1], -3.0), 8.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn time_chart_empty_dataset() {
|
||||||
|
let data = [];
|
||||||
|
let dataset = Dataset::default().data(&data);
|
||||||
|
|
||||||
|
assert_eq!(get_start(&dataset, -100.0), (0, None));
|
||||||
|
assert_eq!(get_start(&dataset, -3.0), (0, None));
|
||||||
|
|
||||||
|
assert_eq!(get_end(&dataset, 0.0), (0, None));
|
||||||
|
assert_eq!(get_end(&dataset, 100.0), (0, None));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn time_chart_test_data_trimming() {
|
fn time_chart_test_data_trimming() {
|
||||||
// Quick test on a completely empty dataset...
|
|
||||||
{
|
|
||||||
let data = [];
|
|
||||||
let dataset = Dataset::default().data(&data);
|
|
||||||
|
|
||||||
assert_eq!(get_start(&dataset, -100.0), (0, None));
|
|
||||||
assert_eq!(get_start(&dataset, -3.0), (0, None));
|
|
||||||
|
|
||||||
assert_eq!(get_end(&dataset, 0.0), (0, None));
|
|
||||||
assert_eq!(get_end(&dataset, 100.0), (0, None));
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = [
|
let data = [
|
||||||
(-3.0, 8.0),
|
(-3.0, 8.0),
|
||||||
(-2.5, 15.0),
|
(-2.5, 15.0),
|
||||||
|
@ -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()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.title(self.generate_title(draw_loc))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(self.border_style);
|
||||||
|
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
TimeChart::new(data)
|
TimeChart::new(data)
|
||||||
.block(
|
.block(block)
|
||||||
Block::default()
|
|
||||||
.title(self.generate_title(draw_loc))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(self.border_style),
|
|
||||||
)
|
|
||||||
.x_axis(x_axis)
|
.x_axis(x_axis)
|
||||||
.y_axis(y_axis)
|
.y_axis(y_axis)
|
||||||
.hidden_legend_constraints(
|
.hidden_legend_constraints(
|
||||||
|
@ -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.",
|
||||||
|
@ -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],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
|
@ -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,6 +118,56 @@ impl Painter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_points<'a>(
|
||||||
|
&self, cpu_widget_state: &CpuWidgetState, cpu_data: &'a [ConvertedCpuData],
|
||||||
|
show_avg_cpu: bool,
|
||||||
|
) -> Vec<GraphData<'a>> {
|
||||||
|
let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 };
|
||||||
|
|
||||||
|
let current_scroll_position = cpu_widget_state.table_state.current_scroll_position;
|
||||||
|
if current_scroll_position == ALL_POSITION {
|
||||||
|
// This case ensures the other cases cannot have the position be equal to 0.
|
||||||
|
cpu_data
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.rev()
|
||||||
|
.map(|(itx, cpu)| {
|
||||||
|
let style = if show_avg_cpu && itx == AVG_POSITION {
|
||||||
|
self.colours.avg_colour_style
|
||||||
|
} else if itx == ALL_POSITION {
|
||||||
|
self.colours.all_colour_style
|
||||||
|
} else {
|
||||||
|
let offset_position = itx - 1; // Because of the all position
|
||||||
|
self.colours.cpu_colour_styles[(offset_position - show_avg_offset)
|
||||||
|
% self.colours.cpu_colour_styles.len()]
|
||||||
|
};
|
||||||
|
|
||||||
|
GraphData {
|
||||||
|
points: &cpu.cpu_data[..],
|
||||||
|
style,
|
||||||
|
name: None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
} else if let Some(cpu) = cpu_data.get(current_scroll_position) {
|
||||||
|
let style = if show_avg_cpu && current_scroll_position == AVG_POSITION {
|
||||||
|
self.colours.avg_colour_style
|
||||||
|
} else {
|
||||||
|
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.len()]
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![GraphData {
|
||||||
|
points: &cpu.cpu_data[..],
|
||||||
|
style,
|
||||||
|
name: None,
|
||||||
|
}]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_cpu_graph<B: Backend>(
|
fn draw_cpu_graph<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,
|
||||||
) {
|
) {
|
||||||
@ -122,7 +175,7 @@ impl Painter {
|
|||||||
const Y_LABELS: [Cow<'static, str>; 2] = [Cow::Borrowed(" 0%"), Cow::Borrowed("100%")];
|
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) {
|
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 cpu_data = &app_state.converted_data.cpu_data;
|
||||||
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, cpu_widget_state.current_display_time];
|
let x_bounds = [0, cpu_widget_state.current_display_time];
|
||||||
let hide_x_labels = should_hide_x_label(
|
let hide_x_labels = should_hide_x_label(
|
||||||
@ -131,56 +184,16 @@ impl Painter {
|
|||||||
&mut cpu_widget_state.autohide_timer,
|
&mut cpu_widget_state.autohide_timer,
|
||||||
draw_loc,
|
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 points = {
|
|
||||||
let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position;
|
|
||||||
if current_scroll_position == ALL_POSITION {
|
|
||||||
// This case ensures the other cases cannot have the position be equal to 0.
|
|
||||||
cpu_data
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.rev()
|
|
||||||
.map(|(itx, cpu)| {
|
|
||||||
let style = if show_avg_cpu && itx == AVG_POSITION {
|
|
||||||
self.colours.avg_colour_style
|
|
||||||
} else if itx == ALL_POSITION {
|
|
||||||
self.colours.all_colour_style
|
|
||||||
} else {
|
|
||||||
let offset_position = itx - 1; // Because of the all position
|
|
||||||
self.colours.cpu_colour_styles[(offset_position - show_avg_offset)
|
|
||||||
% self.colours.cpu_colour_styles.len()]
|
|
||||||
};
|
|
||||||
|
|
||||||
GraphData {
|
let points = self.generate_points(
|
||||||
points: &cpu.cpu_data[..],
|
cpu_widget_state,
|
||||||
style,
|
cpu_data,
|
||||||
name: None,
|
app_state.app_config_fields.show_average_cpu,
|
||||||
}
|
);
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
} else if let Some(cpu) = cpu_data.get(current_scroll_position) {
|
|
||||||
let style = if show_avg_cpu && current_scroll_position == AVG_POSITION {
|
|
||||||
self.colours.avg_colour_style
|
|
||||||
} else {
|
|
||||||
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.len()]
|
|
||||||
};
|
|
||||||
|
|
||||||
vec![GraphData {
|
|
||||||
points: &cpu.cpu_data[..],
|
|
||||||
style,
|
|
||||||
name: None,
|
|
||||||
}]
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
Text::raw(&cpu.cpu_name)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text::raw(&cpu.cpu_name)
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_first_column_hidden = if let Some(calculated_column_width) = ccw.get(0) {
|
|
||||||
*calculated_column_width == 0
|
|
||||||
} else {
|
} else {
|
||||||
false
|
Either::Right(
|
||||||
|
iter::once(&self.colours.all_colour_style)
|
||||||
|
.chain(self.colours.cpu_colour_styles.iter().cycle()),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let truncated_legend = if is_first_column_hidden && cpu.legend_value.is_empty() {
|
let data = {
|
||||||
// For the case where we only have room for one column, display "All" in the normally blank area.
|
let iter = app_state.converted_data.cpu_data.iter().zip(colour_iter);
|
||||||
Text::raw("All")
|
const CPU_WIDTH_CHECK: u16 = 10; // This is hard-coded, it's terrible.
|
||||||
} else {
|
if draw_loc.width < CPU_WIDTH_CHECK {
|
||||||
Text::raw(&cpu.legend_value)
|
Either::Left(iter.map(|(cpu, style)| {
|
||||||
};
|
let row = vec![
|
||||||
|
CellContent::Simple("".into()),
|
||||||
if !is_first_column_hidden
|
CellContent::Simple(if cpu.legend_value.is_empty() {
|
||||||
&& itx == offset_scroll_index
|
cpu.cpu_name.clone().into()
|
||||||
&& itx + start_position == ALL_POSITION
|
} else {
|
||||||
{
|
cpu.legend_value.clone().into()
|
||||||
truncated_name.patch_style(self.colours.currently_selected_text_style);
|
}),
|
||||||
Row::new(vec![truncated_name, truncated_legend])
|
];
|
||||||
} else {
|
TableRow::Styled(row, *style)
|
||||||
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 {
|
} else {
|
||||||
self.colours.cpu_colour_styles[(itx + start_position - ALL_POSITION - 1)
|
Either::Right(iter.map(|(cpu, style)| {
|
||||||
% self.colours.cpu_colour_styles.len()]
|
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();
|
||||||
|
|
||||||
// Note we don't set highlight_style, as it should always be shown for this widget.
|
TableData { data, col_widths }
|
||||||
let border_and_title_style = if is_on_widget {
|
};
|
||||||
|
|
||||||
|
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
||||||
|
let border_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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
border_style,
|
||||||
.current_scroll_position
|
highlighted_text_style,
|
||||||
.saturating_add(1),
|
title: Some(TextTableTitle {
|
||||||
app_state.canvas_data.disk_data.len()
|
title: " Disks ".into(),
|
||||||
);
|
is_expanded: app_state.is_expanded,
|
||||||
|
}),
|
||||||
if title_string.len() <= draw_loc.width.into() {
|
is_on_widget,
|
||||||
title_string
|
draw_border,
|
||||||
} else {
|
show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
|
||||||
" Disk ".to_string()
|
title_style: self.colours.widget_title_style,
|
||||||
}
|
text_style: self.colours.text_style,
|
||||||
} else {
|
left_to_right: true,
|
||||||
" 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,
|
|
||||||
),
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
Spans::from(Span::styled(title_base, self.colours.widget_title_style))
|
|
||||||
};
|
|
||||||
|
|
||||||
let disk_block = if draw_border {
|
|
||||||
Block::default()
|
|
||||||
.title(title)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(border_style)
|
|
||||||
} else if is_on_widget {
|
|
||||||
Block::default()
|
|
||||||
.borders(SIDE_BORDERS)
|
|
||||||
.border_style(self.colours.highlighted_border_style)
|
|
||||||
} else {
|
|
||||||
Block::default().borders(Borders::NONE)
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.draw_text_table(
|
||||||
|
f,
|
||||||
|
draw_loc,
|
||||||
|
&mut disk_widget_state.table_state,
|
||||||
|
&app_state.converted_data.disk_data,
|
||||||
|
app_state.widget_map.get_mut(&widget_id),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()),
|
||||||
});
|
});
|
||||||
|
@ -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)),
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
|
||||||
&& !proc_widget_state
|
|
||||||
.process_search_state
|
|
||||||
.search_state
|
|
||||||
.is_enabled
|
|
||||||
&& !proc_widget_state.is_sort_open
|
|
||||||
{
|
|
||||||
const ESCAPE_ENDING: &str = "── Esc to go back ";
|
|
||||||
|
|
||||||
let (chosen_title_base, expanded_title_base) = {
|
|
||||||
let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING);
|
|
||||||
|
|
||||||
if temp_title_base.len() > draw_loc.width.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,
|
|
||||||
),
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
Spans::from(Span::styled(title_base, self.colours.widget_title_style))
|
|
||||||
};
|
|
||||||
|
|
||||||
let process_block = if draw_border {
|
|
||||||
Block::default()
|
|
||||||
.title(title)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(border_style)
|
|
||||||
} else if is_on_widget {
|
|
||||||
Block::default()
|
|
||||||
.borders(SIDE_BORDERS)
|
|
||||||
.border_style(self.colours.highlighted_border_style)
|
|
||||||
} else {
|
|
||||||
Block::default().borders(Borders::NONE)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(process_data) = &app_state
|
|
||||||
.canvas_data
|
|
||||||
.stringified_process_data_map
|
|
||||||
.get(&widget_id)
|
|
||||||
{
|
|
||||||
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
app_state.app_config_fields.table_gap
|
|
||||||
};
|
|
||||||
let position = get_start_position(
|
|
||||||
usize::from(
|
|
||||||
(draw_loc.height + (1 - table_gap))
|
|
||||||
.saturating_sub(self.table_height_offset),
|
|
||||||
),
|
|
||||||
&proc_widget_state.scroll_state.scroll_direction,
|
|
||||||
&mut proc_widget_state.scroll_state.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,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: Some(TextTableTitle {
|
||||||
|
title: " Processes ".into(),
|
||||||
|
is_expanded: app_state.is_expanded,
|
||||||
|
}),
|
||||||
|
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.table_state,
|
||||||
|
&proc_widget_state.table_data,
|
||||||
|
app_state.widget_map.get_mut(&widget_id),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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,17 +274,14 @@ 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
|
self.colours.invalid_query_style
|
||||||
.is_invalid_search
|
} else if is_on_widget {
|
||||||
{
|
self.colours.highlighted_border_style
|
||||||
self.colours.invalid_query_style
|
} else {
|
||||||
} else if is_on_widget {
|
self.colours.border_style
|
||||||
self.colours.highlighted_border_style
|
};
|
||||||
} else {
|
|
||||||
self.colours.border_style
|
|
||||||
};
|
|
||||||
|
|
||||||
let title = Span::styled(
|
let title = Span::styled(
|
||||||
if draw_border {
|
if draw_border {
|
||||||
@ -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 {
|
||||||
|
(
|
||||||
|
self.colours.highlighted_border_style,
|
||||||
|
self.colours.currently_selected_text_style,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
app_state.app_config_fields.table_gap
|
(self.colours.border_style, self.colours.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..];
|
// TODO: [PROC] Perhaps move this generation elsewhere... or leave it as is but look at partial rendering?
|
||||||
|
let table_data = {
|
||||||
let sort_options = sliced_vec
|
let data = proc_widget_state
|
||||||
.iter()
|
.table_state
|
||||||
.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
|
.columns
|
||||||
.current_scroll_position
|
.iter()
|
||||||
.saturating_sub(start_position),
|
.filter_map(|col| {
|
||||||
));
|
if col.is_hidden {
|
||||||
let current_border_style = if proc_widget_state
|
None
|
||||||
.process_search_state
|
} else {
|
||||||
.search_state
|
Some(TableRow::Raw(vec![col.header.text().clone()]))
|
||||||
.is_invalid_search
|
}
|
||||||
{
|
})
|
||||||
self.colours.invalid_query_style
|
.collect();
|
||||||
} else if is_on_widget {
|
|
||||||
self.colours.highlighted_border_style
|
|
||||||
} else {
|
|
||||||
self.colours.border_style
|
|
||||||
};
|
|
||||||
|
|
||||||
let process_sort_block = if draw_border {
|
TableData {
|
||||||
Block::default()
|
data,
|
||||||
.borders(Borders::ALL)
|
col_widths: vec![usize::from(SORT_MENU_WIDTH)],
|
||||||
.border_style(current_border_style)
|
|
||||||
} else if is_on_widget {
|
|
||||||
Block::default()
|
|
||||||
.borders(SIDE_BORDERS)
|
|
||||||
.border_style(current_border_style)
|
|
||||||
} else {
|
|
||||||
Block::default().borders(Borders::NONE)
|
|
||||||
};
|
|
||||||
|
|
||||||
let highlight_style = if is_on_widget {
|
|
||||||
self.colours.currently_selected_text_style
|
|
||||||
} else {
|
|
||||||
self.colours.text_style
|
|
||||||
};
|
|
||||||
|
|
||||||
let margined_draw_loc = Layout::default()
|
|
||||||
.constraints([Constraint::Percentage(100)])
|
|
||||||
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.split(draw_loc)[0];
|
|
||||||
|
|
||||||
f.render_stateful_widget(
|
|
||||||
Table::new(sort_options)
|
|
||||||
.header(
|
|
||||||
Row::new(vec!["Sort By"])
|
|
||||||
.style(self.colours.table_header_style)
|
|
||||||
.bottom_margin(table_gap),
|
|
||||||
)
|
|
||||||
.block(process_sort_block)
|
|
||||||
.highlight_style(highlight_style)
|
|
||||||
.style(self.colours.text_style)
|
|
||||||
.widths(&[Constraint::Percentage(100)]),
|
|
||||||
margined_draw_loc,
|
|
||||||
column_state,
|
|
||||||
);
|
|
||||||
|
|
||||||
if app_state.should_get_widget_bounds() {
|
|
||||||
// Update draw loc in widget map
|
|
||||||
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
|
|
||||||
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
|
|
||||||
widget.bottom_right_corner = Some((
|
|
||||||
margined_draw_loc.x + margined_draw_loc.width,
|
|
||||||
margined_draw_loc.y + margined_draw_loc.height,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
border_style,
|
||||||
.current_scroll_position
|
highlighted_text_style,
|
||||||
.saturating_add(1),
|
title: Some(TextTableTitle {
|
||||||
app_state.canvas_data.temp_sensor_data.len()
|
title: " Temperatures ".into(),
|
||||||
);
|
is_expanded: app_state.is_expanded,
|
||||||
|
}),
|
||||||
if title_string.len() <= draw_loc.width.into() {
|
is_on_widget,
|
||||||
title_string
|
draw_border,
|
||||||
} else {
|
show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
|
||||||
" Temperatures ".to_string()
|
title_style: self.colours.widget_title_style,
|
||||||
}
|
text_style: self.colours.text_style,
|
||||||
} else {
|
left_to_right: false,
|
||||||
" 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,
|
|
||||||
),
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
Spans::from(Span::styled(title_base, self.colours.widget_title_style))
|
|
||||||
};
|
|
||||||
|
|
||||||
let temp_block = if draw_border {
|
|
||||||
Block::default()
|
|
||||||
.title(title)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(border_style)
|
|
||||||
} else if is_on_widget {
|
|
||||||
Block::default()
|
|
||||||
.borders(SIDE_BORDERS)
|
|
||||||
.border_style(self.colours.highlighted_border_style)
|
|
||||||
} else {
|
|
||||||
Block::default().borders(Borders::NONE)
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.draw_text_table(
|
||||||
|
f,
|
||||||
|
draw_loc,
|
||||||
|
&mut temp_widget_state.table_state,
|
||||||
|
&app_state.converted_data.temp_sensor_data,
|
||||||
|
app_state.widget_map.get_mut(&widget_id),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
287
src/lib.rs
287
src/lib.rs
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
} else {
|
||||||
) -> std::cmp::Ordering {
|
partial_ordering
|
||||||
match a_val.partial_cmp(&b_val) {
|
}
|
||||||
Some(x) => match x {
|
}
|
||||||
Ordering::Greater => {
|
|
||||||
if reverse_order {
|
/// Returns an [`Ordering`] between two [`PartialOrd`]s.
|
||||||
std::cmp::Ordering::Less
|
#[inline]
|
||||||
} else {
|
pub fn partial_ordering<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering {
|
||||||
std::cmp::Ordering::Greater
|
// TODO: Switch to `total_cmp` on 1.62
|
||||||
}
|
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
|
||||||
}
|
}
|
||||||
Ordering::Less => {
|
|
||||||
if reverse_order {
|
/// Returns a reversed [`Ordering`] between two [`PartialOrd`]s.
|
||||||
std::cmp::Ordering::Greater
|
///
|
||||||
} else {
|
/// This is simply a wrapper function around [`partial_ordering`] that reverses
|
||||||
std::cmp::Ordering::Less
|
/// the result.
|
||||||
}
|
#[inline]
|
||||||
}
|
pub fn partial_ordering_rev<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering {
|
||||||
Ordering::Equal => Ordering::Equal,
|
partial_ordering(a, b).reverse()
|
||||||
},
|
}
|
||||||
None => Ordering::Equal,
|
|
||||||
|
#[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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user