From c58b2c2bb9b0013d4f0fddbf39df5c7a6efa9bc4 Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Wed, 9 Sep 2020 21:51:52 -0400 Subject: [PATCH] refactor: rewrite column algorithm (#227) Update how we position and generate column widths to look less terrible. This also adds truncation w/ ellipsis to the columns, and for processes, the state will automatically shrink to a short form (just a character) if there isn't enough space. --- .vscode/settings.json | 1 + clippy.toml | 3 +- src/app.rs | 4 +- src/app/data_farmer.rs | 1 + src/app/data_harvester/cpu.rs | 9 +- src/app/data_harvester/disks.rs | 2 + src/app/states.rs | 41 +++- src/bin/main.rs | 8 + src/canvas.rs | 28 +-- src/canvas/drawing_utils.rs | 155 ++++++++------- src/canvas/widgets/cpu_graph.rs | 76 +++++--- src/canvas/widgets/disk_table.rs | 114 +++++++++-- src/canvas/widgets/network_graph.rs | 25 ++- src/canvas/widgets/process_table.rs | 283 ++++++++++++++++++++-------- src/canvas/widgets/temp_table.rs | 95 ++++++++-- src/constants.rs | 6 +- src/data_conversion.rs | 99 ++++++++-- src/lib.rs | 5 +- 18 files changed, 708 insertions(+), 247 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a9b4df4..18ad7061 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,6 +38,7 @@ "choco", "cmdline", "commandline", + "concat", "crossterm", "curr", "czvf", diff --git a/clippy.toml b/clippy.toml index 42f9d803..b3a62dba 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1,2 @@ -cognitive-complexity-threshold = 100 \ No newline at end of file +cognitive-complexity-threshold = 100 +type-complexity-threshold = 500 \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 4d07f4d0..7e2bb92d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -298,6 +298,7 @@ impl App { .columns .toggle(&processes::ProcessSorting::Pid); + proc_widget_state.requires_redraw = true; self.proc_state.force_update = Some(self.current_widget.widget_id); } } @@ -502,6 +503,7 @@ impl App { } self.proc_state.force_update = Some(self.current_widget.widget_id); + proc_widget_state.requires_redraw = true; } } @@ -1176,7 +1178,7 @@ impl App { } _ => {} } - + proc_widget_state.requires_redraw = true; self.proc_state.force_update = Some(self.current_widget.widget_id); } } diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index 0146ef67..48417ae5 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -188,6 +188,7 @@ impl DataCollection { } fn eat_network(&mut self, network: &network::NetworkHarvest, new_entry: &mut TimedData) { + // FIXME [NETWORKING; CONFIG]: The ability to config this? // FIXME [NETWORKING]: Support bits, support switching between decimal and binary units (move the log part to conversion and switch on the fly) // RX new_entry.rx_data = if network.rx > 0 { diff --git a/src/app/data_harvester/cpu.rs b/src/app/data_harvester/cpu.rs index 56a57fd5..bf6bf6e2 100644 --- a/src/app/data_harvester/cpu.rs +++ b/src/app/data_harvester/cpu.rs @@ -2,7 +2,8 @@ use sysinfo::{ProcessorExt, System, SystemExt}; #[derive(Default, Debug, Clone)] pub struct CpuData { - pub cpu_name: String, + pub cpu_prefix: String, + pub cpu_count: Option, pub cpu_usage: f64, } @@ -15,14 +16,16 @@ pub fn get_cpu_data_list(sys: &System, show_average_cpu: bool) -> CpuHarvest { if show_average_cpu { cpu_vec.push(CpuData { - cpu_name: "AVG".to_string(), + cpu_prefix: "AVG".to_string(), + cpu_count: None, cpu_usage: avg_cpu_usage as f64, }); } for (itx, cpu) in cpu_data.iter().enumerate() { cpu_vec.push(CpuData { - cpu_name: format!("CPU{}", itx), + cpu_prefix: "CPU".to_string(), + cpu_count: Some(itx), cpu_usage: f64::from(cpu.get_cpu_usage()), }); } diff --git a/src/app/data_harvester/disks.rs b/src/app/data_harvester/disks.rs index a9599bad..03c384ad 100644 --- a/src/app/data_harvester/disks.rs +++ b/src/app/data_harvester/disks.rs @@ -24,6 +24,8 @@ pub async fn get_sysinfo_io_usage_list( std::collections::HashMap::new(); Ok(Some(io_hash)) + // TODO: Rename these functions to be like "get_arm_io_usage_list" + // TODO: Sysinfo disk I/O usage. // ...sadly, this cannot be done as of now (other than me writing my own), it requires further // work. See https://github.com/GuillaumeGomez/sysinfo/issues/304. diff --git a/src/app/states.rs b/src/app/states.rs index 318b2175..5cba386d 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -109,6 +109,13 @@ impl AppSearchState { } } +/// Meant for canvas operations involving table column widths. +#[derive(Default)] +pub struct CanvasTableWidthState { + pub desired_column_widths: Vec, + pub calculated_column_widths: Vec, +} + /// ProcessSearchState only deals with process' search's current settings and state. pub struct ProcessSearchState { pub search_state: AppSearchState, @@ -321,6 +328,9 @@ impl ProcColumn { pub fn get_column_headers( &self, proc_sorting_type: &ProcessSorting, sort_reverse: bool, ) -> Vec { + const DOWN_ARROW: char = '▼'; + const UP_ARROW: char = '▲'; + // TODO: Gonna have to figure out how to do left/right GUI notation if we add it. self.ordered_columns .iter() @@ -332,13 +342,20 @@ impl ProcColumn { } if mapping.enabled { - Some(if proc_sorting_type == column_type { - column_type.to_string() - + command_str.as_str() - + if sort_reverse { "▼" } else { "▲" } - } else { - column_type.to_string() + command_str.as_str() - }) + Some(format!( + "{}{}{}", + column_type.to_string(), + command_str.as_str(), + if proc_sorting_type == column_type { + if sort_reverse { + DOWN_ARROW + } else { + UP_ARROW + } + } else { + ' ' + } + )) } else { None } @@ -358,6 +375,8 @@ pub struct ProcWidgetState { pub is_sort_open: bool, pub columns: ProcColumn, pub is_tree_mode: bool, + pub table_width_state: CanvasTableWidthState, + pub requires_redraw: bool, } impl ProcWidgetState { @@ -397,6 +416,8 @@ impl ProcWidgetState { is_sort_open: false, columns, is_tree_mode: false, + table_width_state: CanvasTableWidthState::default(), + requires_redraw: false, } } @@ -595,6 +616,7 @@ pub struct CpuWidgetState { pub autohide_timer: Option, pub scroll_state: AppScrollWidgetState, pub is_multi_graph_mode: bool, + pub table_width_state: CanvasTableWidthState, } impl CpuWidgetState { @@ -605,6 +627,7 @@ impl CpuWidgetState { autohide_timer, scroll_state: AppScrollWidgetState::default(), is_multi_graph_mode: false, + table_width_state: CanvasTableWidthState::default(), } } } @@ -668,12 +691,14 @@ impl MemState { pub struct TempWidgetState { pub scroll_state: AppScrollWidgetState, + pub table_width_state: CanvasTableWidthState, } impl TempWidgetState { pub fn init() -> Self { TempWidgetState { scroll_state: AppScrollWidgetState::default(), + table_width_state: CanvasTableWidthState::default(), } } } @@ -698,12 +723,14 @@ impl TempState { pub struct DiskWidgetState { pub scroll_state: AppScrollWidgetState, + pub table_width_state: CanvasTableWidthState, } impl DiskWidgetState { pub fn init() -> Self { DiskWidgetState { scroll_state: AppScrollWidgetState::default(), + table_width_state: CanvasTableWidthState::default(), } } } diff --git a/src/bin/main.rs b/src/bin/main.rs index dd1cbabe..92cf9f0c 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -106,6 +106,7 @@ fn main() -> Result<()> { termination_hook(); }) .unwrap(); + let mut first_run = true; while !is_terminated.load(Ordering::SeqCst) { if let Ok(recv) = receiver.recv_timeout(Duration::from_millis(TICK_RATE_IN_MILLISECONDS)) { @@ -123,6 +124,13 @@ fn main() -> Result<()> { BottomEvent::Update(data) => { app.data_collection.eat_data(&data); + // This thing is required as otherwise, some widgets can't draw correctly w/o + // some data (or they need to be re-drawn). + if first_run { + first_run = false; + app.is_force_redraw = true; + } + if !app.is_frozen { // Convert all data into tui-compliant components diff --git a/src/canvas.rs b/src/canvas.rs index e276e097..5bb8a438 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -43,6 +43,7 @@ pub struct DisplayableData { pub temp_sensor_data: Vec>, pub single_process_data: Vec, // Contains single process data pub finalized_process_data_map: HashMap>, // What's actually displayed + pub stringified_process_data_map: HashMap)>, bool)>>, // Represents the row and whether it is disabled pub mem_label_percent: String, pub swap_label_percent: String, pub mem_label_frac: String, @@ -67,7 +68,6 @@ pub struct Painter { widget_layout: BottomLayout, derived_widget_draw_locs: Vec>>>, table_height_offset: u16, - requires_boundary_recalculation: bool, } impl Painter { @@ -152,7 +152,6 @@ impl Painter { widget_layout, derived_widget_draw_locs: Vec::default(), table_height_offset: if is_basic_mode { 2 } else { 4 } + table_gap, - requires_boundary_recalculation: true, } } @@ -401,25 +400,18 @@ impl Painter { // Basic mode. This basically removes all graphs but otherwise // the same info. - let cpu_height = (app_state.canvas_data.cpu_data.len() / 4) as u16 - + (if app_state.canvas_data.cpu_data.len() % 4 == 0 { - 0 - } else { - 1 - }); - - // A little hack to force the widget boundary recalculation. This is required here - // as basic mode has a height of 0 initially, which breaks things. - if self.requires_boundary_recalculation { - app_state.is_determining_widget_boundary = true; - } - self.requires_boundary_recalculation = cpu_height == 0; - let vertical_chunks = Layout::default() .direction(Direction::Vertical) .constraints( [ - Constraint::Length(cpu_height), + Constraint::Length( + (app_state.canvas_data.cpu_data.len() / 4) as u16 + + (if app_state.canvas_data.cpu_data.len() % 4 == 0 { + 0 + } else { + 1 + }), + ), Constraint::Length(1), Constraint::Length(2), Constraint::Length(2), @@ -486,7 +478,7 @@ impl Painter { self.draw_basic_table_arrows(&mut f, app_state, vertical_chunks[3], widget_id); } } else { - // Draws using the passed in (or default) layout. NOT basic so far. + // Draws using the passed in (or default) layout. if self.derived_widget_draw_locs.is_empty() || app_state.is_force_redraw { let row_draw_locs = Layout::default() .margin(0) diff --git a/src/canvas/drawing_utils.rs b/src/canvas/drawing_utils.rs index 22d56419..99bf799f 100644 --- a/src/canvas/drawing_utils.rs +++ b/src/canvas/drawing_utils.rs @@ -1,78 +1,107 @@ use crate::app; -use itertools::izip; +use std::cmp::{max, min}; -// TODO: Reverse intrinsic? -/// A somewhat jury-rigged solution to simulate a variable intrinsic layout for -/// table widths. Note that this will do one main pass to try to properly -/// allocate widths. This will thus potentially cut off latter elements -/// (return size of 0) if it is too small (threshold), but will try its best. +/// Return a (hard)-width vector for column widths. /// -/// `width thresholds` and `desired_widths_ratio` should be the same length. -/// Otherwise bad things happen. -pub fn get_variable_intrinsic_widths( - total_width: u16, desired_widths_ratio: &[f64], width_thresholds: &[usize], -) -> (Vec, usize) { - let num_widths = desired_widths_ratio.len(); - let mut resulting_widths: Vec = vec![0; num_widths]; - let mut last_index = 0; +/// * `total_width` is the, well, total width available. **NOTE:** This function automatically +/// takes away 2 from the width as part of the left/right +/// bounds. +/// * `hard_widths` is inflexible column widths. Use a `None` to represent a soft width. +/// * `soft_widths_min` is the lower limit for a soft width. Use `None` if a hard width goes there. +/// * `soft_widths_max` is the upper limit for a soft width, in percentage of the total width. Use +/// `None` if a hard width goes there. +/// * `soft_widths_desired` is the desired soft width. Use `None` if a hard width goes there. +/// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if +/// false. +/// +/// **NOTE:** This function ASSUMES THAT ALL PASSED SLICES ARE OF THE SAME SIZE. +/// +/// **NOTE:** The returned vector may not be the same size as the slices, this is because including +/// 0-constraints breaks tui-rs. +pub fn get_column_widths( + total_width: u16, hard_widths: &[Option], soft_widths_min: &[Option], + soft_widths_max: &[Option], soft_widths_desired: &[Option], left_to_right: bool, +) -> Vec { + let initial_width = total_width - 2; + let mut total_width_left = initial_width; + let mut column_widths: Vec = vec![0; hard_widths.len()]; + let range: Vec = if left_to_right { + (0..hard_widths.len()).collect() + } else { + (0..hard_widths.len()).rev().collect() + }; - let mut remaining_width = (total_width - (num_widths as u16 - 1)) as i32; // Required for spaces... - let desired_widths = desired_widths_ratio - .iter() - .map(|&desired_width_ratio| (desired_width_ratio * total_width as f64) as i32); + for itx in &range { + if let Some(Some(hard_width)) = hard_widths.get(*itx) { + // Hard width... + let space_taken = min(*hard_width, total_width_left); - for (desired_width, resulting_width, width_threshold) in izip!( - desired_widths, - resulting_widths.iter_mut(), - width_thresholds - ) { - *resulting_width = if desired_width < *width_threshold as i32 { - // Try to take threshold, else, 0 - if remaining_width < *width_threshold as i32 { - 0 - } else { - remaining_width -= *width_threshold as i32; - *width_threshold as u16 + // TODO [COLUMN MOVEMENT]: Remove this + if *hard_width > space_taken { + break; } - } else { - // Take as large as possible - if remaining_width < desired_width { - // Check the biggest chunk possible - if remaining_width < *width_threshold as i32 { - 0 + + column_widths[*itx] = space_taken; + total_width_left -= space_taken; + total_width_left = total_width_left.saturating_sub(1); + } else if let ( + Some(Some(soft_width_max)), + Some(Some(soft_width_min)), + Some(Some(soft_width_desired)), + ) = ( + soft_widths_max.get(*itx), + soft_widths_min.get(*itx), + soft_widths_desired.get(*itx), + ) { + // Soft width... + let soft_limit = max( + if soft_width_max.is_sign_negative() { + *soft_width_desired } else { - let temp_width = remaining_width; - remaining_width = 0; - temp_width as u16 + (*soft_width_max * initial_width as f64).ceil() as u16 + }, + *soft_width_min, + ); + let space_taken = min(min(soft_limit, *soft_width_desired), total_width_left); + + // TODO [COLUMN MOVEMENT]: Remove this + if *soft_width_min > space_taken { + break; + } + + column_widths[*itx] = space_taken; + total_width_left -= space_taken; + total_width_left = total_width_left.saturating_sub(1); + } + } + + // Redistribute remaining. + while total_width_left > 0 { + for itx in &range { + if column_widths[*itx] > 0 { + column_widths[*itx] += 1; + total_width_left -= 1; + if total_width_left == 0 { + break; } - } else { - remaining_width -= desired_width; - desired_width as u16 } - }; + } + } - if *resulting_width == 0 { - break; + let mut filtered_column_widths: Vec = vec![]; + let mut still_seeing_zeros = true; + column_widths.iter().rev().for_each(|width| { + if still_seeing_zeros { + if *width != 0 { + still_seeing_zeros = false; + filtered_column_widths.push(*width); + } } else { - last_index += 1; + filtered_column_widths.push(*width); } - } - - // Simple redistribution tactic - if there's any space left, split it evenly amongst all members - if last_index < num_widths && last_index != 0 { - let for_all_widths = (remaining_width / last_index as i32) as u16; - let mut remainder = remaining_width % last_index as i32; - - for resulting_width in &mut resulting_widths { - *resulting_width += for_all_widths; - if remainder > 0 { - *resulting_width += 1; - remainder -= 1; - } - } - } - - (resulting_widths, last_index) + }); + filtered_column_widths.reverse(); + filtered_column_widths } pub fn get_search_start_position( diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs index 86999345..97ba064d 100644 --- a/src/canvas/widgets/cpu_graph.rs +++ b/src/canvas/widgets/cpu_graph.rs @@ -1,11 +1,10 @@ use lazy_static::lazy_static; use std::borrow::Cow; -use std::cmp::max; use crate::{ app::{layout_manager::WidgetDirection, App}, canvas::{ - drawing_utils::{get_start_position, get_variable_intrinsic_widths}, + drawing_utils::{get_column_widths, get_start_position}, Painter, }, constants::*, @@ -20,19 +19,14 @@ use tui::{ widgets::{Axis, Block, Borders, Chart, Dataset, Row, Table}, }; -const CPU_SELECT_LEGEND_HEADER: [&str; 2] = ["CPU", "Show"]; const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"]; const AVG_POSITION: usize = 1; const ALL_POSITION: usize = 0; lazy_static! { - static ref CPU_LEGEND_HEADER_LENS: Vec = CPU_LEGEND_HEADER + static ref CPU_LEGEND_HEADER_LENS: Vec = CPU_LEGEND_HEADER .iter() - .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) - .collect::>(); - static ref CPU_SELECT_LEGEND_HEADER_LENS: Vec = CPU_SELECT_LEGEND_HEADER - .iter() - .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) + .map(|entry| entry.len() as u16) .collect::>(); } @@ -273,6 +267,7 @@ impl CpuGraphWidget for Painter { fn draw_cpu_legend( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { + 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)) { cpu_widget_state.is_legend_hidden = false; @@ -308,11 +303,54 @@ impl CpuGraphWidget for Painter { .saturating_sub(start_position); let show_avg_cpu = app_state.app_config_fields.show_average_cpu; + // Calculate widths + if recalculate_column_widths { + cpu_widget_state.table_width_state.desired_column_widths = vec![6, 4]; + cpu_widget_state.table_width_state.calculated_column_widths = get_column_widths( + draw_loc.width, + &[None, None], + &(CPU_LEGEND_HEADER_LENS + .iter() + .map(|width| Some(*width)) + .collect::>()), + &[Some(0.5), Some(0.5)], + &(cpu_widget_state + .table_width_state + .desired_column_widths + .iter() + .map(|width| Some(*width)) + .collect::>()), + false, + ); + } + + let dcw = &cpu_widget_state.table_width_state.desired_column_widths; + let ccw = &cpu_widget_state.table_width_state.calculated_column_widths; let cpu_rows = sliced_cpu_data.iter().enumerate().filter_map(|(itx, cpu)| { - let cpu_string_row: Vec> = vec![ - Cow::Borrowed(&cpu.cpu_name), - Cow::Borrowed(&cpu.legend_value), - ]; + let truncated_name: Cow<'_, str> = + if let (Some(desired_column_width), Some(calculated_column_width)) = + (dcw.get(0), ccw.get(0)) + { + if *desired_column_width > *calculated_column_width { + Cow::Borrowed(&cpu.short_cpu_name) + } else { + Cow::Borrowed(&cpu.cpu_name) + } + } else { + Cow::Borrowed(&cpu.cpu_name) + }; + let truncated_legend: Cow<'_, str> = + if let Some(calculated_column_width) = ccw.get(0) { + if *calculated_column_width == 0 && cpu.legend_value.is_empty() { + Cow::Borrowed("All") + } else { + Cow::Borrowed(&cpu.legend_value) + } + } else { + Cow::Borrowed(&cpu.legend_value) + }; + + let cpu_string_row: Vec> = vec![truncated_name, truncated_legend]; if cpu_string_row.is_empty() { offset_scroll_index += 1; @@ -341,14 +379,6 @@ impl CpuGraphWidget for Painter { } }); - // Calculate widths - let width = f64::from(draw_loc.width); - let width_ratios = vec![0.5, 0.5]; - - let variable_intrinsic_results = - get_variable_intrinsic_widths(width as u16, &width_ratios, &CPU_LEGEND_HEADER_LENS); - let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; - // Note we don't set highlight_style, as it should always be shown for this widget. let border_and_title_style = if is_on_widget { self.colours.highlighted_border_style @@ -367,7 +397,9 @@ impl CpuGraphWidget for Painter { .header_style(self.colours.table_header_style) .highlight_style(self.colours.currently_selected_text_style) .widths( - &(intrinsic_widths + &(cpu_widget_state + .table_width_state + .calculated_column_widths .iter() .map(|calculated_width| Constraint::Length(*calculated_width as u16)) .collect::>()), diff --git a/src/canvas/widgets/disk_table.rs b/src/canvas/widgets/disk_table.rs index f791500b..440687df 100644 --- a/src/canvas/widgets/disk_table.rs +++ b/src/canvas/widgets/disk_table.rs @@ -1,5 +1,4 @@ use lazy_static::lazy_static; -use std::cmp::max; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, @@ -10,18 +9,20 @@ use tui::{ use crate::{ app, canvas::{ - drawing_utils::{get_start_position, get_variable_intrinsic_widths}, + drawing_utils::{get_column_widths, get_start_position}, Painter, }, constants::*, }; +use std::borrow::Cow; +use unicode_segmentation::UnicodeSegmentation; const DISK_HEADERS: [&str; 7] = ["Disk", "Mount", "Used", "Free", "Total", "R/s", "W/s"]; lazy_static! { - static ref DISK_HEADERS_LENS: Vec = DISK_HEADERS + static ref DISK_HEADERS_LENS: Vec = DISK_HEADERS .iter() - .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) + .map(|entry| entry.len() as u16) .collect::>(); } @@ -37,6 +38,7 @@ impl DiskTableWidget for Painter { &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool, 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 disk_data: &mut [Vec] = &mut app_state.canvas_data.disk_data; let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { @@ -61,16 +63,100 @@ impl DiskTableWidget for Painter { .current_scroll_position .saturating_sub(start_position), )); - let sliced_vec = &mut disk_data[start_position..]; - let disk_rows = sliced_vec.iter().map(|disk| Row::Data(disk.iter())); + let sliced_vec = &disk_data[start_position..]; // Calculate widths - // TODO: [PRETTY] Ellipsis on strings? - let width = f64::from(draw_loc.width); - let width_ratios = [0.2, 0.15, 0.13, 0.13, 0.13, 0.13, 0.13]; - let variable_intrinsic_results = - get_variable_intrinsic_widths(width as u16, &width_ratios, &DISK_HEADERS_LENS); - let intrinsic_widths = &variable_intrinsic_results.0[0..variable_intrinsic_results.1]; + let hard_widths = [None, None, Some(4), Some(6), Some(6), Some(7), Some(7)]; + if recalculate_column_widths { + disk_widget_state.table_width_state.desired_column_widths = { + let mut column_widths = DISK_HEADERS_LENS.clone(); + for row in sliced_vec { + for (col, entry) in row.iter().enumerate() { + if entry.len() as u16 > column_widths[col] { + column_widths[col] = entry.len() as u16; + } + } + } + column_widths + }; + disk_widget_state.table_width_state.desired_column_widths = disk_widget_state + .table_width_state + .desired_column_widths + .iter() + .zip(&hard_widths) + .map(|(current, hard)| { + if let Some(hard) = hard { + if *hard > *current { + *hard + } else { + *current + } + } else { + *current + } + }) + .collect::>(); + + disk_widget_state.table_width_state.calculated_column_widths = get_column_widths( + draw_loc.width, + &hard_widths, + &(DISK_HEADERS_LENS + .iter() + .map(|w| Some(*w)) + .collect::>()), + &[Some(0.2), Some(0.2), None, None, None, None, None], + &(disk_widget_state + .table_width_state + .desired_column_widths + .iter() + .map(|w| Some(*w)) + .collect::>()), + true, + ); + } + + let dcw = &disk_widget_state.table_width_state.desired_column_widths; + let ccw = &disk_widget_state.table_width_state.calculated_column_widths; + let disk_rows = + sliced_vec.iter().map(|disk_row| { + let truncated_data = disk_row.iter().zip(&hard_widths).enumerate().map( + |(itx, (entry, width))| { + if width.is_none() { + if let (Some(desired_col_width), Some(calculated_col_width)) = + (dcw.get(itx), ccw.get(itx)) + { + if *desired_col_width > *calculated_col_width + && *calculated_col_width > 0 + { + let graphemes = + UnicodeSegmentation::graphemes(entry.as_str(), true) + .collect::>(); + + if graphemes.len() > *calculated_col_width as usize + && *calculated_col_width > 1 + { + // Truncate with ellipsis + let first_n = graphemes + [..(*calculated_col_width as usize - 1)] + .concat(); + Cow::Owned(format!("{}…", first_n)) + } else { + Cow::Borrowed(entry) + } + } else { + Cow::Borrowed(entry) + } + } else { + Cow::Borrowed(entry) + } + } else { + Cow::Borrowed(entry) + } + }, + ); + + Row::Data(truncated_data) + }); // TODO: This seems to be bugged? The selected text style gets "stuck"? I think this gets fixed with tui 0.10? let (border_and_title_style, highlight_style) = if is_on_widget { @@ -148,7 +234,9 @@ impl DiskTableWidget for Painter { .highlight_style(highlight_style) .style(self.colours.text_style) .widths( - &(intrinsic_widths + &(disk_widget_state + .table_width_state + .calculated_column_widths .iter() .map(|calculated_width| Constraint::Length(*calculated_width as u16)) .collect::>()), diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/widgets/network_graph.rs index 32887c32..23c1927c 100644 --- a/src/canvas/widgets/network_graph.rs +++ b/src/canvas/widgets/network_graph.rs @@ -3,7 +3,7 @@ use std::cmp::max; use crate::{ app::App, - canvas::{drawing_utils::get_variable_intrinsic_widths, Painter}, + canvas::{drawing_utils::get_column_widths, Painter}, constants::*, utils::gen_util::*, }; @@ -19,9 +19,9 @@ use tui::{ const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"]; lazy_static! { - static ref NETWORK_HEADERS_LENS: Vec = NETWORK_HEADERS + static ref NETWORK_HEADERS_LENS: Vec = NETWORK_HEADERS .iter() - .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) + .map(|entry| entry.len() as u16) .collect::>(); } @@ -342,6 +342,7 @@ impl NetworkGraphWidget for Painter { } } + // TODO: [DEPRECATED] Get rid of this in, like, 0.6...? fn draw_network_labels( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { @@ -362,13 +363,17 @@ impl NetworkGraphWidget for Painter { .map(|val| Row::StyledData(val.iter(), self.colours.text_style)); // Calculate widths - let width_ratios: Vec = vec![0.25, 0.25, 0.25, 0.25]; - let lens: &[usize] = &NETWORK_HEADERS_LENS; - let width = f64::from(draw_loc.width); - - let variable_intrinsic_results = - get_variable_intrinsic_widths(width as u16, &width_ratios, lens); - let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; + let intrinsic_widths = get_column_widths( + draw_loc.width, + &[None, None, None, None], + &[Some(6); 4], + &[Some(0.25); 4], + &(NETWORK_HEADERS_LENS + .iter() + .map(|s| Some(*s)) + .collect::>()), + true, + ); // Draw f.render_widget( diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index cce7835c..ad878263 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -1,9 +1,7 @@ use crate::{ - app::{data_harvester::processes::ProcessSorting, App}, + app::App, canvas::{ - drawing_utils::{ - get_search_start_position, get_start_position, get_variable_intrinsic_widths, - }, + drawing_utils::{get_column_widths, get_search_start_position, get_start_position}, Painter, }, constants::*, @@ -16,6 +14,7 @@ use tui::{ widgets::{Block, Borders, Paragraph, Row, Table, Text}, }; +use std::borrow::Cow; use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation}; use unicode_width::UnicodeWidthStr; @@ -108,7 +107,14 @@ impl ProcessTableWidget for Painter { &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool, widget_id: u64, ) { + let should_get_widget_bounds = app_state.should_get_widget_bounds(); if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) { + let recalculate_column_widths = + should_get_widget_bounds || proc_widget_state.requires_redraw; + if proc_widget_state.requires_redraw { + proc_widget_state.requires_redraw = false; + } + let is_on_widget = widget_id == app_state.current_widget.widget_id; let margined_draw_loc = Layout::default() .constraints([Constraint::Percentage(100)].as_ref()) @@ -170,7 +176,7 @@ impl ProcessTableWidget for Painter { if let Some(process_data) = &app_state .canvas_data - .finalized_process_data_map + .stringified_process_data_map .get(&widget_id) { let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { @@ -197,6 +203,14 @@ impl ProcessTableWidget for Painter { }; let sliced_vec = &process_data[start_position..]; + let processed_sliced_vec = sliced_vec.iter().map(|(data, disabled)| { + ( + data.iter() + .map(|(entry, _alternative)| entry) + .collect::>(), + disabled, + ) + }); let proc_table_state = &mut proc_widget_state.scroll_state.table_state; proc_table_state.select(Some( proc_widget_state @@ -206,85 +220,198 @@ impl ProcessTableWidget for Painter { )); // Draw! - let is_proc_widget_grouped = proc_widget_state.is_grouped; - let is_using_command = proc_widget_state.is_using_command; - let is_tree = proc_widget_state.is_tree_mode; - let mem_enabled = proc_widget_state.columns.is_enabled(&ProcessSorting::Mem); - - // FIXME: [PROC OPTIMIZE] This can definitely be optimized; string references work fine here! - let process_rows = sliced_vec.iter().map(|process| { - let data = vec![ - if is_proc_widget_grouped { - process.group_pids.len().to_string() - } else { - process.pid.to_string() - }, - if is_tree { - if let Some(prefix) = &process.process_description_prefix { - prefix.clone() - } else { - String::default() - } - } else if is_using_command { - process.command.clone() - } else { - process.name.clone() - }, - format!("{:.1}%", process.cpu_percent_usage), - if mem_enabled { - format!("{:.0}{}", process.mem_usage_str.0, process.mem_usage_str.1) - } else { - format!("{:.1}%", process.mem_percent_usage) - }, - process.read_per_sec.clone(), - process.write_per_sec.clone(), - process.total_read.clone(), - process.total_write.clone(), - process.process_state.clone(), - ] - .into_iter(); - - if process.is_disabled_entry { - Row::StyledData(data, self.colours.disabled_text_style) - } else { - Row::Data(data) - } - }); - let process_headers = proc_widget_state.columns.get_column_headers( &proc_widget_state.process_sorting_type, proc_widget_state.is_process_sort_descending, ); - let process_headers_lens: Vec = process_headers - .iter() - .map(|entry| entry.len()) - .collect::>(); - // Calculate widths - let width = f64::from(draw_loc.width); - - // TODO: This is a ugly work-around for now. - let width_ratios = if proc_widget_state.is_grouped { - if proc_widget_state.is_using_command { - vec![0.05, 0.7, 0.05, 0.05, 0.0375, 0.0375, 0.0375, 0.0375] - } else { - vec![0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.15, 0.15] - } - } else if proc_widget_state.is_using_command { - vec![0.05, 0.7, 0.05, 0.05, 0.03, 0.03, 0.03, 0.03] - } else if proc_widget_state.is_tree_mode { - vec![0.05, 0.4, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] + let hard_widths = if proc_widget_state.is_grouped { + vec![ + Some(7), + None, + Some(8), + Some(8), + Some(8), + Some(8), + Some(7), + Some(8), + ] } else { - vec![0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] + vec![ + Some(7), + None, + Some(8), + Some(8), + Some(8), + Some(8), + Some(7), + Some(8), + None, + ] }; - let variable_intrinsic_results = get_variable_intrinsic_widths( - width as u16, - &width_ratios, - &process_headers_lens, - ); - let intrinsic_widths = - &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; + + if recalculate_column_widths { + let mut column_widths = process_headers + .iter() + .map(|entry| UnicodeWidthStr::width(entry.as_str()) as u16) + .collect::>(); + let soft_widths_min = column_widths + .iter() + .map(|width| Some(*width)) + .collect::>(); + + proc_widget_state.table_width_state.desired_column_widths = { + for (row, _disabled) in processed_sliced_vec.clone() { + for (col, entry) in row.iter().enumerate() { + if let Some(col_width) = column_widths.get_mut(col) { + let grapheme_len = UnicodeWidthStr::width(entry.as_str()); + if grapheme_len as u16 > *col_width { + *col_width = grapheme_len as u16; + } + } + } + } + column_widths + }; + + proc_widget_state.table_width_state.desired_column_widths = proc_widget_state + .table_width_state + .desired_column_widths + .iter() + .zip(&hard_widths) + .map(|(current, hard)| { + if let Some(hard) = hard { + if *hard > *current { + *hard + } else { + *current + } + } else { + *current + } + }) + .collect::>(); + + let soft_widths_max = if proc_widget_state.is_grouped { + if proc_widget_state.is_using_command { + vec![None, Some(0.7), None, None, None, None, None, None] + } else if proc_widget_state.is_tree_mode { + vec![None, Some(0.5), None, None, None, None, None, None] + } else { + vec![None, Some(0.4), None, None, None, None, None, None] + } + } else if proc_widget_state.is_using_command { + vec![ + None, + Some(0.7), + None, + None, + None, + None, + None, + None, + Some(0.2), + ] + } else if proc_widget_state.is_tree_mode { + vec![ + None, + Some(0.5), + None, + None, + None, + None, + None, + None, + Some(0.2), + ] + } else { + vec![ + None, + Some(0.3), + None, + None, + None, + None, + None, + None, + Some(0.2), + ] + }; + + proc_widget_state.table_width_state.calculated_column_widths = + get_column_widths( + draw_loc.width, + &hard_widths, + &soft_widths_min, + &soft_widths_max, + &(proc_widget_state + .table_width_state + .desired_column_widths + .iter() + .map(|width| Some(*width)) + .collect::>()), + true, + ); + + // debug!( + // "DCW: {:?}", + // proc_widget_state.table_width_state.desired_column_widths + // ); + // debug!( + // "CCW: {:?}", + // proc_widget_state.table_width_state.calculated_column_widths + // ); + } + + let dcw = &proc_widget_state.table_width_state.desired_column_widths; + let ccw = &proc_widget_state.table_width_state.calculated_column_widths; + + let process_rows = sliced_vec.iter().map(|(data, disabled)| { + let truncated_data = data.iter().zip(&hard_widths).enumerate().map( + |(itx, ((entry, alternative), width))| { + if let (Some(desired_col_width), Some(calculated_col_width)) = + (dcw.get(itx), ccw.get(itx)) + { + if width.is_none() { + if *desired_col_width > *calculated_col_width + && *calculated_col_width > 0 + { + let graphemes = + UnicodeSegmentation::graphemes(entry.as_str(), true) + .collect::>(); + + if let Some(alternative) = alternative { + Cow::Borrowed(alternative) + } else if graphemes.len() > *calculated_col_width as usize + && *calculated_col_width > 1 + { + // Truncate with ellipsis + let first_n = graphemes + [..(*calculated_col_width as usize - 1)] + .concat(); + Cow::Owned(format!("{}…", first_n)) + } else { + Cow::Borrowed(entry) + } + } else { + Cow::Borrowed(entry) + } + } else { + Cow::Borrowed(entry) + } + } else { + Cow::Borrowed(entry) + } + }, + ); + + if *disabled { + Row::StyledData(truncated_data, self.colours.disabled_text_style) + } else { + Row::Data(truncated_data) + } + }); // TODO: gotop's "x out of y" thing is really nice to help keep track of the scroll position. f.render_stateful_widget( @@ -294,7 +421,9 @@ impl ProcessTableWidget for Painter { .highlight_style(highlight_style) .style(self.colours.text_style) .widths( - &(intrinsic_widths + &(proc_widget_state + .table_width_state + .calculated_column_widths .iter() .map(|calculated_width| { Constraint::Length(*calculated_width as u16) diff --git a/src/canvas/widgets/temp_table.rs b/src/canvas/widgets/temp_table.rs index 9ab6f985..b17bb0e0 100644 --- a/src/canvas/widgets/temp_table.rs +++ b/src/canvas/widgets/temp_table.rs @@ -1,6 +1,4 @@ use lazy_static::lazy_static; -use std::cmp::max; - use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, @@ -11,18 +9,20 @@ use tui::{ use crate::{ app, canvas::{ - drawing_utils::{get_start_position, get_variable_intrinsic_widths}, + drawing_utils::{get_column_widths, get_start_position}, Painter, }, constants::*, }; +use std::borrow::Cow; +use unicode_segmentation::UnicodeSegmentation; const TEMP_HEADERS: [&str; 2] = ["Sensor", "Temp"]; lazy_static! { - static ref TEMP_HEADERS_LENS: Vec = TEMP_HEADERS + static ref TEMP_HEADERS_LENS: Vec = TEMP_HEADERS .iter() - .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) + .map(|entry| entry.len() as u16) .collect::>(); } pub trait TempTableWidget { @@ -37,6 +37,7 @@ impl TempTableWidget for Painter { &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool, 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 temp_sensor_data: &mut [Vec] = &mut app_state.canvas_data.temp_sensor_data; @@ -63,14 +64,82 @@ impl TempTableWidget for Painter { .saturating_sub(start_position), )); let sliced_vec = &temp_sensor_data[start_position..]; - let temperature_rows = sliced_vec.iter().map(|temp_row| Row::Data(temp_row.iter())); // Calculate widths - let width = f64::from(draw_loc.width); - let width_ratios = [0.5, 0.5]; - let variable_intrinsic_results = - get_variable_intrinsic_widths(width as u16, &width_ratios, &TEMP_HEADERS_LENS); - let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; + let hard_widths = [None, None]; + if recalculate_column_widths { + temp_widget_state.table_width_state.desired_column_widths = { + let mut column_widths = TEMP_HEADERS_LENS.clone(); + for row in sliced_vec { + for (col, entry) in row.iter().enumerate() { + if entry.len() as u16 > column_widths[col] { + column_widths[col] = entry.len() as u16; + } + } + } + + column_widths + }; + temp_widget_state.table_width_state.calculated_column_widths = get_column_widths( + draw_loc.width, + &hard_widths, + &(TEMP_HEADERS_LENS + .iter() + .map(|width| Some(*width)) + .collect::>()), + &[Some(0.80), Some(-1.0)], + &temp_widget_state + .table_width_state + .desired_column_widths + .iter() + .map(|width| Some(*width)) + .collect::>(), + false, + ); + } + + let dcw = &temp_widget_state.table_width_state.desired_column_widths; + let ccw = &temp_widget_state.table_width_state.calculated_column_widths; + let temperature_rows = + sliced_vec.iter().map(|temp_row| { + let truncated_data = temp_row.iter().zip(&hard_widths).enumerate().map( + |(itx, (entry, width))| { + if width.is_none() { + if let (Some(desired_col_width), Some(calculated_col_width)) = + (dcw.get(itx), ccw.get(itx)) + { + if *desired_col_width > *calculated_col_width + && *calculated_col_width > 0 + { + let graphemes = + UnicodeSegmentation::graphemes(entry.as_str(), true) + .collect::>(); + + if graphemes.len() > *calculated_col_width as usize + && *calculated_col_width > 1 + { + // Truncate with ellipsis + let first_n = graphemes + [..(*calculated_col_width as usize - 1)] + .concat(); + Cow::Owned(format!("{}…", first_n)) + } else { + Cow::Borrowed(entry) + } + } else { + Cow::Borrowed(entry) + } + } else { + Cow::Borrowed(entry) + } + } else { + Cow::Borrowed(entry) + } + }, + ); + + Row::Data(truncated_data) + }); let (border_and_title_style, highlight_style) = if is_on_widget { ( @@ -128,7 +197,9 @@ impl TempTableWidget for Painter { .highlight_style(highlight_style) .style(self.colours.text_style) .widths( - &(intrinsic_widths + &(temp_widget_state + .table_width_state + .calculated_column_widths .iter() .map(|calculated_width| Constraint::Length(*calculated_width as u16)) .collect::>()), diff --git a/src/constants.rs b/src/constants.rs index 99f663ce..6d0bc530 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -19,10 +19,6 @@ pub const MAX_KEY_TIMEOUT_IN_MILLISECONDS: u64 = 1000; // Number of colours to generate for the CPU chart/table pub const NUM_COLOURS: usize = 256; -// Canvas stuff -// The minimum threshold when resizing tables -pub const FORCE_MIN_THRESHOLD: usize = 5; - // Limits for when we should stop showing table gaps/labels (anything less means not shown) pub const TABLE_GAP_HEIGHT_LIMIT: u16 = 7; pub const TIME_LABEL_HEIGHT_LIMIT: u16 = 7; @@ -249,7 +245,7 @@ pub const DEFAULT_BATTERY_LAYOUT: &str = r##" pub const DEFAULT_CONFIG_FILE_PATH: &str = "bottom/bottom.toml"; // Default config file -// FIXME: Update the default config +// FIXME [CHORE]: Update the default config pub const DEFAULT_CONFIG_CONTENT: &str = r##" # This is a default config file for bottom. All of the settings are commented # out by default; if you wish to change them uncomment and modify as you see diff --git a/src/data_conversion.rs b/src/data_conversion.rs index b7e3bb3a..f7e10566 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -2,7 +2,7 @@ //! can actually handle. use crate::Pid; use crate::{ - app::{data_farmer, data_harvester, App, Filter}, + app::{data_farmer, data_harvester, App, Filter, ProcWidgetState}, utils::{self, gen_util::*}, }; use data_harvester::processes::ProcessSorting; @@ -71,6 +71,7 @@ pub struct ConvertedProcessData { #[derive(Clone, Default, Debug)] pub struct ConvertedCpuData { pub cpu_name: String, + pub short_cpu_name: String, /// Tuple is time, value pub cpu_data: Vec, /// Represents the value displayed on the legend. @@ -196,10 +197,24 @@ pub fn convert_cpu_data_points( let mut new_cpu_data = ConvertedCpuData::default(); new_cpu_data.cpu_name = if let Some(cpu_harvest) = current_data.cpu_harvest.get(itx) { - cpu_harvest.cpu_name.to_string() + 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() }; + new_cpu_data.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() + }; cpu_data_vector.push(new_cpu_data); } @@ -216,6 +231,7 @@ pub fn convert_cpu_data_points( let mut extended_vec = vec![ConvertedCpuData { cpu_name: "All".to_string(), + short_cpu_name: "All".to_string(), cpu_data: vec![], legend_value: String::new(), }]; @@ -413,7 +429,7 @@ pub enum ProcessNamingType { pub fn convert_process_data( current_data: &data_farmer::DataCollection, ) -> Vec { - // FIXME: Thread highlighting and hiding support + // TODO [THREAD]: Thread highlighting and hiding support // For macOS see https://github.com/hishamhm/htop/pull/848/files current_data @@ -471,6 +487,7 @@ pub fn tree_process_data( sort_type: &ProcessSorting, is_sort_descending: bool, ) -> Vec { // TODO: [TREE] Allow for collapsing entries. + // TODO: [TREE] Option to sort usage by total branch usage or individual value usage? // Let's first build up a (really terrible) parent -> child mapping... // At the same time, let's make a mapping of PID -> process data! @@ -565,18 +582,14 @@ pub fn tree_process_data( pid_process_mapping: &HashMap, ) { // Sorting is special for tree data. So, by default, things are "sorted" - // via the DFS, except for (at least Unix) PID 1 and 2, which are in that order. - // Otherwise, since this is DFS of the scanned PIDs (which are in order), you actually - // get a REVERSE order --- so, you get higher PIDs earlier than lower ones. - // But this is a tree. So, you'll get a bit of a combination, but the general idea - // is that in a tree level, it's descending order, except, again, for the first layer. - // This is how htop does it by default. + // via the DFS. Otherwise, since this is DFS of the scanned PIDs (which are in order), + // you actually get a REVERSE order --- so, you get higher PIDs earlier than lower ones. // // So how do we "sort"? The current idea is that: // - We sort *per-level*. Say, I want to sort by CPU. The "first level" is sorted // by CPU in terms of its usage. All its direct children are sorted by CPU // with *their* siblings. Etc. - // - The default is thus PIDs in reverse order (descending). We set it to this when + // - The default is thus PIDs in ascending order. We set it to this when // we first enable the mode. // So first, let's look at the children... (post-order again) @@ -701,8 +714,7 @@ pub fn tree_process_data( } /// A DFS traversal to correctly build the prefix lines (the pretty '├' and '─' lines) and - /// the correct order to the PID tree as a vector (DFS is the default order htop seems to use - /// so we're shamelessly copying that). + /// the correct order to the PID tree as a vector. fn build_explored_pids( current_pid: Pid, parent_child_mapping: &HashMap>, prev_drawn_lines: &str, @@ -801,6 +813,65 @@ pub fn tree_process_data( .collect::>() } +pub fn stringify_process_data( + proc_widget_state: &ProcWidgetState, finalized_process_data: &[ConvertedProcessData], +) -> Vec<(Vec<(String, Option)>, bool)> { + let is_proc_widget_grouped = proc_widget_state.is_grouped; + let is_using_command = proc_widget_state.is_using_command; + let is_tree = proc_widget_state.is_tree_mode; + let mem_enabled = proc_widget_state.columns.is_enabled(&ProcessSorting::Mem); + + finalized_process_data + .iter() + .map(|process| { + ( + vec![ + ( + if is_proc_widget_grouped { + process.group_pids.len().to_string() + } else { + process.pid.to_string() + }, + None, + ), + ( + if is_tree { + if let Some(prefix) = &process.process_description_prefix { + prefix.clone() + } else { + String::default() + } + } else if is_using_command { + process.command.clone() + } else { + process.name.clone() + }, + None, + ), + (format!("{:.1}%", process.cpu_percent_usage), None), + ( + if mem_enabled { + format!("{:.0}{}", process.mem_usage_str.0, process.mem_usage_str.1) + } else { + format!("{:.1}%", process.mem_percent_usage) + }, + None, + ), + (process.read_per_sec.clone(), None), + (process.write_per_sec.clone(), None), + (process.total_read.clone(), None), + (process.total_write.clone(), None), + ( + process.process_state.clone(), + Some(process.process_char.to_string()), + ), + ], + process.is_disabled_entry, + ) + }) + .collect() +} + pub fn group_process_data( single_process_data: &[ConvertedProcessData], is_using_command: bool, ) -> Vec { @@ -878,9 +949,9 @@ pub fn group_process_data( wps_f64: p.write_per_sec, tr_f64: p.total_read, tw_f64: p.total_write, - process_state: p.process_state, // TODO: What the heck + process_state: p.process_state, process_description_prefix: None, - process_char: char::default(), // TODO: What the heck + process_char: char::default(), is_disabled_entry: false, } }) diff --git a/src/lib.rs b/src/lib.rs index ee6f2cfc..5793ed8a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -483,7 +483,6 @@ fn update_final_process_list(app: &mut App, widget_id: u64) { if !app.is_frozen { app.canvas_data.single_process_data = convert_process_data(&app.data_collection); } - let process_filter = app.get_process_filter(widget_id); let filtered_process_data: Vec = if is_tree { app.canvas_data @@ -547,6 +546,10 @@ fn update_final_process_list(app: &mut App, widget_id: u64) { proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down; } + app.canvas_data.stringified_process_data_map.insert( + widget_id, + stringify_process_data(&proc_widget_state, &finalized_process_data), + ); app.canvas_data .finalized_process_data_map .insert(widget_id, finalized_process_data);