feature: add new table_gap option with a drawn-line option (#2039)

This PR replaces the `hide_table_gap` option with a new `table_gap` option that controls whether a space is drawn, a line is drawn, or nothing is drawn.
This commit is contained in:
Clement Tsang
2026-04-17 04:02:16 -04:00
committed by GitHub
parent f8b4fef9d3
commit ae425d09d8
20 changed files with 166 additions and 52 deletions
+2
View File
@@ -31,10 +31,12 @@ 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): Support drawing a line separator between the column headers and data.
### Changes
- [#2031](https://github.com/ClementTsang/bottom/pull/2031): Tweak display/hiding logic for a graph widget's legend.
- [#2039](https://github.com/ClementTsang/bottom/pull/2039): Replace `hide_table_gap` with `table_gap`.
### Other
@@ -18,7 +18,6 @@ see information on these options by running `btm -h`, or run `btm --help` to dis
| | bottom. |
| `-m`, `--dot_marker` | Uses a dot marker for graphs. |
| `-e`, `--expanded` | Expand the default widget upon starting the app. |
| `--hide_table_gap` | Hides spacing between table headers and entries. |
| `--hide_time` | Hides the time scale from being shown. |
| `-r`, `--rate <TIME>` | Sets how often data is refreshed. |
| `--retention <TIME>` | How far back data will be stored up to. |
@@ -77,6 +76,7 @@ see information on these options by running `btm -h`, or run `btm --help` to dis
| `--network_use_bytes` | Displays the network widget using bytes. |
| `--network_use_binary_prefix` | Displays the network widget with binary prefixes. |
| `--network_use_log` | Displays the network widget with a log scale. |
| `--show_packets` | Displays packet rate and average packet size info. |
| `--use_old_network_legend` | (DEPRECATED) Uses a separate network legend. |
## Battery Options
@@ -41,7 +41,7 @@ each time:
| `show_table_scroll_position` | Boolean | Shows the scroll position tracker in 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). |
| `read_only` | Boolean | Prevents performing any actions that affect the system (e.g. stopping processes). |
| `network_use_binary_prefix` | Boolean | Displays the network widget with binary prefixes. |
| `network_use_bytes` | Boolean | Displays the network widget using bytes. |
| `network_use_log` | Boolean | Displays the network widget with a log scale. |
@@ -53,5 +53,9 @@ each time:
| `network_legend` | String (one of ["none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right"]) | Where to place the legend for the network widget. |
| `average_cpu_row` | Boolean | Moves the average CPU usage entry to its own row when using basic mode. |
| `tree_collapse` | Boolean | Collapse process tree by default. |
| `autohide_time` | Boolean | Temporarily shows the time scale in graphs. |
| `table_gap` | String (one of ["none", "space", "line"]) | Controls the gap between table headers and data rows. Defaults to "space". |
| `disable_keys` | Boolean | Disables keyboard shortcuts, including the ones that stop bottom. |
| `no_write` | Boolean | Disables writing to the config file. |
| `hide_k_threads` | Boolean | Hide kernel threads by default. |
| `free_arc` | Boolean | Subtract freeable ARC from memory. |
| `free_arc` | Boolean | Subtract freeable ARC from memory usage. |
@@ -1,5 +1,13 @@
# Network
## Settings
If you want to change some of the default behaviour of the network widget, you can configure some things in the config file.
| Field | Type | Functionality |
| -------------- | ------- | -------------------------------------------------- |
| `show_packets` | Boolean | Displays packet rate and average packet size info. |
## Filtering Entries
You can filter out what entries to show by configuring `[network.interface_filter]` .
@@ -1,5 +1,13 @@
# Processes
## Settings
If you want to change some of the default behaviour of the processes widget, you can configure some things in the config file.
| Field | Type | Functionality |
| ------------- | ------- | ---------------------------------- |
| `get_threads` | Boolean | Gather process thread information. |
## Columns
You can configure which columns are shown by the process widget by setting the `columns` setting:
+3 -2
View File
@@ -70,8 +70,9 @@
# Use the old network legend style
#use_old_network_legend = false
# Remove space in tables
#hide_table_gap = false
# Controls the gap between table headers and data rows.
# Options: "none", "space" (default), "line"
#table_gap = "space"
# Show the battery widgets
#battery = false
+16 -16
View File
@@ -113,7 +113,7 @@
]
},
"medium_battery_color": {
"description": "The colour of the battery widget bar when the battery between 10% to 50%.",
"description": "The colour of the battery widget bar when the battery between 10% to\n50%.",
"anyOf": [
{
"$ref": "#/$defs/ColorStr"
@@ -426,12 +426,6 @@
"null"
]
},
"hide_table_gap": {
"type": [
"boolean",
"null"
]
},
"hide_time": {
"type": [
"boolean",
@@ -518,18 +512,16 @@
}
]
},
"show_packets": {
"type": [
"boolean",
"null"
]
},
"show_table_scroll_position": {
"type": [
"boolean",
"null"
]
},
"table_gap": {
"$ref": "#/$defs/TableGap",
"default": "space"
},
"temperature_type": {
"type": [
"string",
@@ -652,7 +644,7 @@
]
},
"cache_color": {
"description": "The colour of the cache label and graph line. Does not do anything on Windows.",
"description": "The colour of the cache label and graph line. Does not do anything on\nWindows.",
"anyOf": [
{
"$ref": "#/$defs/ColorStr"
@@ -712,7 +704,7 @@
]
},
"show_packets": {
"description": "Whether to show packets information (packet rate and average packet size).",
"description": "Displays packet rate and average packet size info.",
"type": [
"boolean",
"null"
@@ -843,7 +835,7 @@
]
},
"ProcessesConfig": {
"description": "Process configuration.",
"description": "Process configuration fields.",
"type": "object",
"properties": {
"columns": {
@@ -995,6 +987,14 @@
}
}
},
"TableGap": {
"type": "string",
"enum": [
"none",
"space",
"line"
]
},
"TableStyle": {
"description": "General styling for table widgets.",
"type": "object",
+4 -3
View File
@@ -16,6 +16,7 @@ use crate::{
components::time_graph::LegendPosition, dialogs::process_kill_dialog::ProcessKillDialog,
},
constants,
options::config::flags::TableGap,
utils::data_units::DataUnit,
widgets::{
DiskWidgetColumn, ProcWidgetColumn, ProcWidgetMode, TempWidgetColumn, TreeCollapsed,
@@ -52,7 +53,7 @@ pub struct AppConfigFields {
pub hide_time: bool,
pub autohide_time: bool,
pub use_old_network_legend: bool,
pub table_gap: u16,
pub table_gap: TableGap,
pub disable_click: bool,
pub disable_keys: bool,
pub enable_gpu: bool,
@@ -2494,13 +2495,13 @@ impl App {
{
let height_diff = brc_y - tlc_y;
if height_diff >= constants::TABLE_GAP_HEIGHT_LIMIT {
1 + self.app_config_fields.table_gap
1 + self.app_config_fields.table_gap.height()
} else {
let min_height_for_header = if self.is_drawing_border() { 3 } else { 1 };
u16::from(height_diff > min_height_for_header)
}
} else {
1 + self.app_config_fields.table_gap
1 + self.app_config_fields.table_gap.height()
}
}
+2 -1
View File
@@ -160,6 +160,7 @@ mod test {
use std::{borrow::Cow, num::NonZeroU16};
use super::*;
use crate::options::config::flags::TableGap;
#[derive(Clone, PartialEq, Eq, Debug)]
struct TestType {
@@ -187,7 +188,7 @@ mod test {
let columns = [Column::hard("a", 10), Column::hard("b", 10)];
let props = DataTableProps {
title: Some("test".into()),
table_gap: 1,
table_gap: TableGap::Space,
left_to_right: false,
is_basic: false,
show_table_scroll_position: true,
+33 -1
View File
@@ -7,6 +7,7 @@ use concat_string::concat_string;
use tui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
symbols::line,
text::{Line, Span, Text},
widgets::{Block, Cell, Padding, Row, Table},
};
@@ -19,6 +20,7 @@ use crate::{
app::layout_manager::BottomWidget,
canvas::{Painter, drawing_utils::widget_block},
constants::TABLE_GAP_HEIGHT_LIMIT,
options::config::flags::TableGap,
utils::strings::truncate_to_text,
};
@@ -189,10 +191,11 @@ where
let show_header = inner_height > 1;
let header_height = u16::from(show_header);
let table_gap_setting = self.props.table_gap;
let table_gap = if !show_header || draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
self.props.table_gap
table_gap_setting.height()
};
if !self.data.is_empty() || !self.first_draw {
@@ -272,6 +275,35 @@ where
let table_state = &mut self.state.table_state;
f.render_stateful_widget(widget, margined_draw_loc, table_state);
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();
let border_style = if draw_info.is_on_widget() {
self.styling.highlighted_border_style
} else {
self.styling.border_style
};
for x in (draw_loc.x + 1)..(draw_loc.x + draw_loc.width - 1) {
if let Some(cell) = buf.cell_mut((x, y)) {
cell.set_symbol(line::NORMAL.horizontal);
cell.set_style(border_style);
}
}
if !self.props.is_basic || draw_info.is_on_widget() {
if let Some(cell) = buf.cell_mut((draw_loc.x, y)) {
cell.set_symbol(line::NORMAL.vertical_right);
cell.set_style(border_style);
}
if let Some(cell) = buf.cell_mut((draw_loc.x + draw_loc.width - 1, y)) {
cell.set_symbol(line::NORMAL.vertical_left);
cell.set_style(border_style);
}
}
}
} else {
let table = Table::new(
once(Row::new(Text::raw("No data"))),
+4 -2
View File
@@ -1,11 +1,13 @@
use std::borrow::Cow;
use crate::options::config::flags::TableGap;
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,
/// Controls the gap between the header and rows.
pub table_gap: TableGap,
/// Whether this table determines column widths from left to right.
pub left_to_right: bool,
+2 -1
View File
@@ -334,6 +334,7 @@ where
#[cfg(test)]
mod test {
use super::*;
use crate::options::config::flags::TableGap;
#[derive(Clone, PartialEq, Eq, Debug)]
struct TestType {
@@ -394,7 +395,7 @@ mod test {
let props = {
let inner = DataTableProps {
title: Some("test".into()),
table_gap: 1,
table_gap: TableGap::Space,
left_to_right: false,
is_basic: false,
show_table_scroll_position: true,
+35 -1
View File
@@ -3,6 +3,7 @@ use std::cmp::min;
use tui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
symbols::line,
text::{Line, Span},
widgets::{Cell, Paragraph, Row, Table, Tabs},
};
@@ -13,6 +14,7 @@ use crate::{
canvas::{Painter, drawing_utils::widget_block},
collection::batteries::BatteryState,
constants::*,
options::config::flags::TableGap,
};
/// Calculate how many bars are to be drawn within basic mode's components.
@@ -40,10 +42,11 @@ impl Painter {
} else {
self.styles.border_style
};
let table_gap_setting = app_state.app_config_fields.table_gap;
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
table_gap_setting.height()
};
let block = {
@@ -208,6 +211,8 @@ impl Painter {
Row::default()
};
let block_inner = block.inner(margined_draw_loc);
// Draw bar
f.render_widget(
Table::new(battery_charge_rows, [Constraint::Percentage(100)])
@@ -226,6 +231,35 @@ impl Painter {
.header(header),
margined_draw_loc,
);
if table_gap > 0 && table_gap_setting == TableGap::Line && battery_harvest.len() > 1
{
let y = block_inner.y + 1;
let buf = f.buffer_mut();
for x in (margined_draw_loc.x + 1)
..(margined_draw_loc.x + margined_draw_loc.width - 1)
{
if let Some(cell) = buf.cell_mut((x, y)) {
cell.set_symbol(line::NORMAL.horizontal);
cell.set_style(border_style);
}
}
if !is_basic || is_selected {
if let Some(cell) = buf.cell_mut((margined_draw_loc.x, y)) {
cell.set_symbol(line::NORMAL.vertical_right);
cell.set_style(border_style);
}
if let Some(cell) =
buf.cell_mut((margined_draw_loc.x + margined_draw_loc.width - 1, y))
{
cell.set_symbol(line::NORMAL.vertical_left);
cell.set_style(border_style);
}
}
}
} else {
let mut contents = vec![Line::default(); table_gap.into()];
+3 -2
View File
@@ -316,8 +316,9 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
# Use the old network legend style
#use_old_network_legend = false
# Remove space in tables
#hide_table_gap = false
# Controls the gap between table headers and data rows.
# Options: "none", "space" (default), "line"
#table_gap = "space"
# Show the battery widgets
#battery = false
+10 -2
View File
@@ -28,7 +28,7 @@ use starship_battery::Manager;
use self::{
args::BottomArgs,
config::{IgnoreList, StringOrNum, layout::Row},
config::{IgnoreList, StringOrNum, flags::TableGap, layout::Row},
};
use crate::{
app::{filter::Filter, layout_manager::*, *},
@@ -314,7 +314,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
hide_time: is_flag_enabled!(hide_time, args.general, config),
autohide_time,
use_old_network_legend: is_flag_enabled!(use_old_network_legend, args.network, config),
table_gap: u16::from(!(is_flag_enabled!(hide_table_gap, args.general, config))),
table_gap: get_table_gap(config),
disable_click: is_flag_enabled!(disable_click, args.general, config),
disable_keys: is_flag_enabled!(disable_keys, args.general, config),
enable_gpu: get_enable_gpu(args, config),
@@ -768,6 +768,14 @@ fn get_default_cpu_selection(args: &BottomArgs, config: &Config) -> config::cpu:
}
}
fn get_table_gap(config: &Config) -> TableGap {
config
.flags
.as_ref()
.map(|flags| flags.table_gap)
.unwrap_or_default()
}
fn get_dedicated_avg_row(config: &Config) -> bool {
config
.flags
-8
View File
@@ -236,14 +236,6 @@ pub struct GeneralArgs {
)]
pub expanded: bool,
#[arg(
long,
action = ArgAction::SetTrue,
help = "Hides spacing between table headers and entries.",
alias = "hide-table-gap"
)]
pub hide_table_gap: bool,
#[arg(long, action = ArgAction::SetTrue, help = "Hides the time scale from being shown.", alias = "hide-time")]
pub hide_time: bool,
+2 -3
View File
@@ -15,9 +15,8 @@ pub(crate) struct DiskConfig {
pub(crate) mount_filter: Option<IgnoreList>,
/// A list of disk widget columns.
///
/// TODO: make this more composable(?) in the future, we might need to
/// rethink how it's done for custom widgets.
// TODO: make this more composable(?) in the future, we might need to
// rethink how it's done for custom widgets.
#[serde(default)]
pub(crate) columns: Option<Vec<DiskWidgetColumn>>,
+23 -2
View File
@@ -2,6 +2,26 @@ use serde::{Deserialize, Serialize};
use super::StringOrNum;
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum TableGap {
None,
#[default]
Space,
Line,
}
impl TableGap {
/// Returns the height in rows that this gap occupies.
pub const fn height(self) -> u16 {
match self {
Self::None => 0,
Self::Space | Self::Line => 1,
}
}
}
// TODO: Break this up.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
@@ -27,7 +47,8 @@ pub(crate) struct GeneralConfig {
pub(crate) default_widget_count: Option<u64>,
pub(crate) expanded: Option<bool>,
pub(crate) use_old_network_legend: Option<bool>,
pub(crate) hide_table_gap: Option<bool>,
#[serde(default)]
pub(crate) table_gap: TableGap,
pub(crate) battery: Option<bool>,
pub(crate) disable_click: Option<bool>,
pub(crate) disable_keys: Option<bool>,
@@ -51,11 +72,11 @@ pub(crate) struct GeneralConfig {
pub(crate) network_use_bytes: Option<bool>,
pub(crate) network_use_log: Option<bool>,
pub(crate) network_use_binary_prefix: Option<bool>,
pub(crate) show_packets: Option<bool>,
pub(crate) disable_gpu: Option<bool>,
pub(crate) enable_cache_memory: Option<bool>,
pub(crate) retention: Option<StringOrNum>,
// FIXME: This makes no sense outside of basic mode, add a basic mode config section.
// FIXME: This also should be moved to CPU-specific... same with all the other entries.
pub(crate) average_cpu_row: Option<bool>,
pub(crate) tree_collapse: Option<bool>,
}
+1 -2
View File
@@ -9,7 +9,6 @@ use super::IgnoreList;
pub(crate) struct NetworkConfig {
/// A filter over the network interface names.
pub(crate) interface_filter: Option<IgnoreList>,
/// Whether to show packets information (packet rate and average packet
/// size).
/// Displays packet rate and average packet size info.
pub(crate) show_packets: Option<bool>,
}
+3 -3
View File
@@ -2,15 +2,15 @@ use serde::Deserialize;
use crate::widgets::ProcColumn;
/// Process configuration.
/// Process configuration fields.
#[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
pub(crate) struct ProcessesConfig {
/// A list of process widget columns.
///
/// TODO: make this more composable(?) in the future, we might need to
/// rethink how it's done for custom widgets
// TODO: make this more composable(?) in the future, we might need to
// rethink how it's done for custom widgets
#[serde(default)]
pub columns: Vec<ProcColumn>,