mirror of
https://github.com/ClementTsang/bottom
synced 2024-11-24 05:03:06 +00:00
refactor: tables V2 (#749)
* refactor: move to new data table implementation * more work towards refactor * move disk and temp over, fix longstanding bug with disk and temp if removing the last value and selected * work towards porting over CPU work towards porting over CPU fix typo partially port over cpu, fix some potentially inefficient concat_string calls more work towards cpu widget migration some refactoring * sortable data sortable data more refactoring some sort refactoring more refactoringgggg column refactoring renaming and reorganizing more refactoring regarding column logic add sort arrows again * move over sort menu * port over process port over process precommit temp temp two, remember to squash work fix broken ltr calculation and CPU hiding add back row styling temp fix a bunch of issues, get proc working more fixes around click fix frozen issues * fix dd process killing * revert some of the persistent config changes from #257 * fix colouring for trees * fix missing entries in tree * keep columns if there is no data * add and remove tests * Fix ellipsis
This commit is contained in:
parent
1e5f0ea2d9
commit
2a740f48f7
49 changed files with 3797 additions and 3505 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -225,6 +225,7 @@ dependencies = [
|
|||
"heim",
|
||||
"indexmap",
|
||||
"itertools",
|
||||
"kstring",
|
||||
"libc",
|
||||
"log",
|
||||
"mach2",
|
||||
|
@ -906,6 +907,15 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
|
||||
|
||||
[[package]]
|
||||
name = "kstring"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747"
|
||||
dependencies = [
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
|
|
|
@ -67,6 +67,7 @@ futures-timer = "3.0.2"
|
|||
fxhash = "0.2.1"
|
||||
indexmap = "1.8.1"
|
||||
itertools = "0.10.3"
|
||||
kstring = { version = "2.0.0", features = ["arc"] }
|
||||
log = { version = "0.4.16", optional = true }
|
||||
nvml-wrapper = { version = "0.7.0", optional = true }
|
||||
once_cell = "1.5.2"
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
cognitive-complexity-threshold = 100
|
||||
type-complexity-threshold = 500
|
||||
too-many-arguments-threshold = 8
|
||||
|
|
320
src/app.rs
320
src/app.rs
|
@ -1,7 +1,6 @@
|
|||
use std::{
|
||||
cmp::{max, min},
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
|
@ -16,12 +15,8 @@ use layout_manager::*;
|
|||
pub use states::*;
|
||||
|
||||
use crate::{
|
||||
components::text_table::SortState,
|
||||
constants,
|
||||
data_conversion::ConvertedData,
|
||||
options::Config,
|
||||
options::ConfigFlags,
|
||||
options::WidgetIdEnabled,
|
||||
units::data_units::DataUnit,
|
||||
utils::error::{BottomError, Result},
|
||||
Pid,
|
||||
|
@ -31,12 +26,15 @@ use self::widgets::{ProcWidget, ProcWidgetMode};
|
|||
|
||||
pub mod data_farmer;
|
||||
pub mod data_harvester;
|
||||
pub mod frozen_state;
|
||||
pub mod layout_manager;
|
||||
mod process_killer;
|
||||
pub mod query;
|
||||
pub mod states;
|
||||
pub mod widgets;
|
||||
|
||||
use frozen_state::FrozenState;
|
||||
|
||||
const MAX_SEARCH_LENGTH: usize = 200;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -45,9 +43,15 @@ pub enum AxisScaling {
|
|||
Linear,
|
||||
}
|
||||
|
||||
impl Default for AxisScaling {
|
||||
fn default() -> Self {
|
||||
AxisScaling::Log
|
||||
}
|
||||
}
|
||||
|
||||
/// AppConfigFields is meant to cover basic fields that would normally be set
|
||||
/// by config files or launch options.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AppConfigFields {
|
||||
pub update_rate_in_milliseconds: u64,
|
||||
pub temperature_type: temperature::TemperatureType,
|
||||
|
@ -95,14 +99,15 @@ pub struct App {
|
|||
#[builder(default, setter(skip))]
|
||||
second_char: Option<char>,
|
||||
|
||||
// FIXME: The way we do deletes is really gross.
|
||||
#[builder(default, setter(skip))]
|
||||
pub dd_err: Option<String>,
|
||||
|
||||
#[builder(default, setter(skip))]
|
||||
to_delete_process_list: Option<(String, Vec<Pid>)>,
|
||||
|
||||
#[builder(default = false, setter(skip))]
|
||||
pub is_frozen: bool,
|
||||
#[builder(default, setter(skip))]
|
||||
pub frozen_state: FrozenState,
|
||||
|
||||
#[builder(default = Instant::now(), setter(skip))]
|
||||
last_key_press: Instant,
|
||||
|
@ -148,8 +153,6 @@ pub struct App {
|
|||
pub current_widget: BottomWidget,
|
||||
pub used_widgets: UsedWidgets,
|
||||
pub filters: DataFilters,
|
||||
pub config: Config, // TODO: Is this even used...?
|
||||
pub config_path: Option<PathBuf>, // TODO: Is this even used...?
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
|
@ -184,7 +187,7 @@ impl App {
|
|||
self.dd_err = None;
|
||||
|
||||
// Unfreeze.
|
||||
self.is_frozen = false;
|
||||
self.frozen_state.thaw();
|
||||
|
||||
// Reset zoom
|
||||
self.reset_cpu_zoom();
|
||||
|
@ -293,26 +296,14 @@ impl App {
|
|||
// Allow usage whilst only in processes
|
||||
|
||||
if !self.ignore_normal_keybinds() {
|
||||
match self.current_widget.widget_type {
|
||||
BottomWidgetType::Cpu => {
|
||||
if let Some(cpu_widget_state) = self
|
||||
.cpu_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
cpu_widget_state.is_multi_graph_mode =
|
||||
!cpu_widget_state.is_multi_graph_mode;
|
||||
}
|
||||
}
|
||||
BottomWidgetType::Proc => {
|
||||
if let BottomWidgetType::Proc = self.current_widget.widget_type {
|
||||
if let Some(proc_widget_state) = self
|
||||
.proc_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
proc_widget_state.toggle_tab();
|
||||
proc_widget_state.on_tab();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -338,7 +329,7 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn toggle_sort(&mut self) {
|
||||
pub fn toggle_sort_menu(&mut self) {
|
||||
let widget_id = self.current_widget.widget_id
|
||||
- match &self.current_widget.widget_type {
|
||||
BottomWidgetType::Proc => 0,
|
||||
|
@ -352,12 +343,7 @@ impl App {
|
|||
|
||||
// If the sort is now open, move left. Otherwise, if the proc sort was selected, force move right.
|
||||
if pws.is_sort_open {
|
||||
if let SortState::Sortable(st) = &pws.table_state.sort_state {
|
||||
pws.sort_table_state.scroll_bar = 0;
|
||||
pws.sort_table_state.current_scroll_position = st
|
||||
.current_index
|
||||
.clamp(0, pws.num_enabled_columns().saturating_sub(1));
|
||||
}
|
||||
pws.sort_table.set_position(pws.table.sort_index());
|
||||
self.move_widget_selection(&WidgetDirection::Left);
|
||||
} else if let BottomWidgetType::ProcSort = self.current_widget.widget_type {
|
||||
self.move_widget_selection(&WidgetDirection::Right);
|
||||
|
@ -376,13 +362,9 @@ impl App {
|
|||
_ => 0,
|
||||
};
|
||||
|
||||
if let Some(proc_widget_state) = self.proc_state.get_mut_widget_state(widget_id) {
|
||||
if let SortState::Sortable(state) =
|
||||
&mut proc_widget_state.table_state.sort_state
|
||||
{
|
||||
state.toggle_order();
|
||||
proc_widget_state.force_data_update();
|
||||
}
|
||||
if let Some(pws) = self.proc_state.get_mut_widget_state(widget_id) {
|
||||
pws.table.toggle_order();
|
||||
pws.force_data_update();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
@ -410,7 +392,6 @@ impl App {
|
|||
|
||||
pub fn toggle_ignore_case(&mut self) {
|
||||
let is_in_search_widget = self.is_in_search_widget();
|
||||
let mut is_case_sensitive: Option<bool> = None;
|
||||
if let Some(proc_widget_state) = self
|
||||
.proc_state
|
||||
.widget_states
|
||||
|
@ -419,48 +400,12 @@ impl App {
|
|||
if is_in_search_widget && proc_widget_state.is_search_enabled() {
|
||||
proc_widget_state.proc_search.search_toggle_ignore_case();
|
||||
proc_widget_state.update_query();
|
||||
|
||||
// Remember, it's the opposite (ignoring case is case "in"sensitive)
|
||||
is_case_sensitive = Some(!proc_widget_state.proc_search.is_ignoring_case);
|
||||
}
|
||||
}
|
||||
|
||||
// Also toggle it in the config file if we actually changed it.
|
||||
if let Some(is_ignoring_case) = is_case_sensitive {
|
||||
if let Some(flags) = &mut self.config.flags {
|
||||
if let Some(map) = &mut flags.search_case_enabled_widgets_map {
|
||||
// Just update the map.
|
||||
let mapping = map.entry(self.current_widget.widget_id - 1).or_default();
|
||||
*mapping = is_ignoring_case;
|
||||
|
||||
flags.search_case_enabled_widgets =
|
||||
Some(WidgetIdEnabled::create_from_hashmap(map));
|
||||
} else {
|
||||
// Map doesn't exist yet... initialize ourselves.
|
||||
let mut map = HashMap::default();
|
||||
map.insert(self.current_widget.widget_id - 1, is_ignoring_case);
|
||||
flags.search_case_enabled_widgets =
|
||||
Some(WidgetIdEnabled::create_from_hashmap(&map));
|
||||
flags.search_case_enabled_widgets_map = Some(map);
|
||||
}
|
||||
} else {
|
||||
// Must initialize it ourselves...
|
||||
let mut map = HashMap::default();
|
||||
map.insert(self.current_widget.widget_id - 1, is_ignoring_case);
|
||||
|
||||
self.config.flags = Some(
|
||||
ConfigFlags::builder()
|
||||
.search_case_enabled_widgets(WidgetIdEnabled::create_from_hashmap(&map))
|
||||
.search_case_enabled_widgets_map(map)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_search_whole_word(&mut self) {
|
||||
let is_in_search_widget = self.is_in_search_widget();
|
||||
let mut is_searching_whole_word: Option<bool> = None;
|
||||
if let Some(proc_widget_state) = self
|
||||
.proc_state
|
||||
.widget_states
|
||||
|
@ -469,52 +414,12 @@ impl App {
|
|||
if is_in_search_widget && proc_widget_state.is_search_enabled() {
|
||||
proc_widget_state.proc_search.search_toggle_whole_word();
|
||||
proc_widget_state.update_query();
|
||||
|
||||
is_searching_whole_word =
|
||||
Some(proc_widget_state.proc_search.is_searching_whole_word);
|
||||
}
|
||||
}
|
||||
|
||||
// Also toggle it in the config file if we actually changed it.
|
||||
if let Some(is_searching_whole_word) = is_searching_whole_word {
|
||||
if let Some(flags) = &mut self.config.flags {
|
||||
if let Some(map) = &mut flags.search_whole_word_enabled_widgets_map {
|
||||
// Just update the map.
|
||||
let mapping = map.entry(self.current_widget.widget_id - 1).or_default();
|
||||
*mapping = is_searching_whole_word;
|
||||
|
||||
flags.search_whole_word_enabled_widgets =
|
||||
Some(WidgetIdEnabled::create_from_hashmap(map));
|
||||
} else {
|
||||
// Map doesn't exist yet... initialize ourselves.
|
||||
let mut map = HashMap::default();
|
||||
map.insert(self.current_widget.widget_id - 1, is_searching_whole_word);
|
||||
flags.search_whole_word_enabled_widgets =
|
||||
Some(WidgetIdEnabled::create_from_hashmap(&map));
|
||||
flags.search_whole_word_enabled_widgets_map = Some(map);
|
||||
}
|
||||
} else {
|
||||
// Must initialize it ourselves...
|
||||
let mut map = HashMap::default();
|
||||
map.insert(self.current_widget.widget_id - 1, is_searching_whole_word);
|
||||
|
||||
self.config.flags = Some(
|
||||
ConfigFlags::builder()
|
||||
.search_whole_word_enabled_widgets(WidgetIdEnabled::create_from_hashmap(
|
||||
&map,
|
||||
))
|
||||
.search_whole_word_enabled_widgets_map(map)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
// self.did_config_fail_to_save = self.update_config_file().is_err();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_search_regex(&mut self) {
|
||||
let is_in_search_widget = self.is_in_search_widget();
|
||||
let mut is_searching_with_regex: Option<bool> = None;
|
||||
if let Some(proc_widget_state) = self
|
||||
.proc_state
|
||||
.widget_states
|
||||
|
@ -523,41 +428,6 @@ impl App {
|
|||
if is_in_search_widget && proc_widget_state.is_search_enabled() {
|
||||
proc_widget_state.proc_search.search_toggle_regex();
|
||||
proc_widget_state.update_query();
|
||||
|
||||
is_searching_with_regex =
|
||||
Some(proc_widget_state.proc_search.is_searching_with_regex);
|
||||
}
|
||||
}
|
||||
|
||||
// Also toggle it in the config file if we actually changed it.
|
||||
if let Some(is_searching_whole_word) = is_searching_with_regex {
|
||||
if let Some(flags) = &mut self.config.flags {
|
||||
if let Some(map) = &mut flags.search_regex_enabled_widgets_map {
|
||||
// Just update the map.
|
||||
let mapping = map.entry(self.current_widget.widget_id - 1).or_default();
|
||||
*mapping = is_searching_whole_word;
|
||||
|
||||
flags.search_regex_enabled_widgets =
|
||||
Some(WidgetIdEnabled::create_from_hashmap(map));
|
||||
} else {
|
||||
// Map doesn't exist yet... initialize ourselves.
|
||||
let mut map = HashMap::default();
|
||||
map.insert(self.current_widget.widget_id - 1, is_searching_whole_word);
|
||||
flags.search_regex_enabled_widgets =
|
||||
Some(WidgetIdEnabled::create_from_hashmap(&map));
|
||||
flags.search_regex_enabled_widgets_map = Some(map);
|
||||
}
|
||||
} else {
|
||||
// Must initialize it ourselves...
|
||||
let mut map = HashMap::default();
|
||||
map.insert(self.current_widget.widget_id - 1, is_searching_whole_word);
|
||||
|
||||
self.config.flags = Some(
|
||||
ConfigFlags::builder()
|
||||
.search_regex_enabled_widgets(WidgetIdEnabled::create_from_hashmap(&map))
|
||||
.search_regex_enabled_widgets_map(map)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1191,26 +1061,15 @@ impl App {
|
|||
.widget_states
|
||||
.get(&self.current_widget.widget_id)
|
||||
{
|
||||
if let Some(table_row) = pws
|
||||
.table_data
|
||||
.data
|
||||
.get(pws.table_state.current_scroll_position)
|
||||
if let Some(current) = pws.table.current_item() {
|
||||
let id = current.id.to_string();
|
||||
if let Some(pids) = pws
|
||||
.id_pid_map
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.or_else(|| Some(vec![current.pid]))
|
||||
{
|
||||
if let Some(col_value) = table_row.row().get(ProcWidget::PROC_NAME_OR_CMD) {
|
||||
let val = col_value.main_text().to_string();
|
||||
if pws.is_using_command() {
|
||||
if let Some(pids) = self.data_collection.process_data.cmd_pid_map.get(&val)
|
||||
{
|
||||
let current_process = (val, pids.clone());
|
||||
|
||||
self.to_delete_process_list = Some(current_process);
|
||||
self.delete_dialog_state.is_showing_dd = true;
|
||||
self.is_determining_widget_boundary = true;
|
||||
}
|
||||
} else if let Some(pids) =
|
||||
self.data_collection.process_data.name_pid_map.get(&val)
|
||||
{
|
||||
let current_process = (val, pids.clone());
|
||||
let current_process = (id, pids);
|
||||
|
||||
self.to_delete_process_list = Some(current_process);
|
||||
self.delete_dialog_state.is_showing_dd = true;
|
||||
|
@ -1218,7 +1077,7 @@ impl App {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXME: This should handle errors.
|
||||
}
|
||||
|
||||
pub fn on_char_key(&mut self, caught_char: char) {
|
||||
|
@ -1382,12 +1241,7 @@ impl App {
|
|||
'k' => self.on_up_key(),
|
||||
'j' => self.on_down_key(),
|
||||
'f' => {
|
||||
self.is_frozen = !self.is_frozen;
|
||||
if self.is_frozen {
|
||||
self.data_collection.freeze();
|
||||
} else {
|
||||
self.data_collection.thaw();
|
||||
}
|
||||
self.frozen_state.toggle(&self.data_collection);
|
||||
}
|
||||
'c' => {
|
||||
if let BottomWidgetType::Proc = self.current_widget.widget_type {
|
||||
|
@ -1452,7 +1306,7 @@ impl App {
|
|||
'-' => self.on_minus(),
|
||||
'=' => self.reset_zoom(),
|
||||
'e' => self.toggle_expand_widget(),
|
||||
's' => self.toggle_sort(),
|
||||
's' => self.toggle_sort_menu(),
|
||||
'I' => self.invert_sort(),
|
||||
'%' => self.toggle_percentages(),
|
||||
_ => {}
|
||||
|
@ -1979,8 +1833,7 @@ impl App {
|
|||
.proc_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
proc_widget_state.table_state.current_scroll_position = 0;
|
||||
proc_widget_state.table_state.scroll_direction = ScrollDirection::Up;
|
||||
proc_widget_state.table.set_first();
|
||||
}
|
||||
}
|
||||
BottomWidgetType::ProcSort => {
|
||||
|
@ -1988,8 +1841,7 @@ impl App {
|
|||
.proc_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id - 2)
|
||||
{
|
||||
proc_widget_state.sort_table_state.current_scroll_position = 0;
|
||||
proc_widget_state.sort_table_state.scroll_direction = ScrollDirection::Up;
|
||||
proc_widget_state.sort_table.set_first();
|
||||
}
|
||||
}
|
||||
BottomWidgetType::Temp => {
|
||||
|
@ -1997,8 +1849,7 @@ impl App {
|
|||
.temp_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
temp_widget_state.table_state.current_scroll_position = 0;
|
||||
temp_widget_state.table_state.scroll_direction = ScrollDirection::Up;
|
||||
temp_widget_state.table.set_first();
|
||||
}
|
||||
}
|
||||
BottomWidgetType::Disk => {
|
||||
|
@ -2006,8 +1857,7 @@ impl App {
|
|||
.disk_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
disk_widget_state.table_state.current_scroll_position = 0;
|
||||
disk_widget_state.table_state.scroll_direction = ScrollDirection::Up;
|
||||
disk_widget_state.table.set_first();
|
||||
}
|
||||
}
|
||||
BottomWidgetType::CpuLegend => {
|
||||
|
@ -2015,8 +1865,7 @@ impl App {
|
|||
.cpu_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id - 1)
|
||||
{
|
||||
cpu_widget_state.table_state.current_scroll_position = 0;
|
||||
cpu_widget_state.table_state.scroll_direction = ScrollDirection::Up;
|
||||
cpu_widget_state.table.set_first();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2038,9 +1887,7 @@ impl App {
|
|||
.proc_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
proc_widget_state.table_state.current_scroll_position =
|
||||
proc_widget_state.table_data.data.len().saturating_sub(1);
|
||||
proc_widget_state.table_state.scroll_direction = ScrollDirection::Down;
|
||||
proc_widget_state.table.set_last();
|
||||
}
|
||||
}
|
||||
BottomWidgetType::ProcSort => {
|
||||
|
@ -2048,9 +1895,7 @@ impl App {
|
|||
.proc_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id - 2)
|
||||
{
|
||||
proc_widget_state.sort_table_state.current_scroll_position =
|
||||
proc_widget_state.num_enabled_columns() - 1;
|
||||
proc_widget_state.sort_table_state.scroll_direction = ScrollDirection::Down;
|
||||
proc_widget_state.sort_table.set_last();
|
||||
}
|
||||
}
|
||||
BottomWidgetType::Temp => {
|
||||
|
@ -2058,11 +1903,7 @@ impl App {
|
|||
.temp_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
if !self.converted_data.temp_sensor_data.data.is_empty() {
|
||||
temp_widget_state.table_state.current_scroll_position =
|
||||
self.converted_data.temp_sensor_data.data.len() - 1;
|
||||
temp_widget_state.table_state.scroll_direction = ScrollDirection::Down;
|
||||
}
|
||||
temp_widget_state.table.set_last();
|
||||
}
|
||||
}
|
||||
BottomWidgetType::Disk => {
|
||||
|
@ -2070,10 +1911,8 @@ impl App {
|
|||
.disk_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
if !self.converted_data.disk_data.data.is_empty() {
|
||||
disk_widget_state.table_state.current_scroll_position =
|
||||
self.converted_data.disk_data.data.len() - 1;
|
||||
disk_widget_state.table_state.scroll_direction = ScrollDirection::Down;
|
||||
if !self.converted_data.disk_data.is_empty() {
|
||||
disk_widget_state.table.set_last();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2082,11 +1921,7 @@ impl App {
|
|||
.cpu_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id - 1)
|
||||
{
|
||||
let cap = self.converted_data.cpu_data.len();
|
||||
if cap > 0 {
|
||||
cpu_widget_state.table_state.current_scroll_position = cap - 1;
|
||||
cpu_widget_state.table_state.scroll_direction = ScrollDirection::Down;
|
||||
}
|
||||
cpu_widget_state.table.set_last();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
@ -2128,10 +1963,9 @@ impl App {
|
|||
.proc_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id - 2)
|
||||
{
|
||||
let num_entries = proc_widget_state.num_enabled_columns();
|
||||
proc_widget_state
|
||||
.sort_table_state
|
||||
.update_position(num_to_change_by, num_entries);
|
||||
.sort_table
|
||||
.increment_position(num_to_change_by);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2141,9 +1975,7 @@ impl App {
|
|||
.widget_states
|
||||
.get_mut(&(self.current_widget.widget_id - 1))
|
||||
{
|
||||
cpu_widget_state
|
||||
.table_state
|
||||
.update_position(num_to_change_by, self.converted_data.cpu_data.len());
|
||||
cpu_widget_state.table.increment_position(num_to_change_by);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2153,9 +1985,7 @@ impl App {
|
|||
.proc_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
proc_widget_state
|
||||
.table_state
|
||||
.update_position(num_to_change_by, proc_widget_state.table_data.data.len())
|
||||
proc_widget_state.table.increment_position(num_to_change_by)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -2167,10 +1997,7 @@ impl App {
|
|||
.widget_states
|
||||
.get_mut(&self.current_widget.widget_id)
|
||||
{
|
||||
temp_widget_state.table_state.update_position(
|
||||
num_to_change_by,
|
||||
self.converted_data.temp_sensor_data.data.len(),
|
||||
);
|
||||
temp_widget_state.table.increment_position(num_to_change_by);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2180,9 +2007,7 @@ impl App {
|
|||
.widget_states
|
||||
.get_mut(&self.current_widget.widget_id)
|
||||
{
|
||||
disk_widget_state
|
||||
.table_state
|
||||
.update_position(num_to_change_by, self.converted_data.disk_data.data.len());
|
||||
disk_widget_state.table.increment_position(num_to_change_by);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2267,7 +2092,7 @@ impl App {
|
|||
.widget_states
|
||||
.get_mut(&self.current_widget.widget_id)
|
||||
{
|
||||
pws.toggle_tree_branch();
|
||||
pws.toggle_current_tree_branch_entry();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2645,45 +2470,34 @@ impl App {
|
|||
.get_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
if let Some(visual_index) =
|
||||
proc_widget_state.table_state.table_state.selected()
|
||||
proc_widget_state.table.tui_selected()
|
||||
{
|
||||
// If in tree mode, also check to see if this click is on
|
||||
// the same entry as the already selected one - if it is,
|
||||
// then we minimize.
|
||||
|
||||
let is_tree_mode = matches!(
|
||||
proc_widget_state.mode,
|
||||
ProcWidgetMode::Tree { .. }
|
||||
);
|
||||
let change =
|
||||
offset_clicked_entry as i64 - visual_index as i64;
|
||||
|
||||
let previous_scroll_position = proc_widget_state
|
||||
.table_state
|
||||
.current_scroll_position;
|
||||
self.change_process_position(change);
|
||||
|
||||
let new_position = self.change_process_position(
|
||||
offset_clicked_entry as i64 - visual_index as i64,
|
||||
);
|
||||
|
||||
if is_tree_mode {
|
||||
if let Some(new_position) = new_position {
|
||||
if previous_scroll_position == new_position {
|
||||
// If in tree mode, also check to see if this click is on
|
||||
// the same entry as the already selected one - if it is,
|
||||
// then we minimize.
|
||||
if is_tree_mode && change == 0 {
|
||||
self.toggle_collapsing_process_branch();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BottomWidgetType::ProcSort => {
|
||||
// TODO: [Feature] This could sort if you double click!
|
||||
if let Some(proc_widget_state) = self
|
||||
.proc_state
|
||||
.get_widget_state(self.current_widget.widget_id - 2)
|
||||
{
|
||||
if let Some(visual_index) = proc_widget_state
|
||||
.sort_table_state
|
||||
.table_state
|
||||
.selected()
|
||||
if let Some(visual_index) =
|
||||
proc_widget_state.sort_table.tui_selected()
|
||||
{
|
||||
self.change_process_sort_position(
|
||||
offset_clicked_entry as i64 - visual_index as i64,
|
||||
|
@ -2697,7 +2511,7 @@ impl App {
|
|||
.get_widget_state(self.current_widget.widget_id - 1)
|
||||
{
|
||||
if let Some(visual_index) =
|
||||
cpu_widget_state.table_state.table_state.selected()
|
||||
cpu_widget_state.table.tui_selected()
|
||||
{
|
||||
self.change_cpu_legend_position(
|
||||
offset_clicked_entry as i64 - visual_index as i64,
|
||||
|
@ -2711,7 +2525,7 @@ impl App {
|
|||
.get_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
if let Some(visual_index) =
|
||||
temp_widget_state.table_state.table_state.selected()
|
||||
temp_widget_state.table.tui_selected()
|
||||
{
|
||||
self.change_temp_position(
|
||||
offset_clicked_entry as i64 - visual_index as i64,
|
||||
|
@ -2725,7 +2539,7 @@ impl App {
|
|||
.get_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
if let Some(visual_index) =
|
||||
disk_widget_state.table_state.table_state.selected()
|
||||
disk_widget_state.table.tui_selected()
|
||||
{
|
||||
self.change_disk_position(
|
||||
offset_clicked_entry as i64 - visual_index as i64,
|
||||
|
@ -2744,10 +2558,11 @@ impl App {
|
|||
.proc_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
if let SortState::Sortable(st) =
|
||||
&mut proc_widget_state.table_state.sort_state
|
||||
if proc_widget_state
|
||||
.table
|
||||
.try_select_location(x, y)
|
||||
.is_some()
|
||||
{
|
||||
if st.try_select_location(x, y).is_some() {
|
||||
proc_widget_state.force_data_update();
|
||||
}
|
||||
}
|
||||
|
@ -2755,7 +2570,6 @@ impl App {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BottomWidgetType::Battery => {
|
||||
if let Some(battery_widget_state) = self
|
||||
.battery_state
|
||||
|
|
|
@ -33,7 +33,7 @@ use regex::Regex;
|
|||
pub type TimeOffset = f64;
|
||||
pub type Value = f64;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TimedData {
|
||||
pub rx_data: Value,
|
||||
pub tx_data: Value,
|
||||
|
@ -45,19 +45,11 @@ pub struct TimedData {
|
|||
pub arc_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>>,
|
||||
|
||||
|
@ -68,28 +60,10 @@ pub struct ProcessData {
|
|||
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);
|
||||
|
@ -100,8 +74,6 @@ impl ProcessData {
|
|||
}
|
||||
});
|
||||
|
||||
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
|
||||
|
@ -137,14 +109,14 @@ impl ProcessData {
|
|||
/// collected, and what is needed to convert into a displayable form.
|
||||
///
|
||||
/// If the app is *frozen* - that is, we do not want to *display* any changing
|
||||
/// data, keep updating this, don't convert to canvas displayable data!
|
||||
/// data, keep updating this. As of 2021-09-08, we just clone the current collection
|
||||
/// when it freezes to have a snapshot floating around.
|
||||
///
|
||||
/// Note that with this method, the *app* thread is responsible for cleaning -
|
||||
/// not the data collector.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataCollection {
|
||||
pub current_instant: Instant,
|
||||
pub frozen_instant: Option<Instant>,
|
||||
pub timed_data_vec: Vec<(Instant, TimedData)>,
|
||||
pub network_harvest: network::NetworkHarvest,
|
||||
pub memory_harvest: memory::MemHarvest,
|
||||
|
@ -167,7 +139,6 @@ impl Default for DataCollection {
|
|||
fn default() -> Self {
|
||||
DataCollection {
|
||||
current_instant: Instant::now(),
|
||||
frozen_instant: None,
|
||||
timed_data_vec: Vec::default(),
|
||||
network_harvest: network::NetworkHarvest::default(),
|
||||
memory_harvest: memory::MemHarvest::default(),
|
||||
|
@ -210,14 +181,6 @@ impl DataCollection {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn freeze(&mut self) {
|
||||
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) {
|
||||
let current_time = Instant::now();
|
||||
|
||||
|
|
|
@ -16,10 +16,15 @@ cfg_if::cfg_if! {
|
|||
|
||||
pub type LoadAvgHarvest = [f32; 3];
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CpuDataType {
|
||||
Avg,
|
||||
Cpu(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CpuData {
|
||||
pub cpu_prefix: String,
|
||||
pub cpu_count: Option<usize>,
|
||||
pub data_type: CpuDataType,
|
||||
pub cpu_usage: f64,
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@ cfg_if::cfg_if! {
|
|||
}
|
||||
}
|
||||
|
||||
use crate::data_harvester::cpu::{CpuData, CpuHarvest, PastCpuTotal, PastCpuWork};
|
||||
use crate::data_harvester::cpu::{CpuData, CpuDataType, CpuHarvest, PastCpuTotal, PastCpuWork};
|
||||
|
||||
use futures::StreamExt;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
|
@ -62,8 +63,7 @@ pub async fn get_cpu_data_list(
|
|||
let present_times = convert_cpu_times(&present);
|
||||
new_cpu_times.push(present_times);
|
||||
cpu_deque.push_back(CpuData {
|
||||
cpu_prefix: "CPU".to_string(),
|
||||
cpu_count: Some(itx),
|
||||
data_type: CpuDataType::Cpu(itx),
|
||||
cpu_usage: calculate_cpu_usage_percentage(
|
||||
convert_cpu_times(&past),
|
||||
present_times,
|
||||
|
@ -72,8 +72,7 @@ pub async fn get_cpu_data_list(
|
|||
} else {
|
||||
new_cpu_times.push((0.0, 0.0));
|
||||
cpu_deque.push_back(CpuData {
|
||||
cpu_prefix: "CPU".to_string(),
|
||||
cpu_count: Some(itx),
|
||||
data_type: CpuDataType::Cpu(itx),
|
||||
cpu_usage: 0.0,
|
||||
});
|
||||
}
|
||||
|
@ -96,8 +95,7 @@ pub async fn get_cpu_data_list(
|
|||
(
|
||||
present_times,
|
||||
CpuData {
|
||||
cpu_prefix: "CPU".to_string(),
|
||||
cpu_count: Some(itx),
|
||||
data_type: CpuDataType::Cpu(itx),
|
||||
cpu_usage: calculate_cpu_usage_percentage(
|
||||
(*past_cpu_work, *past_cpu_total),
|
||||
present_times,
|
||||
|
@ -108,8 +106,7 @@ pub async fn get_cpu_data_list(
|
|||
(
|
||||
(*past_cpu_work, *past_cpu_total),
|
||||
CpuData {
|
||||
cpu_prefix: "CPU".to_string(),
|
||||
cpu_count: Some(itx),
|
||||
data_type: CpuDataType::Cpu(itx),
|
||||
cpu_usage: 0.0,
|
||||
},
|
||||
)
|
||||
|
@ -147,8 +144,7 @@ pub async fn get_cpu_data_list(
|
|||
|
||||
*previous_average_cpu_time = Some(new_average_cpu_time);
|
||||
cpu_deque.push_front(CpuData {
|
||||
cpu_prefix: "AVG".to_string(),
|
||||
cpu_count: None,
|
||||
data_type: CpuDataType::Avg,
|
||||
cpu_usage,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -116,7 +116,6 @@ fn get_linux_cpu_usage(
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn read_proc(
|
||||
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,
|
||||
|
|
|
@ -60,16 +60,16 @@ pub(crate) struct extern_proc {
|
|||
/// Process identifier.
|
||||
pub p_pid: pid_t,
|
||||
|
||||
/// Save parent pid during ptrace. XXX
|
||||
/// Save parent pid during ptrace.
|
||||
pub p_oppid: pid_t,
|
||||
|
||||
/// Sideways return value from fdopen. XXX
|
||||
/// Sideways return value from fdopen.
|
||||
pub p_dupfd: i32,
|
||||
|
||||
/// where user stack was allocated
|
||||
pub user_stack: caddr_t,
|
||||
|
||||
/// XXX Which thread is exiting?
|
||||
/// Which thread is exiting?
|
||||
pub exit_thread: *mut c_void,
|
||||
|
||||
/// allow to debug
|
||||
|
|
|
@ -26,7 +26,7 @@ pub struct TempHarvest {
|
|||
pub temperature: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Copy)]
|
||||
pub enum TemperatureType {
|
||||
Celsius,
|
||||
Kelvin,
|
||||
|
|
44
src/app/frozen_state.rs
Normal file
44
src/app/frozen_state.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use super::DataCollection;
|
||||
|
||||
/// The [`FrozenState`] indicates whether the application state should be frozen. It is either not frozen or
|
||||
/// frozen and containing a copy of the state at the time.
|
||||
pub enum FrozenState {
|
||||
NotFrozen,
|
||||
Frozen(Box<DataCollection>),
|
||||
}
|
||||
|
||||
impl Default for FrozenState {
|
||||
fn default() -> Self {
|
||||
Self::NotFrozen
|
||||
}
|
||||
}
|
||||
|
||||
pub type IsFrozen = bool;
|
||||
|
||||
impl FrozenState {
|
||||
/// Checks whether the [`FrozenState`] is currently frozen.
|
||||
pub fn is_frozen(&self) -> IsFrozen {
|
||||
matches!(self, FrozenState::Frozen(_))
|
||||
}
|
||||
|
||||
/// Freezes the [`FrozenState`].
|
||||
pub fn freeze(&mut self, data: Box<DataCollection>) {
|
||||
*self = FrozenState::Frozen(data);
|
||||
}
|
||||
|
||||
/// Unfreezes the [`FrozenState`].
|
||||
pub fn thaw(&mut self) {
|
||||
*self = FrozenState::NotFrozen;
|
||||
}
|
||||
|
||||
/// Toggles the [`FrozenState`] and returns whether it is now frozen.
|
||||
pub fn toggle(&mut self, data: &DataCollection) -> IsFrozen {
|
||||
if self.is_frozen() {
|
||||
self.thaw();
|
||||
false
|
||||
} else {
|
||||
self.freeze(Box::new(data.clone()));
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,11 +4,10 @@ use unicode_segmentation::GraphemeCursor;
|
|||
|
||||
use crate::{
|
||||
app::{layout_manager::BottomWidgetType, query::*},
|
||||
components::text_table::{CellContent, TableComponentColumn, TableComponentState, WidthBounds},
|
||||
constants,
|
||||
};
|
||||
|
||||
use super::widgets::{DiskWidgetState, ProcWidget, TempWidgetState};
|
||||
use super::widgets::{CpuWidgetState, DiskTableWidget, ProcWidget, TempWidgetState};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScrollDirection {
|
||||
|
@ -30,13 +29,6 @@ pub enum CursorDirection {
|
|||
Right,
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum KillSignal {
|
||||
Cancel,
|
||||
|
@ -184,42 +176,6 @@ impl NetState {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct CpuWidgetState {
|
||||
pub current_display_time: u64,
|
||||
pub is_legend_hidden: bool,
|
||||
pub autohide_timer: Option<Instant>,
|
||||
pub table_state: TableComponentState,
|
||||
pub is_multi_graph_mode: bool,
|
||||
}
|
||||
|
||||
impl CpuWidgetState {
|
||||
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 {
|
||||
current_display_time,
|
||||
is_legend_hidden: false,
|
||||
autohide_timer,
|
||||
table_state,
|
||||
is_multi_graph_mode: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CpuState {
|
||||
pub force_update: Option<u64>,
|
||||
pub widget_states: HashMap<u64, CpuWidgetState>,
|
||||
|
@ -296,19 +252,19 @@ impl TempState {
|
|||
}
|
||||
|
||||
pub struct DiskState {
|
||||
pub widget_states: HashMap<u64, DiskWidgetState>,
|
||||
pub widget_states: HashMap<u64, DiskTableWidget>,
|
||||
}
|
||||
|
||||
impl DiskState {
|
||||
pub fn init(widget_states: HashMap<u64, DiskWidgetState>) -> Self {
|
||||
pub fn init(widget_states: HashMap<u64, DiskTableWidget>) -> Self {
|
||||
DiskState { widget_states }
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut DiskWidgetState> {
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut DiskTableWidget> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&DiskWidgetState> {
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&DiskTableWidget> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
pub mod process_table_widget;
|
||||
pub use process_table_widget::*;
|
||||
// FIXME: Move this outside of app, along with components?
|
||||
|
||||
pub mod temperature_table_widget;
|
||||
pub use temperature_table_widget::*;
|
||||
pub mod process_table;
|
||||
pub use process_table::*;
|
||||
|
||||
pub mod disk_table_widget;
|
||||
pub use disk_table_widget::*;
|
||||
pub mod temperature_table;
|
||||
pub use temperature_table::*;
|
||||
|
||||
pub mod disk_table;
|
||||
pub use disk_table::*;
|
||||
|
||||
pub mod cpu_graph;
|
||||
pub use cpu_graph::*;
|
||||
|
|
175
src/app/widgets/cpu_graph.rs
Normal file
175
src/app/widgets/cpu_graph.rs
Normal file
|
@ -0,0 +1,175 @@
|
|||
use std::{borrow::Cow, time::Instant};
|
||||
|
||||
use concat_string::concat_string;
|
||||
|
||||
use tui::{style::Style, text::Text, widgets::Row};
|
||||
|
||||
use crate::{
|
||||
app::{data_harvester::cpu::CpuDataType, AppConfigFields},
|
||||
canvas::{canvas_colours::CanvasColours, Painter},
|
||||
components::data_table::{
|
||||
Column, ColumnHeader, DataTable, DataTableColumn, DataTableProps, DataTableStyling,
|
||||
DataToCell,
|
||||
},
|
||||
data_conversion::CpuWidgetData,
|
||||
utils::gen_util::truncate_text,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CpuWidgetStyling {
|
||||
pub all: Style,
|
||||
pub avg: Style,
|
||||
pub entries: Vec<Style>,
|
||||
}
|
||||
|
||||
impl CpuWidgetStyling {
|
||||
fn from_colours(colours: &CanvasColours) -> Self {
|
||||
let entries = if colours.cpu_colour_styles.is_empty() {
|
||||
vec![Style::default()]
|
||||
} else {
|
||||
colours.cpu_colour_styles.clone()
|
||||
};
|
||||
|
||||
Self {
|
||||
all: colours.all_colour_style,
|
||||
avg: colours.avg_colour_style,
|
||||
entries,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum CpuWidgetColumn {
|
||||
CPU,
|
||||
Use,
|
||||
}
|
||||
|
||||
impl ColumnHeader for CpuWidgetColumn {
|
||||
fn text(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
CpuWidgetColumn::CPU => "CPU".into(),
|
||||
CpuWidgetColumn::Use => "Use%".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataToCell<CpuWidgetColumn> for CpuWidgetData {
|
||||
fn to_cell<'a>(&'a self, column: &CpuWidgetColumn, calculated_width: u16) -> Option<Text<'a>> {
|
||||
const CPU_HIDE_BREAKPOINT: u16 = 5;
|
||||
|
||||
// This is a bit of a hack, but apparently we can avoid having to do any fancy checks
|
||||
// of showing the "All" on a specific column if the other is hidden by just always
|
||||
// showing it on the CPU (first) column - if there isn't room for it, it will just collapse
|
||||
// down.
|
||||
//
|
||||
// This is the same for the use percentages - we just *always* show them, and *always* hide the CPU column if
|
||||
// it is too small.
|
||||
match &self {
|
||||
CpuWidgetData::All => match column {
|
||||
CpuWidgetColumn::CPU => Some(truncate_text("All", calculated_width)),
|
||||
CpuWidgetColumn::Use => None,
|
||||
},
|
||||
CpuWidgetData::Entry {
|
||||
data_type,
|
||||
data: _,
|
||||
last_entry,
|
||||
} => match column {
|
||||
CpuWidgetColumn::CPU => {
|
||||
if calculated_width == 0 {
|
||||
None
|
||||
} else {
|
||||
match data_type {
|
||||
CpuDataType::Avg => Some(truncate_text("AVG", calculated_width)),
|
||||
CpuDataType::Cpu(index) => {
|
||||
let index_str = index.to_string();
|
||||
let text = if calculated_width < CPU_HIDE_BREAKPOINT {
|
||||
truncate_text(&index_str, calculated_width)
|
||||
} else {
|
||||
truncate_text(
|
||||
&concat_string!("CPU", index_str),
|
||||
calculated_width,
|
||||
)
|
||||
};
|
||||
|
||||
Some(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CpuWidgetColumn::Use => Some(truncate_text(
|
||||
&format!("{:.0}%", last_entry.round()),
|
||||
calculated_width,
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {
|
||||
let style = match self {
|
||||
CpuWidgetData::All => painter.colours.all_colour_style,
|
||||
CpuWidgetData::Entry {
|
||||
data_type,
|
||||
data: _,
|
||||
last_entry: _,
|
||||
} => match data_type {
|
||||
CpuDataType::Avg => painter.colours.avg_colour_style,
|
||||
CpuDataType::Cpu(index) => {
|
||||
painter.colours.cpu_colour_styles
|
||||
[index % painter.colours.cpu_colour_styles.len()]
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
row.style(style)
|
||||
}
|
||||
|
||||
fn column_widths<C: DataTableColumn<CpuWidgetColumn>>(
|
||||
_data: &[Self], _columns: &[C],
|
||||
) -> Vec<u16>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![1, 3]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CpuWidgetState {
|
||||
pub current_display_time: u64,
|
||||
pub is_legend_hidden: bool,
|
||||
pub show_avg: bool,
|
||||
pub autohide_timer: Option<Instant>,
|
||||
pub table: DataTable<CpuWidgetData, CpuWidgetColumn>,
|
||||
pub styling: CpuWidgetStyling,
|
||||
}
|
||||
|
||||
impl CpuWidgetState {
|
||||
pub fn new(
|
||||
config: &AppConfigFields, current_display_time: u64, autohide_timer: Option<Instant>,
|
||||
colours: &CanvasColours,
|
||||
) -> Self {
|
||||
const COLUMNS: [Column<CpuWidgetColumn>; 2] = [
|
||||
Column::soft(CpuWidgetColumn::CPU, Some(0.5)),
|
||||
Column::soft(CpuWidgetColumn::Use, Some(0.5)),
|
||||
];
|
||||
|
||||
let props = DataTableProps {
|
||||
title: None,
|
||||
table_gap: config.table_gap,
|
||||
left_to_right: false,
|
||||
is_basic: false,
|
||||
show_table_scroll_position: false, // TODO: Should this be possible?
|
||||
show_current_entry_when_unfocused: true,
|
||||
};
|
||||
|
||||
let styling = DataTableStyling::from_colours(colours);
|
||||
|
||||
CpuWidgetState {
|
||||
current_display_time,
|
||||
is_legend_hidden: false,
|
||||
show_avg: config.show_average_cpu,
|
||||
autohide_timer,
|
||||
table: DataTable::new(COLUMNS, props, styling),
|
||||
styling: CpuWidgetStyling::from_colours(colours),
|
||||
}
|
||||
}
|
||||
}
|
147
src/app/widgets/disk_table.rs
Normal file
147
src/app/widgets/disk_table.rs
Normal file
|
@ -0,0 +1,147 @@
|
|||
use std::{borrow::Cow, cmp::max};
|
||||
|
||||
use kstring::KString;
|
||||
use tui::text::Text;
|
||||
|
||||
use crate::{
|
||||
app::AppConfigFields,
|
||||
canvas::canvas_colours::CanvasColours,
|
||||
components::data_table::{
|
||||
Column, ColumnHeader, DataTable, DataTableColumn, DataTableProps, DataTableStyling,
|
||||
DataToCell,
|
||||
},
|
||||
utils::gen_util::{get_decimal_bytes, truncate_text},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DiskWidgetData {
|
||||
pub name: KString,
|
||||
pub mount_point: KString,
|
||||
pub free_bytes: Option<u64>,
|
||||
pub used_bytes: Option<u64>,
|
||||
pub total_bytes: Option<u64>,
|
||||
pub io_read: KString,
|
||||
pub io_write: KString,
|
||||
}
|
||||
|
||||
impl DiskWidgetData {
|
||||
pub fn free_space(&self) -> KString {
|
||||
if let Some(free_bytes) = self.free_bytes {
|
||||
let converted_free_space = get_decimal_bytes(free_bytes);
|
||||
format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1).into()
|
||||
} else {
|
||||
"N/A".into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn total_space(&self) -> KString {
|
||||
if let Some(total_bytes) = self.total_bytes {
|
||||
let converted_total_space = get_decimal_bytes(total_bytes);
|
||||
format!(
|
||||
"{:.*}{}",
|
||||
0, converted_total_space.0, converted_total_space.1
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
"N/A".into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn usage(&self) -> KString {
|
||||
if let (Some(used_bytes), Some(total_bytes)) = (self.used_bytes, self.total_bytes) {
|
||||
format!("{:.0}%", used_bytes as f64 / total_bytes as f64 * 100_f64).into()
|
||||
} else {
|
||||
"N/A".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DiskWidgetColumn {
|
||||
Disk,
|
||||
Mount,
|
||||
Used,
|
||||
Free,
|
||||
Total,
|
||||
IoRead,
|
||||
IoWrite,
|
||||
}
|
||||
|
||||
impl ColumnHeader for DiskWidgetColumn {
|
||||
fn text(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
DiskWidgetColumn::Disk => "Disk",
|
||||
DiskWidgetColumn::Mount => "Mount",
|
||||
DiskWidgetColumn::Used => "Used",
|
||||
DiskWidgetColumn::Free => "Free",
|
||||
DiskWidgetColumn::Total => "Total",
|
||||
DiskWidgetColumn::IoRead => "R/s",
|
||||
DiskWidgetColumn::IoWrite => "W/s",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl DataToCell<DiskWidgetColumn> for DiskWidgetData {
|
||||
fn to_cell<'a>(&'a self, column: &DiskWidgetColumn, calculated_width: u16) -> Option<Text<'a>> {
|
||||
let text = match column {
|
||||
DiskWidgetColumn::Disk => truncate_text(&self.name, calculated_width),
|
||||
DiskWidgetColumn::Mount => truncate_text(&self.mount_point, calculated_width),
|
||||
DiskWidgetColumn::Used => truncate_text(&self.usage(), calculated_width),
|
||||
DiskWidgetColumn::Free => truncate_text(&self.free_space(), calculated_width),
|
||||
DiskWidgetColumn::Total => truncate_text(&self.total_space(), calculated_width),
|
||||
DiskWidgetColumn::IoRead => truncate_text(&self.io_read, calculated_width),
|
||||
DiskWidgetColumn::IoWrite => truncate_text(&self.io_write, calculated_width),
|
||||
};
|
||||
|
||||
Some(text)
|
||||
}
|
||||
|
||||
fn column_widths<C: DataTableColumn<DiskWidgetColumn>>(
|
||||
data: &[Self], _columns: &[C],
|
||||
) -> Vec<u16>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut widths = vec![0; 7];
|
||||
|
||||
data.iter().for_each(|row| {
|
||||
widths[0] = max(widths[0], row.name.len() as u16);
|
||||
widths[1] = max(widths[1], row.mount_point.len() as u16);
|
||||
});
|
||||
|
||||
widths
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiskTableWidget {
|
||||
pub table: DataTable<DiskWidgetData, DiskWidgetColumn>,
|
||||
}
|
||||
|
||||
impl DiskTableWidget {
|
||||
pub fn new(config: &AppConfigFields, colours: &CanvasColours) -> Self {
|
||||
const COLUMNS: [Column<DiskWidgetColumn>; 7] = [
|
||||
Column::soft(DiskWidgetColumn::Disk, Some(0.2)),
|
||||
Column::soft(DiskWidgetColumn::Mount, Some(0.2)),
|
||||
Column::hard(DiskWidgetColumn::Used, 4),
|
||||
Column::hard(DiskWidgetColumn::Free, 6),
|
||||
Column::hard(DiskWidgetColumn::Total, 6),
|
||||
Column::hard(DiskWidgetColumn::IoRead, 7),
|
||||
Column::hard(DiskWidgetColumn::IoWrite, 7),
|
||||
];
|
||||
|
||||
let props = DataTableProps {
|
||||
title: Some(" Disks ".into()),
|
||||
table_gap: config.table_gap,
|
||||
left_to_right: true,
|
||||
is_basic: config.use_basic_mode,
|
||||
show_table_scroll_position: config.show_table_scroll_position,
|
||||
show_current_entry_when_unfocused: false,
|
||||
};
|
||||
|
||||
let styling = DataTableStyling::from_colours(colours);
|
||||
|
||||
Self {
|
||||
table: DataTable::new(COLUMNS, props, styling),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
use crate::components::text_table::{
|
||||
CellContent, TableComponentColumn, TableComponentState, WidthBounds,
|
||||
};
|
||||
|
||||
pub struct DiskWidgetState {
|
||||
pub table_state: TableComponentState,
|
||||
}
|
||||
|
||||
impl Default for DiskWidgetState {
|
||||
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 {
|
||||
table_state: TableComponentState::new(
|
||||
DISK_HEADERS
|
||||
.iter()
|
||||
.zip(WIDTHS)
|
||||
.map(|(header, width)| {
|
||||
TableComponentColumn::new_custom(CellContent::new(*header, None), width)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
812
src/app/widgets/process_table.rs
Normal file
812
src/app/widgets/process_table.rs
Normal file
|
@ -0,0 +1,812 @@
|
|||
use std::{borrow::Cow, collections::hash_map::Entry};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
data_farmer::{DataCollection, ProcessData},
|
||||
data_harvester::processes::ProcessHarvest,
|
||||
query::*,
|
||||
AppConfigFields, AppSearchState,
|
||||
},
|
||||
canvas::canvas_colours::CanvasColours,
|
||||
components::data_table::{
|
||||
Column, ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataTableProps,
|
||||
DataTableStyling, SortColumn, SortDataTable, SortDataTableProps, SortOrder,
|
||||
},
|
||||
Pid,
|
||||
};
|
||||
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use itertools::Itertools;
|
||||
|
||||
pub mod proc_widget_column;
|
||||
pub use proc_widget_column::*;
|
||||
|
||||
pub mod proc_widget_data;
|
||||
pub use proc_widget_data::*;
|
||||
|
||||
mod sort_table;
|
||||
use sort_table::SortTableColumn;
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ProcWidgetMode {
|
||||
Tree { collapsed_pids: FxHashSet<Pid> },
|
||||
Grouped,
|
||||
Normal,
|
||||
}
|
||||
|
||||
type ProcessTable = SortDataTable<ProcWidgetData, ProcColumn>;
|
||||
type SortTable = DataTable<Cow<'static, str>, SortTableColumn>;
|
||||
type StringPidMap = FxHashMap<String, Vec<Pid>>;
|
||||
|
||||
pub struct ProcWidget {
|
||||
pub mode: ProcWidgetMode,
|
||||
|
||||
/// The state of the search box.
|
||||
pub proc_search: ProcessSearchState,
|
||||
|
||||
/// The state of the main table.
|
||||
pub table: ProcessTable,
|
||||
|
||||
/// The stored process data for this specific table.
|
||||
pub table_data: Vec<ProcWidgetData>,
|
||||
|
||||
/// The state of the togglable table that controls sorting.
|
||||
pub sort_table: SortTable,
|
||||
|
||||
/// A name-to-pid mapping.
|
||||
pub id_pid_map: StringPidMap,
|
||||
|
||||
pub is_sort_open: bool,
|
||||
pub force_rerender: bool,
|
||||
pub force_update_data: bool,
|
||||
}
|
||||
|
||||
impl ProcWidget {
|
||||
pub const PID_OR_COUNT: usize = 0;
|
||||
pub const PROC_NAME_OR_CMD: usize = 1;
|
||||
pub const CPU: usize = 2;
|
||||
pub const MEM: usize = 3;
|
||||
pub const RPS: usize = 4;
|
||||
pub const WPS: usize = 5;
|
||||
pub const T_READ: usize = 6;
|
||||
pub const T_WRITE: usize = 7;
|
||||
#[cfg(target_family = "unix")]
|
||||
pub const USER: usize = 8;
|
||||
#[cfg(target_family = "unix")]
|
||||
pub const STATE: usize = 9;
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
pub const STATE: usize = 8;
|
||||
|
||||
fn new_sort_table(config: &AppConfigFields, colours: &CanvasColours) -> SortTable {
|
||||
const COLUMNS: [Column<SortTableColumn>; 1] = [Column::hard(SortTableColumn, 7)];
|
||||
|
||||
let props = DataTableProps {
|
||||
title: None,
|
||||
table_gap: config.table_gap,
|
||||
left_to_right: true,
|
||||
is_basic: false,
|
||||
show_table_scroll_position: false,
|
||||
show_current_entry_when_unfocused: false,
|
||||
};
|
||||
|
||||
let styling = DataTableStyling::from_colours(colours);
|
||||
|
||||
DataTable::new(COLUMNS, props, styling)
|
||||
}
|
||||
|
||||
fn new_process_table(
|
||||
config: &AppConfigFields, colours: &CanvasColours, mode: &ProcWidgetMode, is_count: bool,
|
||||
is_command: bool, show_memory_as_values: bool,
|
||||
) -> ProcessTable {
|
||||
let (default_index, default_order) = if matches!(mode, ProcWidgetMode::Tree { .. }) {
|
||||
(Self::PID_OR_COUNT, SortOrder::Ascending)
|
||||
} else {
|
||||
(Self::CPU, SortOrder::Descending)
|
||||
};
|
||||
|
||||
let columns = {
|
||||
use ProcColumn::*;
|
||||
|
||||
let pid_or_count = SortColumn::new(if is_count { Count } else { Pid });
|
||||
let name_or_cmd = SortColumn::soft(if is_command { Command } else { Name }, Some(0.3));
|
||||
let cpu = SortColumn::new(CpuPercent).default_descending();
|
||||
let mem = SortColumn::new(if show_memory_as_values {
|
||||
MemoryVal
|
||||
} else {
|
||||
MemoryPercent
|
||||
})
|
||||
.default_descending();
|
||||
let rps = SortColumn::hard(ReadPerSecond, 8).default_descending();
|
||||
let wps = SortColumn::hard(WritePerSecond, 8).default_descending();
|
||||
let tr = SortColumn::hard(TotalRead, 8).default_descending();
|
||||
let tw = SortColumn::hard(TotalWrite, 8).default_descending();
|
||||
let state = SortColumn::hard(State, 7);
|
||||
|
||||
vec![
|
||||
pid_or_count,
|
||||
name_or_cmd,
|
||||
cpu,
|
||||
mem,
|
||||
rps,
|
||||
wps,
|
||||
tr,
|
||||
tw,
|
||||
#[cfg(target_family = "unix")]
|
||||
SortColumn::soft(User, Some(0.05)),
|
||||
state,
|
||||
]
|
||||
};
|
||||
|
||||
let inner_props = DataTableProps {
|
||||
title: Some(" Processes ".into()),
|
||||
table_gap: config.table_gap,
|
||||
left_to_right: true,
|
||||
is_basic: config.use_basic_mode,
|
||||
show_table_scroll_position: config.show_table_scroll_position,
|
||||
show_current_entry_when_unfocused: false,
|
||||
};
|
||||
let props = SortDataTableProps {
|
||||
inner: inner_props,
|
||||
sort_index: default_index,
|
||||
order: default_order,
|
||||
};
|
||||
|
||||
let styling = DataTableStyling::from_colours(colours);
|
||||
|
||||
DataTable::new_sortable(columns, props, styling)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
config: &AppConfigFields, mode: ProcWidgetMode, is_case_sensitive: bool,
|
||||
is_match_whole_word: bool, is_use_regex: bool, show_memory_as_values: bool,
|
||||
is_command: bool, colours: &CanvasColours,
|
||||
) -> Self {
|
||||
let process_search_state = {
|
||||
let mut pss = ProcessSearchState::default();
|
||||
|
||||
if is_case_sensitive {
|
||||
// By default it's off
|
||||
pss.search_toggle_ignore_case();
|
||||
}
|
||||
if is_match_whole_word {
|
||||
pss.search_toggle_whole_word();
|
||||
}
|
||||
if is_use_regex {
|
||||
pss.search_toggle_regex();
|
||||
}
|
||||
|
||||
pss
|
||||
};
|
||||
|
||||
let is_count = matches!(mode, ProcWidgetMode::Grouped);
|
||||
let sort_table = Self::new_sort_table(config, colours);
|
||||
let table = Self::new_process_table(
|
||||
config,
|
||||
colours,
|
||||
&mode,
|
||||
is_count,
|
||||
is_command,
|
||||
show_memory_as_values,
|
||||
);
|
||||
|
||||
let id_pid_map = FxHashMap::default();
|
||||
|
||||
ProcWidget {
|
||||
proc_search: process_search_state,
|
||||
table,
|
||||
table_data: vec![],
|
||||
sort_table,
|
||||
id_pid_map,
|
||||
is_sort_open: false,
|
||||
mode,
|
||||
force_rerender: true,
|
||||
force_update_data: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_using_command(&self) -> bool {
|
||||
self.table
|
||||
.columns
|
||||
.get(ProcWidget::PROC_NAME_OR_CMD)
|
||||
.map(|col| matches!(col.inner(), ProcColumn::Command))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_mem_percent(&self) -> bool {
|
||||
self.table
|
||||
.columns
|
||||
.get(ProcWidget::MEM)
|
||||
.map(|col| matches!(col.inner(), ProcColumn::MemoryPercent))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn get_query(&self) -> &Option<Query> {
|
||||
if self.proc_search.search_state.is_invalid_or_blank_search() {
|
||||
&None
|
||||
} else {
|
||||
&self.proc_search.search_state.query
|
||||
}
|
||||
}
|
||||
|
||||
/// This function *only* updates the displayed process data. If there is a need to update the actual *stored* data,
|
||||
/// call it before this function.
|
||||
pub fn update_displayed_process_data(&mut self, data_collection: &DataCollection) {
|
||||
self.table_data = match &self.mode {
|
||||
ProcWidgetMode::Grouped | ProcWidgetMode::Normal => {
|
||||
self.get_normal_data(&data_collection.process_data.process_harvest)
|
||||
}
|
||||
ProcWidgetMode::Tree { collapsed_pids } => {
|
||||
self.get_tree_data(collapsed_pids, data_collection)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn get_tree_data(
|
||||
&self, collapsed_pids: &FxHashSet<Pid>, data_collection: &DataCollection,
|
||||
) -> Vec<ProcWidgetData> {
|
||||
const BRANCH_END: char = '└';
|
||||
const BRANCH_VERTICAL: char = '│';
|
||||
const BRANCH_SPLIT: char = '├';
|
||||
const BRANCH_HORIZONTAL: char = '─';
|
||||
|
||||
let search_query = self.get_query();
|
||||
let is_using_command = self.is_using_command();
|
||||
let is_mem_percent = self.is_mem_percent();
|
||||
|
||||
let ProcessData {
|
||||
process_harvest,
|
||||
process_parent_mapping,
|
||||
orphan_pids,
|
||||
..
|
||||
} = &data_collection.process_data;
|
||||
|
||||
let kept_pids = data_collection
|
||||
.process_data
|
||||
.process_harvest
|
||||
.iter()
|
||||
.map(|(pid, process)| {
|
||||
(
|
||||
*pid,
|
||||
search_query
|
||||
.as_ref()
|
||||
.map(|q| q.check(process, is_using_command))
|
||||
.unwrap_or(true),
|
||||
)
|
||||
})
|
||||
.collect::<FxHashMap<_, _>>();
|
||||
|
||||
let filtered_tree = {
|
||||
let mut filtered_tree = FxHashMap::default();
|
||||
|
||||
// We do a simple BFS traversal to build our filtered parent-to-tree mappings.
|
||||
let mut visited_pids = FxHashMap::default();
|
||||
let mut stack = orphan_pids
|
||||
.iter()
|
||||
.filter_map(|process| process_harvest.get(process))
|
||||
.collect_vec();
|
||||
|
||||
while let Some(process) = stack.last() {
|
||||
let is_process_matching = *kept_pids.get(&process.pid).unwrap_or(&false);
|
||||
|
||||
if let Some(children_pids) = process_parent_mapping.get(&process.pid) {
|
||||
if children_pids
|
||||
.iter()
|
||||
.all(|pid| visited_pids.contains_key(pid))
|
||||
{
|
||||
let shown_children = children_pids
|
||||
.iter()
|
||||
.filter(|pid| visited_pids.get(*pid).copied().unwrap_or(false))
|
||||
.collect_vec();
|
||||
let is_shown = is_process_matching || !shown_children.is_empty();
|
||||
visited_pids.insert(process.pid, is_shown);
|
||||
|
||||
if is_shown {
|
||||
filtered_tree.insert(
|
||||
process.pid,
|
||||
shown_children
|
||||
.into_iter()
|
||||
.filter_map(|pid| {
|
||||
process_harvest.get(pid).map(|process| process.pid)
|
||||
})
|
||||
.collect_vec(),
|
||||
);
|
||||
}
|
||||
|
||||
stack.pop();
|
||||
} else {
|
||||
children_pids
|
||||
.iter()
|
||||
.filter_map(|process| process_harvest.get(process))
|
||||
.rev()
|
||||
.for_each(|process| {
|
||||
stack.push(process);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if is_process_matching {
|
||||
filtered_tree.insert(process.pid, vec![]);
|
||||
}
|
||||
|
||||
visited_pids.insert(process.pid, is_process_matching);
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
filtered_tree
|
||||
};
|
||||
|
||||
let mut data = vec![];
|
||||
let mut prefixes = vec![];
|
||||
let mut stack = orphan_pids
|
||||
.iter()
|
||||
.filter_map(|pid| {
|
||||
if filtered_tree.contains_key(pid) {
|
||||
process_harvest.get(pid).map(|process| {
|
||||
ProcWidgetData::from_data(process, is_using_command, is_mem_percent)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
self.try_sort(&mut stack);
|
||||
|
||||
let mut length_stack = vec![stack.len()];
|
||||
|
||||
while let (Some(process), Some(siblings_left)) = (stack.pop(), length_stack.last_mut()) {
|
||||
*siblings_left -= 1;
|
||||
|
||||
let disabled = !*kept_pids.get(&process.pid).unwrap_or(&false);
|
||||
let is_last = *siblings_left == 0;
|
||||
|
||||
if collapsed_pids.contains(&process.pid) {
|
||||
let mut summed_process = process.clone();
|
||||
|
||||
if let Some(children_pids) = filtered_tree.get(&process.pid) {
|
||||
let mut sum_queue = children_pids
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
process_harvest.get(child).map(|p| {
|
||||
ProcWidgetData::from_data(p, is_using_command, is_mem_percent)
|
||||
})
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
while let Some(process) = sum_queue.pop() {
|
||||
summed_process.add(&process);
|
||||
|
||||
if let Some(pids) = filtered_tree.get(&process.pid) {
|
||||
sum_queue.extend(pids.iter().filter_map(|child| {
|
||||
process_harvest.get(child).map(|p| {
|
||||
ProcWidgetData::from_data(p, is_using_command, is_mem_percent)
|
||||
})
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let prefix = if prefixes.is_empty() {
|
||||
"+ ".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{}{}{} + ",
|
||||
prefixes.join(""),
|
||||
if is_last { BRANCH_END } else { BRANCH_SPLIT },
|
||||
BRANCH_HORIZONTAL
|
||||
)
|
||||
};
|
||||
|
||||
data.push(summed_process.prefix(Some(prefix)).disabled(disabled));
|
||||
} else {
|
||||
let prefix = if prefixes.is_empty() {
|
||||
String::default()
|
||||
} else {
|
||||
format!(
|
||||
"{}{}{} ",
|
||||
prefixes.join(""),
|
||||
if is_last { BRANCH_END } else { BRANCH_SPLIT },
|
||||
BRANCH_HORIZONTAL
|
||||
)
|
||||
};
|
||||
let pid = process.pid;
|
||||
data.push(process.prefix(Some(prefix)).disabled(disabled));
|
||||
|
||||
if let Some(children_pids) = filtered_tree.get(&pid) {
|
||||
if prefixes.is_empty() {
|
||||
prefixes.push(String::default());
|
||||
} else {
|
||||
prefixes.push(if is_last {
|
||||
" ".to_string()
|
||||
} else {
|
||||
format!("{} ", BRANCH_VERTICAL)
|
||||
});
|
||||
}
|
||||
|
||||
let mut children = children_pids
|
||||
.iter()
|
||||
.filter_map(|child_pid| {
|
||||
process_harvest.get(child_pid).map(|p| {
|
||||
ProcWidgetData::from_data(p, is_using_command, is_mem_percent)
|
||||
})
|
||||
})
|
||||
.collect_vec();
|
||||
self.try_rev_sort(&mut children);
|
||||
length_stack.push(children.len());
|
||||
stack.extend(children);
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(children_left) = length_stack.last() {
|
||||
if *children_left == 0 {
|
||||
length_stack.pop();
|
||||
prefixes.pop();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
fn get_normal_data(
|
||||
&mut self, process_harvest: &FxHashMap<Pid, ProcessHarvest>,
|
||||
) -> Vec<ProcWidgetData> {
|
||||
let search_query = self.get_query();
|
||||
let is_using_command = self.is_using_command();
|
||||
let is_mem_percent = self.is_mem_percent();
|
||||
|
||||
let filtered_iter = process_harvest.values().filter(|process| {
|
||||
search_query
|
||||
.as_ref()
|
||||
.map(|query| query.check(process, is_using_command))
|
||||
.unwrap_or(true)
|
||||
});
|
||||
|
||||
let mut id_pid_map: FxHashMap<String, Vec<Pid>> = FxHashMap::default();
|
||||
let mut filtered_data: Vec<ProcWidgetData> = if let ProcWidgetMode::Grouped = self.mode {
|
||||
let mut id_process_mapping: FxHashMap<String, ProcessHarvest> = FxHashMap::default();
|
||||
for process in filtered_iter {
|
||||
let id = if is_using_command {
|
||||
&process.command
|
||||
} else {
|
||||
&process.name
|
||||
};
|
||||
let pid = process.pid;
|
||||
|
||||
match id_pid_map.entry(id.clone()) {
|
||||
Entry::Occupied(mut occupied) => {
|
||||
occupied.get_mut().push(pid);
|
||||
}
|
||||
Entry::Vacant(vacant) => {
|
||||
vacant.insert(vec![pid]);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(grouped_process_harvest) = id_process_mapping.get_mut(id) {
|
||||
grouped_process_harvest.add(process);
|
||||
} else {
|
||||
id_process_mapping.insert(id.clone(), process.clone());
|
||||
}
|
||||
}
|
||||
|
||||
id_process_mapping
|
||||
.values()
|
||||
.map(|process| {
|
||||
let id = if is_using_command {
|
||||
&process.command
|
||||
} else {
|
||||
&process.name
|
||||
};
|
||||
|
||||
let num_similar = id_pid_map.get(id).map(|val| val.len()).unwrap_or(1) as u64;
|
||||
|
||||
ProcWidgetData::from_data(process, is_using_command, is_mem_percent)
|
||||
.num_similar(num_similar)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
filtered_iter
|
||||
.map(|process| ProcWidgetData::from_data(process, is_using_command, is_mem_percent))
|
||||
.collect()
|
||||
};
|
||||
|
||||
self.id_pid_map = id_pid_map;
|
||||
self.try_sort(&mut filtered_data);
|
||||
filtered_data
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn try_sort(&self, filtered_data: &mut [ProcWidgetData]) {
|
||||
if let Some(column) = self.table.columns.get(self.table.sort_index()) {
|
||||
column.sort_by(filtered_data, self.table.order());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn try_rev_sort(&self, filtered_data: &mut [ProcWidgetData]) {
|
||||
if let Some(column) = self.table.columns.get(self.table.sort_index()) {
|
||||
column.sort_by(
|
||||
filtered_data,
|
||||
match self.table.order() {
|
||||
SortOrder::Ascending => SortOrder::Descending,
|
||||
SortOrder::Descending => SortOrder::Ascending,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_mut_proc_col(&mut self, index: usize) -> Option<&mut ProcColumn> {
|
||||
self.table.columns.get_mut(index).map(|col| col.inner_mut())
|
||||
}
|
||||
|
||||
pub fn toggle_mem_percentage(&mut self) {
|
||||
if let Some(mem) = self.get_mut_proc_col(Self::MEM) {
|
||||
match mem {
|
||||
ProcColumn::MemoryVal => {
|
||||
*mem = ProcColumn::MemoryPercent;
|
||||
}
|
||||
ProcColumn::MemoryPercent => {
|
||||
*mem = ProcColumn::MemoryVal;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
self.force_data_update();
|
||||
}
|
||||
}
|
||||
|
||||
/// Forces an update of the data stored.
|
||||
#[inline]
|
||||
pub fn force_data_update(&mut self) {
|
||||
self.force_update_data = true;
|
||||
}
|
||||
|
||||
/// Forces an entire rerender and update of the data stored.
|
||||
#[inline]
|
||||
pub fn force_rerender_and_update(&mut self) {
|
||||
self.force_rerender = true;
|
||||
self.force_update_data = true;
|
||||
}
|
||||
|
||||
/// Marks the selected column as hidden, and automatically resets the selected column to CPU
|
||||
/// and descending if that column was selected.
|
||||
fn hide_column(&mut self, index: usize) {
|
||||
if let Some(col) = self.table.columns.get_mut(index) {
|
||||
col.is_hidden = true;
|
||||
|
||||
if self.table.sort_index() == index {
|
||||
self.table.set_sort_index(Self::CPU);
|
||||
self.table.set_order(SortOrder::Descending);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the selected column as shown.
|
||||
fn show_column(&mut self, index: usize) {
|
||||
if let Some(col) = self.table.columns.get_mut(index) {
|
||||
col.is_hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Select a column. If the column is already selected, then just toggle the sort order.
|
||||
pub fn select_column(&mut self, new_sort_index: usize) {
|
||||
self.table.set_sort_index(new_sort_index);
|
||||
self.force_data_update();
|
||||
}
|
||||
|
||||
pub fn toggle_current_tree_branch_entry(&mut self) {
|
||||
if let ProcWidgetMode::Tree { collapsed_pids } = &mut self.mode {
|
||||
if let Some(process) = self.table.current_item() {
|
||||
let pid = process.pid;
|
||||
|
||||
if !collapsed_pids.remove(&pid) {
|
||||
collapsed_pids.insert(pid);
|
||||
}
|
||||
self.force_data_update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_command(&mut self) {
|
||||
if let Some(col) = self.table.columns.get_mut(Self::PROC_NAME_OR_CMD) {
|
||||
let inner = col.inner_mut();
|
||||
match inner {
|
||||
ProcColumn::Name => {
|
||||
*inner = ProcColumn::Command;
|
||||
if let ColumnWidthBounds::Soft { max_percentage, .. } = col.bounds_mut() {
|
||||
*max_percentage = Some(0.5);
|
||||
}
|
||||
}
|
||||
ProcColumn::Command => {
|
||||
*inner = ProcColumn::Name;
|
||||
if let ColumnWidthBounds::Soft { max_percentage, .. } = col.bounds_mut() {
|
||||
*max_percentage = match self.mode {
|
||||
ProcWidgetMode::Tree { .. } => Some(0.5),
|
||||
ProcWidgetMode::Grouped | ProcWidgetMode::Normal => Some(0.3),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
self.force_rerender_and_update();
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggles the appropriate columns/settings when tab is pressed.
|
||||
///
|
||||
/// If count is enabled, we should set the mode to [`ProcWidgetMode::Grouped`], and switch off the User and State
|
||||
/// columns. We should also move the user off of the columns if they were selected, as those columns are now hidden
|
||||
/// (handled by internal method calls), and go back to the "defaults".
|
||||
///
|
||||
/// Otherwise, if count is disabled, then the User and State columns should be re-enabled, and the mode switched
|
||||
/// to [`ProcWidgetMode::Normal`].
|
||||
pub fn on_tab(&mut self) {
|
||||
if !matches!(self.mode, ProcWidgetMode::Tree { .. }) {
|
||||
if let Some(sort_col) = self.table.columns.get_mut(Self::PID_OR_COUNT) {
|
||||
let col = sort_col.inner_mut();
|
||||
match col {
|
||||
ProcColumn::Pid => {
|
||||
*col = ProcColumn::Count;
|
||||
sort_col.default_order = SortOrder::Descending;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
self.hide_column(Self::USER);
|
||||
self.hide_column(Self::STATE);
|
||||
self.mode = ProcWidgetMode::Grouped;
|
||||
}
|
||||
ProcColumn::Count => {
|
||||
*col = ProcColumn::Pid;
|
||||
sort_col.default_order = SortOrder::Ascending;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
self.show_column(Self::USER);
|
||||
self.show_column(Self::STATE);
|
||||
self.mode = ProcWidgetMode::Normal;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
self.force_rerender_and_update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn column_text(&self) -> Vec<Cow<'static, str>> {
|
||||
self.table
|
||||
.columns
|
||||
.iter()
|
||||
.filter(|c| !c.is_hidden)
|
||||
.map(|c| c.inner().text())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn get_search_cursor_position(&self) -> usize {
|
||||
self.proc_search.search_state.grapheme_cursor.cur_cursor()
|
||||
}
|
||||
|
||||
pub fn get_char_cursor_position(&self) -> usize {
|
||||
self.proc_search.search_state.char_cursor_position
|
||||
}
|
||||
|
||||
pub fn is_search_enabled(&self) -> bool {
|
||||
self.proc_search.search_state.is_enabled
|
||||
}
|
||||
|
||||
pub fn get_current_search_query(&self) -> &String {
|
||||
&self.proc_search.search_state.current_search_query
|
||||
}
|
||||
|
||||
pub fn update_query(&mut self) {
|
||||
if self
|
||||
.proc_search
|
||||
.search_state
|
||||
.current_search_query
|
||||
.is_empty()
|
||||
{
|
||||
self.proc_search.search_state.is_blank_search = true;
|
||||
self.proc_search.search_state.is_invalid_search = false;
|
||||
self.proc_search.search_state.error_message = None;
|
||||
} else {
|
||||
match parse_query(
|
||||
&self.proc_search.search_state.current_search_query,
|
||||
self.proc_search.is_searching_whole_word,
|
||||
self.proc_search.is_ignoring_case,
|
||||
self.proc_search.is_searching_with_regex,
|
||||
) {
|
||||
Ok(parsed_query) => {
|
||||
self.proc_search.search_state.query = Some(parsed_query);
|
||||
self.proc_search.search_state.is_blank_search = false;
|
||||
self.proc_search.search_state.is_invalid_search = false;
|
||||
self.proc_search.search_state.error_message = None;
|
||||
}
|
||||
Err(err) => {
|
||||
self.proc_search.search_state.is_blank_search = false;
|
||||
self.proc_search.search_state.is_invalid_search = true;
|
||||
self.proc_search.search_state.error_message = Some(err.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
self.table.state.display_start_index = 0;
|
||||
self.table.state.current_index = 0;
|
||||
|
||||
self.force_data_update();
|
||||
}
|
||||
|
||||
pub fn clear_search(&mut self) {
|
||||
self.proc_search.search_state.reset();
|
||||
self.force_data_update();
|
||||
}
|
||||
|
||||
pub fn search_walk_forward(&mut self, start_position: usize) {
|
||||
self.proc_search
|
||||
.search_state
|
||||
.grapheme_cursor
|
||||
.next_boundary(
|
||||
&self.proc_search.search_state.current_search_query[start_position..],
|
||||
start_position,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn search_walk_back(&mut self, start_position: usize) {
|
||||
self.proc_search
|
||||
.search_state
|
||||
.grapheme_cursor
|
||||
.prev_boundary(
|
||||
&self.proc_search.search_state.current_search_query[..start_position],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Returns the number of columns *enabled*. Note this differs from *visible* - a column may be enabled but not
|
||||
/// visible (e.g. off screen).
|
||||
pub fn num_enabled_columns(&self) -> usize {
|
||||
self.table.columns.iter().filter(|c| !c.is_hidden).count()
|
||||
}
|
||||
|
||||
/// Sets the [`ProcWidget`]'s current sort index to whatever was in the sort table if possible, then closes the
|
||||
/// sort table.
|
||||
pub(crate) fn use_sort_table_value(&mut self) {
|
||||
self.table.set_sort_index(self.sort_table.current_index());
|
||||
|
||||
self.is_sort_open = false;
|
||||
self.force_rerender_and_update();
|
||||
}
|
||||
}
|
122
src/app/widgets/process_table/proc_widget_column.rs
Normal file
122
src/app/widgets/process_table/proc_widget_column.rs
Normal file
|
@ -0,0 +1,122 @@
|
|||
use crate::{
|
||||
components::data_table::{ColumnHeader, SortsRow},
|
||||
utils::gen_util::sort_partial_fn,
|
||||
};
|
||||
|
||||
use std::{borrow::Cow, cmp::Reverse};
|
||||
|
||||
use super::ProcWidgetData;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum ProcColumn {
|
||||
CpuPercent,
|
||||
MemoryVal,
|
||||
MemoryPercent,
|
||||
Pid,
|
||||
Count,
|
||||
Name,
|
||||
Command,
|
||||
ReadPerSecond,
|
||||
WritePerSecond,
|
||||
TotalRead,
|
||||
TotalWrite,
|
||||
State,
|
||||
User,
|
||||
}
|
||||
|
||||
impl ColumnHeader for ProcColumn {
|
||||
fn text(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ProcColumn::CpuPercent => "CPU%",
|
||||
ProcColumn::MemoryVal => "Mem",
|
||||
ProcColumn::MemoryPercent => "Mem%",
|
||||
ProcColumn::Pid => "PID",
|
||||
ProcColumn::Count => "Count",
|
||||
ProcColumn::Name => "Name",
|
||||
ProcColumn::Command => "Command",
|
||||
ProcColumn::ReadPerSecond => "R/s",
|
||||
ProcColumn::WritePerSecond => "W/s",
|
||||
ProcColumn::TotalRead => "T.Read",
|
||||
ProcColumn::TotalWrite => "T.Write",
|
||||
ProcColumn::State => "State",
|
||||
ProcColumn::User => "User",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn header(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ProcColumn::CpuPercent => "CPU%(c)",
|
||||
ProcColumn::MemoryVal => "Mem(m)",
|
||||
ProcColumn::MemoryPercent => "Mem%(m)",
|
||||
ProcColumn::Pid => "PID(p)",
|
||||
ProcColumn::Count => "Count",
|
||||
ProcColumn::Name => "Name(n)",
|
||||
ProcColumn::Command => "Command(n)",
|
||||
ProcColumn::ReadPerSecond => "R/s",
|
||||
ProcColumn::WritePerSecond => "W/s",
|
||||
ProcColumn::TotalRead => "T.Read",
|
||||
ProcColumn::TotalWrite => "T.Write",
|
||||
ProcColumn::State => "State",
|
||||
ProcColumn::User => "User",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl SortsRow<ProcWidgetData> for ProcColumn {
|
||||
fn sort_data(&self, data: &mut [ProcWidgetData], descending: bool) {
|
||||
match self {
|
||||
ProcColumn::CpuPercent => {
|
||||
data.sort_by(|a, b| {
|
||||
sort_partial_fn(descending)(a.cpu_usage_percent, b.cpu_usage_percent)
|
||||
});
|
||||
}
|
||||
ProcColumn::MemoryVal | ProcColumn::MemoryPercent => {
|
||||
data.sort_by(|a, b| sort_partial_fn(descending)(&a.mem_usage, &b.mem_usage));
|
||||
}
|
||||
ProcColumn::Pid => {
|
||||
data.sort_by(|a, b| sort_partial_fn(descending)(a.pid, b.pid));
|
||||
}
|
||||
ProcColumn::Count => {
|
||||
data.sort_by(|a, b| sort_partial_fn(descending)(a.num_similar, b.num_similar));
|
||||
}
|
||||
ProcColumn::Name | ProcColumn::Command => {
|
||||
if descending {
|
||||
data.sort_by_cached_key(|pd| Reverse(pd.id.to_lowercase()));
|
||||
} else {
|
||||
data.sort_by_cached_key(|pd| pd.id.to_lowercase());
|
||||
}
|
||||
}
|
||||
ProcColumn::ReadPerSecond => {
|
||||
data.sort_by(|a, b| sort_partial_fn(descending)(a.rps, b.rps));
|
||||
}
|
||||
ProcColumn::WritePerSecond => {
|
||||
data.sort_by(|a, b| sort_partial_fn(descending)(a.wps, b.wps));
|
||||
}
|
||||
ProcColumn::TotalRead => {
|
||||
data.sort_by(|a, b| sort_partial_fn(descending)(a.total_read, b.total_read));
|
||||
}
|
||||
ProcColumn::TotalWrite => {
|
||||
data.sort_by(|a, b| sort_partial_fn(descending)(a.total_write, b.total_write));
|
||||
}
|
||||
ProcColumn::State => {
|
||||
if descending {
|
||||
data.sort_by_cached_key(|pd| Reverse(pd.process_state.to_lowercase()));
|
||||
} else {
|
||||
data.sort_by_cached_key(|pd| pd.process_state.to_lowercase());
|
||||
}
|
||||
}
|
||||
ProcColumn::User => {
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
if descending {
|
||||
data.sort_by_cached_key(|pd| Reverse(pd.user.to_lowercase()));
|
||||
} else {
|
||||
data.sort_by_cached_key(|pd| pd.user.to_lowercase());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
274
src/app/widgets/process_table/proc_widget_data.rs
Normal file
274
src/app/widgets/process_table/proc_widget_data.rs
Normal file
|
@ -0,0 +1,274 @@
|
|||
use std::{
|
||||
cmp::{max, Ordering},
|
||||
fmt::Display,
|
||||
};
|
||||
|
||||
use concat_string::concat_string;
|
||||
use tui::{text::Text, widgets::Row};
|
||||
|
||||
use crate::{
|
||||
app::data_harvester::processes::ProcessHarvest,
|
||||
canvas::Painter,
|
||||
components::data_table::{DataTableColumn, DataToCell},
|
||||
data_conversion::{binary_byte_string, dec_bytes_per_second_string, dec_bytes_string},
|
||||
utils::gen_util::truncate_text,
|
||||
Pid,
|
||||
};
|
||||
|
||||
use super::proc_widget_column::ProcColumn;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum IdType {
|
||||
Name(String),
|
||||
Command(String),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Id {
|
||||
id_type: IdType,
|
||||
prefix: Option<String>,
|
||||
}
|
||||
|
||||
impl Id {
|
||||
/// Returns the ID as a lowercase [`String`], with no prefix. This is primarily useful for
|
||||
/// cases like sorting where we treat everything as the same case (e.g. `Discord` comes before
|
||||
/// `dkms`).
|
||||
pub fn to_lowercase(&self) -> String {
|
||||
match &self.id_type {
|
||||
IdType::Name(name) => name.to_lowercase(),
|
||||
IdType::Command(cmd) => cmd.to_lowercase(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the ID as a borrowed [`str`] with no prefix.
|
||||
pub fn as_str(&self) -> &str {
|
||||
match &self.id_type {
|
||||
IdType::Name(name) => name.as_str(),
|
||||
IdType::Command(cmd) => cmd.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the ID as a [`String`] with prefix.
|
||||
pub fn to_prefixed_string(&self) -> String {
|
||||
if let Some(prefix) = &self.prefix {
|
||||
concat_string!(
|
||||
prefix,
|
||||
match &self.id_type {
|
||||
IdType::Name(name) => name,
|
||||
IdType::Command(cmd) => cmd,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
match &self.id_type {
|
||||
IdType::Name(name) => name.to_string(),
|
||||
IdType::Command(cmd) => cmd.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Id {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Can reduce this to 32 bytes.
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub enum MemUsage {
|
||||
Percent(f64),
|
||||
Bytes(u64),
|
||||
}
|
||||
|
||||
impl PartialOrd for MemUsage {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match (self, other) {
|
||||
(MemUsage::Percent(a), MemUsage::Percent(b)) => a.partial_cmp(b),
|
||||
(MemUsage::Bytes(a), MemUsage::Bytes(b)) => a.partial_cmp(b),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MemUsage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MemUsage::Percent(percent) => f.write_fmt(format_args!("{:.1}%", percent)),
|
||||
MemUsage::Bytes(bytes) => f.write_str(&binary_byte_string(*bytes)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProcWidgetData {
|
||||
pub pid: Pid,
|
||||
pub ppid: Option<Pid>,
|
||||
pub id: Id,
|
||||
pub cpu_usage_percent: f64,
|
||||
pub mem_usage: MemUsage,
|
||||
pub rps: u64,
|
||||
pub wps: u64,
|
||||
pub total_read: u64,
|
||||
pub total_write: u64,
|
||||
pub process_state: String,
|
||||
pub process_char: char,
|
||||
#[cfg(target_family = "unix")]
|
||||
pub user: String,
|
||||
pub num_similar: u64,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
impl ProcWidgetData {
|
||||
pub fn from_data(process: &ProcessHarvest, is_command: bool, is_mem_percent: bool) -> Self {
|
||||
let id = Id {
|
||||
id_type: if is_command {
|
||||
IdType::Command(process.command.clone())
|
||||
} else {
|
||||
IdType::Name(process.name.clone())
|
||||
},
|
||||
prefix: None,
|
||||
};
|
||||
|
||||
let mem_usage = if is_mem_percent {
|
||||
MemUsage::Percent(process.mem_usage_percent)
|
||||
} else {
|
||||
MemUsage::Bytes(process.mem_usage_bytes)
|
||||
};
|
||||
|
||||
Self {
|
||||
pid: process.pid,
|
||||
ppid: process.parent_pid,
|
||||
id,
|
||||
cpu_usage_percent: process.cpu_usage_percent,
|
||||
mem_usage,
|
||||
rps: process.read_bytes_per_sec,
|
||||
wps: process.write_bytes_per_sec,
|
||||
total_read: process.total_read_bytes,
|
||||
total_write: process.total_write_bytes,
|
||||
process_state: process.process_state.0.clone(),
|
||||
process_char: process.process_state.1,
|
||||
#[cfg(target_family = "unix")]
|
||||
user: process.user.to_string(),
|
||||
num_similar: 1,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_similar(mut self, num_similar: u64) -> Self {
|
||||
self.num_similar = num_similar;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn prefix(mut self, prefix: Option<String>) -> Self {
|
||||
self.id.prefix = prefix;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add(&mut self, other: &Self) {
|
||||
self.cpu_usage_percent += other.cpu_usage_percent;
|
||||
self.mem_usage = match (&self.mem_usage, &other.mem_usage) {
|
||||
(MemUsage::Percent(a), MemUsage::Percent(b)) => MemUsage::Percent(a + b),
|
||||
(MemUsage::Bytes(a), MemUsage::Bytes(b)) => MemUsage::Bytes(a + b),
|
||||
(MemUsage::Percent(_), MemUsage::Bytes(_))
|
||||
| (MemUsage::Bytes(_), MemUsage::Percent(_)) => {
|
||||
unreachable!("trying to add together two different memory usage types!")
|
||||
}
|
||||
};
|
||||
self.rps += other.rps;
|
||||
self.wps += other.wps;
|
||||
self.total_read += other.total_read;
|
||||
self.total_write += other.total_write;
|
||||
}
|
||||
|
||||
fn to_string(&self, column: &ProcColumn) -> String {
|
||||
match column {
|
||||
ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent),
|
||||
ProcColumn::MemoryVal | ProcColumn::MemoryPercent => self.mem_usage.to_string(),
|
||||
ProcColumn::Pid => self.pid.to_string(),
|
||||
ProcColumn::Count => self.num_similar.to_string(),
|
||||
ProcColumn::Name | ProcColumn::Command => self.id.to_prefixed_string(),
|
||||
ProcColumn::ReadPerSecond => dec_bytes_per_second_string(self.rps),
|
||||
ProcColumn::WritePerSecond => dec_bytes_per_second_string(self.wps),
|
||||
ProcColumn::TotalRead => dec_bytes_string(self.total_read),
|
||||
ProcColumn::TotalWrite => dec_bytes_string(self.total_write),
|
||||
ProcColumn::State => self.process_char.to_string(),
|
||||
ProcColumn::User => {
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
self.user.clone()
|
||||
}
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
{
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataToCell<ProcColumn> for ProcWidgetData {
|
||||
fn to_cell<'a>(&'a self, column: &ProcColumn, calculated_width: u16) -> Option<Text<'a>> {
|
||||
Some(truncate_text(
|
||||
&match column {
|
||||
ProcColumn::CpuPercent => {
|
||||
format!("{:.1}%", self.cpu_usage_percent)
|
||||
}
|
||||
ProcColumn::MemoryVal | ProcColumn::MemoryPercent => self.mem_usage.to_string(),
|
||||
ProcColumn::Pid => self.pid.to_string(),
|
||||
ProcColumn::Count => self.num_similar.to_string(),
|
||||
ProcColumn::Name | ProcColumn::Command => self.id.to_prefixed_string(),
|
||||
ProcColumn::ReadPerSecond => dec_bytes_per_second_string(self.rps),
|
||||
ProcColumn::WritePerSecond => dec_bytes_per_second_string(self.wps),
|
||||
ProcColumn::TotalRead => dec_bytes_string(self.total_read),
|
||||
ProcColumn::TotalWrite => dec_bytes_string(self.total_write),
|
||||
ProcColumn::State => {
|
||||
if calculated_width < 8 {
|
||||
self.process_char.to_string()
|
||||
} else {
|
||||
self.process_state.clone()
|
||||
}
|
||||
}
|
||||
ProcColumn::User => {
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
self.user.clone()
|
||||
}
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
{
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
calculated_width,
|
||||
))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {
|
||||
if self.disabled {
|
||||
row.style(painter.colours.disabled_text_style)
|
||||
} else {
|
||||
row
|
||||
}
|
||||
}
|
||||
|
||||
fn column_widths<C: DataTableColumn<ProcColumn>>(data: &[Self], columns: &[C]) -> Vec<u16>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut widths = vec![0; columns.len()];
|
||||
|
||||
for d in data {
|
||||
for (w, c) in widths.iter_mut().zip(columns) {
|
||||
*w = max(*w, d.to_string(c.inner()).len() as u16);
|
||||
}
|
||||
}
|
||||
|
||||
widths
|
||||
}
|
||||
}
|
42
src/app/widgets/process_table/sort_table.rs
Normal file
42
src/app/widgets/process_table/sort_table.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use tui::text::Text;
|
||||
|
||||
use crate::{
|
||||
components::data_table::{ColumnHeader, DataTableColumn, DataToCell},
|
||||
utils::gen_util::truncate_text,
|
||||
};
|
||||
|
||||
pub struct SortTableColumn;
|
||||
|
||||
impl ColumnHeader for SortTableColumn {
|
||||
fn text(&self) -> std::borrow::Cow<'static, str> {
|
||||
"Sort By".into()
|
||||
}
|
||||
}
|
||||
|
||||
impl DataToCell<SortTableColumn> for &'static str {
|
||||
fn to_cell<'a>(&'a self, _column: &SortTableColumn, calculated_width: u16) -> Option<Text<'a>> {
|
||||
Some(truncate_text(self, calculated_width))
|
||||
}
|
||||
|
||||
fn column_widths<C: DataTableColumn<SortTableColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![data.iter().map(|d| d.len() as u16).max().unwrap_or(0)]
|
||||
}
|
||||
}
|
||||
|
||||
impl DataToCell<SortTableColumn> for Cow<'static, str> {
|
||||
fn to_cell<'a>(&'a self, _column: &SortTableColumn, calculated_width: u16) -> Option<Text<'a>> {
|
||||
Some(truncate_text(self, calculated_width))
|
||||
}
|
||||
|
||||
fn column_widths<C: DataTableColumn<SortTableColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![data.iter().map(|d| d.len() as u16).max().unwrap_or(0)]
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
101
src/app/widgets/temperature_table.rs
Normal file
101
src/app/widgets/temperature_table.rs
Normal file
|
@ -0,0 +1,101 @@
|
|||
use std::{borrow::Cow, cmp::max};
|
||||
|
||||
use concat_string::concat_string;
|
||||
use kstring::KString;
|
||||
use tui::text::Text;
|
||||
|
||||
use crate::{
|
||||
app::{data_harvester::temperature::TemperatureType, AppConfigFields},
|
||||
canvas::canvas_colours::CanvasColours,
|
||||
components::data_table::{
|
||||
Column, ColumnHeader, DataTable, DataTableColumn, DataTableProps, DataTableStyling,
|
||||
DataToCell,
|
||||
},
|
||||
utils::gen_util::truncate_text,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TempWidgetData {
|
||||
pub sensor: KString,
|
||||
pub temperature_value: u64,
|
||||
pub temperature_type: TemperatureType,
|
||||
}
|
||||
|
||||
pub enum TempWidgetColumn {
|
||||
Sensor,
|
||||
Temp,
|
||||
}
|
||||
|
||||
impl ColumnHeader for TempWidgetColumn {
|
||||
fn text(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
TempWidgetColumn::Sensor => "Sensor".into(),
|
||||
TempWidgetColumn::Temp => "Temp".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TempWidgetData {
|
||||
pub fn temperature(&self) -> KString {
|
||||
let temp_val = self.temperature_value.to_string();
|
||||
let temp_type = match self.temperature_type {
|
||||
TemperatureType::Celsius => "°C",
|
||||
TemperatureType::Kelvin => "K",
|
||||
TemperatureType::Fahrenheit => "°F",
|
||||
};
|
||||
concat_string!(temp_val, temp_type).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl DataToCell<TempWidgetColumn> for TempWidgetData {
|
||||
fn to_cell<'a>(&'a self, column: &TempWidgetColumn, calculated_width: u16) -> Option<Text<'a>> {
|
||||
Some(match column {
|
||||
TempWidgetColumn::Sensor => truncate_text(&self.sensor, calculated_width),
|
||||
TempWidgetColumn::Temp => truncate_text(&self.temperature(), calculated_width),
|
||||
})
|
||||
}
|
||||
|
||||
fn column_widths<C: DataTableColumn<TempWidgetColumn>>(
|
||||
data: &[TempWidgetData], _columns: &[C],
|
||||
) -> Vec<u16>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut widths = vec![0; 2];
|
||||
|
||||
data.iter().for_each(|row| {
|
||||
widths[0] = max(widths[0], row.sensor.len() as u16);
|
||||
widths[1] = max(widths[1], row.temperature().len() as u16);
|
||||
});
|
||||
|
||||
widths
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TempWidgetState {
|
||||
pub table: DataTable<TempWidgetData, TempWidgetColumn>,
|
||||
}
|
||||
|
||||
impl TempWidgetState {
|
||||
pub fn new(config: &AppConfigFields, colours: &CanvasColours) -> Self {
|
||||
const COLUMNS: [Column<TempWidgetColumn>; 2] = [
|
||||
Column::soft(TempWidgetColumn::Sensor, Some(0.8)),
|
||||
Column::soft(TempWidgetColumn::Temp, None),
|
||||
];
|
||||
|
||||
let props = DataTableProps {
|
||||
title: Some(" Temperatures ".into()),
|
||||
table_gap: config.table_gap,
|
||||
left_to_right: false,
|
||||
is_basic: config.use_basic_mode,
|
||||
show_table_scroll_position: config.show_table_scroll_position,
|
||||
show_current_entry_when_unfocused: false,
|
||||
};
|
||||
|
||||
let styling = DataTableStyling::from_colours(colours);
|
||||
|
||||
Self {
|
||||
table: DataTable::new(COLUMNS, props, styling),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
use crate::components::text_table::{
|
||||
CellContent, TableComponentColumn, TableComponentState, WidthBounds,
|
||||
};
|
||||
|
||||
pub struct TempWidgetState {
|
||||
pub table_state: TableComponentState,
|
||||
}
|
||||
|
||||
impl Default for TempWidgetState {
|
||||
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 {
|
||||
table_state: TableComponentState::new(
|
||||
TEMP_HEADERS
|
||||
.iter()
|
||||
.zip(WIDTHS)
|
||||
.map(|(header, width)| {
|
||||
TableComponentColumn::new_custom(CellContent::new(*header, None), width)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,13 @@
|
|||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use bottom::{canvas, constants::*, data_conversion::*, options::*, *};
|
||||
use bottom::{
|
||||
canvas::{self, canvas_colours::CanvasColours},
|
||||
constants::*,
|
||||
data_conversion::*,
|
||||
options::*,
|
||||
*,
|
||||
};
|
||||
|
||||
use std::{
|
||||
boxed::Box,
|
||||
|
@ -47,6 +53,12 @@ fn main() -> Result<()> {
|
|||
get_widget_layout(&matches, &config)
|
||||
.context("Found an issue while trying to build the widget layout.")?;
|
||||
|
||||
// FIXME: Should move this into build app or config
|
||||
let colours = {
|
||||
let colour_scheme = get_color_scheme(&matches, &config)?;
|
||||
CanvasColours::new(colour_scheme, &config)?
|
||||
};
|
||||
|
||||
// Create "app" struct, which will control most of the program and store settings/state
|
||||
let mut app = build_app(
|
||||
&matches,
|
||||
|
@ -54,12 +66,11 @@ fn main() -> Result<()> {
|
|||
&widget_layout,
|
||||
default_widget_id,
|
||||
&default_widget_type_option,
|
||||
config_path,
|
||||
&colours,
|
||||
)?;
|
||||
|
||||
// Create painter and set colours.
|
||||
let mut painter =
|
||||
canvas::Painter::init(widget_layout, &config, get_color_scheme(&matches, &config)?)?;
|
||||
let mut painter = canvas::Painter::init(widget_layout, colours)?;
|
||||
|
||||
// Create termination mutex and cvar
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
|
@ -150,7 +161,7 @@ fn main() -> Result<()> {
|
|||
app.is_force_redraw = true;
|
||||
}
|
||||
|
||||
if !app.is_frozen {
|
||||
if !app.frozen_state.is_frozen() {
|
||||
// Convert all data into tui-compliant components
|
||||
|
||||
// Network
|
||||
|
@ -177,12 +188,15 @@ fn main() -> Result<()> {
|
|||
|
||||
// Disk
|
||||
if app.used_widgets.use_disk {
|
||||
app.converted_data.disk_data = convert_disk_row(&app.data_collection);
|
||||
app.converted_data.ingest_disk_data(&app.data_collection);
|
||||
}
|
||||
|
||||
// Temperatures
|
||||
if app.used_widgets.use_temp {
|
||||
app.converted_data.temp_sensor_data = convert_temp_row(&app);
|
||||
app.converted_data.ingest_temp_data(
|
||||
&app.data_collection,
|
||||
app.app_config_fields.temperature_type,
|
||||
)
|
||||
}
|
||||
|
||||
// Memory
|
||||
|
@ -208,13 +222,9 @@ fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
if app.used_widgets.use_cpu {
|
||||
// CPU
|
||||
|
||||
convert_cpu_data_points(
|
||||
&app.data_collection,
|
||||
&mut app.converted_data.cpu_data,
|
||||
);
|
||||
if app.used_widgets.use_cpu {
|
||||
app.converted_data.ingest_cpu_data(&app.data_collection);
|
||||
app.converted_data.load_avg_data = app.data_collection.load_avg_harvest;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,12 +18,11 @@ use crate::{
|
|||
App,
|
||||
},
|
||||
constants::*,
|
||||
options::Config,
|
||||
utils::error,
|
||||
utils::error::BottomError,
|
||||
};
|
||||
|
||||
mod canvas_colours;
|
||||
pub mod canvas_colours;
|
||||
mod dialogs;
|
||||
mod drawing_utils;
|
||||
mod widgets;
|
||||
|
@ -77,9 +76,7 @@ pub struct Painter {
|
|||
}
|
||||
|
||||
impl Painter {
|
||||
pub fn init(
|
||||
widget_layout: BottomLayout, config: &Config, colour_scheme: ColourScheme,
|
||||
) -> anyhow::Result<Self> {
|
||||
pub fn init(widget_layout: BottomLayout, colours: CanvasColours) -> anyhow::Result<Self> {
|
||||
// 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
|
||||
// based on the console size.
|
||||
|
@ -148,7 +145,7 @@ impl Painter {
|
|||
});
|
||||
|
||||
let mut painter = Painter {
|
||||
colours: CanvasColours::default(),
|
||||
colours,
|
||||
height: 0,
|
||||
width: 0,
|
||||
styled_help_text: Vec::default(),
|
||||
|
@ -161,11 +158,6 @@ impl Painter {
|
|||
derived_widget_draw_locs: Vec::default(),
|
||||
};
|
||||
|
||||
if let ColourScheme::Custom = colour_scheme {
|
||||
painter.generate_config_colours(config)?;
|
||||
} else {
|
||||
painter.generate_colour_scheme(colour_scheme)?;
|
||||
}
|
||||
painter.complete_painter_init();
|
||||
|
||||
Ok(painter)
|
||||
|
@ -181,47 +173,6 @@ impl Painter {
|
|||
}
|
||||
}
|
||||
|
||||
fn generate_config_colours(&mut self, config: &Config) -> anyhow::Result<()> {
|
||||
if let Some(colours) = &config.colors {
|
||||
self.colours.set_colours_from_palette(colours)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_colour_scheme(&mut self, colour_scheme: ColourScheme) -> anyhow::Result<()> {
|
||||
match colour_scheme {
|
||||
ColourScheme::Default => {
|
||||
// Don't have to do anything.
|
||||
}
|
||||
ColourScheme::DefaultLight => {
|
||||
self.colours
|
||||
.set_colours_from_palette(&*DEFAULT_LIGHT_MODE_COLOUR_PALETTE)?;
|
||||
}
|
||||
ColourScheme::Gruvbox => {
|
||||
self.colours
|
||||
.set_colours_from_palette(&*GRUVBOX_COLOUR_PALETTE)?;
|
||||
}
|
||||
ColourScheme::GruvboxLight => {
|
||||
self.colours
|
||||
.set_colours_from_palette(&*GRUVBOX_LIGHT_COLOUR_PALETTE)?;
|
||||
}
|
||||
ColourScheme::Nord => {
|
||||
self.colours
|
||||
.set_colours_from_palette(&*NORD_COLOUR_PALETTE)?;
|
||||
}
|
||||
ColourScheme::NordLight => {
|
||||
self.colours
|
||||
.set_colours_from_palette(&*NORD_LIGHT_COLOUR_PALETTE)?;
|
||||
}
|
||||
ColourScheme::Custom => {
|
||||
// This case should never occur, just do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Must be run once before drawing, but after setting colours.
|
||||
/// This is to set some remaining styles and text.
|
||||
fn complete_painter_init(&mut self) {
|
||||
|
@ -275,7 +226,7 @@ impl Painter {
|
|||
use BottomWidgetType::*;
|
||||
|
||||
terminal.draw(|f| {
|
||||
let (terminal_size, frozen_draw_loc) = if app_state.is_frozen {
|
||||
let (terminal_size, frozen_draw_loc) = if app_state.frozen_state.is_frozen() {
|
||||
let split_loc = Layout::default()
|
||||
.constraints([Constraint::Min(0), Constraint::Length(1)])
|
||||
.split(f.size());
|
||||
|
@ -445,14 +396,12 @@ impl Painter {
|
|||
f,
|
||||
app_state,
|
||||
rect[0],
|
||||
true,
|
||||
app_state.current_widget.widget_id,
|
||||
),
|
||||
Temp => self.draw_temp_table(
|
||||
f,
|
||||
app_state,
|
||||
rect[0],
|
||||
true,
|
||||
app_state.current_widget.widget_id,
|
||||
),
|
||||
Net => self.draw_network_graph(
|
||||
|
@ -548,13 +497,9 @@ impl Painter {
|
|||
later_widget_id = Some(widget_id);
|
||||
if vertical_chunks[3].width >= 2 {
|
||||
match basic_table_widget_state.currently_displayed_widget_type {
|
||||
Disk => self.draw_disk_table(
|
||||
f,
|
||||
app_state,
|
||||
vertical_chunks[3],
|
||||
false,
|
||||
widget_id,
|
||||
),
|
||||
Disk => {
|
||||
self.draw_disk_table(f, app_state, vertical_chunks[3], widget_id)
|
||||
}
|
||||
Proc | ProcSort => {
|
||||
let wid = widget_id
|
||||
- match basic_table_widget_state.currently_displayed_widget_type
|
||||
|
@ -571,13 +516,9 @@ impl Painter {
|
|||
wid,
|
||||
);
|
||||
}
|
||||
Temp => self.draw_temp_table(
|
||||
f,
|
||||
app_state,
|
||||
vertical_chunks[3],
|
||||
false,
|
||||
widget_id,
|
||||
),
|
||||
Temp => {
|
||||
self.draw_temp_table(f, app_state, vertical_chunks[3], widget_id)
|
||||
}
|
||||
Battery => self.draw_battery_display(
|
||||
f,
|
||||
app_state,
|
||||
|
@ -708,12 +649,8 @@ impl Painter {
|
|||
Cpu => self.draw_cpu(f, app_state, *widget_draw_loc, widget.widget_id),
|
||||
Mem => self.draw_memory_graph(f, app_state, *widget_draw_loc, widget.widget_id),
|
||||
Net => self.draw_network(f, app_state, *widget_draw_loc, widget.widget_id),
|
||||
Temp => {
|
||||
self.draw_temp_table(f, app_state, *widget_draw_loc, true, widget.widget_id)
|
||||
}
|
||||
Disk => {
|
||||
self.draw_disk_table(f, app_state, *widget_draw_loc, true, widget.widget_id)
|
||||
}
|
||||
Temp => self.draw_temp_table(f, app_state, *widget_draw_loc, widget.widget_id),
|
||||
Disk => self.draw_disk_table(f, app_state, *widget_draw_loc, widget.widget_id),
|
||||
Proc => self.draw_process_widget(
|
||||
f,
|
||||
app_state,
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
use crate::{options::ConfigColours, utils::error};
|
||||
use crate::{
|
||||
constants::*,
|
||||
options::{Config, ConfigColours},
|
||||
utils::error,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use colour_utils::*;
|
||||
use tui::style::{Color, Style};
|
||||
|
||||
use super::ColourScheme;
|
||||
mod colour_utils;
|
||||
|
||||
pub struct CanvasColours {
|
||||
|
@ -78,6 +84,36 @@ impl Default for CanvasColours {
|
|||
}
|
||||
|
||||
impl CanvasColours {
|
||||
pub fn new(colour_scheme: ColourScheme, config: &Config) -> anyhow::Result<Self> {
|
||||
let mut canvas_colours = Self::default();
|
||||
|
||||
match colour_scheme {
|
||||
ColourScheme::Default => {}
|
||||
ColourScheme::DefaultLight => {
|
||||
canvas_colours.set_colours_from_palette(&*DEFAULT_LIGHT_MODE_COLOUR_PALETTE)?;
|
||||
}
|
||||
ColourScheme::Gruvbox => {
|
||||
canvas_colours.set_colours_from_palette(&*GRUVBOX_COLOUR_PALETTE)?;
|
||||
}
|
||||
ColourScheme::GruvboxLight => {
|
||||
canvas_colours.set_colours_from_palette(&*GRUVBOX_LIGHT_COLOUR_PALETTE)?;
|
||||
}
|
||||
ColourScheme::Nord => {
|
||||
canvas_colours.set_colours_from_palette(&*NORD_COLOUR_PALETTE)?;
|
||||
}
|
||||
ColourScheme::NordLight => {
|
||||
canvas_colours.set_colours_from_palette(&*NORD_LIGHT_COLOUR_PALETTE)?;
|
||||
}
|
||||
ColourScheme::Custom => {
|
||||
if let Some(colors) = &config.colors {
|
||||
canvas_colours.set_colours_from_palette(colors)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(canvas_colours)
|
||||
}
|
||||
|
||||
pub fn set_colours_from_palette(&mut self, colours: &ConfigColours) -> anyhow::Result<()> {
|
||||
if let Some(border_color) = &colours.border_color {
|
||||
self.set_border_colour(border_color)
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
app::App,
|
||||
canvas::{drawing_utils::*, Painter},
|
||||
constants::*,
|
||||
data_conversion::ConvertedCpuData,
|
||||
data_conversion::CpuWidgetData,
|
||||
};
|
||||
|
||||
use tui::{
|
||||
|
@ -21,7 +21,7 @@ impl Painter {
|
|||
) {
|
||||
// Skip the first element, it's the "all" element
|
||||
if app_state.converted_data.cpu_data.len() > 1 {
|
||||
let cpu_data: &[ConvertedCpuData] = &app_state.converted_data.cpu_data[1..];
|
||||
let cpu_data: &[CpuWidgetData] = &app_state.converted_data.cpu_data[1..];
|
||||
|
||||
// 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
|
||||
|
@ -70,69 +70,70 @@ impl Painter {
|
|||
// We do +4 as if it's too few bars in the bar length, it's kinda pointless.
|
||||
let cpu_bars = if chunk_width >= COMBINED_SPACING + 4 {
|
||||
let bar_length = chunk_width - COMBINED_SPACING;
|
||||
(0..num_cpus)
|
||||
.map(|cpu_index| {
|
||||
let use_percentage =
|
||||
if let Some(cpu_usage) = cpu_data[cpu_index].cpu_data.last() {
|
||||
cpu_usage.1
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let num_bars = calculate_basic_use_bars(use_percentage, bar_length);
|
||||
format!(
|
||||
cpu_data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, cpu)| match &cpu {
|
||||
CpuWidgetData::All => None,
|
||||
CpuWidgetData::Entry {
|
||||
data_type: _,
|
||||
data: _,
|
||||
last_entry,
|
||||
} => {
|
||||
let num_bars = calculate_basic_use_bars(*last_entry, bar_length);
|
||||
Some(format!(
|
||||
"{:3}[{}{}{:3.0}%]",
|
||||
if app_state.app_config_fields.show_average_cpu {
|
||||
if cpu_index == 0 {
|
||||
if index == 0 {
|
||||
"AVG".to_string()
|
||||
} else {
|
||||
(cpu_index - 1).to_string()
|
||||
(index - 1).to_string()
|
||||
}
|
||||
} else {
|
||||
cpu_index.to_string()
|
||||
index.to_string()
|
||||
},
|
||||
"|".repeat(num_bars),
|
||||
" ".repeat(bar_length - num_bars),
|
||||
use_percentage.round(),
|
||||
)
|
||||
last_entry.round(),
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else if chunk_width >= REDUCED_SPACING {
|
||||
(0..num_cpus)
|
||||
.map(|cpu_index| {
|
||||
let use_percentage =
|
||||
if let Some(cpu_usage) = cpu_data[cpu_index].cpu_data.last() {
|
||||
cpu_usage.1
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
format!(
|
||||
cpu_data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, cpu)| match &cpu {
|
||||
CpuWidgetData::All => None,
|
||||
CpuWidgetData::Entry {
|
||||
data_type: _,
|
||||
data: _,
|
||||
last_entry,
|
||||
} => Some(format!(
|
||||
"{:3} {:3.0}%",
|
||||
if app_state.app_config_fields.show_average_cpu {
|
||||
if cpu_index == 0 {
|
||||
if index == 0 {
|
||||
"AVG".to_string()
|
||||
} else {
|
||||
(cpu_index - 1).to_string()
|
||||
(index - 1).to_string()
|
||||
}
|
||||
} else {
|
||||
cpu_index.to_string()
|
||||
index.to_string()
|
||||
},
|
||||
use_percentage.round(),
|
||||
)
|
||||
last_entry.round(),
|
||||
)),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
(0..num_cpus)
|
||||
.map(|cpu_index| {
|
||||
let use_percentage =
|
||||
if let Some(cpu_usage) = cpu_data[cpu_index].cpu_data.last() {
|
||||
cpu_usage.1
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
format!("{:3.0}%", use_percentage.round(),)
|
||||
cpu_data
|
||||
.iter()
|
||||
.filter_map(|cpu| match &cpu {
|
||||
CpuWidgetData::All => None,
|
||||
CpuWidgetData::Entry {
|
||||
data_type: _,
|
||||
data: _,
|
||||
last_entry,
|
||||
} => Some(format!("{:3.0}%", last_entry.round())),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
use std::{borrow::Cow, iter};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
app::{layout_manager::WidgetDirection, App, CpuWidgetState},
|
||||
app::{layout_manager::WidgetDirection, widgets::CpuWidgetState, App},
|
||||
canvas::{drawing_utils::should_hide_x_label, Painter},
|
||||
components::{
|
||||
text_table::{CellContent, TextTable},
|
||||
data_table::{DrawInfo, SelectionState},
|
||||
time_graph::{GraphData, TimeGraph},
|
||||
},
|
||||
data_conversion::{ConvertedCpuData, TableData, TableRow},
|
||||
data_conversion::CpuWidgetData,
|
||||
};
|
||||
|
||||
use concat_string::concat_string;
|
||||
|
||||
use itertools::Either;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
|
@ -119,19 +118,21 @@ impl Painter {
|
|||
}
|
||||
|
||||
fn generate_points<'a>(
|
||||
&self, cpu_widget_state: &CpuWidgetState, cpu_data: &'a [ConvertedCpuData],
|
||||
show_avg_cpu: bool,
|
||||
&self, cpu_widget_state: &CpuWidgetState, cpu_data: &'a [CpuWidgetData], 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;
|
||||
let current_scroll_position = cpu_widget_state.table.state.current_index;
|
||||
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)| {
|
||||
.filter_map(|(itx, cpu)| {
|
||||
match &cpu {
|
||||
CpuWidgetData::All => None,
|
||||
CpuWidgetData::Entry { data, .. } => {
|
||||
let style = if show_avg_cpu && itx == AVG_POSITION {
|
||||
self.colours.avg_colour_style
|
||||
} else if itx == ALL_POSITION {
|
||||
|
@ -142,14 +143,18 @@ impl Painter {
|
|||
% self.colours.cpu_colour_styles.len()]
|
||||
};
|
||||
|
||||
GraphData {
|
||||
points: &cpu.cpu_data[..],
|
||||
Some(GraphData {
|
||||
points: &data[..],
|
||||
style,
|
||||
name: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else if let Some(cpu) = cpu_data.get(current_scroll_position) {
|
||||
} else if let Some(CpuWidgetData::Entry { data, .. }) =
|
||||
cpu_data.get(current_scroll_position)
|
||||
{
|
||||
let style = if show_avg_cpu && current_scroll_position == AVG_POSITION {
|
||||
self.colours.avg_colour_style
|
||||
} else {
|
||||
|
@ -159,7 +164,7 @@ impl Painter {
|
|||
};
|
||||
|
||||
vec![GraphData {
|
||||
points: &cpu.cpu_data[..],
|
||||
points: &data[..],
|
||||
style,
|
||||
name: None,
|
||||
}]
|
||||
|
@ -227,86 +232,24 @@ impl Painter {
|
|||
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))
|
||||
{
|
||||
// TODO: This line (and the one above, see caller) is pretty dumb but I guess needed.
|
||||
// TODO: This line (and the one above, see caller) is pretty dumb but I guess needed for now. Refactor if possible!
|
||||
cpu_widget_state.is_legend_hidden = false;
|
||||
|
||||
let show_avg_cpu = app_state.app_config_fields.show_average_cpu;
|
||||
let cpu_data = {
|
||||
let col_widths = vec![1, 3]; // TODO: Should change this to take const generics (usize) and an array.
|
||||
let colour_iter = if show_avg_cpu {
|
||||
Either::Left(
|
||||
iter::once(&self.colours.all_colour_style)
|
||||
.chain(iter::once(&self.colours.avg_colour_style))
|
||||
.chain(self.colours.cpu_colour_styles.iter().cycle()),
|
||||
)
|
||||
} else {
|
||||
Either::Right(
|
||||
iter::once(&self.colours.all_colour_style)
|
||||
.chain(self.colours.cpu_colour_styles.iter().cycle()),
|
||||
)
|
||||
};
|
||||
|
||||
let data = {
|
||||
let iter = app_state.converted_data.cpu_data.iter().zip(colour_iter);
|
||||
const CPU_WIDTH_CHECK: u16 = 10; // This is hard-coded, it's terrible.
|
||||
if draw_loc.width < CPU_WIDTH_CHECK {
|
||||
Either::Left(iter.map(|(cpu, style)| {
|
||||
let row = vec![
|
||||
CellContent::Simple("".into()),
|
||||
CellContent::Simple(if cpu.legend_value.is_empty() {
|
||||
cpu.cpu_name.clone().into()
|
||||
} else {
|
||||
cpu.legend_value.clone().into()
|
||||
}),
|
||||
];
|
||||
TableRow::Styled(row, *style)
|
||||
}))
|
||||
} else {
|
||||
Either::Right(iter.map(|(cpu, style)| {
|
||||
let row = vec![
|
||||
CellContent::HasAlt {
|
||||
alt: cpu.short_cpu_name.clone().into(),
|
||||
main: cpu.cpu_name.clone().into(),
|
||||
},
|
||||
CellContent::Simple(cpu.legend_value.clone().into()),
|
||||
];
|
||||
TableRow::Styled(row, *style)
|
||||
}))
|
||||
}
|
||||
}
|
||||
.collect();
|
||||
|
||||
TableData { data, col_widths }
|
||||
};
|
||||
|
||||
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
||||
let border_style = if is_on_widget {
|
||||
self.colours.highlighted_border_style
|
||||
} else {
|
||||
self.colours.border_style
|
||||
|
||||
let draw_info = DrawInfo {
|
||||
loc: draw_loc,
|
||||
force_redraw: app_state.is_force_redraw,
|
||||
recalculate_column_widths,
|
||||
selection_state: SelectionState::new(app_state.is_expanded, is_on_widget),
|
||||
};
|
||||
|
||||
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: self.colours.currently_selected_text_style, // We always highlight the selected CPU entry... not sure if I like this though.
|
||||
title: None,
|
||||
is_on_widget,
|
||||
draw_border: true,
|
||||
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: false,
|
||||
}
|
||||
.draw_text_table(
|
||||
cpu_widget_state.table.draw(
|
||||
f,
|
||||
draw_loc,
|
||||
&mut cpu_widget_state.table_state,
|
||||
&cpu_data,
|
||||
None,
|
||||
&draw_info,
|
||||
app_state.converted_data.cpu_data.clone(),
|
||||
app_state.widget_map.get_mut(&widget_id),
|
||||
self,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,51 +1,32 @@
|
|||
use tui::{backend::Backend, layout::Rect, terminal::Frame};
|
||||
|
||||
use crate::{
|
||||
app,
|
||||
app::{self},
|
||||
canvas::Painter,
|
||||
components::text_table::{TextTable, TextTableTitle},
|
||||
components::data_table::{DrawInfo, SelectionState},
|
||||
};
|
||||
|
||||
impl Painter {
|
||||
pub fn draw_disk_table<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, widget_id: u64,
|
||||
) {
|
||||
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) {
|
||||
let is_on_widget = app_state.current_widget.widget_id == widget_id;
|
||||
let (border_style, highlighted_text_style) = if is_on_widget {
|
||||
(
|
||||
self.colours.highlighted_border_style,
|
||||
self.colours.currently_selected_text_style,
|
||||
)
|
||||
} else {
|
||||
(self.colours.border_style, self.colours.text_style)
|
||||
};
|
||||
TextTable {
|
||||
table_gap: app_state.app_config_fields.table_gap,
|
||||
is_force_redraw: app_state.is_force_redraw,
|
||||
|
||||
let draw_info = DrawInfo {
|
||||
loc: draw_loc,
|
||||
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: " Disks ".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(
|
||||
selection_state: SelectionState::new(app_state.is_expanded, is_on_widget),
|
||||
};
|
||||
|
||||
disk_widget_state.table.draw(
|
||||
f,
|
||||
draw_loc,
|
||||
&mut disk_widget_state.table_state,
|
||||
&app_state.converted_data.disk_data,
|
||||
&draw_info,
|
||||
app_state.converted_data.disk_data.clone(),
|
||||
app_state.widget_map.get_mut(&widget_id),
|
||||
self,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use crate::{
|
||||
app::App,
|
||||
canvas::{drawing_utils::get_search_start_position, Painter},
|
||||
components::text_table::{TextTable, TextTableTitle},
|
||||
components::data_table::{DrawInfo, SelectionState},
|
||||
constants::*,
|
||||
data_conversion::{TableData, TableRow},
|
||||
};
|
||||
|
||||
use tui::{
|
||||
|
@ -54,10 +53,10 @@ impl Painter {
|
|||
.split(proc_draw_loc);
|
||||
proc_draw_loc = processes_chunk[1];
|
||||
|
||||
self.draw_sort_table(f, app_state, processes_chunk[0], draw_border, widget_id + 2);
|
||||
self.draw_sort_table(f, app_state, processes_chunk[0], 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, widget_id);
|
||||
}
|
||||
|
||||
if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) {
|
||||
|
@ -73,8 +72,7 @@ impl Painter {
|
|||
///
|
||||
/// This should not be directly called.
|
||||
fn draw_processes_table<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
) {
|
||||
let should_get_widget_bounds = app_state.should_get_widget_bounds();
|
||||
if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) {
|
||||
|
@ -82,47 +80,20 @@ impl Painter {
|
|||
should_get_widget_bounds || proc_widget_state.force_rerender;
|
||||
|
||||
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
||||
let (border_style, highlighted_text_style) = if is_on_widget {
|
||||
(
|
||||
self.colours.highlighted_border_style,
|
||||
self.colours.currently_selected_text_style,
|
||||
)
|
||||
} else {
|
||||
(self.colours.border_style, self.colours.text_style)
|
||||
|
||||
let draw_info = DrawInfo {
|
||||
loc: draw_loc,
|
||||
force_redraw: app_state.is_force_redraw,
|
||||
recalculate_column_widths,
|
||||
selection_state: SelectionState::new(app_state.is_expanded, is_on_widget),
|
||||
};
|
||||
|
||||
// TODO: [Refactor] This is an ugly hack to add the disabled style...
|
||||
// this could be solved by storing style locally to the widget.
|
||||
for row in &mut proc_widget_state.table_data.data {
|
||||
if let TableRow::Styled(_, style) = row {
|
||||
*style = style.patch(self.colours.disabled_text_style);
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
proc_widget_state.table.draw(
|
||||
f,
|
||||
draw_loc,
|
||||
&mut proc_widget_state.table_state,
|
||||
&proc_widget_state.table_data,
|
||||
&draw_info,
|
||||
proc_widget_state.table_data.clone(),
|
||||
app_state.widget_map.get_mut(&widget_id),
|
||||
self,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -141,14 +112,14 @@ impl Painter {
|
|||
cursor_position: usize, query: &str, currently_selected_text_style: tui::style::Style,
|
||||
text_style: tui::style::Style,
|
||||
) -> Vec<Span<'a>> {
|
||||
let mut current_grapheme_posn = 0;
|
||||
let mut current_grapheme_pos = 0;
|
||||
|
||||
if is_on_widget {
|
||||
let mut res = grapheme_indices
|
||||
.filter_map(|grapheme| {
|
||||
current_grapheme_posn += UnicodeWidthStr::width(grapheme.1);
|
||||
current_grapheme_pos += UnicodeWidthStr::width(grapheme.1);
|
||||
|
||||
if current_grapheme_posn <= start_position {
|
||||
if current_grapheme_pos <= start_position {
|
||||
None
|
||||
} else {
|
||||
let styled = if grapheme.0 == cursor_position {
|
||||
|
@ -338,68 +309,29 @@ impl Painter {
|
|||
///
|
||||
/// This should not be directly called.
|
||||
fn draw_sort_table<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
) {
|
||||
let should_get_widget_bounds = app_state.should_get_widget_bounds();
|
||||
if let Some(proc_widget_state) =
|
||||
app_state.proc_state.widget_states.get_mut(&(widget_id - 2))
|
||||
{
|
||||
let recalculate_column_widths =
|
||||
should_get_widget_bounds || proc_widget_state.force_rerender;
|
||||
if let Some(pws) = app_state.proc_state.widget_states.get_mut(&(widget_id - 2)) {
|
||||
let recalculate_column_widths = should_get_widget_bounds || pws.force_rerender;
|
||||
|
||||
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
||||
let (border_style, highlighted_text_style) = if is_on_widget {
|
||||
(
|
||||
self.colours.highlighted_border_style,
|
||||
self.colours.currently_selected_text_style,
|
||||
)
|
||||
} else {
|
||||
(self.colours.border_style, self.colours.text_style)
|
||||
};
|
||||
|
||||
// TODO: [PROC] Perhaps move this generation elsewhere... or leave it as is but look at partial rendering?
|
||||
let table_data = {
|
||||
let data = proc_widget_state
|
||||
.table_state
|
||||
.columns
|
||||
.iter()
|
||||
.filter_map(|col| {
|
||||
if col.is_hidden {
|
||||
None
|
||||
} else {
|
||||
Some(TableRow::Raw(vec![col.header.text().clone()]))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
TableData {
|
||||
data,
|
||||
col_widths: vec![usize::from(SORT_MENU_WIDTH)],
|
||||
}
|
||||
};
|
||||
|
||||
TextTable {
|
||||
table_gap: app_state.app_config_fields.table_gap,
|
||||
is_force_redraw: app_state.is_force_redraw,
|
||||
let draw_info = DrawInfo {
|
||||
loc: draw_loc,
|
||||
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(
|
||||
selection_state: SelectionState::new(app_state.is_expanded, is_on_widget),
|
||||
};
|
||||
|
||||
let data = pws.column_text();
|
||||
|
||||
pws.sort_table.draw(
|
||||
f,
|
||||
draw_loc,
|
||||
&mut proc_widget_state.sort_table_state,
|
||||
&table_data,
|
||||
&draw_info,
|
||||
data,
|
||||
app_state.widget_map.get_mut(&widget_id),
|
||||
self,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,50 +3,30 @@ use tui::{backend::Backend, layout::Rect, terminal::Frame};
|
|||
use crate::{
|
||||
app,
|
||||
canvas::Painter,
|
||||
components::text_table::{TextTable, TextTableTitle},
|
||||
components::data_table::{DrawInfo, SelectionState},
|
||||
};
|
||||
|
||||
impl Painter {
|
||||
pub fn draw_temp_table<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, widget_id: u64,
|
||||
) {
|
||||
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) {
|
||||
let is_on_widget = app_state.current_widget.widget_id == widget_id;
|
||||
|
||||
let (border_style, highlighted_text_style) = if is_on_widget {
|
||||
(
|
||||
self.colours.highlighted_border_style,
|
||||
self.colours.currently_selected_text_style,
|
||||
)
|
||||
} else {
|
||||
(self.colours.border_style, self.colours.text_style)
|
||||
};
|
||||
TextTable {
|
||||
table_gap: app_state.app_config_fields.table_gap,
|
||||
is_force_redraw: app_state.is_force_redraw,
|
||||
let draw_info = DrawInfo {
|
||||
loc: draw_loc,
|
||||
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: " Temperatures ".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: false,
|
||||
}
|
||||
.draw_text_table(
|
||||
selection_state: SelectionState::new(app_state.is_expanded, is_on_widget),
|
||||
};
|
||||
|
||||
temp_widget_state.table.draw(
|
||||
f,
|
||||
draw_loc,
|
||||
&mut temp_widget_state.table_state,
|
||||
&app_state.converted_data.temp_sensor_data,
|
||||
&draw_info,
|
||||
app_state.converted_data.temp_data.clone(),
|
||||
app_state.widget_map.get_mut(&widget_id),
|
||||
self,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
mod tui_widget;
|
||||
|
||||
pub mod data_table;
|
||||
pub mod time_graph;
|
||||
|
||||
pub mod text_table;
|
||||
|
|
242
src/components/data_table.rs
Normal file
242
src/components/data_table.rs
Normal file
|
@ -0,0 +1,242 @@
|
|||
use std::{convert::TryInto, marker::PhantomData};
|
||||
|
||||
pub mod column;
|
||||
pub use column::*;
|
||||
|
||||
pub mod styling;
|
||||
pub use styling::*;
|
||||
|
||||
pub mod props;
|
||||
pub use props::DataTableProps;
|
||||
|
||||
pub mod state;
|
||||
pub use state::{DataTableState, ScrollDirection};
|
||||
|
||||
pub mod draw;
|
||||
pub use draw::*;
|
||||
|
||||
pub mod data_type;
|
||||
pub use data_type::*;
|
||||
|
||||
pub mod sortable;
|
||||
pub use sortable::*;
|
||||
|
||||
/// A [`DataTable`] is a component that displays data in a tabular form.
|
||||
///
|
||||
/// Note that [`DataTable`] takes a generic type `S`, bounded by [`SortType`]. This controls whether this table
|
||||
/// expects sorted data or not, with two expected types:
|
||||
///
|
||||
/// - [`Unsortable`]: The default if otherwise not specified. This table does not expect sorted data.
|
||||
/// - [`Sortable`]: This table expects sorted data, and there are helper functions to
|
||||
/// facilitate things like sorting based on a selected column, shortcut column selection support, mouse column
|
||||
/// selection support, etc.
|
||||
pub struct DataTable<DataType, Header, S = Unsortable, C = Column<Header>> {
|
||||
pub columns: Vec<C>,
|
||||
pub state: DataTableState,
|
||||
pub props: DataTableProps,
|
||||
pub styling: DataTableStyling,
|
||||
data: Vec<DataType>,
|
||||
sort_type: S,
|
||||
first_draw: bool,
|
||||
_pd: PhantomData<(DataType, S, Header)>,
|
||||
}
|
||||
|
||||
impl<DataType: DataToCell<H>, H: ColumnHeader> DataTable<DataType, H, Unsortable, Column<H>> {
|
||||
pub fn new<C: Into<Vec<Column<H>>>>(
|
||||
columns: C, props: DataTableProps, styling: DataTableStyling,
|
||||
) -> Self {
|
||||
Self {
|
||||
columns: columns.into(),
|
||||
state: DataTableState::default(),
|
||||
props,
|
||||
styling,
|
||||
data: vec![],
|
||||
sort_type: Unsortable,
|
||||
first_draw: true,
|
||||
_pd: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<DataType: DataToCell<H>, H: ColumnHeader, S: SortType, C: DataTableColumn<H>>
|
||||
DataTable<DataType, H, S, C>
|
||||
{
|
||||
/// Sets the scroll position to the first value.
|
||||
pub fn set_first(&mut self) {
|
||||
self.state.current_index = 0;
|
||||
self.state.scroll_direction = ScrollDirection::Up;
|
||||
}
|
||||
|
||||
/// Sets the scroll position to the last value.
|
||||
pub fn set_last(&mut self) {
|
||||
self.state.current_index = self.data.len().saturating_sub(1);
|
||||
self.state.scroll_direction = ScrollDirection::Down;
|
||||
}
|
||||
|
||||
/// Updates the scroll position to be valid for the number of entries.
|
||||
fn set_data(&mut self, data: Vec<DataType>) {
|
||||
self.data = data;
|
||||
let max_pos = self.data.len().saturating_sub(1);
|
||||
if self.state.current_index > max_pos {
|
||||
self.state.current_index = max_pos;
|
||||
self.state.display_start_index = 0;
|
||||
self.state.scroll_direction = ScrollDirection::Down;
|
||||
}
|
||||
}
|
||||
|
||||
/// Increments the scroll position if possible by a positive/negative offset. If there is a
|
||||
/// valid change, this function will also return the new position wrapped in an [`Option`].
|
||||
pub fn increment_position(&mut self, change: i64) -> Option<usize> {
|
||||
let max_index = self.data.len();
|
||||
let current_index = self.state.current_index;
|
||||
|
||||
if change == 0
|
||||
|| (change > 0 && current_index == max_index)
|
||||
|| (change < 0 && current_index == 0)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let csp: Result<i64, _> = self.state.current_index.try_into();
|
||||
if let Ok(csp) = csp {
|
||||
let proposed: Result<usize, _> = (csp + change).try_into();
|
||||
if let Ok(proposed) = proposed {
|
||||
if proposed < self.data.len() {
|
||||
self.state.current_index = proposed;
|
||||
self.state.scroll_direction = if change < 0 {
|
||||
ScrollDirection::Up
|
||||
} else {
|
||||
ScrollDirection::Down
|
||||
};
|
||||
|
||||
return Some(self.state.current_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Updates the scroll position to a selected index.
|
||||
#[allow(clippy::comparison_chain)]
|
||||
pub fn set_position(&mut self, new_index: usize) {
|
||||
let new_index = new_index.clamp(0, self.data.len().saturating_sub(1));
|
||||
if self.state.current_index < new_index {
|
||||
self.state.scroll_direction = ScrollDirection::Down;
|
||||
} else if self.state.current_index > new_index {
|
||||
self.state.scroll_direction = ScrollDirection::Up;
|
||||
}
|
||||
self.state.current_index = new_index;
|
||||
}
|
||||
|
||||
/// Returns the current scroll index.
|
||||
pub fn current_index(&self) -> usize {
|
||||
self.state.current_index
|
||||
}
|
||||
|
||||
/// Optionally returns the currently selected item, if there is one.
|
||||
pub fn current_item(&self) -> Option<&DataType> {
|
||||
self.data.get(self.state.current_index)
|
||||
}
|
||||
|
||||
/// Returns tui-rs' internal selection.
|
||||
pub fn tui_selected(&self) -> Option<usize> {
|
||||
self.state.table_state.selected()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
struct TestType {
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl DataToCell<&'static str> for TestType {
|
||||
fn to_cell<'a>(
|
||||
&'a self, _column: &&'static str, _calculated_width: u16,
|
||||
) -> Option<tui::text::Text<'a>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn column_widths<C: DataTableColumn<&'static str>>(
|
||||
_data: &[Self], _columns: &[C],
|
||||
) -> Vec<u16>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_data_table_operations() {
|
||||
let columns = [Column::hard("a", 10), Column::hard("b", 10)];
|
||||
let props = DataTableProps {
|
||||
title: Some("test".into()),
|
||||
table_gap: 1,
|
||||
left_to_right: false,
|
||||
is_basic: false,
|
||||
show_table_scroll_position: true,
|
||||
show_current_entry_when_unfocused: false,
|
||||
};
|
||||
let styling = DataTableStyling::default();
|
||||
|
||||
let mut table = DataTable::new(columns, props, styling);
|
||||
table.set_data((0..=4).map(|index| TestType { index }).collect::<Vec<_>>());
|
||||
|
||||
table.set_last();
|
||||
assert_eq!(table.current_index(), 4);
|
||||
assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
|
||||
|
||||
table.set_first();
|
||||
assert_eq!(table.current_index(), 0);
|
||||
assert_eq!(table.state.scroll_direction, ScrollDirection::Up);
|
||||
|
||||
table.set_position(4);
|
||||
assert_eq!(table.current_index(), 4);
|
||||
assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
|
||||
|
||||
table.set_position(100);
|
||||
assert_eq!(table.current_index(), 4);
|
||||
assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
|
||||
assert_eq!(table.current_item(), Some(&TestType { index: 4 }));
|
||||
|
||||
table.increment_position(-1);
|
||||
assert_eq!(table.current_index(), 3);
|
||||
assert_eq!(table.state.scroll_direction, ScrollDirection::Up);
|
||||
assert_eq!(table.current_item(), Some(&TestType { index: 3 }));
|
||||
|
||||
table.increment_position(-3);
|
||||
assert_eq!(table.current_index(), 0);
|
||||
assert_eq!(table.state.scroll_direction, ScrollDirection::Up);
|
||||
assert_eq!(table.current_item(), Some(&TestType { index: 0 }));
|
||||
|
||||
table.increment_position(-3);
|
||||
assert_eq!(table.current_index(), 0);
|
||||
assert_eq!(table.state.scroll_direction, ScrollDirection::Up);
|
||||
assert_eq!(table.current_item(), Some(&TestType { index: 0 }));
|
||||
|
||||
table.increment_position(1);
|
||||
assert_eq!(table.current_index(), 1);
|
||||
assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
|
||||
assert_eq!(table.current_item(), Some(&TestType { index: 1 }));
|
||||
|
||||
table.increment_position(3);
|
||||
assert_eq!(table.current_index(), 4);
|
||||
assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
|
||||
assert_eq!(table.current_item(), Some(&TestType { index: 4 }));
|
||||
|
||||
table.increment_position(10);
|
||||
assert_eq!(table.current_index(), 4);
|
||||
assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
|
||||
assert_eq!(table.current_item(), Some(&TestType { index: 4 }));
|
||||
|
||||
table.set_data((0..=2).map(|index| TestType { index }).collect::<Vec<_>>());
|
||||
assert_eq!(table.current_index(), 2);
|
||||
assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
|
||||
assert_eq!(table.current_item(), Some(&TestType { index: 2 }));
|
||||
}
|
||||
}
|
257
src/components/data_table/column.rs
Normal file
257
src/components/data_table/column.rs
Normal file
|
@ -0,0 +1,257 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{max, min},
|
||||
};
|
||||
|
||||
/// A bound on the width of a column.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ColumnWidthBounds {
|
||||
/// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point.
|
||||
Soft {
|
||||
/// 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),
|
||||
|
||||
/// A width of this type always resizes to the column header's text width.
|
||||
FollowHeader,
|
||||
}
|
||||
|
||||
pub trait ColumnHeader {
|
||||
/// The "text" version of the column header.
|
||||
fn text(&self) -> Cow<'static, str>;
|
||||
|
||||
/// The version displayed when drawing the table. Defaults to [`ColumnHeader::text`].
|
||||
#[inline(always)]
|
||||
fn header(&self) -> Cow<'static, str> {
|
||||
self.text()
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnHeader for &'static str {
|
||||
fn text(&self) -> Cow<'static, str> {
|
||||
Cow::Borrowed(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnHeader for String {
|
||||
fn text(&self) -> Cow<'static, str> {
|
||||
Cow::Owned(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DataTableColumn<H: ColumnHeader> {
|
||||
fn inner(&self) -> &H;
|
||||
|
||||
fn inner_mut(&mut self) -> &mut H;
|
||||
|
||||
fn bounds(&self) -> ColumnWidthBounds;
|
||||
|
||||
fn bounds_mut(&mut self) -> &mut ColumnWidthBounds;
|
||||
|
||||
fn is_hidden(&self) -> bool;
|
||||
|
||||
fn set_is_hidden(&mut self, is_hidden: bool);
|
||||
|
||||
/// The actually displayed "header".
|
||||
fn header(&self) -> Cow<'static, str>;
|
||||
|
||||
/// The header length, along with any required additional lengths for things like arrows.
|
||||
/// Defaults to getting the length of [`DataTableColumn::header`].
|
||||
fn header_len(&self) -> usize {
|
||||
self.header().len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Column<H> {
|
||||
/// The inner column header.
|
||||
inner: H,
|
||||
|
||||
/// A restriction on this column's width.
|
||||
bounds: ColumnWidthBounds,
|
||||
|
||||
/// Marks that this column is currently "hidden", and should *always* be skipped.
|
||||
is_hidden: bool,
|
||||
}
|
||||
|
||||
impl<H: ColumnHeader> DataTableColumn<H> for Column<H> {
|
||||
#[inline]
|
||||
fn inner(&self) -> &H {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn inner_mut(&mut self) -> &mut H {
|
||||
&mut self.inner
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bounds(&self) -> ColumnWidthBounds {
|
||||
self.bounds
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bounds_mut(&mut self) -> &mut ColumnWidthBounds {
|
||||
&mut self.bounds
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_hidden(&self) -> bool {
|
||||
self.is_hidden
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_is_hidden(&mut self, is_hidden: bool) {
|
||||
self.is_hidden = is_hidden;
|
||||
}
|
||||
|
||||
fn header(&self) -> Cow<'static, str> {
|
||||
self.inner.text()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: ColumnHeader> Column<H> {
|
||||
pub const fn new(inner: H) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
bounds: ColumnWidthBounds::FollowHeader,
|
||||
is_hidden: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn hard(inner: H, width: u16) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
bounds: ColumnWidthBounds::Hard(width),
|
||||
is_hidden: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn soft(inner: H, max_percentage: Option<f32>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
bounds: ColumnWidthBounds::Soft {
|
||||
desired: 0,
|
||||
max_percentage,
|
||||
},
|
||||
is_hidden: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CalculateColumnWidths<H> {
|
||||
/// Calculates widths for the columns of this table, given the current width when called.
|
||||
///
|
||||
/// * `total_width` is the total width on the canvas that the columns can try and work with.
|
||||
/// * `left_to_right` is whether to size from left-to-right (`true`) or right-to-left (`false`).
|
||||
fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<u16>;
|
||||
}
|
||||
|
||||
impl<H, C> CalculateColumnWidths<H> for [C]
|
||||
where
|
||||
H: ColumnHeader,
|
||||
C: DataTableColumn<H>,
|
||||
{
|
||||
fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<u16> {
|
||||
use itertools::Either;
|
||||
|
||||
let mut total_width_left = total_width;
|
||||
let mut calculated_widths = vec![0; self.len()];
|
||||
let columns = if left_to_right {
|
||||
Either::Left(self.iter().zip(calculated_widths.iter_mut()))
|
||||
} else {
|
||||
Either::Right(self.iter().zip(calculated_widths.iter_mut()).rev())
|
||||
};
|
||||
|
||||
let mut num_columns = 0;
|
||||
for (column, calculated_width) in columns {
|
||||
if column.is_hidden() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match &column.bounds() {
|
||||
ColumnWidthBounds::Soft {
|
||||
desired,
|
||||
max_percentage,
|
||||
} => {
|
||||
let min_width = column.header_len() as u16;
|
||||
if min_width > total_width_left {
|
||||
break;
|
||||
}
|
||||
|
||||
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 {
|
||||
break;
|
||||
} else if space_taken > 0 {
|
||||
total_width_left = total_width_left.saturating_sub(space_taken + 1);
|
||||
*calculated_width = space_taken;
|
||||
num_columns += 1;
|
||||
}
|
||||
}
|
||||
ColumnWidthBounds::Hard(width) => {
|
||||
let min_width = *width;
|
||||
if min_width > total_width_left || min_width == 0 {
|
||||
break;
|
||||
} else if min_width > 0 {
|
||||
total_width_left = total_width_left.saturating_sub(min_width + 1);
|
||||
*calculated_width = min_width;
|
||||
num_columns += 1;
|
||||
}
|
||||
}
|
||||
ColumnWidthBounds::FollowHeader => {
|
||||
let min_width = column.header_len() as u16;
|
||||
if min_width > total_width_left || min_width == 0 {
|
||||
break;
|
||||
} else if min_width > 0 {
|
||||
total_width_left = total_width_left.saturating_sub(min_width + 1);
|
||||
*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 width in calculated_widths.iter_mut() {
|
||||
if num_dist == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if *width > 0 {
|
||||
if total_width_left > 0 {
|
||||
*width += amount_per_slot + 1;
|
||||
total_width_left -= 1;
|
||||
} else {
|
||||
*width += amount_per_slot;
|
||||
}
|
||||
|
||||
num_dist -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
calculated_widths
|
||||
}
|
||||
}
|
26
src/components/data_table/data_type.rs
Normal file
26
src/components/data_table/data_type.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use tui::{text::Text, widgets::Row};
|
||||
|
||||
use crate::canvas::Painter;
|
||||
|
||||
use super::{ColumnHeader, DataTableColumn};
|
||||
|
||||
pub trait DataToCell<H>
|
||||
where
|
||||
H: ColumnHeader,
|
||||
{
|
||||
/// Given data, a column, and its corresponding width, return what should be displayed in the [`DataTable`](super::DataTable).
|
||||
fn to_cell<'a>(&'a self, column: &H, calculated_width: u16) -> Option<Text<'a>>;
|
||||
|
||||
/// Apply styling to the generated [`Row`] of cells.
|
||||
///
|
||||
/// The default implementation just returns the `row` that is passed in.
|
||||
#[inline(always)]
|
||||
fn style_row<'a>(&self, row: Row<'a>, _painter: &Painter) -> Row<'a> {
|
||||
row
|
||||
}
|
||||
|
||||
/// Returns the desired column widths in light of having seen data.
|
||||
fn column_widths<C: DataTableColumn<H>>(data: &[Self], columns: &[C]) -> Vec<u16>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
289
src/components/data_table/draw.rs
Normal file
289
src/components/data_table/draw.rs
Normal file
|
@ -0,0 +1,289 @@
|
|||
use std::{
|
||||
cmp::{max, min},
|
||||
iter::once,
|
||||
};
|
||||
|
||||
use concat_string::concat_string;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, Row, Table},
|
||||
Frame,
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::{
|
||||
app::layout_manager::BottomWidget,
|
||||
canvas::Painter,
|
||||
constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
|
||||
};
|
||||
|
||||
use super::{
|
||||
CalculateColumnWidths, ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataToCell,
|
||||
SortType,
|
||||
};
|
||||
|
||||
pub enum SelectionState {
|
||||
NotSelected,
|
||||
Selected,
|
||||
Expanded,
|
||||
}
|
||||
|
||||
impl SelectionState {
|
||||
pub fn new(is_expanded: bool, is_on_widget: bool) -> Self {
|
||||
if is_expanded {
|
||||
SelectionState::Expanded
|
||||
} else if is_on_widget {
|
||||
SelectionState::Selected
|
||||
} else {
|
||||
SelectionState::NotSelected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`DrawInfo`] is information required on each draw call.
|
||||
pub struct DrawInfo {
|
||||
pub loc: Rect,
|
||||
pub force_redraw: bool,
|
||||
pub recalculate_column_widths: bool,
|
||||
pub selection_state: SelectionState,
|
||||
}
|
||||
|
||||
impl DrawInfo {
|
||||
pub fn is_on_widget(&self) -> bool {
|
||||
matches!(self.selection_state, SelectionState::Selected)
|
||||
|| matches!(self.selection_state, SelectionState::Expanded)
|
||||
}
|
||||
|
||||
pub fn is_expanded(&self) -> bool {
|
||||
matches!(self.selection_state, SelectionState::Expanded)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DataType, H, S, C> DataTable<DataType, H, S, C>
|
||||
where
|
||||
DataType: DataToCell<H>,
|
||||
H: ColumnHeader,
|
||||
S: SortType,
|
||||
C: DataTableColumn<H>,
|
||||
{
|
||||
fn block<'a>(&self, draw_info: &'a DrawInfo, data_len: usize) -> Block<'a> {
|
||||
let border_style = match draw_info.selection_state {
|
||||
SelectionState::NotSelected => self.styling.border_style,
|
||||
SelectionState::Selected | SelectionState::Expanded => {
|
||||
self.styling.highlighted_border_style
|
||||
}
|
||||
};
|
||||
|
||||
if !self.props.is_basic {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(border_style);
|
||||
|
||||
if let Some(title) = self.generate_title(draw_info, data_len) {
|
||||
block.title(title)
|
||||
} else {
|
||||
block
|
||||
}
|
||||
} else if draw_info.is_on_widget() {
|
||||
// Implies it is basic mode but selected.
|
||||
Block::default()
|
||||
.borders(SIDE_BORDERS)
|
||||
.border_style(border_style)
|
||||
} else {
|
||||
Block::default().borders(Borders::NONE)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a title, given the available space.
|
||||
pub fn generate_title<'a>(
|
||||
&self, draw_info: &'a DrawInfo, total_items: usize,
|
||||
) -> Option<Spans<'a>> {
|
||||
self.props.title.as_ref().map(|title| {
|
||||
let current_index = self.state.current_index.saturating_add(1);
|
||||
let draw_loc = draw_info.loc;
|
||||
let title_style = self.styling.title_style;
|
||||
let border_style = if draw_info.is_on_widget() {
|
||||
self.styling.highlighted_border_style
|
||||
} else {
|
||||
self.styling.border_style
|
||||
};
|
||||
|
||||
let title = if self.props.show_table_scroll_position {
|
||||
let pos = current_index.to_string();
|
||||
let tot = total_items.to_string();
|
||||
let title_string = concat_string!(title, "(", pos, " of ", tot, ") ");
|
||||
|
||||
if title_string.len() + 2 <= draw_loc.width.into() {
|
||||
title_string
|
||||
} else {
|
||||
title.to_string()
|
||||
}
|
||||
} else {
|
||||
title.to_string()
|
||||
};
|
||||
|
||||
if draw_info.is_expanded() {
|
||||
let title_base = concat_string!(title, "── Esc to go back ");
|
||||
let lines = "─".repeat(usize::from(draw_loc.width).saturating_sub(
|
||||
UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2,
|
||||
));
|
||||
let esc = concat_string!("─", lines, "─ Esc to go back ");
|
||||
Spans::from(vec![
|
||||
Span::styled(title, title_style),
|
||||
Span::styled(esc, border_style),
|
||||
])
|
||||
} else {
|
||||
Spans::from(Span::styled(title, title_style))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn draw<B: Backend>(
|
||||
&mut self, f: &mut Frame<'_, B>, draw_info: &DrawInfo, data: Vec<DataType>,
|
||||
widget: Option<&mut BottomWidget>, painter: &Painter,
|
||||
) {
|
||||
self.set_data(data);
|
||||
|
||||
let draw_horizontal = !self.props.is_basic || draw_info.is_on_widget();
|
||||
let draw_loc = draw_info.loc;
|
||||
let margined_draw_loc = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)])
|
||||
.horizontal_margin(if draw_horizontal { 0 } else { 1 })
|
||||
.direction(Direction::Horizontal)
|
||||
.split(draw_loc)[0];
|
||||
|
||||
let block = self.block(draw_info, self.data.len());
|
||||
|
||||
let (inner_width, inner_height) = {
|
||||
let inner_rect = block.inner(margined_draw_loc);
|
||||
self.state.inner_rect = inner_rect;
|
||||
(inner_rect.width, inner_rect.height)
|
||||
};
|
||||
|
||||
if inner_width == 0 || inner_height == 0 {
|
||||
f.render_widget(block, margined_draw_loc);
|
||||
} else {
|
||||
// Calculate widths
|
||||
if draw_info.recalculate_column_widths {
|
||||
let col_widths = DataType::column_widths(&self.data, &self.columns);
|
||||
|
||||
self.columns
|
||||
.iter_mut()
|
||||
.zip(&col_widths)
|
||||
.for_each(|(column, &width)| {
|
||||
let header_len = column.header_len() as u16;
|
||||
if let ColumnWidthBounds::Soft {
|
||||
desired,
|
||||
max_percentage: _,
|
||||
} = &mut column.bounds_mut()
|
||||
{
|
||||
*desired = max(header_len, width);
|
||||
}
|
||||
});
|
||||
|
||||
self.state.calculated_widths = self
|
||||
.columns
|
||||
.calculate_column_widths(inner_width, self.props.left_to_right);
|
||||
|
||||
// Update draw loc in widget map
|
||||
if let Some(widget) = widget {
|
||||
widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
|
||||
widget.bottom_right_corner =
|
||||
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
|
||||
}
|
||||
}
|
||||
|
||||
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.props.table_gap
|
||||
};
|
||||
|
||||
let columns = &self.columns;
|
||||
if !self.data.is_empty() || !self.first_draw {
|
||||
self.first_draw = false; // TODO: Doing it this way is fine, but it could be done better (e.g. showing custom no results/entries message)
|
||||
|
||||
let rows = {
|
||||
let num_rows =
|
||||
usize::from(inner_height.saturating_sub(table_gap + header_height));
|
||||
self.state
|
||||
.get_start_position(num_rows, draw_info.force_redraw);
|
||||
let start = self.state.display_start_index;
|
||||
let end = min(self.data.len(), start + num_rows);
|
||||
self.state
|
||||
.table_state
|
||||
.select(Some(self.state.current_index.saturating_sub(start)));
|
||||
|
||||
self.data[start..end].iter().map(|data_row| {
|
||||
let row = Row::new(
|
||||
columns
|
||||
.iter()
|
||||
.zip(&self.state.calculated_widths)
|
||||
.filter_map(|(column, &width)| {
|
||||
data_row.to_cell(column.inner(), width)
|
||||
}),
|
||||
);
|
||||
|
||||
data_row.style_row(row, painter)
|
||||
})
|
||||
};
|
||||
|
||||
let headers = self
|
||||
.sort_type
|
||||
.build_header(columns, &self.state.calculated_widths)
|
||||
.style(self.styling.header_style)
|
||||
.bottom_margin(table_gap);
|
||||
|
||||
let widget = {
|
||||
let highlight_style = if draw_info.is_on_widget()
|
||||
|| self.props.show_current_entry_when_unfocused
|
||||
{
|
||||
self.styling.highlighted_text_style
|
||||
} else {
|
||||
self.styling.text_style
|
||||
};
|
||||
let mut table = Table::new(rows)
|
||||
.block(block)
|
||||
.highlight_style(highlight_style)
|
||||
.style(self.styling.text_style);
|
||||
|
||||
if show_header {
|
||||
table = table.header(headers);
|
||||
}
|
||||
|
||||
table
|
||||
};
|
||||
|
||||
let table_state = &mut self.state.table_state;
|
||||
f.render_stateful_widget(
|
||||
widget.widths(
|
||||
&(self
|
||||
.state
|
||||
.calculated_widths
|
||||
.iter()
|
||||
.filter_map(|&width| {
|
||||
if width == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(Constraint::Length(width))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()),
|
||||
),
|
||||
margined_draw_loc,
|
||||
table_state,
|
||||
);
|
||||
} else {
|
||||
let table = Table::new(once(Row::new(Text::raw("No data"))))
|
||||
.block(block)
|
||||
.style(self.styling.text_style)
|
||||
.widths(&[Constraint::Percentage(100)]);
|
||||
f.render_widget(table, margined_draw_loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
src/components/data_table/props.rs
Normal file
21
src/components/data_table/props.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
pub struct DataTableProps {
|
||||
/// An optional title for the table.
|
||||
pub title: Option<Cow<'static, str>>,
|
||||
|
||||
/// The size of the gap between the header and rows.
|
||||
pub table_gap: u16,
|
||||
|
||||
/// Whether this table determines column widths from left to right.
|
||||
pub left_to_right: bool,
|
||||
|
||||
/// Whether this table is a basic table. This affects the borders.
|
||||
pub is_basic: bool,
|
||||
|
||||
/// Whether to show the table scroll position.
|
||||
pub show_table_scroll_position: bool,
|
||||
|
||||
/// Whether to show the current entry as highlighted when not focused.
|
||||
pub show_current_entry_when_unfocused: bool,
|
||||
}
|
536
src/components/data_table/sortable.rs
Normal file
536
src/components/data_table/sortable.rs
Normal file
|
@ -0,0 +1,536 @@
|
|||
use std::{borrow::Cow, marker::PhantomData};
|
||||
|
||||
use concat_string::concat_string;
|
||||
use itertools::Itertools;
|
||||
use tui::widgets::Row;
|
||||
|
||||
use crate::utils::gen_util::truncate_text;
|
||||
|
||||
use super::{
|
||||
ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataTableProps, DataTableState,
|
||||
DataTableStyling, DataToCell,
|
||||
};
|
||||
|
||||
/// Denotes the sort order.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SortOrder {
|
||||
Ascending,
|
||||
Descending,
|
||||
}
|
||||
|
||||
impl Default for SortOrder {
|
||||
fn default() -> Self {
|
||||
Self::Ascending
|
||||
}
|
||||
}
|
||||
|
||||
/// Denotes the [`DataTable`] is unsorted.
|
||||
pub struct Unsortable;
|
||||
|
||||
/// Denotes the [`DataTable`] is sorted.
|
||||
pub struct Sortable {
|
||||
/// The currently selected sort index.
|
||||
pub sort_index: usize,
|
||||
|
||||
/// The current sorting order.
|
||||
pub order: SortOrder,
|
||||
}
|
||||
|
||||
/// The [`SortType`] trait is meant to be used in the typing of a [`DataTable`]
|
||||
/// to denote whether the table is meant to display/store sorted or unsorted data.
|
||||
///
|
||||
/// Note that the trait is [sealed](https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed),
|
||||
/// and therefore only [`Unsortable`] and [`Sortable`] can implement it.
|
||||
pub trait SortType: private::Sealed {
|
||||
/// Constructs the table header.
|
||||
fn build_header<H, C>(&self, columns: &[C], widths: &[u16]) -> Row<'_>
|
||||
where
|
||||
H: ColumnHeader,
|
||||
C: DataTableColumn<H>,
|
||||
{
|
||||
Row::new(columns.iter().zip(widths).filter_map(|(c, &width)| {
|
||||
if width == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(truncate_text(&c.header(), width))
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
use super::{Sortable, Unsortable};
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
impl Sealed for Unsortable {}
|
||||
impl Sealed for Sortable {}
|
||||
}
|
||||
|
||||
impl SortType for Unsortable {}
|
||||
|
||||
impl SortType for Sortable {
|
||||
fn build_header<H, C>(&self, columns: &[C], widths: &[u16]) -> Row<'_>
|
||||
where
|
||||
H: ColumnHeader,
|
||||
C: DataTableColumn<H>,
|
||||
{
|
||||
const UP_ARROW: &str = "▲";
|
||||
const DOWN_ARROW: &str = "▼";
|
||||
|
||||
Row::new(
|
||||
columns
|
||||
.iter()
|
||||
.zip(widths)
|
||||
.enumerate()
|
||||
.filter_map(|(index, (c, &width))| {
|
||||
if width == 0 {
|
||||
None
|
||||
} else if index == self.sort_index {
|
||||
let arrow = match self.order {
|
||||
SortOrder::Ascending => UP_ARROW,
|
||||
SortOrder::Descending => DOWN_ARROW,
|
||||
};
|
||||
Some(truncate_text(&concat_string!(c.header(), arrow), width))
|
||||
} else {
|
||||
Some(truncate_text(&c.header(), width))
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SortsRow<DataType> {
|
||||
/// Sorts data.
|
||||
fn sort_data(&self, data: &mut [DataType], descending: bool);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SortColumn<DataType, T> {
|
||||
/// The inner column header.
|
||||
inner: T,
|
||||
|
||||
/// The default sort order.
|
||||
pub default_order: SortOrder,
|
||||
|
||||
/// A restriction on this column's width.
|
||||
pub bounds: ColumnWidthBounds,
|
||||
|
||||
/// Marks that this column is currently "hidden", and should *always* be skipped.
|
||||
pub is_hidden: bool,
|
||||
|
||||
_pd: PhantomData<DataType>,
|
||||
}
|
||||
|
||||
impl<DataType, T> DataTableColumn<T> for SortColumn<DataType, T>
|
||||
where
|
||||
T: ColumnHeader + SortsRow<DataType>,
|
||||
{
|
||||
#[inline]
|
||||
fn inner(&self) -> &T {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn inner_mut(&mut self) -> &mut T {
|
||||
&mut self.inner
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bounds(&self) -> ColumnWidthBounds {
|
||||
self.bounds
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bounds_mut(&mut self) -> &mut ColumnWidthBounds {
|
||||
&mut self.bounds
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_hidden(&self) -> bool {
|
||||
self.is_hidden
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_is_hidden(&mut self, is_hidden: bool) {
|
||||
self.is_hidden = is_hidden;
|
||||
}
|
||||
|
||||
fn header(&self) -> Cow<'static, str> {
|
||||
self.inner.header()
|
||||
}
|
||||
|
||||
fn header_len(&self) -> usize {
|
||||
self.header().len() + 1
|
||||
}
|
||||
}
|
||||
|
||||
impl<DataType, T> SortColumn<DataType, T>
|
||||
where
|
||||
T: ColumnHeader + SortsRow<DataType>,
|
||||
{
|
||||
/// Creates a new [`SortColumn`] with a width that follows the header width, which has no shortcut and sorts by
|
||||
/// default in ascending order ([`SortOrder::Ascending`]).
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
bounds: ColumnWidthBounds::FollowHeader,
|
||||
is_hidden: false,
|
||||
default_order: SortOrder::default(),
|
||||
_pd: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`SortColumn`] with a hard width, which has no shortcut and sorts by default in
|
||||
/// ascending order ([`SortOrder::Ascending`]).
|
||||
pub fn hard(inner: T, width: u16) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
bounds: ColumnWidthBounds::Hard(width),
|
||||
is_hidden: false,
|
||||
default_order: SortOrder::default(),
|
||||
_pd: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`SortColumn`] with a soft width, which has no shortcut and sorts by default in
|
||||
/// ascending order ([`SortOrder::Ascending`]).
|
||||
pub fn soft(inner: T, max_percentage: Option<f32>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
bounds: ColumnWidthBounds::Soft {
|
||||
desired: 0,
|
||||
max_percentage,
|
||||
},
|
||||
is_hidden: false,
|
||||
default_order: SortOrder::default(),
|
||||
_pd: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the default sort order to [`SortOrder::Ascending`].
|
||||
pub fn default_ascending(mut self) -> Self {
|
||||
self.default_order = SortOrder::Ascending;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the default sort order to [`SortOrder::Descending`].
|
||||
pub fn default_descending(mut self) -> Self {
|
||||
self.default_order = SortOrder::Descending;
|
||||
self
|
||||
}
|
||||
|
||||
/// Given a [`SortColumn`] and the sort order, sort a mutable slice of associated data.
|
||||
pub fn sort_by(&self, data: &mut [DataType], order: SortOrder) {
|
||||
let descending = matches!(order, SortOrder::Descending);
|
||||
self.inner.sort_data(data, descending);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SortDataTableProps {
|
||||
pub inner: DataTableProps,
|
||||
pub sort_index: usize,
|
||||
pub order: SortOrder,
|
||||
}
|
||||
|
||||
/// A type alias for a sortable [`DataTable`].
|
||||
pub type SortDataTable<DataType, H> = DataTable<DataType, H, Sortable, SortColumn<DataType, H>>;
|
||||
|
||||
impl<DataType, H> SortDataTable<DataType, H>
|
||||
where
|
||||
DataType: DataToCell<H>,
|
||||
H: ColumnHeader + SortsRow<DataType>,
|
||||
{
|
||||
pub fn new_sortable<C: Into<Vec<SortColumn<DataType, H>>>>(
|
||||
columns: C, props: SortDataTableProps, styling: DataTableStyling,
|
||||
) -> Self {
|
||||
Self {
|
||||
columns: columns.into(),
|
||||
state: DataTableState::default(),
|
||||
props: props.inner,
|
||||
styling,
|
||||
sort_type: Sortable {
|
||||
sort_index: props.sort_index,
|
||||
order: props.order,
|
||||
},
|
||||
first_draw: true,
|
||||
data: vec![],
|
||||
_pd: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the current sort order.
|
||||
pub fn set_order(&mut self, order: SortOrder) {
|
||||
self.sort_type.order = order;
|
||||
}
|
||||
|
||||
/// Gets the current sort order.
|
||||
pub fn order(&self) -> SortOrder {
|
||||
self.sort_type.order
|
||||
}
|
||||
|
||||
/// Toggles the current sort order.
|
||||
pub fn toggle_order(&mut self) {
|
||||
self.sort_type.order = match self.sort_type.order {
|
||||
SortOrder::Ascending => SortOrder::Descending,
|
||||
SortOrder::Descending => SortOrder::Ascending,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.state.inner_rect.height > 1 && self.state.inner_rect.y == y {
|
||||
if let Some(index) = self.get_range(x) {
|
||||
self.set_sort_index(index);
|
||||
Some(self.sort_type.sort_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 set_sort_index(&mut self, index: usize) {
|
||||
if self.sort_type.sort_index == index {
|
||||
self.toggle_order();
|
||||
} else if let Some(col) = self.columns.get(index) {
|
||||
self.sort_type.sort_index = index;
|
||||
self.sort_type.order = col.default_order;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current sort index.
|
||||
pub fn sort_index(&self) -> usize {
|
||||
self.sort_type.sort_index
|
||||
}
|
||||
|
||||
/// Given a `needle` coordinate, select the corresponding index and value.
|
||||
fn get_range(&self, needle: u16) -> Option<usize> {
|
||||
let mut start = self.state.inner_rect.x;
|
||||
let range = self
|
||||
.state
|
||||
.calculated_widths
|
||||
.iter()
|
||||
.map(|width| {
|
||||
let entry_start = start;
|
||||
start += width + 1; // +1 for the gap b/w cols.
|
||||
|
||||
entry_start
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
match range.binary_search(&needle) {
|
||||
Ok(index) => Some(index),
|
||||
Err(index) => index.checked_sub(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
struct TestType {
|
||||
index: usize,
|
||||
data: u64,
|
||||
}
|
||||
|
||||
enum ColumnType {
|
||||
Index,
|
||||
Data,
|
||||
}
|
||||
|
||||
impl DataToCell<ColumnType> for TestType {
|
||||
fn to_cell<'a>(
|
||||
&'a self, _column: &ColumnType, _calculated_width: u16,
|
||||
) -> Option<tui::text::Text<'a>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn column_widths<C: DataTableColumn<ColumnType>>(_data: &[Self], _columns: &[C]) -> Vec<u16>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnHeader for ColumnType {
|
||||
fn text(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ColumnType::Index => "Index".into(),
|
||||
ColumnType::Data => "Data".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SortsRow<TestType> for ColumnType {
|
||||
fn sort_data(&self, data: &mut [TestType], descending: bool) {
|
||||
match self {
|
||||
ColumnType::Index => data.sort_by_key(|t| t.index),
|
||||
ColumnType::Data => data.sort_by_key(|t| t.data),
|
||||
}
|
||||
|
||||
if descending {
|
||||
data.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sorting() {
|
||||
let columns = [
|
||||
SortColumn::new(ColumnType::Index),
|
||||
SortColumn::new(ColumnType::Data),
|
||||
];
|
||||
let props = {
|
||||
let inner = DataTableProps {
|
||||
title: Some("test".into()),
|
||||
table_gap: 1,
|
||||
left_to_right: false,
|
||||
is_basic: false,
|
||||
show_table_scroll_position: true,
|
||||
show_current_entry_when_unfocused: false,
|
||||
};
|
||||
|
||||
SortDataTableProps {
|
||||
inner,
|
||||
sort_index: 0,
|
||||
order: SortOrder::Descending,
|
||||
}
|
||||
};
|
||||
|
||||
let styling = DataTableStyling::default();
|
||||
|
||||
let mut table = DataTable::new_sortable(columns, props, styling);
|
||||
let mut data = vec![
|
||||
TestType {
|
||||
index: 4,
|
||||
data: 100,
|
||||
},
|
||||
TestType {
|
||||
index: 1,
|
||||
data: 200,
|
||||
},
|
||||
TestType {
|
||||
index: 0,
|
||||
data: 300,
|
||||
},
|
||||
TestType {
|
||||
index: 3,
|
||||
data: 400,
|
||||
},
|
||||
TestType {
|
||||
index: 2,
|
||||
data: 500,
|
||||
},
|
||||
];
|
||||
|
||||
table
|
||||
.columns
|
||||
.get(table.sort_type.sort_index)
|
||||
.unwrap()
|
||||
.sort_by(&mut data, SortOrder::Ascending);
|
||||
assert_eq!(
|
||||
data,
|
||||
vec![
|
||||
TestType {
|
||||
index: 0,
|
||||
data: 300,
|
||||
},
|
||||
TestType {
|
||||
index: 1,
|
||||
data: 200,
|
||||
},
|
||||
TestType {
|
||||
index: 2,
|
||||
data: 500,
|
||||
},
|
||||
TestType {
|
||||
index: 3,
|
||||
data: 400,
|
||||
},
|
||||
TestType {
|
||||
index: 4,
|
||||
data: 100,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
table
|
||||
.columns
|
||||
.get(table.sort_type.sort_index)
|
||||
.unwrap()
|
||||
.sort_by(&mut data, SortOrder::Descending);
|
||||
assert_eq!(
|
||||
data,
|
||||
vec![
|
||||
TestType {
|
||||
index: 4,
|
||||
data: 100,
|
||||
},
|
||||
TestType {
|
||||
index: 3,
|
||||
data: 400,
|
||||
},
|
||||
TestType {
|
||||
index: 2,
|
||||
data: 500,
|
||||
},
|
||||
TestType {
|
||||
index: 1,
|
||||
data: 200,
|
||||
},
|
||||
TestType {
|
||||
index: 0,
|
||||
data: 300,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
table.set_sort_index(1);
|
||||
table
|
||||
.columns
|
||||
.get(table.sort_type.sort_index)
|
||||
.unwrap()
|
||||
.sort_by(&mut data, SortOrder::Ascending);
|
||||
assert_eq!(
|
||||
data,
|
||||
vec![
|
||||
TestType {
|
||||
index: 4,
|
||||
data: 100,
|
||||
},
|
||||
TestType {
|
||||
index: 1,
|
||||
data: 200,
|
||||
},
|
||||
TestType {
|
||||
index: 0,
|
||||
data: 300,
|
||||
},
|
||||
TestType {
|
||||
index: 3,
|
||||
data: 400,
|
||||
},
|
||||
TestType {
|
||||
index: 2,
|
||||
data: 500,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
90
src/components/data_table/state.rs
Normal file
90
src/components/data_table/state.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use tui::{layout::Rect, widgets::TableState};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ScrollDirection {
|
||||
// UP means scrolling up --- this usually DECREMENTS
|
||||
Up,
|
||||
|
||||
// DOWN means scrolling down --- this usually INCREMENTS
|
||||
Down,
|
||||
}
|
||||
|
||||
impl Default for ScrollDirection {
|
||||
fn default() -> Self {
|
||||
ScrollDirection::Down
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal state representation of a [`DataTable`](super::DataTable).
|
||||
pub struct DataTableState {
|
||||
/// The index from where to start displaying the rows.
|
||||
pub display_start_index: usize,
|
||||
|
||||
/// The current scroll position.
|
||||
pub current_index: usize,
|
||||
|
||||
/// The direction of the last attempted scroll.
|
||||
pub scroll_direction: ScrollDirection,
|
||||
|
||||
/// tui-rs' internal table state.
|
||||
pub table_state: TableState,
|
||||
|
||||
/// The calculated widths.
|
||||
pub calculated_widths: Vec<u16>,
|
||||
|
||||
/// The current inner [`Rect`].
|
||||
pub inner_rect: Rect,
|
||||
}
|
||||
|
||||
impl Default for DataTableState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
display_start_index: 0,
|
||||
current_index: 0,
|
||||
scroll_direction: ScrollDirection::Down,
|
||||
calculated_widths: vec![],
|
||||
table_state: TableState::default(),
|
||||
inner_rect: Rect::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataTableState {
|
||||
/// Gets the starting position of a table.
|
||||
pub fn get_start_position(&mut self, num_rows: usize, is_force_redraw: bool) {
|
||||
let mut start_index = self.display_start_index;
|
||||
let current_scroll_position = self.current_index;
|
||||
let scroll_direction = self.scroll_direction;
|
||||
|
||||
if is_force_redraw {
|
||||
start_index = 0;
|
||||
}
|
||||
|
||||
self.display_start_index = match scroll_direction {
|
||||
ScrollDirection::Down => {
|
||||
if current_scroll_position < start_index + 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
|
||||
start_index
|
||||
} else if current_scroll_position >= num_rows {
|
||||
// Else if the current position past the last element visible in the list, omit
|
||||
// until we can see that element
|
||||
current_scroll_position - num_rows + 1
|
||||
} else {
|
||||
// Else, if it is not past the last element visible, do not omit anything
|
||||
0
|
||||
}
|
||||
}
|
||||
ScrollDirection::Up => {
|
||||
if current_scroll_position <= start_index {
|
||||
// If it's past the first element, then show from that element downwards
|
||||
current_scroll_position
|
||||
} else if current_scroll_position >= start_index + num_rows {
|
||||
current_scroll_position - num_rows + 1
|
||||
} else {
|
||||
start_index
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
26
src/components/data_table/styling.rs
Normal file
26
src/components/data_table/styling.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use tui::style::Style;
|
||||
|
||||
use crate::canvas::canvas_colours::CanvasColours;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DataTableStyling {
|
||||
pub header_style: Style,
|
||||
pub border_style: Style,
|
||||
pub highlighted_border_style: Style,
|
||||
pub text_style: Style,
|
||||
pub highlighted_text_style: Style,
|
||||
pub title_style: Style,
|
||||
}
|
||||
|
||||
impl DataTableStyling {
|
||||
pub fn from_colours(colours: &CanvasColours) -> Self {
|
||||
Self {
|
||||
header_style: colours.table_header_style,
|
||||
border_style: colours.border_style,
|
||||
highlighted_border_style: colours.highlighted_border_style,
|
||||
text_style: colours.text_style,
|
||||
highlighted_text_style: colours.currently_selected_text_style,
|
||||
title_style: colours.widget_title_style,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
pub mod draw;
|
||||
pub use draw::*;
|
||||
|
||||
pub mod state;
|
||||
pub use state::*;
|
|
@ -1,505 +0,0 @@
|
|||
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},
|
||||
components::text_table::SortOrder,
|
||||
constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
|
||||
data_conversion::{TableData, TableRow},
|
||||
};
|
||||
|
||||
use super::{
|
||||
CellContent, SortState, TableComponentColumn, TableComponentHeader, TableComponentState,
|
||||
WidthBounds,
|
||||
};
|
||||
|
||||
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 {
|
||||
SortOrder::Ascending => UP_ARROW,
|
||||
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,678 +0,0 @@
|
|||
use std::{borrow::Cow, convert::TryInto, ops::Range};
|
||||
|
||||
use itertools::Itertools;
|
||||
use tui::{layout::Rect, widgets::TableState};
|
||||
|
||||
use crate::app::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> {
|
||||
let min_index = 0;
|
||||
let max_index = num_entries.saturating_sub(1);
|
||||
|
||||
if change == 0
|
||||
|| (change > 0 && self.current_scroll_position == max_index)
|
||||
|| (change < 0 && self.current_scroll_position == min_index)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let csp: Result<i64, _> = self.current_scroll_position.try_into();
|
||||
if let Ok(csp) = csp {
|
||||
self.current_scroll_position =
|
||||
(csp + change).clamp(min_index as i64, max_index as i64) as usize;
|
||||
|
||||
if change < 0 {
|
||||
self.scroll_direction = ScrollDirection::Up;
|
||||
} else {
|
||||
self.scroll_direction = ScrollDirection::Down;
|
||||
}
|
||||
|
||||
Some(self.current_scroll_position)
|
||||
} else {
|
||||
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 clamp to max possible scroll index 14.
|
||||
check_scroll_update(s, 5, 15, Some(14), 14);
|
||||
|
||||
// Update by 1. Should do nothing (already at max index 14).
|
||||
check_scroll_update(s, 1, 15, None, 14);
|
||||
|
||||
// Update by -15. Should clamp to index 0.
|
||||
check_scroll_update(s, -15, 15, Some(0), 0);
|
||||
|
||||
// Update by -1. Should do nothing (already at min index 0).
|
||||
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 clamp to 14.
|
||||
check_scroll_update(s, 15, 15, Some(14), 14);
|
||||
|
||||
// Update by 15 but with a larger bound. Should clamp 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));
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||
use super::tui_widget::time_chart::{Axis, Dataset, TimeChart, DEFAULT_LEGEND_CONSTRAINTS};
|
||||
|
||||
/// A single graph point.
|
||||
pub type Point = (f64, f64);
|
||||
pub type Point = (f64, f64); // FIXME: Move this to tui time chart?
|
||||
|
||||
/// Represents the data required by the [`TimeGraph`].
|
||||
pub struct GraphData<'a> {
|
||||
|
@ -70,9 +70,12 @@ impl<'a> TimeGraph<'a> {
|
|||
if self.hide_x_labels {
|
||||
Axis::default().bounds(adjusted_x_bounds)
|
||||
} else {
|
||||
let xb_one = (self.x_bounds[1] / 1000).to_string();
|
||||
let xb_zero = (self.x_bounds[0] / 1000).to_string();
|
||||
|
||||
let x_labels = vec![
|
||||
Span::raw(concat_string!((self.x_bounds[1] / 1000).to_string(), "s")),
|
||||
Span::raw(concat_string!((self.x_bounds[0] / 1000).to_string(), "s")),
|
||||
Span::raw(concat_string!(xb_one, "s")),
|
||||
Span::raw(concat_string!(xb_zero, "s")),
|
||||
];
|
||||
|
||||
Axis::default()
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
//! This mainly concerns converting collected data into things that the canvas
|
||||
//! can actually handle.
|
||||
|
||||
use crate::components::text_table::CellContent;
|
||||
use crate::components::time_graph::Point;
|
||||
use crate::{app::AxisScaling, units::data_units::DataUnit, Pid};
|
||||
use crate::{
|
||||
app::{data_farmer, data_harvester, App},
|
||||
utils::gen_util::*,
|
||||
use crate::app::data_farmer::DataCollection;
|
||||
use crate::app::data_harvester::cpu::CpuDataType;
|
||||
use crate::app::{
|
||||
data_harvester::temperature::TemperatureType,
|
||||
widgets::{DiskWidgetData, TempWidgetData},
|
||||
};
|
||||
use crate::components::time_graph::Point;
|
||||
use crate::utils::gen_util::*;
|
||||
use crate::{app::AxisScaling, units::data_units::DataUnit};
|
||||
|
||||
use concat_string::concat_string;
|
||||
use fxhash::FxHashMap;
|
||||
use kstring::KString;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ConvertedBatteryData {
|
||||
|
@ -22,27 +23,6 @@ pub struct ConvertedBatteryData {
|
|||
pub health: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct TableData {
|
||||
pub data: Vec<TableRow>,
|
||||
pub col_widths: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TableRow {
|
||||
Raw(Vec<CellContent>),
|
||||
Styled(Vec<CellContent>, tui::style::Style),
|
||||
}
|
||||
|
||||
impl TableRow {
|
||||
pub fn row(&self) -> &[CellContent] {
|
||||
match self {
|
||||
TableRow::Raw(data) => data,
|
||||
TableRow::Styled(data, _) => data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ConvertedNetworkData {
|
||||
pub rx: Vec<Point>,
|
||||
|
@ -60,14 +40,15 @@ pub struct ConvertedNetworkData {
|
|||
// mean_tx: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct ConvertedCpuData {
|
||||
pub cpu_name: String,
|
||||
pub short_cpu_name: String,
|
||||
/// Tuple is time, value
|
||||
pub cpu_data: Vec<Point>,
|
||||
/// Represents the value displayed on the legend.
|
||||
pub legend_value: String,
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CpuWidgetData {
|
||||
All,
|
||||
Entry {
|
||||
data_type: CpuDataType,
|
||||
/// A point here represents time (x) and value (y).
|
||||
data: Vec<Point>,
|
||||
last_entry: f64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -78,14 +59,8 @@ pub struct ConvertedData {
|
|||
pub total_tx_display: String,
|
||||
pub network_data_rx: Vec<Point>,
|
||||
pub network_data_tx: Vec<Point>,
|
||||
pub disk_data: TableData,
|
||||
pub temp_sensor_data: TableData,
|
||||
|
||||
/// A mapping from a process name to any PID with that name.
|
||||
pub process_name_pid_map: FxHashMap<String, Vec<Pid>>,
|
||||
|
||||
/// A mapping from a process command to any PID with that name.
|
||||
pub process_cmd_pid_map: FxHashMap<String, Vec<Pid>>,
|
||||
pub disk_data: Vec<DiskWidgetData>,
|
||||
pub temp_data: Vec<TempWidgetData>,
|
||||
|
||||
pub mem_labels: Option<(String, String)>,
|
||||
pub swap_labels: Option<(String, String)>,
|
||||
|
@ -95,199 +70,119 @@ pub struct ConvertedData {
|
|||
pub swap_data: Vec<Point>,
|
||||
pub arc_data: Vec<Point>,
|
||||
pub load_avg_data: [f32; 3],
|
||||
pub cpu_data: Vec<ConvertedCpuData>,
|
||||
pub cpu_data: Vec<CpuWidgetData>,
|
||||
pub battery_data: Vec<ConvertedBatteryData>,
|
||||
}
|
||||
|
||||
pub fn convert_temp_row(app: &App) -> TableData {
|
||||
let current_data = &app.data_collection;
|
||||
let temp_type = &app.app_config_fields.temperature_type;
|
||||
let mut col_widths = vec![0; 2];
|
||||
impl ConvertedData {
|
||||
// TODO: Can probably heavily reduce this step to avoid clones.
|
||||
pub fn ingest_disk_data(&mut self, data: &DataCollection) {
|
||||
self.disk_data.clear();
|
||||
|
||||
let mut sensor_vector: Vec<TableRow> = current_data
|
||||
.temp_harvest
|
||||
data.disk_harvest
|
||||
.iter()
|
||||
.map(|temp_harvest| {
|
||||
let row = vec![
|
||||
CellContent::Simple(temp_harvest.name.clone().into()),
|
||||
CellContent::Simple(
|
||||
concat_string!(
|
||||
(temp_harvest.temperature.ceil() as u64).to_string(),
|
||||
match temp_type {
|
||||
data_harvester::temperature::TemperatureType::Celsius => "°C",
|
||||
data_harvester::temperature::TemperatureType::Kelvin => "K",
|
||||
data_harvester::temperature::TemperatureType::Fahrenheit => "°F",
|
||||
}
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
];
|
||||
|
||||
col_widths.iter_mut().zip(&row).for_each(|(curr, r)| {
|
||||
*curr = std::cmp::max(*curr, r.len());
|
||||
});
|
||||
|
||||
TableRow::Raw(row)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if sensor_vector.is_empty() {
|
||||
sensor_vector.push(TableRow::Raw(vec![
|
||||
CellContent::Simple("No Sensors Found".into()),
|
||||
CellContent::Simple("".into()),
|
||||
]));
|
||||
}
|
||||
|
||||
TableData {
|
||||
data: sensor_vector,
|
||||
col_widths,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> TableData {
|
||||
let mut disk_vector: Vec<TableRow> = Vec::new();
|
||||
let mut col_widths = vec![0; 8];
|
||||
|
||||
current_data
|
||||
.disk_harvest
|
||||
.iter()
|
||||
.zip(¤t_data.io_labels)
|
||||
.zip(&data.io_labels)
|
||||
.for_each(|(disk, (io_read, io_write))| {
|
||||
let free_space_fmt = if let Some(free_space) = disk.free_space {
|
||||
let converted_free_space = get_decimal_bytes(free_space);
|
||||
format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1).into()
|
||||
} else {
|
||||
"N/A".into()
|
||||
};
|
||||
let total_space_fmt = if let Some(total_space) = disk.total_space {
|
||||
let converted_total_space = get_decimal_bytes(total_space);
|
||||
format!(
|
||||
"{:.*}{}",
|
||||
0, converted_total_space.0, converted_total_space.1
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
"N/A".into()
|
||||
};
|
||||
|
||||
let usage_fmt = if let (Some(used_space), Some(total_space)) =
|
||||
(disk.used_space, disk.total_space)
|
||||
{
|
||||
format!("{:.0}%", used_space as f64 / total_space as f64 * 100_f64).into()
|
||||
} else {
|
||||
"N/A".into()
|
||||
};
|
||||
|
||||
let row = vec![
|
||||
CellContent::Simple(disk.name.clone().into()),
|
||||
CellContent::Simple(disk.mount_point.clone().into()),
|
||||
CellContent::Simple(usage_fmt),
|
||||
CellContent::Simple(free_space_fmt),
|
||||
CellContent::Simple(total_space_fmt),
|
||||
CellContent::Simple(io_read.clone().into()),
|
||||
CellContent::Simple(io_write.clone().into()),
|
||||
];
|
||||
col_widths.iter_mut().zip(&row).for_each(|(curr, r)| {
|
||||
*curr = std::cmp::max(*curr, r.len());
|
||||
self.disk_data.push(DiskWidgetData {
|
||||
name: KString::from_ref(&disk.name),
|
||||
mount_point: KString::from_ref(&disk.mount_point),
|
||||
free_bytes: disk.free_space,
|
||||
used_bytes: disk.used_space,
|
||||
total_bytes: disk.total_space,
|
||||
io_read: io_read.into(),
|
||||
io_write: io_write.into(),
|
||||
});
|
||||
disk_vector.push(TableRow::Raw(row));
|
||||
});
|
||||
|
||||
if disk_vector.is_empty() {
|
||||
disk_vector.push(TableRow::Raw(vec![
|
||||
CellContent::Simple("No Disks Found".into()),
|
||||
CellContent::Simple("".into()),
|
||||
]));
|
||||
self.disk_data.shrink_to_fit();
|
||||
}
|
||||
|
||||
TableData {
|
||||
data: disk_vector,
|
||||
col_widths,
|
||||
pub fn ingest_temp_data(&mut self, data: &DataCollection, temperature_type: TemperatureType) {
|
||||
self.temp_data.clear();
|
||||
|
||||
data.temp_harvest.iter().for_each(|temp_harvest| {
|
||||
self.temp_data.push(TempWidgetData {
|
||||
sensor: KString::from_ref(&temp_harvest.name),
|
||||
temperature_value: temp_harvest.temperature.ceil() as u64,
|
||||
temperature_type,
|
||||
});
|
||||
});
|
||||
|
||||
self.temp_data.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_cpu_data_points(
|
||||
current_data: &data_farmer::DataCollection, existing_cpu_data: &mut Vec<ConvertedCpuData>,
|
||||
) {
|
||||
let current_time = if let Some(frozen_instant) = current_data.frozen_instant {
|
||||
frozen_instant
|
||||
} else {
|
||||
current_data.current_instant
|
||||
};
|
||||
pub fn ingest_cpu_data(&mut self, current_data: &DataCollection) {
|
||||
let current_time = current_data.current_instant;
|
||||
|
||||
// Initialize cpu_data_vector if the lengths don't match...
|
||||
// (Re-)initialize the vector if the lengths don't match...
|
||||
if let Some((_time, data)) = ¤t_data.timed_data_vec.last() {
|
||||
if data.cpu_data.len() + 1 != existing_cpu_data.len() {
|
||||
*existing_cpu_data = vec![ConvertedCpuData {
|
||||
cpu_name: "All".to_string(),
|
||||
short_cpu_name: "".to_string(),
|
||||
cpu_data: vec![],
|
||||
legend_value: String::new(),
|
||||
}];
|
||||
|
||||
existing_cpu_data.extend(
|
||||
if data.cpu_data.len() + 1 != self.cpu_data.len() {
|
||||
self.cpu_data = Vec::with_capacity(data.cpu_data.len() + 1);
|
||||
self.cpu_data.push(CpuWidgetData::All);
|
||||
self.cpu_data.extend(
|
||||
data.cpu_data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(itx, cpu_usage)| ConvertedCpuData {
|
||||
cpu_name: if let Some(cpu_harvest) = current_data.cpu_harvest.get(itx) {
|
||||
if let Some(cpu_count) = cpu_harvest.cpu_count {
|
||||
format!("{}{}", cpu_harvest.cpu_prefix, cpu_count)
|
||||
} else {
|
||||
cpu_harvest.cpu_prefix.to_string()
|
||||
}
|
||||
} else {
|
||||
String::default()
|
||||
},
|
||||
short_cpu_name: if let Some(cpu_harvest) = current_data.cpu_harvest.get(itx)
|
||||
{
|
||||
if let Some(cpu_count) = cpu_harvest.cpu_count {
|
||||
cpu_count.to_string()
|
||||
} else {
|
||||
cpu_harvest.cpu_prefix.to_string()
|
||||
}
|
||||
} else {
|
||||
String::default()
|
||||
},
|
||||
legend_value: format!("{:.0}%", cpu_usage.round()),
|
||||
cpu_data: vec![],
|
||||
.zip(¤t_data.cpu_harvest)
|
||||
.map(|(cpu_usage, data)| CpuWidgetData::Entry {
|
||||
data_type: data.data_type,
|
||||
data: vec![],
|
||||
last_entry: *cpu_usage,
|
||||
})
|
||||
.collect::<Vec<ConvertedCpuData>>(),
|
||||
.collect::<Vec<CpuWidgetData>>(),
|
||||
);
|
||||
} else {
|
||||
existing_cpu_data
|
||||
self.cpu_data
|
||||
.iter_mut()
|
||||
.skip(1)
|
||||
.zip(&data.cpu_data)
|
||||
.for_each(|(cpu, cpu_usage)| {
|
||||
cpu.cpu_data = vec![];
|
||||
cpu.legend_value = format!("{:.0}%", cpu_usage.round());
|
||||
.for_each(|(mut cpu, cpu_usage)| match &mut cpu {
|
||||
CpuWidgetData::All => unreachable!(),
|
||||
CpuWidgetData::Entry {
|
||||
data_type: _,
|
||||
data,
|
||||
last_entry,
|
||||
} => {
|
||||
// A bit faster to just update all the times, so we just clear the vector.
|
||||
data.clear();
|
||||
*last_entry = *cpu_usage;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (time, data) in ¤t_data.timed_data_vec {
|
||||
let time_from_start: f64 = (current_time.duration_since(*time).as_millis() as f64).floor();
|
||||
// TODO: [Opt] Can probably avoid data deduplication - store the shift + data + original once.
|
||||
// Now push all the data.
|
||||
for (itx, mut cpu) in &mut self.cpu_data.iter_mut().skip(1).enumerate() {
|
||||
match &mut cpu {
|
||||
CpuWidgetData::All => unreachable!(),
|
||||
CpuWidgetData::Entry {
|
||||
data_type: _,
|
||||
data,
|
||||
last_entry: _,
|
||||
} => {
|
||||
for (time, timed_data) in ¤t_data.timed_data_vec {
|
||||
let time_start: f64 =
|
||||
(current_time.duration_since(*time).as_millis() as f64).floor();
|
||||
|
||||
for (itx, cpu) in data.cpu_data.iter().enumerate() {
|
||||
if let Some(cpu_data) = existing_cpu_data.get_mut(itx + 1) {
|
||||
cpu_data.cpu_data.push((-time_from_start, *cpu));
|
||||
}
|
||||
if let Some(val) = timed_data.cpu_data.get(itx) {
|
||||
data.push((-time_start, *val));
|
||||
}
|
||||
|
||||
if *time == current_time {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
data.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_mem_data_points(current_data: &data_farmer::DataCollection) -> Vec<Point> {
|
||||
pub fn convert_mem_data_points(current_data: &DataCollection) -> Vec<Point> {
|
||||
let mut result: Vec<Point> = Vec::new();
|
||||
let current_time = if let Some(frozen_instant) = current_data.frozen_instant {
|
||||
frozen_instant
|
||||
} else {
|
||||
current_data.current_instant
|
||||
};
|
||||
let current_time = current_data.current_instant;
|
||||
|
||||
for (time, data) in ¤t_data.timed_data_vec {
|
||||
if let Some(mem_data) = data.mem_data {
|
||||
|
@ -303,13 +198,9 @@ pub fn convert_mem_data_points(current_data: &data_farmer::DataCollection) -> Ve
|
|||
result
|
||||
}
|
||||
|
||||
pub fn convert_swap_data_points(current_data: &data_farmer::DataCollection) -> Vec<Point> {
|
||||
pub fn convert_swap_data_points(current_data: &DataCollection) -> Vec<Point> {
|
||||
let mut result: Vec<Point> = Vec::new();
|
||||
let current_time = if let Some(frozen_instant) = current_data.frozen_instant {
|
||||
frozen_instant
|
||||
} else {
|
||||
current_data.current_instant
|
||||
};
|
||||
let current_time = current_data.current_instant;
|
||||
|
||||
for (time, data) in ¤t_data.timed_data_vec {
|
||||
if let Some(swap_data) = data.swap_data {
|
||||
|
@ -326,7 +217,7 @@ pub fn convert_swap_data_points(current_data: &data_farmer::DataCollection) -> V
|
|||
}
|
||||
|
||||
pub fn convert_mem_labels(
|
||||
current_data: &data_farmer::DataCollection,
|
||||
current_data: &DataCollection,
|
||||
) -> (Option<(String, String)>, Option<(String, String)>) {
|
||||
/// Returns the unit type and denominator for given total amount of memory in kibibytes.
|
||||
fn return_unit_and_denominator_for_mem_kib(mem_total_kib: u64) -> (&'static str, f64) {
|
||||
|
@ -396,17 +287,13 @@ pub fn convert_mem_labels(
|
|||
}
|
||||
|
||||
pub fn get_rx_tx_data_points(
|
||||
current_data: &data_farmer::DataCollection, network_scale_type: &AxisScaling,
|
||||
network_unit_type: &DataUnit, network_use_binary_prefix: bool,
|
||||
current_data: &DataCollection, network_scale_type: &AxisScaling, network_unit_type: &DataUnit,
|
||||
network_use_binary_prefix: bool,
|
||||
) -> (Vec<Point>, Vec<Point>) {
|
||||
let mut rx: Vec<Point> = Vec::new();
|
||||
let mut tx: Vec<Point> = Vec::new();
|
||||
|
||||
let current_time = if let Some(frozen_instant) = current_data.frozen_instant {
|
||||
frozen_instant
|
||||
} else {
|
||||
current_data.current_instant
|
||||
};
|
||||
let current_time = current_data.current_instant;
|
||||
|
||||
for (time, data) in ¤t_data.timed_data_vec {
|
||||
let time_from_start: f64 = (current_time.duration_since(*time).as_millis() as f64).floor();
|
||||
|
@ -447,9 +334,8 @@ pub fn get_rx_tx_data_points(
|
|||
}
|
||||
|
||||
pub fn convert_network_data_points(
|
||||
current_data: &data_farmer::DataCollection, need_four_points: bool,
|
||||
network_scale_type: &AxisScaling, network_unit_type: &DataUnit,
|
||||
network_use_binary_prefix: bool,
|
||||
current_data: &DataCollection, need_four_points: bool, network_scale_type: &AxisScaling,
|
||||
network_unit_type: &DataUnit, network_use_binary_prefix: bool,
|
||||
) -> ConvertedNetworkData {
|
||||
let (rx, tx) = get_rx_tx_data_points(
|
||||
current_data,
|
||||
|
@ -607,10 +493,19 @@ pub fn dec_bytes_per_second_string(value: u64) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a string given a value that is converted to the closest SI-variant.
|
||||
/// If the value is greater than a giga-X, then it will return a decimal place.
|
||||
pub fn dec_bytes_string(value: u64) -> String {
|
||||
let converted_values = get_decimal_bytes(value);
|
||||
if value >= GIGA_LIMIT {
|
||||
format!("{:.*}{}", 1, converted_values.0, converted_values.1)
|
||||
} else {
|
||||
format!("{:.*}{}", 0, converted_values.0, converted_values.1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "battery")]
|
||||
pub fn convert_battery_harvest(
|
||||
current_data: &data_farmer::DataCollection,
|
||||
) -> Vec<ConvertedBatteryData> {
|
||||
pub fn convert_battery_harvest(current_data: &DataCollection) -> Vec<ConvertedBatteryData> {
|
||||
current_data
|
||||
.battery_harvest
|
||||
.iter()
|
||||
|
@ -657,7 +552,9 @@ pub fn convert_battery_harvest(
|
|||
}
|
||||
|
||||
#[cfg(feature = "zfs")]
|
||||
pub fn convert_arc_labels(current_data: &data_farmer::DataCollection) -> Option<(String, String)> {
|
||||
pub fn convert_arc_labels(
|
||||
current_data: &crate::app::data_farmer::DataCollection,
|
||||
) -> Option<(String, String)> {
|
||||
/// Returns the unit type and denominator for given total amount of memory in kibibytes.
|
||||
fn return_unit_and_denominator_for_mem_kib(mem_total_kib: u64) -> (&'static str, f64) {
|
||||
if mem_total_kib < 1024 {
|
||||
|
@ -701,13 +598,11 @@ pub fn convert_arc_labels(current_data: &data_farmer::DataCollection) -> Option<
|
|||
}
|
||||
|
||||
#[cfg(feature = "zfs")]
|
||||
pub fn convert_arc_data_points(current_data: &data_farmer::DataCollection) -> Vec<Point> {
|
||||
pub fn convert_arc_data_points(
|
||||
current_data: &crate::app::data_farmer::DataCollection,
|
||||
) -> Vec<Point> {
|
||||
let mut result: Vec<Point> = Vec::new();
|
||||
let current_time = if let Some(frozen_instant) = current_data.frozen_instant {
|
||||
frozen_instant
|
||||
} else {
|
||||
current_data.current_instant
|
||||
};
|
||||
let current_time = current_data.current_instant;
|
||||
|
||||
for (time, data) in ¤t_data.timed_data_vec {
|
||||
if let Some(arc_data) = data.arc_data {
|
||||
|
|
27
src/lib.rs
27
src/lib.rs
|
@ -131,7 +131,7 @@ pub fn handle_key_event_or_break(
|
|||
KeyCode::F(2) => app.toggle_search_whole_word(),
|
||||
KeyCode::F(3) => app.toggle_search_regex(),
|
||||
KeyCode::F(5) => app.toggle_tree_mode(),
|
||||
KeyCode::F(6) => app.toggle_sort(),
|
||||
KeyCode::F(6) => app.toggle_sort_menu(),
|
||||
KeyCode::F(9) => app.start_killing_process(),
|
||||
KeyCode::PageDown => app.on_page_down(),
|
||||
KeyCode::PageUp => app.on_page_up(),
|
||||
|
@ -322,33 +322,40 @@ pub fn panic_hook(panic_info: &PanicInfo<'_>) {
|
|||
}
|
||||
|
||||
pub fn update_data(app: &mut App) {
|
||||
let data_source = match &app.frozen_state {
|
||||
app::frozen_state::FrozenState::NotFrozen => &app.data_collection,
|
||||
app::frozen_state::FrozenState::Frozen(data) => data,
|
||||
};
|
||||
|
||||
for proc in app.proc_state.widget_states.values_mut() {
|
||||
if proc.force_update_data {
|
||||
proc.update_displayed_process_data(&app.data_collection);
|
||||
proc.update_displayed_process_data(data_source);
|
||||
proc.force_update_data = false;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Make this CPU force update less terrible.
|
||||
if app.cpu_state.force_update.is_some() {
|
||||
convert_cpu_data_points(&app.data_collection, &mut app.converted_data.cpu_data);
|
||||
app.converted_data.load_avg_data = app.data_collection.load_avg_harvest;
|
||||
app.converted_data.ingest_cpu_data(data_source);
|
||||
app.converted_data.load_avg_data = data_source.load_avg_harvest;
|
||||
app.cpu_state.force_update = None;
|
||||
}
|
||||
|
||||
// TODO: [OPT] Prefer reassignment over new vectors?
|
||||
if app.mem_state.force_update.is_some() {
|
||||
app.converted_data.mem_data = convert_mem_data_points(&app.data_collection);
|
||||
app.converted_data.swap_data = convert_swap_data_points(&app.data_collection);
|
||||
app.converted_data.mem_data = convert_mem_data_points(data_source);
|
||||
app.converted_data.swap_data = convert_swap_data_points(data_source);
|
||||
#[cfg(feature = "zfs")]
|
||||
{
|
||||
app.converted_data.arc_data = convert_arc_data_points(&app.data_collection);
|
||||
app.converted_data.arc_data = convert_arc_data_points(data_source);
|
||||
}
|
||||
|
||||
app.mem_state.force_update = None;
|
||||
}
|
||||
|
||||
if app.net_state.force_update.is_some() {
|
||||
let (rx, tx) = get_rx_tx_data_points(
|
||||
&app.data_collection,
|
||||
data_source,
|
||||
&app.app_config_fields.network_scale_type,
|
||||
&app.app_config_fields.network_unit_type,
|
||||
app.app_config_fields.network_use_binary_prefix,
|
||||
|
@ -411,7 +418,7 @@ pub fn create_collection_thread(
|
|||
app_config_fields: &app::AppConfigFields, filters: app::DataFilters,
|
||||
used_widget_set: UsedWidgets,
|
||||
) -> std::thread::JoinHandle<()> {
|
||||
let temp_type = app_config_fields.temperature_type.clone();
|
||||
let temp_type = app_config_fields.temperature_type;
|
||||
let use_current_cpu_total = app_config_fields.use_current_cpu_total;
|
||||
let show_average_cpu = app_config_fields.show_average_cpu;
|
||||
let update_rate_in_milliseconds = app_config_fields.update_rate_in_milliseconds;
|
||||
|
@ -444,7 +451,7 @@ pub fn create_collection_thread(
|
|||
data_state.data.cleanup();
|
||||
}
|
||||
ThreadControlEvent::UpdateConfig(app_config_fields) => {
|
||||
data_state.set_temperature_type(app_config_fields.temperature_type.clone());
|
||||
data_state.set_temperature_type(app_config_fields.temperature_type);
|
||||
data_state
|
||||
.set_use_current_cpu_total(app_config_fields.use_current_cpu_total);
|
||||
data_state.set_show_average_cpu(app_config_fields.show_average_cpu);
|
||||
|
|
|
@ -4,7 +4,6 @@ use std::{
|
|||
borrow::Cow,
|
||||
collections::{HashMap, HashSet},
|
||||
convert::TryInto,
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
time::Instant,
|
||||
};
|
||||
|
@ -12,10 +11,10 @@ use std::{
|
|||
use crate::{
|
||||
app::{
|
||||
layout_manager::*,
|
||||
widgets::{DiskWidgetState, ProcWidget, ProcWidgetMode, TempWidgetState},
|
||||
widgets::{CpuWidgetState, DiskTableWidget, ProcWidget, ProcWidgetMode, TempWidgetState},
|
||||
*,
|
||||
},
|
||||
canvas::ColourScheme,
|
||||
canvas::{canvas_colours::CanvasColours, ColourScheme},
|
||||
constants::*,
|
||||
units::data_units::DataUnit,
|
||||
utils::error::{self, BottomError},
|
||||
|
@ -252,7 +251,7 @@ pub struct IgnoreList {
|
|||
pub fn build_app(
|
||||
matches: &clap::ArgMatches, config: &mut Config, widget_layout: &BottomLayout,
|
||||
default_widget_id: u64, default_widget_type_option: &Option<BottomWidgetType>,
|
||||
config_path: Option<PathBuf>,
|
||||
colours: &CanvasColours,
|
||||
) -> Result<App> {
|
||||
use BottomWidgetType::*;
|
||||
let autohide_time = get_autohide_time(matches, config);
|
||||
|
@ -272,7 +271,7 @@ pub fn build_app(
|
|||
let mut net_state_map: HashMap<u64, NetWidgetState> = HashMap::new();
|
||||
let mut proc_state_map: HashMap<u64, ProcWidget> = 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, DiskTableWidget> = HashMap::new();
|
||||
let mut battery_state_map: HashMap<u64, BatteryWidgetState> = HashMap::new();
|
||||
|
||||
let autohide_timer = if autohide_time {
|
||||
|
@ -295,6 +294,37 @@ pub fn build_app(
|
|||
let network_scale_type = get_network_scale_type(matches, config);
|
||||
let network_use_binary_prefix = get_network_use_binary_prefix(matches, config);
|
||||
|
||||
let app_config_fields = AppConfigFields {
|
||||
update_rate_in_milliseconds: get_update_rate_in_milliseconds(matches, config)
|
||||
.context("Update 'rate' in your config file.")?,
|
||||
temperature_type: get_temperature(matches, config)
|
||||
.context("Update 'temperature_type' in your config file.")?,
|
||||
show_average_cpu: get_show_average_cpu(matches, config),
|
||||
use_dot: get_use_dot(matches, config),
|
||||
left_legend: get_use_left_legend(matches, config),
|
||||
use_current_cpu_total: get_use_current_cpu_total(matches, config),
|
||||
use_basic_mode,
|
||||
default_time_value,
|
||||
time_interval: get_time_interval(matches, config)
|
||||
.context("Update 'time_delta' in your config file.")?,
|
||||
hide_time: get_hide_time(matches, config),
|
||||
autohide_time,
|
||||
use_old_network_legend: get_use_old_network_legend(matches, config),
|
||||
table_gap: if get_hide_table_gap(matches, config) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
},
|
||||
disable_click: get_disable_click(matches, config),
|
||||
// no_write: get_no_write(matches, config),
|
||||
no_write: false,
|
||||
show_table_scroll_position: get_show_table_scroll_position(matches, config),
|
||||
is_advanced_kill,
|
||||
network_scale_type,
|
||||
network_unit_type,
|
||||
network_use_binary_prefix,
|
||||
};
|
||||
|
||||
for row in &widget_layout.rows {
|
||||
for col in &row.children {
|
||||
for col_row in &col.children {
|
||||
|
@ -337,7 +367,12 @@ pub fn build_app(
|
|||
Cpu => {
|
||||
cpu_state_map.insert(
|
||||
widget.widget_id,
|
||||
CpuWidgetState::init(default_time_value, autohide_timer),
|
||||
CpuWidgetState::new(
|
||||
&app_config_fields,
|
||||
default_time_value,
|
||||
autohide_timer,
|
||||
colours,
|
||||
),
|
||||
);
|
||||
}
|
||||
Mem => {
|
||||
|
@ -365,21 +400,29 @@ pub fn build_app(
|
|||
|
||||
proc_state_map.insert(
|
||||
widget.widget_id,
|
||||
ProcWidget::init(
|
||||
ProcWidget::new(
|
||||
&app_config_fields,
|
||||
mode,
|
||||
is_case_sensitive,
|
||||
is_match_whole_word,
|
||||
is_use_regex,
|
||||
show_memory_as_values,
|
||||
is_default_command,
|
||||
colours,
|
||||
),
|
||||
);
|
||||
}
|
||||
Disk => {
|
||||
disk_state_map.insert(widget.widget_id, DiskWidgetState::default());
|
||||
disk_state_map.insert(
|
||||
widget.widget_id,
|
||||
DiskTableWidget::new(&app_config_fields, colours),
|
||||
);
|
||||
}
|
||||
Temp => {
|
||||
temp_state_map.insert(widget.widget_id, TempWidgetState::default());
|
||||
temp_state_map.insert(
|
||||
widget.widget_id,
|
||||
TempWidgetState::new(&app_config_fields, colours),
|
||||
);
|
||||
}
|
||||
Battery => {
|
||||
battery_state_map
|
||||
|
@ -417,37 +460,6 @@ pub fn build_app(
|
|||
None
|
||||
};
|
||||
|
||||
let app_config_fields = AppConfigFields {
|
||||
update_rate_in_milliseconds: get_update_rate_in_milliseconds(matches, config)
|
||||
.context("Update 'rate' in your config file.")?,
|
||||
temperature_type: get_temperature(matches, config)
|
||||
.context("Update 'temperature_type' in your config file.")?,
|
||||
show_average_cpu: get_show_average_cpu(matches, config),
|
||||
use_dot: get_use_dot(matches, config),
|
||||
left_legend: get_use_left_legend(matches, config),
|
||||
use_current_cpu_total: get_use_current_cpu_total(matches, config),
|
||||
use_basic_mode,
|
||||
default_time_value,
|
||||
time_interval: get_time_interval(matches, config)
|
||||
.context("Update 'time_delta' in your config file.")?,
|
||||
hide_time: get_hide_time(matches, config),
|
||||
autohide_time,
|
||||
use_old_network_legend: get_use_old_network_legend(matches, config),
|
||||
table_gap: if get_hide_table_gap(matches, config) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
},
|
||||
disable_click: get_disable_click(matches, config),
|
||||
// no_write: get_no_write(matches, config),
|
||||
no_write: false,
|
||||
show_table_scroll_position: get_show_table_scroll_position(matches, config),
|
||||
is_advanced_kill,
|
||||
network_scale_type,
|
||||
network_unit_type,
|
||||
network_use_binary_prefix,
|
||||
};
|
||||
|
||||
let used_widgets = UsedWidgets {
|
||||
use_cpu: used_widget_set.get(&Cpu).is_some() || used_widget_set.get(&BasicCpu).is_some(),
|
||||
use_mem: used_widget_set.get(&Mem).is_some() || used_widget_set.get(&BasicMem).is_some(),
|
||||
|
@ -528,8 +540,6 @@ pub fn build_app(
|
|||
temp_filter,
|
||||
net_filter,
|
||||
})
|
||||
.config(config.clone())
|
||||
.config_path(config_path)
|
||||
.build())
|
||||
}
|
||||
|
||||
|
|
|
@ -3,3 +3,9 @@ pub enum DataUnit {
|
|||
Byte,
|
||||
Bit,
|
||||
}
|
||||
|
||||
impl Default for DataUnit {
|
||||
fn default() -> Self {
|
||||
DataUnit::Bit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use concat_string::concat_string;
|
||||
use tui::text::Text;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
pub const KILO_LIMIT: u64 = 1000;
|
||||
pub const MEGA_LIMIT: u64 = 1_000_000;
|
||||
pub const GIGA_LIMIT: u64 = 1_000_000_000;
|
||||
|
@ -92,10 +96,24 @@ pub fn get_decimal_prefix(quantity: u64, unit: &str) -> (f64, String) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Truncates text if it is too long, and adds an ellipsis at the end if needed.
|
||||
pub fn truncate_text<'a, U: Into<usize>>(content: &str, width: U) -> Text<'a> {
|
||||
let width = width.into();
|
||||
let graphemes: Vec<&str> = UnicodeSegmentation::graphemes(content, true).collect();
|
||||
|
||||
if graphemes.len() > width && width > 0 {
|
||||
// Truncate with ellipsis
|
||||
let first_n = graphemes[..(width - 1)].concat();
|
||||
Text::raw(concat_string!(first_n, "…"))
|
||||
} else {
|
||||
Text::raw(content.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn sort_partial_fn<T: std::cmp::PartialOrd>(is_reverse: bool) -> fn(T, T) -> Ordering {
|
||||
if is_reverse {
|
||||
partial_ordering_rev
|
||||
pub fn sort_partial_fn<T: std::cmp::PartialOrd>(is_descending: bool) -> fn(T, T) -> Ordering {
|
||||
if is_descending {
|
||||
partial_ordering_desc
|
||||
} else {
|
||||
partial_ordering
|
||||
}
|
||||
|
@ -113,7 +131,7 @@ pub fn partial_ordering<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering {
|
|||
/// This is simply a wrapper function around [`partial_ordering`] that reverses
|
||||
/// the result.
|
||||
#[inline]
|
||||
pub fn partial_ordering_rev<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering {
|
||||
pub fn partial_ordering_desc<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering {
|
||||
partial_ordering(a, b).reverse()
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue