Skip to content

Commit bcc18b9

Browse files
committed
Add config for progress bars
1 parent b7fba3e commit bcc18b9

11 files changed

Lines changed: 166 additions & 19 deletions

File tree

docs/content/configuration/config-file/styling.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,4 @@ These can be set under `[styles.widgets]`:
166166
| `selected_text` | Text styling for text when representing something that is selected | `selected_text = { color = "black", bg_color = "blue", bold = true }` |
167167
| `disabled_text` | Text styling for text when representing something that is disabled | `disabled_text = { color = "black", bg_color = "blue", bold = true }` |
168168
| `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 }` |
169+
| `progress_bar_chars` | Characters to use for progress bars | `progress_bar_chars = ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"]` |

src/canvas/components/pipe_gauge.rs

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ pub enum LabelLimit {
2020
#[derive(Debug, Clone)]
2121
pub struct PipeGauge<'a> {
2222
block: Option<Block<'a>>,
23+
/// Characters to use for the progress bar
24+
progress_chars: &'a [char],
2325
ratio: f64,
2426
start_label: Option<Line<'a>>,
2527
inner_label: Option<Line<'a>>,
@@ -28,8 +30,8 @@ pub struct PipeGauge<'a> {
2830
hide_parts: LabelLimit,
2931
}
3032

