From 3260ff4663baed9b2186ee130306dc71e5d521fd Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Sat, 28 Nov 2020 15:37:06 -0500 Subject: [PATCH] feature: Add scroll indicator to keep track of table position in widgets. (#333) Adds the option to enable an "out of" indicator for scrollable table widgets (using --show_table_scroll_position). --- .github/workflows/deployment.yml | 2 + CHANGELOG.md | 10 +++++ README.md | 52 ++++++++++++------------ src/app.rs | 1 + src/app/layout_manager.rs | 4 +- src/canvas/widgets/disk_table.rs | 56 +++++++++++++++++++++----- src/canvas/widgets/process_table.rs | 62 +++++++++++++++++++++++++---- src/canvas/widgets/temp_table.rs | 59 +++++++++++++++++++++------ src/clap.rs | 8 ++++ src/options.rs | 15 +++++++ 10 files changed, 214 insertions(+), 55 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index d6dbb4b1..2ea2bd25 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -203,6 +203,8 @@ jobs: run: | strip target/${{ matrix.triple.target }}/release/btm + # TODO: Strip ARM + - name: Bundle release and completion (Windows) if: matrix.triple.os == 'windows-2019' shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index cf257620..a5b0cebb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.0] - Unreleased + +## Features + +- [#333](https://github.com/ClementTsang/bottom/pull/333): Adds an "out of" indicator that can be enabled using `--show_table_scroll_position` to help keep track of scrolled position. + +## Changes + +## Bug Fixes + ## [0.5.3] - 2020-11-26 ## Bug Fixes diff --git a/README.md b/README.md index 3c3533cb..2b8ffe00 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,7 @@ Use `btm --help` for more information. --mem_as_value Defaults to showing process memory usage by value. -r, --rate Sets a refresh rate in ms. -R, --regex Enables regex by default. + --show_table_scroll_position Shows the scroll position tracker in table widgets -d, --time_delta The amount in ms changed upon zooming. -T, --tree Defaults to showing the process widget in tree mode. --use_old_network_legend DEPRECATED - uses the older network legend. @@ -528,31 +529,32 @@ The following options can be set under `[flags]` to achieve the same effect as p These are the following supported flag config values, which correspond to the flag of the same name described in [Flags](#flags): -| Field | Type | -| ------------------------ | ------------------------------------------------------------------------------------- | -| `hide_avg_cpu` | Boolean | -| `dot_marker` | Boolean | -| `left_legend` | Boolean | -| `current_usage` | Boolean | -| `group_processes` | Boolean | -| `case_sensitive` | Boolean | -| `whole_word` | Boolean | -| `regex` | Boolean | -| `show_disabled_data` | Boolean | -| `basic` | Boolean | -| `hide_table_count` | Boolean | -| `use_old_network_legend` | Boolean | -| `battery` | Boolean | -| `rate` | Unsigned Int (represents milliseconds) | -| `default_time_value` | Unsigned Int (represents milliseconds) | -| `time_delta` | Unsigned Int (represents milliseconds) | -| `temperature_type` | String (one of ["k", "f", "c", "kelvin", "fahrenheit", "celsius"]) | -| `default_widget_type` | String (one of ["cpu", "proc", "net", "temp", "mem", "disk"], same as layout options) | -| `default_widget_count` | Unsigned Int (represents which `default_widget_type`) | -| `disable_click` | Boolean | -| `color` | String (one of ["default", "default-light", "gruvbox", "gruvbox-light"]) | -| `mem_as_value` | Boolean | -| `tree` | Boolean | +| Field | Type | +| ---------------------------- | ------------------------------------------------------------------------------------- | +| `hide_avg_cpu` | Boolean | +| `dot_marker` | Boolean | +| `left_legend` | Boolean | +| `current_usage` | Boolean | +| `group_processes` | Boolean | +| `case_sensitive` | Boolean | +| `whole_word` | Boolean | +| `regex` | Boolean | +| `show_disabled_data` | Boolean | +| `basic` | Boolean | +| `hide_table_count` | Boolean | +| `use_old_network_legend` | Boolean | +| `battery` | Boolean | +| `rate` | Unsigned Int (represents milliseconds) | +| `default_time_value` | Unsigned Int (represents milliseconds) | +| `time_delta` | Unsigned Int (represents milliseconds) | +| `temperature_type` | String (one of ["k", "f", "c", "kelvin", "fahrenheit", "celsius"]) | +| `default_widget_type` | String (one of ["cpu", "proc", "net", "temp", "mem", "disk"], same as layout options) | +| `default_widget_count` | Unsigned Int (represents which `default_widget_type`) | +| `disable_click` | Boolean | +| `color` | String (one of ["default", "default-light", "gruvbox", "gruvbox-light"]) | +| `mem_as_value` | Boolean | +| `tree` | Boolean | +| `show_table_scroll_position` | Boolean | #### Theming diff --git a/src/app.rs b/src/app.rs index 1d2d3a50..d66ce89c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -48,6 +48,7 @@ pub struct AppConfigFields { pub table_gap: u16, pub disable_click: bool, pub no_write: bool, + pub show_table_scroll_position: bool, } /// For filtering out information diff --git a/src/app/layout_manager.rs b/src/app/layout_manager.rs index c856c42d..05e73822 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -868,11 +868,11 @@ pub struct BottomWidget { #[builder(default = None)] pub parent_reflector: Option<(WidgetDirection, u64)>, - /// Top left corner when drawn, for mouse click detection + /// Top left corner when drawn, for mouse click detection. (x, y) #[builder(default = None)] pub top_left_corner: Option<(u16, u16)>, - /// Bottom right corner when drawn, for mouse click detection + /// Bottom right corner when drawn, for mouse click detection. (x, y) #[builder(default = None)] pub bottom_right_corner: Option<(u16, u16)>, } diff --git a/src/canvas/widgets/disk_table.rs b/src/canvas/widgets/disk_table.rs index 0fee787a..14cb4f60 100644 --- a/src/canvas/widgets/disk_table.rs +++ b/src/canvas/widgets/disk_table.rs @@ -42,7 +42,6 @@ impl DiskTableWidget for Painter { ) { 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 { 0 } else { @@ -65,7 +64,7 @@ impl DiskTableWidget for Painter { .current_scroll_position .saturating_sub(start_position), )); - let sliced_vec = &disk_data[start_position..]; + let sliced_vec = &app_state.canvas_data.disk_data[start_position..]; // Calculate widths let hard_widths = [None, None, Some(4), Some(6), Some(6), Some(7), Some(7)]; @@ -154,7 +153,6 @@ impl DiskTableWidget for Painter { 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_style, highlight_style) = if is_on_widget { ( self.colours.highlighted_border_style, @@ -164,22 +162,62 @@ impl DiskTableWidget for Painter { (self.colours.border_style, self.colours.text_style) }; + let title_base = if app_state.app_config_fields.show_table_scroll_position { + let title_string = format!( + " Disk ({} of {}) ", + disk_widget_state + .scroll_state + .current_scroll_position + .saturating_add(1), + app_state.canvas_data.disk_data.len() + ); + + if title_string.len() <= draw_loc.width as usize { + title_string + } else { + " Disk ".to_string() + } + } else { + " Disk ".to_string() + }; + let title = if app_state.is_expanded { - const TITLE_BASE: &str = " Disk ── Esc to go back "; + const ESCAPE_ENDING: &str = "── Esc to go back "; + + let (chosen_title_base, expanded_title_base) = { + let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING); + + if temp_title_base.len() > draw_loc.width as usize { + ( + " Disk ".to_string(), + format!("{}{}", " Disk ".to_string(), ESCAPE_ENDING), + ) + } else { + (title_base, temp_title_base) + } + }; + Spans::from(vec![ - Span::styled(" Disk ", self.colours.widget_title_style), + Span::styled(chosen_title_base, self.colours.widget_title_style), Span::styled( format!( "─{}─ Esc to go back ", - "─".repeat(usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2 - )) + "─".repeat( + usize::from(draw_loc.width).saturating_sub( + UnicodeSegmentation::graphemes( + expanded_title_base.as_str(), + true + ) + .count() + + 2 + ) + ) ), border_style, ), ]) } else { - Spans::from(Span::styled(" Disk ", self.colours.widget_title_style)) + Spans::from(Span::styled(title_base, self.colours.widget_title_style)) }; let disk_block = if draw_border { diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index e9e9ec40..338118b4 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -207,6 +207,33 @@ impl ProcessTableWidget for Painter { (self.colours.border_style, self.colours.text_style) }; + let title_base = if app_state.app_config_fields.show_table_scroll_position { + if let Some(finalized_process_data) = app_state + .canvas_data + .finalized_process_data_map + .get(&widget_id) + { + let title = format!( + " Processes ({} of {}) ", + proc_widget_state + .scroll_state + .current_scroll_position + .saturating_add(1), + finalized_process_data.len() + ); + + if title.len() <= draw_loc.width as usize { + title + } else { + " Processes ".to_string() + } + } else { + " Processes ".to_string() + } + } else { + " Processes ".to_string() + }; + let title = if app_state.is_expanded && !proc_widget_state .process_search_state @@ -214,21 +241,42 @@ impl ProcessTableWidget for Painter { .is_enabled && !proc_widget_state.is_sort_open { - const TITLE_BASE: &str = " Processes ── Esc to go back "; + const ESCAPE_ENDING: &str = "── Esc to go back "; + + let (chosen_title_base, expanded_title_base) = { + let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING); + + if temp_title_base.len() > draw_loc.width as usize { + ( + " Processes ".to_string(), + format!("{}{}", " Processes ".to_string(), ESCAPE_ENDING), + ) + } else { + (title_base, temp_title_base) + } + }; + Spans::from(vec![ - Span::styled(" Processes ", self.colours.widget_title_style), + Span::styled(chosen_title_base, self.colours.widget_title_style), Span::styled( format!( "─{}─ Esc to go back ", - "─".repeat(usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2 - )) + "─".repeat( + usize::from(draw_loc.width).saturating_sub( + UnicodeSegmentation::graphemes( + expanded_title_base.as_str(), + true + ) + .count() + + 2 + ) + ) ), border_style, ), ]) } else { - Spans::from(Span::styled(" Processes ", self.colours.widget_title_style)) + Spans::from(Span::styled(title_base, self.colours.widget_title_style)) }; let process_block = if draw_border { @@ -281,6 +329,7 @@ impl ProcessTableWidget for Painter { disabled, ) }); + let proc_table_state = &mut proc_widget_state.scroll_state.table_state; proc_table_state.select(Some( proc_widget_state @@ -434,7 +483,6 @@ impl ProcessTableWidget for Painter { } }); - // FIXME: gotop's "x out of y" thing is really nice to help keep track of the scroll position. Add to everything? f.render_stateful_widget( Table::new(process_headers.iter(), process_rows) .block(process_block) diff --git a/src/canvas/widgets/temp_table.rs b/src/canvas/widgets/temp_table.rs index 07b5d730..f4e6a9c1 100644 --- a/src/canvas/widgets/temp_table.rs +++ b/src/canvas/widgets/temp_table.rs @@ -42,8 +42,6 @@ impl TempTableWidget for Painter { ) { 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; - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { 0 } else { @@ -66,7 +64,7 @@ impl TempTableWidget for Painter { .current_scroll_position .saturating_sub(start_position), )); - let sliced_vec = &temp_sensor_data[start_position..]; + let sliced_vec = &app_state.canvas_data.temp_sensor_data[start_position..]; // Calculate widths let hard_widths = [None, None]; @@ -153,25 +151,62 @@ impl TempTableWidget for Painter { (self.colours.border_style, self.colours.text_style) }; + let title_base = if app_state.app_config_fields.show_table_scroll_position { + let title_string = format!( + " Temperatures ({} of {}) ", + temp_widget_state + .scroll_state + .current_scroll_position + .saturating_add(1), + app_state.canvas_data.temp_sensor_data.len() + ); + + if title_string.len() <= draw_loc.width as usize { + title_string + } else { + " Temperatures ".to_string() + } + } else { + " Temperatures ".to_string() + }; + let title = if app_state.is_expanded { - const TITLE_BASE: &str = " Temperatures ── Esc to go back "; + const ESCAPE_ENDING: &str = "── Esc to go back "; + + let (chosen_title_base, expanded_title_base) = { + let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING); + + if temp_title_base.len() > draw_loc.width as usize { + ( + " Temperatures ".to_string(), + format!("{}{}", " Temperatures ".to_string(), ESCAPE_ENDING), + ) + } else { + (title_base, temp_title_base) + } + }; + Spans::from(vec![ - Span::styled(" Temperatures ", self.colours.widget_title_style), + Span::styled(chosen_title_base, self.colours.widget_title_style), Span::styled( format!( "─{}─ Esc to go back ", - "─".repeat(usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2 - )) + "─".repeat( + usize::from(draw_loc.width).saturating_sub( + UnicodeSegmentation::graphemes( + expanded_title_base.as_str(), + true + ) + .count() + + 2 + ) + ) ), border_style, ), ]) } else { - Spans::from(Span::styled( - " Temperatures ", - self.colours.widget_title_style, - )) + Spans::from(Span::styled(title_base, self.colours.widget_title_style)) }; let temp_block = if draw_border { diff --git a/src/clap.rs b/src/clap.rs index f4b9222b..50939f5a 100644 --- a/src/clap.rs +++ b/src/clap.rs @@ -135,6 +135,13 @@ Hides the spacing between table headers and entries.\n\n", "\ Completely hides the time scaling from being shown.\n\n", ); + let show_table_scroll_position = Arg::with_name("show_table_scroll_position") + .long("show_table_scroll_position") + .help("Shows the scroll position tracker in table widgets") + .long_help( + "\ + Shows the list scroll position tracker in the widget title for table widgets.\n\n", + ); let left_legend = Arg::with_name("left_legend") .short("l") .long("left_legend") @@ -366,6 +373,7 @@ Defaults to showing the process widget in tree mode.\n\n", .arg(hide_avg_cpu) .arg(hide_table_gap) .arg(hide_time) + .arg(show_table_scroll_position) .arg(left_legend) // .arg(no_write) .arg(rate) diff --git a/src/options.rs b/src/options.rs index 53ccd223..cfdeca88 100644 --- a/src/options.rs +++ b/src/options.rs @@ -147,6 +147,9 @@ pub struct ConfigFlags { #[builder(default, setter(strip_option))] pub tree: Option, + + #[builder(default, setter(strip_option))] + show_table_scroll_position: Option, } #[derive(Clone, Default, Debug, Deserialize, Serialize)] @@ -388,6 +391,7 @@ pub fn build_app( 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), }; let used_widgets = UsedWidgets { @@ -972,3 +976,14 @@ fn get_is_default_tree(matches: &clap::ArgMatches<'static>, config: &Config) -> } false } + +fn get_show_table_scroll_position(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { + if matches.is_present("show_table_scroll_position") { + return true; + } else if let Some(flags) = &config.flags { + if let Some(show_table_scroll_position) = flags.show_table_scroll_position { + return show_table_scroll_position; + } + } + false +}