mirror of
https://github.com/ClementTsang/bottom.git
synced 2026-05-03 21:40:32 +00:00
feature: basic scrollbar support for tables and dialogs (#2046)
Adds scrollbars to dialogs (help, process kill) and optionally tables.
This commit is contained in:
+3
-2
@@ -31,10 +31,11 @@ That said, these are more guidelines rather than hard rules, though the project
|
||||
|
||||
- [#1938](https://github.com/ClementTsang/bottom/pull/1938), [#1980](https://github.com/ClementTsang/bottom/pull/1980): Report average packet size and packet rate.
|
||||
- [#2003](https://github.com/ClementTsang/bottom/pull/2003): Configurable default sort column for temperature and disk table widgets.
|
||||
- [#1979](https://github.com/ClementTsang/bottom/pull/1979): Add global `bg_color` option to set widget background colour.
|
||||
- [#2039](https://github.com/ClementTsang/bottom/pull/2039): Add support for drawing a line separator between the column headers and data.
|
||||
- [#1979](https://github.com/ClementTsang/bottom/pull/1979): Add a global `bg_color` config option to set widget background colour.
|
||||
- [#2039](https://github.com/ClementTsang/bottom/pull/2039): Add a config option for drawing a line separator (`table_gap`) between the column headers and data.
|
||||
- [#1948](https://github.com/ClementTsang/bottom/pull/1948): Add support for both an `!=` operator and `!` negation prefixes in query searches.
|
||||
- [#2045](https://github.com/ClementTsang/bottom/pull/2045): Add support for showing a decimal place for CPU usage
|
||||
- [#2046](https://github.com/ClementTsang/bottom/pull/2046): Add a `show_table_scroll_bar` config option to show a scroll bar on table widgets.
|
||||
|
||||
### Changes
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ each time:
|
||||
| `process_memory_as_value` | Boolean | Defaults to showing process memory usage by value. |
|
||||
| `tree` | Boolean | Defaults to showing the process widget in tree mode. |
|
||||
| `show_table_scroll_position` | Boolean | Shows the scroll position tracker in table widgets. |
|
||||
| `show_table_scroll_bar` | Boolean | Shows a scroll bar on the right edge of table widgets. |
|
||||
| `process_command` | Boolean | Show processes as their commands by default. |
|
||||
| `disable_advanced_kill` | Boolean | Disable being able to send signals to processes on supported Unix-like systems. Only available on Linux, macOS, and FreeBSD. |
|
||||
| `read_only` | Boolean | Prevents performing any actions that affect the system (e.g. stopping processes). |
|
||||
|
||||
@@ -92,6 +92,9 @@
|
||||
# Shows an indicator in table widgets tracking where in the list you are.
|
||||
#show_table_scroll_position = false
|
||||
|
||||
# Show a scroll bar on the right edge of table widgets.
|
||||
#show_table_scroll_bar = false
|
||||
|
||||
# Show processes as their commands by default in the process widget.
|
||||
#process_command = false
|
||||
|
||||
|
||||
@@ -519,6 +519,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"show_table_scroll_bar": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"show_table_scroll_position": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
||||
@@ -60,6 +60,7 @@ pub struct AppConfigFields {
|
||||
pub enable_gpu: bool,
|
||||
pub enable_cache_memory: bool,
|
||||
pub show_table_scroll_position: bool,
|
||||
pub show_table_scroll_bar: bool,
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
|
||||
pub is_advanced_kill: bool,
|
||||
pub is_read_only: bool,
|
||||
|
||||
@@ -192,6 +192,7 @@ mod test {
|
||||
left_to_right: false,
|
||||
is_basic: false,
|
||||
show_table_scroll_position: true,
|
||||
show_table_scroll_bar: false,
|
||||
show_current_entry_when_unfocused: false,
|
||||
};
|
||||
let styling = DataTableStyling::default();
|
||||
|
||||
@@ -18,7 +18,11 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
app::layout_manager::BottomWidget,
|
||||
canvas::{Painter, drawing_utils::widget_block},
|
||||
canvas::{
|
||||
Painter,
|
||||
components::scroll_bar::{ScrollBarArgs, draw_scroll_bar},
|
||||
drawing_utils::widget_block,
|
||||
},
|
||||
constants::TABLE_GAP_HEIGHT_LIMIT,
|
||||
options::config::flags::TableGap,
|
||||
utils::strings::truncate_to_text,
|
||||
@@ -146,9 +150,14 @@ where
|
||||
.split(draw_loc)[0];
|
||||
|
||||
let mut block = self.block(draw_info, self.data.len());
|
||||
if self.props.is_basic && !draw_info.is_on_widget() {
|
||||
block = block.padding(Padding::horizontal(1))
|
||||
}
|
||||
|
||||
let horizontal_padding = if self.props.is_basic && !draw_info.is_on_widget() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let right_padding = horizontal_padding + u16::from(self.props.show_table_scroll_bar);
|
||||
block = block.padding(Padding::new(horizontal_padding, right_padding, 0, 0));
|
||||
|
||||
let (inner_width, inner_height) = {
|
||||
let inner_rect = block.inner(margined_draw_loc);
|
||||
@@ -208,10 +217,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let num_rows: usize = inner_height
|
||||
.saturating_sub(table_gap + header_height)
|
||||
.into();
|
||||
|
||||
let columns = &self.columns;
|
||||
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;
|
||||
@@ -276,6 +287,26 @@ where
|
||||
let table_state = &mut self.state.table_state;
|
||||
f.render_stateful_widget(widget, margined_draw_loc, table_state);
|
||||
|
||||
if self.props.show_table_scroll_bar {
|
||||
let scrollbar_area = Rect {
|
||||
x: self.state.inner_rect.x + self.state.inner_rect.width,
|
||||
y: self.state.inner_rect.y + header_height + table_gap,
|
||||
width: 1,
|
||||
height: num_rows as u16,
|
||||
};
|
||||
|
||||
draw_scroll_bar(
|
||||
f,
|
||||
scrollbar_area,
|
||||
ScrollBarArgs {
|
||||
content_length: self.data.len(),
|
||||
viewport_length: num_rows,
|
||||
position: self.state.current_index,
|
||||
style: self.styling.text_style,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if table_gap > 0 && table_gap_setting == TableGap::Line && show_header {
|
||||
let y = self.state.inner_rect.y + header_height;
|
||||
let buf = f.buffer_mut();
|
||||
|
||||
@@ -18,6 +18,9 @@ pub struct DataTableProps {
|
||||
/// Whether to show the table scroll position.
|
||||
pub show_table_scroll_position: bool,
|
||||
|
||||
/// Whether to show a scroll bar on the right edge of the table.
|
||||
pub show_table_scroll_bar: bool,
|
||||
|
||||
/// Whether to show the current entry as highlighted when not focused.
|
||||
pub show_current_entry_when_unfocused: bool,
|
||||
}
|
||||
|
||||
@@ -399,6 +399,7 @@ mod test {
|
||||
left_to_right: false,
|
||||
is_basic: false,
|
||||
show_table_scroll_position: true,
|
||||
show_table_scroll_bar: false,
|
||||
show_current_entry_when_unfocused: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
pub mod data_table;
|
||||
pub mod pipe_gauge;
|
||||
pub mod scroll_bar;
|
||||
pub mod time_graph;
|
||||
pub mod widget_carousel;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//! A shared helper for drawing a vertical scroll bar.
|
||||
|
||||
use tui::{
|
||||
Frame,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols::{self, scrollbar},
|
||||
widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
};
|
||||
|
||||
/// Arguments for [`draw_scroll_bar`].
|
||||
pub struct ScrollBarArgs {
|
||||
/// Total number of items in the content.
|
||||
pub content_length: usize,
|
||||
/// Number of items that can be seen in the viewport.
|
||||
pub viewport_length: usize,
|
||||
/// Current scroll position in the list of items.
|
||||
pub position: usize,
|
||||
/// Style to be applied to the scrollbar.
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
/// Returns a [`Rect`] for a vertical scroll bar drawn just inside the right
|
||||
/// border of a dialog block.
|
||||
pub fn dialog_scroll_bar_area(block_area: Rect) -> Rect {
|
||||
Rect {
|
||||
x: block_area.x + block_area.width.saturating_sub(2),
|
||||
y: block_area.y + 1,
|
||||
width: 1,
|
||||
height: block_area.height.saturating_sub(2),
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a vertical scroll bar in `area`.
|
||||
pub fn draw_scroll_bar(f: &mut Frame<'_>, area: Rect, args: ScrollBarArgs) {
|
||||
if args.content_length <= args.viewport_length || area.width == 0 || area.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
const SYMBOLS: scrollbar::Set<'_> = scrollbar::Set {
|
||||
track: "",
|
||||
thumb: symbols::block::FULL,
|
||||
begin: "▲",
|
||||
end: "▼",
|
||||
};
|
||||
|
||||
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||
.style(args.style)
|
||||
.symbols(SYMBOLS);
|
||||
|
||||
let mut state = ScrollbarState::new(args.content_length)
|
||||
.position(args.position)
|
||||
.viewport_content_length(args.viewport_length);
|
||||
|
||||
f.render_stateful_widget(scrollbar, area, &mut state);
|
||||
}
|
||||
@@ -4,13 +4,17 @@ use tui::{
|
||||
Frame,
|
||||
layout::{Alignment, Rect},
|
||||
text::{Line, Span},
|
||||
widgets::{Paragraph, Wrap},
|
||||
widgets::{Padding, Paragraph, Wrap},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
canvas::{Painter, drawing_utils::dialog_block},
|
||||
canvas::{
|
||||
Painter,
|
||||
components::scroll_bar::{ScrollBarArgs, dialog_scroll_bar_area, draw_scroll_bar},
|
||||
drawing_utils::dialog_block,
|
||||
},
|
||||
constants::{self, HELP_TEXT},
|
||||
};
|
||||
|
||||
@@ -41,20 +45,23 @@ impl Painter {
|
||||
pub fn draw_help_dialog(&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect) {
|
||||
let styled_help_text = self.help_text_lines();
|
||||
|
||||
// Reserve one column on the right for the scroll bar.
|
||||
let block = dialog_block(self.styles.border_type, self.styles.border_style)
|
||||
.title_top(Line::styled(" Help ", self.styles.widget_title_style))
|
||||
.title_top(
|
||||
Line::styled(" Esc to close ", self.styles.widget_title_style).right_aligned(),
|
||||
);
|
||||
)
|
||||
.padding(Padding::right(1));
|
||||
|
||||
if app_state.should_get_widget_bounds() {
|
||||
// We must also recalculate how many lines are wrapping to properly get
|
||||
// scrolling to work on small terminal sizes... oh joy.
|
||||
|
||||
app_state.help_dialog_state.height = block.inner(draw_loc).height;
|
||||
let inner = block.inner(draw_loc);
|
||||
app_state.help_dialog_state.height = inner.height;
|
||||
|
||||
let mut overflow_buffer = 0;
|
||||
let paragraph_width = max(draw_loc.width.saturating_sub(2), 1);
|
||||
let paragraph_width = max(inner.width, 1);
|
||||
let mut prev_section_len = 0;
|
||||
|
||||
constants::HELP_TEXT
|
||||
@@ -113,5 +120,29 @@ impl Painter {
|
||||
)),
|
||||
draw_loc,
|
||||
);
|
||||
|
||||
let scrollbar_area = dialog_scroll_bar_area(draw_loc);
|
||||
let content_length = app_state
|
||||
.help_dialog_state
|
||||
.scroll_state
|
||||
.max_scroll_index
|
||||
.into();
|
||||
let viewport_length = app_state.help_dialog_state.height.into();
|
||||
let position = app_state
|
||||
.help_dialog_state
|
||||
.scroll_state
|
||||
.current_scroll_index
|
||||
.into();
|
||||
|
||||
draw_scroll_bar(
|
||||
f,
|
||||
scrollbar_area,
|
||||
ScrollBarArgs {
|
||||
content_length,
|
||||
viewport_length,
|
||||
position,
|
||||
style: self.styles.text_style,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ use tui::{
|
||||
widgets::{Paragraph, Wrap},
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
|
||||
use crate::canvas::components::scroll_bar::{
|
||||
ScrollBarArgs, dialog_scroll_bar_area, draw_scroll_bar,
|
||||
};
|
||||
use crate::{
|
||||
canvas::drawing_utils::dialog_block, collection::processes::Pid, options::config::style::Styles,
|
||||
};
|
||||
@@ -747,13 +751,25 @@ impl ProcessKillDialog {
|
||||
|
||||
max as u16
|
||||
};
|
||||
let [button_draw_area] =
|
||||
|
||||
let [list_area] =
|
||||
Layout::horizontal([Constraint::Length(LONGEST_SIGNAL_TEXT_LENGTH)])
|
||||
.flex(Flex::Center)
|
||||
.areas(button_draw_area);
|
||||
|
||||
*last_button_draw_area = button_draw_area;
|
||||
f.render_stateful_widget(buttons, button_draw_area, state);
|
||||
*last_button_draw_area = list_area;
|
||||
f.render_stateful_widget(buttons, list_area, state);
|
||||
|
||||
draw_scroll_bar(
|
||||
f,
|
||||
dialog_scroll_bar_area(draw_area),
|
||||
ScrollBarArgs {
|
||||
content_length: SIGNAL_TEXT.len(),
|
||||
viewport_length: list_area.height as usize,
|
||||
position: state.selected().unwrap_or(0),
|
||||
style: styles.text_style,
|
||||
},
|
||||
);
|
||||
}
|
||||
ButtonState::Simple {
|
||||
yes,
|
||||
|
||||
@@ -340,6 +340,9 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
|
||||
# Shows an indicator in table widgets tracking where in the list you are.
|
||||
#show_table_scroll_position = false
|
||||
|
||||
# Show a scroll bar on the right edge of table widgets.
|
||||
#show_table_scroll_bar = false
|
||||
|
||||
# Show processes as their commands by default in the process widget.
|
||||
#process_command = false
|
||||
|
||||
|
||||
@@ -325,6 +325,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
args.general,
|
||||
config
|
||||
),
|
||||
show_table_scroll_bar: get_show_table_scroll_bar(config),
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
|
||||
is_advanced_kill,
|
||||
is_read_only,
|
||||
@@ -785,6 +786,14 @@ fn get_table_gap(config: &Config) -> TableGap {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_show_table_scroll_bar(config: &Config) -> bool {
|
||||
config
|
||||
.flags
|
||||
.as_ref()
|
||||
.and_then(|flags| flags.show_table_scroll_bar)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn get_dedicated_avg_row(config: &Config) -> bool {
|
||||
config
|
||||
.flags
|
||||
|
||||
@@ -58,6 +58,7 @@ pub(crate) struct GeneralConfig {
|
||||
pub(crate) process_memory_as_value: Option<bool>,
|
||||
pub(crate) tree: Option<bool>,
|
||||
pub(crate) show_table_scroll_position: Option<bool>,
|
||||
pub(crate) show_table_scroll_bar: Option<bool>,
|
||||
pub(crate) process_command: Option<bool>,
|
||||
// This does nothing on Windows, but we leave it enabled to make the config file consistent
|
||||
// across platforms.
|
||||
|
||||
@@ -153,6 +153,7 @@ impl CpuWidgetState {
|
||||
left_to_right: false,
|
||||
is_basic: false,
|
||||
show_table_scroll_position: false, // TODO: Should this be possible?
|
||||
show_table_scroll_bar: config.show_table_scroll_bar,
|
||||
show_current_entry_when_unfocused: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -326,6 +326,7 @@ impl DiskTableWidget {
|
||||
left_to_right: true,
|
||||
is_basic: config.use_basic_mode,
|
||||
show_table_scroll_position: config.show_table_scroll_position,
|
||||
show_table_scroll_bar: config.show_table_scroll_bar,
|
||||
show_current_entry_when_unfocused: false,
|
||||
},
|
||||
sort_index: match &config.default_disk_sort_column {
|
||||
|
||||
@@ -244,6 +244,7 @@ impl ProcWidgetState {
|
||||
left_to_right: true,
|
||||
is_basic: false,
|
||||
show_table_scroll_position: false,
|
||||
show_table_scroll_bar: false,
|
||||
show_current_entry_when_unfocused: false,
|
||||
};
|
||||
let styling = DataTableStyling::from_palette(palette);
|
||||
@@ -261,6 +262,7 @@ impl ProcWidgetState {
|
||||
left_to_right: true,
|
||||
is_basic: config.use_basic_mode,
|
||||
show_table_scroll_position: config.show_table_scroll_position,
|
||||
show_table_scroll_bar: config.show_table_scroll_bar,
|
||||
show_current_entry_when_unfocused: false,
|
||||
};
|
||||
let props = SortDataTableProps {
|
||||
|
||||
@@ -135,6 +135,7 @@ impl TempWidgetState {
|
||||
left_to_right: false,
|
||||
is_basic: config.use_basic_mode,
|
||||
show_table_scroll_position: config.show_table_scroll_position,
|
||||
show_table_scroll_bar: config.show_table_scroll_bar,
|
||||
show_current_entry_when_unfocused: false,
|
||||
},
|
||||
// This is hard-coded, but there's only two columns so it's fine.
|
||||
|
||||
Reference in New Issue
Block a user