diff --git a/docs/content/configuration/config-file/styling.md b/docs/content/configuration/config-file/styling.md index cd6e9c55a..b4308358d 100644 --- a/docs/content/configuration/config-file/styling.md +++ b/docs/content/configuration/config-file/styling.md @@ -166,3 +166,4 @@ These can be set under `[styles.widgets]`: | `selected_text` | Text styling for text when representing something that is selected | `selected_text = { color = "black", bg_color = "blue", bold = true }` | | `disabled_text` | Text styling for text when representing something that is disabled | `disabled_text = { color = "black", bg_color = "blue", bold = true }` | | `thread_text` | Text styling for text when representing process threads. Only usable on Linux at the moment. | `thread_text = { color = "green", bg_color = "blue", bold = true }` | +| `progress_bar_chars` | Characters to use for progress bars | `progress_bar_chars = ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"]` | diff --git a/src/canvas/components/pipe_gauge.rs b/src/canvas/components/pipe_gauge.rs index 2024f200b..12c422fb3 100644 --- a/src/canvas/components/pipe_gauge.rs +++ b/src/canvas/components/pipe_gauge.rs @@ -20,6 +20,8 @@ pub enum LabelLimit { #[derive(Debug, Clone)] pub struct PipeGauge<'a> { block: Option>, + /// Characters to use for the progress bar + progress_chars: &'a [char], ratio: f64, start_label: Option>, inner_label: Option>, @@ -28,8 +30,8 @@ pub struct PipeGauge<'a> { hide_parts: LabelLimit, } -impl Default for PipeGauge<'_> { - fn default() -> Self { +impl<'a> PipeGauge<'a> { + pub fn new(progress_chars: &'a [char]) -> Self { Self { block: None, ratio: 0.0, @@ -38,11 +40,10 @@ impl Default for PipeGauge<'_> { label_style: Style::default(), gauge_style: Style::default(), hide_parts: LabelLimit::default(), + progress_chars, } } -} -impl<'a> PipeGauge<'a> { /// The ratio, a value from 0.0 to 1.0 (any other greater or less will be /// clamped) represents the portion of the pipe gauge to fill. /// @@ -196,11 +197,9 @@ impl Widget for PipeGauge<'_> { gauge_area.width, ); - let pipe_end = - start + (f64::from(end.saturating_sub(start)) * self.ratio).floor() as u16; - for col in start..pipe_end { + for (char, col) in progress_bar(self.progress_chars, start, end, self.ratio) { if let Some(cell) = buf.cell_mut((col, row)) { - cell.set_symbol("|").set_style(Style { + cell.set_char(char).set_style(Style { fg: self.gauge_style.fg, bg: None, add_modifier: self.gauge_style.add_modifier, @@ -221,3 +220,87 @@ impl Widget for PipeGauge<'_> { } } } + +/// Returns an iterator over characters of the progress bar, and their positions +/// +/// # Arguments +/// +/// - `chars`: The characters to use for the progress bar +/// - `bar_start`: Start position +/// - `bar_end`: End position +/// - `ratio`: How full the progress bar is +/// +/// # Panics +/// +/// `chars` must be non-empty +fn progress_bar( + chars: &[char], bar_start: u16, bar_end: u16, ratio: f64, +) -> impl Iterator { + let bar_len = f64::from(bar_end.saturating_sub(bar_start)) * ratio; + + // Length of the bar, without accounting for the partial final character + let bar_len_truncated = bar_len.floor(); + + // The final progress character to display. + // This might be `None` if we can't display even the minimum segment, in + // which case we won't display anything at all. + // + // This might happen when, for example, we have 5 progress characters: [1, 2, 3, 4, .], + // 10 cells, and our progress is 50.1%. We will display 5 full cells: + // + // 50.1%: ..... + // + // If it was 50.2% progress, we would display 5 full cells, and 1 cell with the first character: + // + // 50.2%: .....1 + // ^ extra + let final_progress_char = { + // The ratio of a single progress bar character that we lost due to truncation + // + // This ratio will be displayed as a "partial" character + let final_char_ratio = (bar_len - bar_len_truncated).clamp(0.0, 1.0); + + let char_index = (final_char_ratio * chars.len() as f64).floor() as usize; + + // -1 because 0-based indexing + char_index.checked_sub(1).and_then(|it| chars.get(it)) + }; + + let bar_end = bar_start + bar_len_truncated as u16; + + (bar_start..bar_end) + .map(move |pos| (*chars.last().expect("chars is non-empty"), pos)) + .chain(final_progress_char.map(|ch| (*ch, bar_end))) +} + +#[cfg(test)] +mod tests { + #[test] + fn progress_bar() { + let bars = (0..11) + .map(|i| { + let fill = i as f64 * 0.1; + super::progress_bar(&['1', '2', '3', '4', '.'], 0, 2, fill) + .map(|(ch, _)| ch) + .collect::>() + }) + .collect::>(); + + assert_eq!( + bars, + vec![ + vec![], + vec!['1'], + vec!['2'], + vec!['3'], + vec!['4'], + vec!['.'], + vec!['.', '1'], + vec!['.', '2'], + vec!['.', '3'], + vec!['.', '4'], + vec!['.', '.'] + ] + ); + } +} diff --git a/src/canvas/widgets/cpu_basic.rs b/src/canvas/widgets/cpu_basic.rs index 8d2a9bda4..b827710b5 100644 --- a/src/canvas/widgets/cpu_basic.rs +++ b/src/canvas/widgets/cpu_basic.rs @@ -61,7 +61,7 @@ impl Painter { avg_loc.width -= 2; f.render_widget( - PipeGauge::default() + PipeGauge::new(&self.styles.progress_bar_chars.0) .gauge_style(style) .label_style(style) .inner_label(inner) @@ -128,7 +128,7 @@ impl Painter { for ((start_label, inner_label, ratio, style), row) in chunk.zip(rows.iter()) { f.render_widget( - PipeGauge::default() + PipeGauge::new(&self.styles.progress_bar_chars.0) .gauge_style(style) .label_style(style) .inner_label(inner_label) diff --git a/src/canvas/widgets/mem_basic.rs b/src/canvas/widgets/mem_basic.rs index a2f66d44d..a8193236c 100644 --- a/src/canvas/widgets/mem_basic.rs +++ b/src/canvas/widgets/mem_basic.rs @@ -73,7 +73,7 @@ impl Painter { }; draw_widgets.push( - PipeGauge::default() + PipeGauge::new(&self.styles.progress_bar_chars.0) .ratio(ram_percentage / 100.0) .start_label("RAM") .inner_label(ram_label) @@ -86,7 +86,7 @@ impl Painter { let swap_label = memory_label(swap_harvest, app_state.basic_mode_use_percent); draw_widgets.push( - PipeGauge::default() + PipeGauge::new(&self.styles.progress_bar_chars.0) .ratio(swap_percentage / 100.0) .start_label("SWP") .inner_label(swap_label) @@ -103,7 +103,7 @@ impl Painter { memory_label(cache_harvest, app_state.basic_mode_use_percent); draw_widgets.push( - PipeGauge::default() + PipeGauge::new(&self.styles.progress_bar_chars.0) .ratio(cache_percentage / 100.0) .start_label("CHE") .inner_label(cache_fraction_label) @@ -121,7 +121,7 @@ impl Painter { memory_label(arc_harvest, app_state.basic_mode_use_percent); draw_widgets.push( - PipeGauge::default() + PipeGauge::new(&self.styles.progress_bar_chars.0) .ratio(arc_percentage / 100.0) .start_label("ARC") .inner_label(arc_fraction_label) @@ -152,7 +152,7 @@ impl Painter { }; draw_widgets.push( - PipeGauge::default() + PipeGauge::new(&self.styles.progress_bar_chars.0) .ratio(percentage / 100.0) .start_label("GPU") .inner_label(label) diff --git a/src/options/config/style.rs b/src/options/config/style.rs index cc569c959..7c0dc314e 100644 --- a/src/options/config/style.rs +++ b/src/options/config/style.rs @@ -25,7 +25,9 @@ use utils::{opt, set_colour, set_colour_list, set_style}; use widgets::WidgetStyle; use super::Config; -use crate::options::{OptionError, OptionResult, args::BottomArgs}; +use crate::options::{ + OptionError, OptionResult, args::BottomArgs, config::style::widgets::ProgressBarChars, +}; #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))] @@ -127,6 +129,7 @@ pub struct Styles { #[cfg(target_os = "linux")] pub(crate) thread_text_style: Style, pub(crate) border_type: BorderType, + pub(crate) progress_bar_chars: ProgressBarChars, } impl Default for Styles { diff --git a/src/options/config/style/themes/default.rs b/src/options/config/style/themes/default.rs index 8d3e9cded..61864f029 100644 --- a/src/options/config/style/themes/default.rs +++ b/src/options/config/style/themes/default.rs @@ -4,7 +4,7 @@ use tui::{ }; use super::color; -use crate::options::config::style::Styles; +use crate::options::config::style::{Styles, widgets::ProgressBarChars}; impl Styles { pub(crate) fn default_palette() -> Self { @@ -69,6 +69,7 @@ impl Styles { border_type: BorderType::Plain, #[cfg(target_os = "linux")] thread_text_style: color!(Color::Green), + progress_bar_chars: ProgressBarChars::default(), } } diff --git a/src/options/config/style/themes/gruvbox.rs b/src/options/config/style/themes/gruvbox.rs index c415be161..4e14a3d13 100644 --- a/src/options/config/style/themes/gruvbox.rs +++ b/src/options/config/style/themes/gruvbox.rs @@ -4,7 +4,7 @@ use tui::{ }; use super::{color, hex}; -use crate::options::config::style::{Styles, themes::hex_colour}; +use crate::options::config::style::{Styles, themes::hex_colour, widgets::ProgressBarChars}; impl Styles { pub(crate) fn gruvbox_palette() -> Self { @@ -69,6 +69,7 @@ impl Styles { border_type: BorderType::Plain, #[cfg(target_os = "linux")] thread_text_style: hex!("#458588"), + progress_bar_chars: ProgressBarChars::default(), } } @@ -134,6 +135,7 @@ impl Styles { border_type: BorderType::Plain, #[cfg(target_os = "linux")] thread_text_style: hex!("#458588"), + progress_bar_chars: ProgressBarChars::default(), } } } diff --git a/src/options/config/style/themes/nord.rs b/src/options/config/style/themes/nord.rs index 2e46d5c45..f98777370 100644 --- a/src/options/config/style/themes/nord.rs +++ b/src/options/config/style/themes/nord.rs @@ -4,7 +4,7 @@ use tui::{ }; use super::{color, hex}; -use crate::options::config::style::{Styles, themes::hex_colour}; +use crate::options::config::style::{Styles, themes::hex_colour, widgets::ProgressBarChars}; impl Styles { pub(crate) fn nord_palette() -> Self { @@ -57,6 +57,7 @@ impl Styles { border_type: BorderType::Plain, #[cfg(target_os = "linux")] thread_text_style: hex!("#a3be8c"), + progress_bar_chars: ProgressBarChars::default(), } } @@ -110,6 +111,7 @@ impl Styles { border_type: BorderType::Plain, #[cfg(target_os = "linux")] thread_text_style: hex!("#a3be8c"), + progress_bar_chars: ProgressBarChars::default(), } } } diff --git a/src/options/config/style/widgets.rs b/src/options/config/style/widgets.rs index 6c33c55af..ae2287701 100644 --- a/src/options/config/style/widgets.rs +++ b/src/options/config/style/widgets.rs @@ -33,4 +33,59 @@ pub(crate) struct WidgetStyle { /// Widget borders type. pub(crate) widget_border_type: Option, + + /// Progress bar characters to use + pub(crate) progress_bar_chars: Option, +} + +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub(crate) struct ProgressBarChars(pub(crate) Vec); + +impl<'de> Deserialize<'de> for ProgressBarChars { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Vec::::deserialize(deserializer).and_then(|chars| { + if chars.is_empty() { + Err(::custom( + "the array of progress bar characters must be non-empty", + )) + } else { + Ok(Self(chars)) + } + }) + } +} + +impl Serialize for ProgressBarChars { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + +#[cfg(feature = "generate_schema")] +impl schemars::JsonSchema for ProgressBarChars { + fn schema_name() -> std::borrow::Cow<'static, str> { + Vec::::schema_name() + } + + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + let mut schema = generator.subschema_for::>(); + schema.insert( + "minItems".into(), + serde_json::Value::Number(serde_json::Number::from(1u64)), + ); + schema + } +} + +impl Default for ProgressBarChars { + fn default() -> Self { + Self(vec!['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']) + } } diff --git a/tests/invalid_configs/empty_progress_bar_chars.toml b/tests/invalid_configs/empty_progress_bar_chars.toml new file mode 100644 index 000000000..2bcf59bd0 --- /dev/null +++ b/tests/invalid_configs/empty_progress_bar_chars.toml @@ -0,0 +1,2 @@ +[styles.widgets] +progress_bar_chars = [] \ No newline at end of file diff --git a/tests/valid_configs/progress_bar_chars.toml b/tests/valid_configs/progress_bar_chars.toml new file mode 100644 index 000000000..20b098eb9 --- /dev/null +++ b/tests/valid_configs/progress_bar_chars.toml @@ -0,0 +1,2 @@ +[styles.widgets] +progress_bar_chars = ["x"]