From 875e8926871e04d8bd368e6c316cb67261542ded Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:28:29 -0400 Subject: [PATCH] feature: add more pipe gauge bar fill options (#2245) Adds block and square character options for pipe gauge widgets. May still redesign this (maybe add option for a background), but this is fine for now. --- CHANGELOG.md | 1 + .../configuration/config-file/styling.md | 21 +- sample_configs/default_config.toml | 6 +- schema/nightly/bottom.json | 61 ++++-- scripts/schema_gen/src/main.rs | 2 + src/canvas/components/pipe_gauge.rs | 189 ++++++++++++++++-- src/canvas/widgets/battery_display.rs | 3 +- src/canvas/widgets/cpu_basic.rs | 6 +- src/canvas/widgets/mem_basic.rs | 15 +- src/constants.rs | 6 +- src/options/config/style.rs | 19 +- src/options/config/style/bars.rs | 75 +++++++ src/options/config/style/borders.rs | 1 + src/options/config/style/themes/default.rs | 3 +- src/options/config/style/themes/gruvbox.rs | 7 +- src/options/config/style/themes/nord.rs | 7 +- src/options/config/style/widgets.rs | 6 +- tests/valid_configs/all_styling.toml | 1 + 18 files changed, 370 insertions(+), 59 deletions(-) create mode 100644 src/options/config/style/bars.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 25bba9d3..16e06861 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ That said, these are more guidelines rather than hard rules, though the project ### Features - [#2239](https://github.com/ClementTsang/bottom/pull/2239): Initial Intel GPU support for Linux to get process GPU usage. +- [#1434](https://github.com/ClementTsang/bottom/issues/1434): Support solid bars using block and square characters via `styles.widgets.bar_type`. ### Other diff --git a/docs/content/configuration/config-file/styling.md b/docs/content/configuration/config-file/styling.md index 21c512af..a7e22522 100644 --- a/docs/content/configuration/config-file/styling.md +++ b/docs/content/configuration/config-file/styling.md @@ -187,13 +187,14 @@ These can be set under `[styles.graphs]`: These can be set under `[styles.widgets]`: -| Config field | Details | Examples | -| ------------------------ | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -| `border_colour` | The colour of the widgets' borders | `border_colour = "white"` | -| `selected_border_colour` | The colour of a widget's borders when the widget is selected | `selected_border_colour = "white"` | -| `widget_title` | Text styling for a widget's title | `widget_title = { colour = "black", bg_colour = "blue", bold = true }` | -| `bg_colour` | The background colour of the widgets | `bg_colour = "black"` | -| `text` | Text styling for text in general | `text = { colour = "black", bg_colour = "blue", bold = true }` | -| `selected_text` | Text styling for text when representing something that is selected | `selected_text = { colour = "black", bg_colour = "blue", bold = true }` | -| `disabled_text` | Text styling for text when representing something that is disabled | `disabled_text = { colour = "black", bg_colour = "blue", bold = true }` | -| `thread_text` | Text styling for text when representing process thread, only usable on Linux at the moment | `thread_text = { colour = "green", bg_colour = "blue", bold = true }` | +| Config field | Details | Examples | +| ------------------------ | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `border_colour` | The colour of the widgets' borders | `border_colour = "white"` | +| `selected_border_colour` | The colour of a widget's borders when the widget is selected | `selected_border_colour = "white"` | +| `widget_title` | Text styling for a widget's title | `widget_title = { colour = "black", bg_colour = "blue", bold = true }` | +| `bg_colour` | The background colour of the widgets | `bg_colour = "black"` | +| `text` | Text styling for text in general | `text = { colour = "black", bg_colour = "blue", bold = true }` | +| `selected_text` | Text styling for text when representing something that is selected | `selected_text = { colour = "black", bg_colour = "blue", bold = true }` | +| `disabled_text` | Text styling for text when representing something that is disabled | `disabled_text = { colour = "black", bg_colour = "blue", bold = true }` | +| `thread_text` | Text styling for text when representing process thread, only usable on Linux at the moment | `thread_text = { colour = "green", bg_colour = "blue", bold = true }` | +| `bar_type` | What to fill bar widgets with (e.g. basic CPU/memory); either `"pipe"` (default), `"block"`, or `"square"` | `bar_type = "block"` | diff --git a/sample_configs/default_config.toml b/sample_configs/default_config.toml index fc719c35..4f735ead 100644 --- a/sample_configs/default_config.toml +++ b/sample_configs/default_config.toml @@ -474,10 +474,12 @@ #text = {colour = "gray"} #selected_text = {colour = "black", bg_colour = "light blue"} #disabled_text = {colour = "dark gray"} -# Disabled by default +# Disabled by default. #bg_colour = "black" -# Only on Linux +# Only supported on Linux. #thread_text = {colour = "green"} +# One of "pipe", "block", or "square". +#bar_type = "pipe" # Layout - layouts follow a pattern like this: # [[row]] represents a row in the application. diff --git a/schema/nightly/bottom.json b/schema/nightly/bottom.json index 8d36f366..a82a4ee6 100644 --- a/schema/nightly/bottom.json +++ b/schema/nightly/bottom.json @@ -268,7 +268,7 @@ ] }, "include_unmounted": { - "description": "Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.", + "description": "Whether to include block devices that aren't currently mounted\n(currently Linux only). Defaults to false.", "type": [ "boolean", "null" @@ -322,14 +322,14 @@ "type": "object", "properties": { "include_unmounted": { - "description": "Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.", + "description": "Whether to include block devices that aren't currently mounted\n(currently Linux only). Defaults to false.", "type": [ "boolean", "null" ] }, "legend": { - "description": "Whether to label legend entries by device name or mount point. Defaults to disk name.", + "description": "Whether to label legend entries by device name or mount point. Defaults\nto disk name.", "anyOf": [ { "$ref": "#/$defs/DiskGraphLegend" @@ -812,7 +812,7 @@ "type": "object", "properties": { "cache_memory": { - "description": "Whether to collect and display cache and buffer memory. Not available on Windows.", + "description": "Whether to collect and display cache and buffer memory. Not available on\nWindows.", "type": [ "boolean", "null" @@ -931,14 +931,14 @@ ] }, "start_zeroed": { - "description": "Zeroes out the total network usage (\"All\") counters so it shows the total usage\nsince the app is started, rather than the total usage since boot.", + "description": "Zeroes out the total network usage (\"All\") counters so it shows the\ntotal usage since the app is started, rather than the total usage\nsince boot.", "type": [ "boolean", "null" ] }, "use_binary_prefix": { - "description": "Displays the network widget with a binary prefix (e.g. kibibits) rather than a decimal\nprefix (e.g. kilobits). Defaults to decimal prefixes.", + "description": "Displays the network widget with a binary prefix (e.g. kibibits) rather\nthan a decimal prefix (e.g. kilobits). Defaults to decimal prefixes.", "type": [ "boolean", "null" @@ -952,7 +952,7 @@ ] }, "use_log": { - "description": "Displays the network widget with a log scale. Defaults to a non-log scale.", + "description": "Displays the network widget with a log scale. Defaults to a non-log\nscale.", "type": [ "boolean", "null" @@ -1101,14 +1101,14 @@ } }, "current_usage": { - "description": "Calculates process CPU usage as a percentage of current usage rather than total usage.", + "description": "Calculates process CPU usage as a percentage of current usage rather\nthan total usage.", "type": [ "boolean", "null" ] }, "default_grouped": { - "description": "Groups processes with the same name by default. No effect if `--tree` is set.", + "description": "Groups processes with the same name by default. No effect if `--tree` is\nset.", "type": [ "boolean", "null" @@ -1140,7 +1140,7 @@ ] }, "disable_advanced_kill": { - "description": "Disable the advanced kill dialog and just show the basic one with no options.", + "description": "Disable the advanced kill dialog and just show the basic one with no\noptions.", "type": [ "boolean", "null" @@ -1441,7 +1441,7 @@ "default": null }, "max_temp": { - "description": "An upper temperature value for the graph; entries higher than this will be hidden. If not set,\nthere is no limit.\n\nIs in the configured temperature unit.", + "description": "An upper temperature value for the graph; entries higher than this will\nbe hidden. If not set, there is no limit.\n\nIs in the configured temperature unit.", "type": [ "number", "null" @@ -1538,19 +1538,50 @@ } ] }, + "WidgetBarType": { + "description": "The type of character used to fill in bars, such as the ones used by the\nbasic CPU and memory widgets.", + "oneOf": [ + { + "description": "Fill bars with pipe characters (`|`).", + "type": "string", + "const": "pipe" + }, + { + "description": "Fill bars with block characters (`█`, `▉`, etc.).", + "type": "string", + "const": "block" + }, + { + "description": "Fill bars with square characters (`■`).", + "type": "string", + "const": "square" + } + ] + }, "WidgetBorderType": { "type": "string", "enum": [ - "Default", - "Rounded", - "Double", - "Thick" + "default", + "rounded", + "double", + "thick" ] }, "WidgetStyle": { "description": "General styling for generic widgets.", "type": "object", "properties": { + "bar_type": { + "description": "What character to fill in bars with, such as the ones used by the basic\nCPU and memory widgets.", + "anyOf": [ + { + "$ref": "#/$defs/WidgetBarType" + }, + { + "type": "null" + } + ] + }, "bg_colour": { "description": "Background colour for widgets.", "anyOf": [ diff --git a/scripts/schema_gen/src/main.rs b/scripts/schema_gen/src/main.rs index 6dd0a761..23d83637 100644 --- a/scripts/schema_gen/src/main.rs +++ b/scripts/schema_gen/src/main.rs @@ -44,6 +44,8 @@ macro_rules! generate_column_schemas { } fn generate_schema(schema_options: SchemaOptions) -> anyhow::Result<()> { + // TODO: remove newlines in descriptions? + let mut schema = schemars::schema_for!(config::Config); { // TODO: Maybe make this case insensitive? See https://stackoverflow.com/a/68639341 diff --git a/src/canvas/components/pipe_gauge.rs b/src/canvas/components/pipe_gauge.rs index 5b64ac22..90261d18 100644 --- a/src/canvas/components/pipe_gauge.rs +++ b/src/canvas/components/pipe_gauge.rs @@ -2,6 +2,7 @@ use ratatui::{ buffer::Buffer, layout::Rect, style::Style, + symbols, text::Line, widgets::{Block, Widget}, }; @@ -16,7 +17,41 @@ pub enum LabelLimit { StartLabel, } -/// A widget to measure something, using pipe characters ('|') as a unit. +/// What bar character type to use. +#[derive(Debug, Clone, Default, Copy)] +pub enum BarType { + #[default] + /// The pipe character (`|`) + Pipe, + /// Block characters (`█`, `▉`, etc.) + Block, + /// Square characters (`■`) + Square, +} + +impl BarType { + #[inline] + fn is_block(&self) -> bool { + matches!(self, BarType::Block) + } +} + +fn get_unicode_block(frac: f64) -> &'static str { + match (frac * 8.0).round() as u16 { + 0 => " ", + 1 => symbols::block::ONE_EIGHTH, + 2 => symbols::block::ONE_QUARTER, + 3 => symbols::block::THREE_EIGHTHS, + 4 => symbols::block::HALF, + 5 => symbols::block::FIVE_EIGHTHS, + 6 => symbols::block::THREE_QUARTERS, + 7 => symbols::block::SEVEN_EIGHTHS, + 8 => symbols::block::FULL, + _ => unreachable!("this case should never occur"), + } +} + +/// A widget to measure something, using pipe characters ('|') or horizontal bar characters as a unit. #[derive(Debug, Clone)] pub struct PipeGauge<'a> { block: Option>, @@ -26,6 +61,7 @@ pub struct PipeGauge<'a> { label_style: Style, gauge_style: Style, hide_parts: LabelLimit, + bar_type: BarType, } impl Default for PipeGauge<'_> { @@ -38,6 +74,7 @@ impl Default for PipeGauge<'_> { label_style: Style::default(), gauge_style: Style::default(), hide_parts: LabelLimit::default(), + bar_type: BarType::default(), } } } @@ -89,6 +126,12 @@ impl<'a> PipeGauge<'a> { self.hide_parts = hide_parts; self } + + /// What type of bar character to use. + pub fn bar_type(mut self, bar_type: BarType) -> Self { + self.bar_type = bar_type; + self + } } impl Widget for PipeGauge<'_> { @@ -184,11 +227,11 @@ impl Widget for PipeGauge<'_> { ); } LabelLimit::Auto(_) | LabelLimit::None => { + // FIXME: "[" and "]" don't look that great with block bars. let (start, _) = buf.set_line(col, row, &Line::from("["), gauge_area.width); if start >= gauge_area.right() { return; } - let (end, _) = buf.set_line( (gauge_area.x + gauge_area.width).saturating_sub(1), row, @@ -196,22 +239,40 @@ impl Widget for PipeGauge<'_> { gauge_area.width, ); - let pipe_end = end.saturating_sub(1).min( - start + (f64::from(end.saturating_sub(start)) * self.ratio).floor() as u16, - ); + let bar_end = end.saturating_sub(1); + let filled_width = f64::from(bar_end.saturating_sub(start)) * self.ratio; + let pipe_end = bar_end.min(start + filled_width.floor() as u16); + + let symbol = match self.bar_type { + BarType::Pipe => "|", + BarType::Block => symbols::block::FULL, + BarType::Square => "■", + }; + + let bar_style = Style { + fg: self.gauge_style.fg, + bg: None, + add_modifier: self.gauge_style.add_modifier, + sub_modifier: self.gauge_style.sub_modifier, + underline_color: None, + }; for col in start..pipe_end { if let Some(cell) = buf.cell_mut((col, row)) { - cell.set_symbol("|").set_style(Style { - fg: self.gauge_style.fg, - bg: None, - add_modifier: self.gauge_style.add_modifier, - sub_modifier: self.gauge_style.sub_modifier, - underline_color: None, - }); + cell.set_symbol(symbol).set_style(bar_style); } } + // Unlike pipes, blocks can also show the leftover fraction of a cell. + // Based on what Ratatui does! + if self.bar_type.is_block() + && pipe_end < bar_end + && let Some(cell) = buf.cell_mut((pipe_end, row)) + { + cell.set_symbol(get_unicode_block(filled_width.fract())) + .set_style(bar_style); + } + if (end_label.width() as u16) < end.saturating_sub(start) { let gauge_end = gauge_area .right() @@ -223,3 +284,107 @@ impl Widget for PipeGauge<'_> { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_unicode_block() { + // Test exact values. + assert_eq!(get_unicode_block(0.0), " "); + assert_eq!(get_unicode_block(0.125), symbols::block::ONE_EIGHTH); + assert_eq!(get_unicode_block(0.25), symbols::block::ONE_QUARTER); + assert_eq!(get_unicode_block(0.375), symbols::block::THREE_EIGHTHS); + assert_eq!(get_unicode_block(0.5), symbols::block::HALF); + assert_eq!(get_unicode_block(0.625), symbols::block::FIVE_EIGHTHS); + assert_eq!(get_unicode_block(0.75), symbols::block::THREE_QUARTERS); + assert_eq!(get_unicode_block(0.875), symbols::block::SEVEN_EIGHTHS); + assert_eq!(get_unicode_block(1.0), symbols::block::FULL); + + // Test rounding. + assert_eq!(get_unicode_block(0.05), " "); + assert_eq!(get_unicode_block(0.1), symbols::block::ONE_EIGHTH); + assert_eq!(get_unicode_block(0.3), symbols::block::ONE_QUARTER); + assert_eq!(get_unicode_block(0.4), symbols::block::THREE_EIGHTHS); + assert_eq!(get_unicode_block(0.55), symbols::block::HALF); + assert_eq!(get_unicode_block(0.6), symbols::block::FIVE_EIGHTHS); + assert_eq!(get_unicode_block(0.8), symbols::block::THREE_QUARTERS); + assert_eq!(get_unicode_block(0.9), symbols::block::SEVEN_EIGHTHS); + assert_eq!(get_unicode_block(0.99), symbols::block::FULL); + } + + /// Create a [`PipeGauge`] and return what it would have rendered. + fn render_gauge( + ratio: f64, bar_type: BarType, start_label: Option<&str>, inner_label: Option<&str>, + ) -> String { + const WIDTH: u16 = 12; + + let area = Rect::new(0, 0, WIDTH, 1); + let mut buf = Buffer::empty(area); + let mut gauge = PipeGauge::default().ratio(ratio).bar_type(bar_type); + + if let Some(start_label) = start_label { + gauge = gauge.start_label(start_label.to_owned()); + } + + if let Some(inner_label) = inner_label { + gauge = gauge.inner_label(inner_label.to_owned()); + } + + gauge.render(area, &mut buf); + + (0..WIDTH).map(|x| buf[(x, 0)].symbol()).collect() + } + + #[test] + fn test_pipe_bars() { + assert_eq!(render_gauge(0.0, BarType::Pipe, None, None), "[ ]"); + assert_eq!(render_gauge(0.5, BarType::Pipe, None, None), "[||||| ]"); + assert_eq!( + render_gauge(0.95, BarType::Pipe, None, None), + "[||||||||| ]" + ); + assert_eq!(render_gauge(1.0, BarType::Pipe, None, None), "[||||||||||]"); + } + + #[test] + fn test_solid_bars() { + assert_eq!( + render_gauge(0.0, BarType::Block, None, None), + "[ ]" + ); + assert_eq!( + render_gauge(0.5, BarType::Block, None, None), + "[█████ ]" + ); + assert_eq!( + render_gauge(0.55, BarType::Block, None, None), + "[█████▌ ]" + ); + assert_eq!( + render_gauge(0.9, BarType::Block, None, None), + "[█████████ ]" + ); + assert_eq!( + render_gauge(1.0, BarType::Block, None, None), + "[██████████]" + ); + } + + #[test] + fn test_labelled_bars() { + assert_eq!( + render_gauge(0.5, BarType::Pipe, Some("CPU"), Some(" 50%")), + "CPU[||| 50%]" + ); + assert_eq!( + render_gauge(0.5, BarType::Block, Some("CPU"), Some(" 50%")), + "CPU[███ 50%]" + ); + assert_eq!( + render_gauge(1.0, BarType::Block, Some("CPU"), Some("100%")), + "CPU[███100%]" + ); + } +} diff --git a/src/canvas/widgets/battery_display.rs b/src/canvas/widgets/battery_display.rs index c032b121..78f7e78e 100644 --- a/src/canvas/widgets/battery_display.rs +++ b/src/canvas/widgets/battery_display.rs @@ -214,7 +214,8 @@ impl Painter { let block_inner = block.inner(margined_draw_loc); - // Draw bar + // Draw bar. + // TODO: Switch to pipe_gauge. f.render_widget( Table::new(battery_charge_rows, [Constraint::Percentage(100)]) .block(block.clone()) diff --git a/src/canvas/widgets/cpu_basic.rs b/src/canvas/widgets/cpu_basic.rs index 2c8190b0..d14727e1 100644 --- a/src/canvas/widgets/cpu_basic.rs +++ b/src/canvas/widgets/cpu_basic.rs @@ -71,7 +71,8 @@ impl Painter { .label_style(style) .inner_label(inner) .start_label(outer) - .ratio(ratio.into()), + .ratio(ratio.into()) + .bar_type(self.styles.bar_type), avg_loc, ); avg_row_count += 1; @@ -139,7 +140,8 @@ impl Painter { .inner_label(inner_label) .start_label(start_label) .ratio(ratio.into()) - .hide_parts(hide_parts), + .hide_parts(hide_parts) + .bar_type(self.styles.bar_type), *row, ); } diff --git a/src/canvas/widgets/mem_basic.rs b/src/canvas/widgets/mem_basic.rs index f378e8f7..16302eee 100644 --- a/src/canvas/widgets/mem_basic.rs +++ b/src/canvas/widgets/mem_basic.rs @@ -81,7 +81,8 @@ impl Painter { .start_label("RAM") .inner_label(ram_label) .label_style(self.styles.ram_style) - .gauge_style(self.styles.ram_style), + .gauge_style(self.styles.ram_style) + .bar_type(self.styles.bar_type), ); if let Some(swap_harvest) = &data.swap_harvest { @@ -94,7 +95,8 @@ impl Painter { .start_label("SWP") .inner_label(swap_label) .label_style(self.styles.swap_style) - .gauge_style(self.styles.swap_style), + .gauge_style(self.styles.swap_style) + .bar_type(self.styles.bar_type), ); } @@ -111,7 +113,8 @@ impl Painter { .start_label("CHE") .inner_label(cache_fraction_label) .label_style(self.styles.cache_style) - .gauge_style(self.styles.cache_style), + .gauge_style(self.styles.cache_style) + .bar_type(self.styles.bar_type), ); } } @@ -129,7 +132,8 @@ impl Painter { .start_label("ARC") .inner_label(arc_fraction_label) .label_style(self.styles.arc_style) - .gauge_style(self.styles.arc_style), + .gauge_style(self.styles.arc_style) + .bar_type(self.styles.bar_type), ); } } @@ -160,7 +164,8 @@ impl Painter { .start_label("GPU") .inner_label(label) .label_style(style) - .gauge_style(style), + .gauge_style(style) + .bar_type(self.styles.bar_type), ); } } diff --git a/src/constants.rs b/src/constants.rs index 3ed9f5f4..e39c1c94 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -723,10 +723,12 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott #text = {colour = "gray"} #selected_text = {colour = "black", bg_colour = "light blue"} #disabled_text = {colour = "dark gray"} -# Disabled by default +# Disabled by default. #bg_colour = "black" -# Only on Linux +# Only supported on Linux. #thread_text = {colour = "green"} +# One of "pipe", "block", or "square". +#bar_type = "pipe" # Layout - layouts follow a pattern like this: # [[row]] represents a row in the application. diff --git a/src/options/config/style.rs b/src/options/config/style.rs index 40d4f634..f06b8182 100644 --- a/src/options/config/style.rs +++ b/src/options/config/style.rs @@ -1,5 +1,6 @@ //! Config options around styling. +mod bars; mod battery; mod borders; mod cpu; @@ -29,8 +30,9 @@ use utils::{opt, set_colour, set_colour_list, set_style}; use widgets::WidgetStyle; use super::Config; -use crate::options::{ - OptionError, OptionResult, args::BottomArgs, config::style::utils::set_bg_colour, +use crate::{ + canvas::components::pipe_gauge::BarType, + options::{OptionError, OptionResult, args::BottomArgs, config::style::utils::set_bg_colour}, }; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -143,6 +145,7 @@ pub struct Styles { #[cfg(target_os = "linux")] pub(crate) thread_text_style: Style, pub(crate) border_type: BorderType, + pub(crate) bar_type: BarType, } impl Default for Styles { @@ -261,10 +264,14 @@ impl Styles { selected_border_colour ); - if let Some(widgets) = &config.widgets - && let Some(widget_borders) = widgets.widget_border_type - { - self.border_type = widget_borders.into(); + if let Some(widgets) = &config.widgets { + if let Some(widget_borders) = widgets.widget_border_type { + self.border_type = widget_borders.into(); + } + + if let Some(bar_type) = widgets.bar_type { + self.bar_type = bar_type.into(); + } } Ok(()) diff --git a/src/options/config/style/bars.rs b/src/options/config/style/bars.rs new file mode 100644 index 00000000..f6b2886a --- /dev/null +++ b/src/options/config/style/bars.rs @@ -0,0 +1,75 @@ +use serde::{Deserialize, Serialize}; + +use crate::canvas::components::pipe_gauge::BarType; + +/// The type of character used to fill in bars, such as the ones used by the +/// basic CPU and memory widgets. +#[derive(Default, Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub(crate) enum WidgetBarType { + /// Fill bars with pipe characters (`|`). + #[default] + Pipe, + /// Fill bars with block characters (`█`, `▉`, etc.). + Block, + /// Fill bars with square characters (`■`). + Square, +} + +impl<'de> Deserialize<'de> for WidgetBarType { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?.to_lowercase(); + match value.as_str() { + "pipe" => Ok(WidgetBarType::Pipe), + "block" => Ok(WidgetBarType::Block), + "square" => Ok(WidgetBarType::Square), + _ => Err(serde::de::Error::custom("doesn't match any bar type")), + } + } +} + +impl From for BarType { + fn from(value: WidgetBarType) -> Self { + match value { + WidgetBarType::Pipe => BarType::Pipe, + WidgetBarType::Block => BarType::Block, + WidgetBarType::Square => BarType::Square, + } + } +} + +#[cfg(test)] +mod test { + use super::WidgetBarType; + use crate::options::config::style::widgets::WidgetStyle; + + fn parse(value: &str) -> anyhow::Result> { + let style = toml_edit::de::from_str::(&format!("bar_type = {value}"))?; + + Ok(style.bar_type) + } + + #[test] + fn valid_bar_types() { + assert_eq!(parse("\"pipe\"").unwrap(), Some(WidgetBarType::Pipe)); + assert_eq!(parse("\"block\"").unwrap(), Some(WidgetBarType::Block)); + assert_eq!(parse("\"square\"").unwrap(), Some(WidgetBarType::Square)); + + // Casing shouldn't matter. + assert_eq!(parse("\"PIPE\"").unwrap(), Some(WidgetBarType::Pipe)); + assert_eq!(parse("\"BLOCK\"").unwrap(), Some(WidgetBarType::Block)); + assert_eq!(parse("\"SQUARE\"").unwrap(), Some(WidgetBarType::Square)); + } + + #[test] + fn invalid_bar_types() { + assert!(parse("\"bar\"").is_err()); + assert!(parse("\"\"").is_err()); + assert!(parse("true").is_err()); + } +} diff --git a/src/options/config/style/borders.rs b/src/options/config/style/borders.rs index 1c0d7b0e..eaa5e96c 100644 --- a/src/options/config/style/borders.rs +++ b/src/options/config/style/borders.rs @@ -2,6 +2,7 @@ use ratatui::widgets::BorderType; use serde::{Deserialize, Serialize}; #[derive(Default, Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "lowercase")] #[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))] #[cfg_attr(test, derive(PartialEq, Eq))] pub(crate) enum WidgetBorderType { diff --git a/src/options/config/style/themes/default.rs b/src/options/config/style/themes/default.rs index 4fdd587a..d582976c 100644 --- a/src/options/config/style/themes/default.rs +++ b/src/options/config/style/themes/default.rs @@ -4,7 +4,7 @@ use ratatui::{ }; use super::colour; -use crate::options::config::style::Styles; +use crate::{canvas::components::pipe_gauge::BarType, options::config::style::Styles}; impl Styles { pub(crate) fn default_palette() -> Self { @@ -75,6 +75,7 @@ impl Styles { invalid_query_style: colour!(Color::Red), disabled_text_style: colour!(Color::DarkGray), border_type: BorderType::Plain, + bar_type: BarType::default(), #[cfg(target_os = "linux")] thread_text_style: colour!(Color::Green), } diff --git a/src/options/config/style/themes/gruvbox.rs b/src/options/config/style/themes/gruvbox.rs index 91729e75..f4417fce 100644 --- a/src/options/config/style/themes/gruvbox.rs +++ b/src/options/config/style/themes/gruvbox.rs @@ -4,7 +4,10 @@ use ratatui::{ }; use super::{colour, hex}; -use crate::options::config::style::{Styles, themes::hex_colour}; +use crate::{ + canvas::components::pipe_gauge::BarType, + options::config::style::{Styles, themes::hex_colour}, +}; impl Styles { pub(crate) fn gruvbox_palette() -> Self { @@ -75,6 +78,7 @@ impl Styles { invalid_query_style: colour!(Color::Red), disabled_text_style: hex!("#665c54"), border_type: BorderType::Plain, + bar_type: BarType::default(), #[cfg(target_os = "linux")] thread_text_style: hex!("#458588"), } @@ -154,6 +158,7 @@ impl Styles { invalid_query_style: colour!(Color::Red), disabled_text_style: hex!("#d5c4a1"), border_type: BorderType::Plain, + bar_type: BarType::default(), #[cfg(target_os = "linux")] thread_text_style: hex!("#458588"), } diff --git a/src/options/config/style/themes/nord.rs b/src/options/config/style/themes/nord.rs index 7aec0653..5a60c7c8 100644 --- a/src/options/config/style/themes/nord.rs +++ b/src/options/config/style/themes/nord.rs @@ -4,7 +4,10 @@ use ratatui::{ }; use super::{colour, hex}; -use crate::options::config::style::{Styles, themes::hex_colour}; +use crate::{ + canvas::components::pipe_gauge::BarType, + options::config::style::{Styles, themes::hex_colour}, +}; impl Styles { pub(crate) fn nord_palette() -> Self { @@ -63,6 +66,7 @@ impl Styles { invalid_query_style: colour!(Color::Red), disabled_text_style: hex!("#4c566a"), border_type: BorderType::Plain, + bar_type: BarType::default(), #[cfg(target_os = "linux")] thread_text_style: hex!("#a3be8c"), } @@ -124,6 +128,7 @@ impl Styles { invalid_query_style: colour!(Color::Red), disabled_text_style: hex!("#d8dee9"), border_type: BorderType::Plain, + bar_type: BarType::default(), #[cfg(target_os = "linux")] thread_text_style: hex!("#a3be8c"), } diff --git a/src/options/config/style/widgets.rs b/src/options/config/style/widgets.rs index 0f169a0d..7690699c 100644 --- a/src/options/config/style/widgets.rs +++ b/src/options/config/style/widgets.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use super::{ColourStr, TextStyleConfig, borders::WidgetBorderType}; +use super::{ColourStr, TextStyleConfig, bars::WidgetBarType, borders::WidgetBorderType}; /// General styling for generic widgets. #[derive(Clone, Debug, Default, Deserialize, Serialize)] @@ -37,4 +37,8 @@ pub(crate) struct WidgetStyle { /// Background colour for widgets. #[serde(alias = "bg_color")] pub(crate) bg_colour: Option, + + /// What character to fill in bars with, such as the ones used by the basic + /// CPU and memory widgets. + pub(crate) bar_type: Option, } diff --git a/tests/valid_configs/all_styling.toml b/tests/valid_configs/all_styling.toml index 75791e04..610b488f 100644 --- a/tests/valid_configs/all_styling.toml +++ b/tests/valid_configs/all_styling.toml @@ -64,3 +64,4 @@ text = { colour = "gray" } selected_text = { colour = "black", bg_colour = "light blue" } disabled_text = { colour = "dark gray" } bg_colour = "black" +bar_type = "block"