31-
impl Default for PipeGauge<'_> {
32-
fn default() -> Self {
33+
impl<'a> PipeGauge<'a> {
34+
pub fn new(progress_chars: &'a [char]) -> Self {
3335
Self {
3436
block: None,
3537
ratio: 0.0,
@@ -38,11 +40,10 @@ impl Default for PipeGauge<'_> {
3840
label_style: Style::default(),
3941
gauge_style: Style::default(),
4042
hide_parts: LabelLimit::default(),
43+
progress_chars,
4144
}
4245
}
43-
}
4446

45-
impl<'a> PipeGauge<'a> {
4647
/// The ratio, a value from 0.0 to 1.0 (any other greater or less will be
4748
/// clamped) represents the portion of the pipe gauge to fill.
4849
///
@@ -196,11 +197,9 @@ impl Widget for PipeGauge<'_> {
196197
gauge_area.width,
197198
);
198199

199-
let pipe_end =
200-
start + (f64::from(end.saturating_sub(start)) * self.ratio).floor() as u16;
201-
for col in start..pipe_end {
200+
for (char, col) in progress_bar(self.progress_chars, start, end, self.ratio) {
202201
if let Some(cell) = buf.cell_mut((col, row)) {
203-
cell.set_symbol("|").set_style(Style {
202+
cell.set_char(char).set_style(Style {
204203
fg: self.gauge_style.fg,
205204
bg: None,
206205
add_modifier: self.gauge_style.add_modifier,
@@ -221,3 +220,83 @@ impl Widget for PipeGauge<'_> {
221220
}
222221
}
223222
}
223+
224+
/// Returns an iterator over characters of the progress bar, and their positions
225+
///
226+
/// # Arguments
227+
///
228+
/// - `chars`: The characters to use for the progress bar
229+
/// - `start`: Start position
230+
/// - `end`: End position
231+
/// - `ratio`: How full the progress bar is
232+
fn progress_bar(
233+
chars: &[char], bar_start: u16, end: u16, ratio: f64,
234+
) -> impl Iterator<Item = (char, u16)> {
235+
let bar_len = f64::from(end.saturating_sub(bar_start)) * ratio;
236+
237+
// Length of the bar, without accounting for the partial final character
238+
let bar_len_truncated = bar_len.floor();
239+
240+
// The final progress character to display.
241+
// This might be `None` if we can't display even the minimum segment, in
242+
// which case we won't display anything at all.
243+
//
244+
// This might happen when, for example, we have 5 progress characters: [1, 2, 3, 4, .],
245+
// 10 cells, and our progress is 50.1%. We will display 5 full cells:
246+
//
247+
// 50.1%: .....
248+
//
249+
// If it was 50.2% progress, we would display 5 full cells, and 1 cell with the first character:
250+
//
251+
// 50.2%: .....1
252+
// ^ extra
253+
let final_progress_char = {
254+
// The ratio of a single progress bar character that we lost due to truncation
255+
//
256+
// This ratio will be displayed as a "partial" character
257+
let final_char_ratio = (bar_len - bar_len_truncated).clamp(0.0, 1.0);
258+
259+
let char_index = (final_char_ratio * chars.len() as f64).floor() as usize;
260+
261+
// 0-based indexing
262+
char_index.checked_sub(1).and_then(|it| chars.get(it))
263+
};
264+
265+
let bar_end = bar_start + bar_len_truncated as u16;
266+
267+
(bar_start..bar_end)
268+
.map(move |pos| (*chars.last().expect("chars is non-empty"), pos))
269+
.chain(final_progress_char.map(|ch| (*ch, bar_end)))
270+
}
271+
272+
#[cfg(test)]
273+
mod tests {
274+
#[test]
275+
fn progress_bar() {
276+
let bars = (0..11)
277+
.map(|i| {
278+
let fill = i as f64 * 0.1;
279+
super::progress_bar(&['1', '2', '3', '4', '.'], 0, 2, fill)
280+
.map(|(ch, _)| ch)
281+
.collect::<Vec<_>>()
282+
})
283+
.collect::<Vec<_>>();
284+
285+
assert_eq!(
286+
bars,
287+
vec![
288+
vec![],
289+
vec!['1'],
290+
vec!['2'],
291+
vec!['3'],
292+
vec!['4'],
293+
vec!['.'],
294+
vec!['.', '1'],
295+
vec!['.', '2'],
296+
vec!['.', '3'],
297+
vec!['.', '4'],
298+
vec!['.', '.']
299+
]
300+
);
301+
}
302+
}

src/canvas/widgets/cpu_basic.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ impl Painter {
6161
avg_loc.width -= 2;
6262

6363
f.render_widget(
64-
PipeGauge::default()
64+
PipeGauge::new(&self.styles.progress_bar_chars.0)
6565
.gauge_style(style)
6666
.label_style(style)
6767
.inner_label(inner)
@@ -128,7 +128,7 @@ impl Painter {
128128

129129
for ((start_label, inner_label, ratio, style), row) in chunk.zip(rows.iter()) {
130130
f.render_widget(
131-
PipeGauge::default()
131+
PipeGauge::new(&self.styles.progress_bar_chars.0)
132132
.gauge_style(style)
133133
.label_style(style)
134134
.inner_label(inner_label)

src/canvas/widgets/mem_basic.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ impl Painter {
7373
};
7474

7575
draw_widgets.push(
76-
PipeGauge::default()
76+
PipeGauge::new(&self.styles.progress_bar_chars.0)
7777
.ratio(ram_percentage / 100.0)
7878
.start_label("RAM")
7979
.inner_label(ram_label)
@@ -86,7 +86,7 @@ impl Painter {
8686
let swap_label = memory_label(swap_harvest, app_state.basic_mode_use_percent);
8787

8888
draw_widgets.push(
89-
PipeGauge::default()
89+
PipeGauge::new(&self.styles.progress_bar_chars.0)
9090
.ratio(swap_percentage / 100.0)
9191
.start_label("SWP")
9292
.inner_label(swap_label)
@@ -103,7 +103,7 @@ impl Painter {
103103
memory_label(cache_harvest, app_state.basic_mode_use_percent);
104104

105105
draw_widgets.push(
106-
PipeGauge::default()
106+
PipeGauge::new(&self.styles.progress_bar_chars.0)
107107
.ratio(cache_percentage / 100.0)
108108
.start_label("CHE")
109109
.inner_label(cache_fraction_label)
@@ -121,7 +121,7 @@ impl Painter {
121121
memory_label(arc_harvest, app_state.basic_mode_use_percent);
122122

123123
draw_widgets.push(
124-
PipeGauge::default()
124+
PipeGauge::new(&self.styles.progress_bar_chars.0)
125125
.ratio(arc_percentage / 100.0)
126126
.start_label("ARC")
127127
.inner_label(arc_fraction_label)
@@ -152,7 +152,7 @@ impl Painter {
152152
};
153153

154154
draw_widgets.push(
155-
PipeGauge::default()
155+
PipeGauge::new(&self.styles.progress_bar_chars.0)
156156
.ratio(percentage / 100.0)
157157
.start_label("GPU")
158158
.inner_label(label)

src/options/config/style.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ use utils::{opt, set_colour, set_colour_list, set_style};
2525
use widgets::WidgetStyle;
2626

2727
use super::Config;
28-
use crate::options::{OptionError, OptionResult, args::BottomArgs};
28+
use crate::options::{
29+
OptionError, OptionResult, args::BottomArgs, config::style::widgets::ProgressBarChars,
30+
};
2931

3032
#[derive(Clone, Debug, Deserialize, Serialize)]
3133
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
@@ -127,6 +129,7 @@ pub struct Styles {
127129
#[cfg(target_os = "linux")]
128130
pub(crate) thread_text_style: Style,
129131
pub(crate) border_type: BorderType,
132+
pub(crate) progress_bar_chars: ProgressBarChars,
130133
}
131134

132135
impl Default for Styles {

src/options/config/style/themes/default.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use tui::{
44
};
55

66
use super::color;
7-
use crate::options::config::style::Styles;
7+
use crate::options::config::style::{Styles, widgets::ProgressBarChars};
88

99
impl Styles {
1010
pub(crate) fn default_palette() -> Self {
@@ -69,6 +69,7 @@ impl Styles {
6969
border_type: BorderType::Plain,
7070
#[cfg(target_os = "linux")]
7171
thread_text_style: color!(Color::Green),
72+
progress_bar_chars: ProgressBarChars::default(),
7273
}
7374
}
7475

src/options/config/style/themes/gruvbox.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use tui::{
44
};
55

66
use super::{color, hex};
7-
use crate::options::config::style::{Styles, themes::hex_colour};
7+
use crate::options::config::style::{Styles, themes::hex_colour, widgets::ProgressBarChars};
88

99
impl Styles {
1010
pub(crate) fn gruvbox_palette() -> Self {
@@ -69,6 +69,7 @@ impl Styles {
6969
border_type: BorderType::Plain,
7070
#[cfg(target_os = "linux")]
7171
thread_text_style: hex!("#458588"),
72+
progress_bar_chars: ProgressBarChars::default(),
7273
}
7374
}
7475

@@ -134,6 +135,7 @@ impl Styles {
134135
border_type: BorderType::Plain,
135136
#[cfg(target_os = "linux")]
136137
thread_text_style: hex!("#458588"),
138+
progress_bar_chars: ProgressBarChars::default(),
137139
}
138140
}
139141
}

src/options/config/style/themes/nord.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use tui::{
44
};
55

66
use super::{color, hex};
7-
use crate::options::config::style::{Styles, themes::hex_colour};
7+
use crate::options::config::style::{Styles, themes::hex_colour, widgets::ProgressBarChars};
88

99
impl Styles {
1010
pub(crate) fn nord_palette() -> Self {
@@ -57,6 +57,7 @@ impl Styles {
5757
border_type: BorderType::Plain,
5858
#[cfg(target_os = "linux")]
5959
thread_text_style: hex!("#a3be8c"),
60+
progress_bar_chars: ProgressBarChars::default(),
6061
}
6162
}
6263

@@ -110,6 +111,7 @@ impl Styles {
110111
border_type: BorderType::Plain,
111112
#[cfg(target_os = "linux")]
112113
thread_text_style: hex!("#a3be8c"),
114+
progress_bar_chars: ProgressBarChars::default(),
113115
}
114116
}
115117
}

src/options/config/style/widgets.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,59 @@ pub(crate) struct WidgetStyle {
3333

3434
/// Widget borders type.
3535
pub(crate) widget_border_type: Option<WidgetBorderType>,
36+
37+
/// Progress bar characters to use
38+
pub(crate) progress_bar_chars: Option<ProgressBarChars>,
39+
}
40+
41+
#[derive(Clone, Debug)]
42+
#[cfg_attr(test, derive(PartialEq, Eq))]
43+
pub(crate) struct ProgressBarChars(pub(crate) Vec<char>);
44+
45+
impl<'de> Deserialize<'de> for ProgressBarChars {
46+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
47+
where
48+
D: serde::Deserializer<'de>,
49+
{
50+
Vec::<char>::deserialize(deserializer).and_then(|chars| {
51+
if chars.is_empty() {
52+
Err(<D::Error as serde::de::Error>::custom(
53+
"the array of progress bar characters must be non-empty",
54+
))
55+
} else {
56+
Ok(Self(chars))
57+
}
58+
})
59+
}
60+
}
61+
62+
impl Serialize for ProgressBarChars {
63+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
64+
where
65+
S: serde::Serializer,
66+
{
67+
self.0.serialize(serializer)
68+
}
69+
}
70+
71+
#[cfg(feature = "generate_schema")]
72+
impl schemars::JsonSchema for ProgressBarChars {
73+
fn schema_name() -> std::borrow::Cow<'static, str> {
74+
Vec::<char>::schema_name()
75+
}
76+
77+
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
78+
let mut schema = generator.subschema_for::<Vec<char>>();
79+
schema.insert(
80+
"minItems".into(),
81+
serde_json::Value::Number(serde_json::Number::from(1u64)),
82+
);
83+
schema
84+
}
85+
}
86+
87+
impl Default for ProgressBarChars {
88+
fn default() -> Self {
89+
Self(vec!['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'])
90+
}
3691
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[styles.widgets]
2+
progress_bar_chars = []

0 commit comments

Comments
 (0)