use rich::cells::cell_len;
use rich::measure::Measurement;
use rich::{Console, ConsoleOptions, Overflow, Renderable, Segment, Style, Text};
use crate::format;
pub const STYLES: &[(&str, &str)] = &[
("size_bar.used", "green"),
("size_bar.high", "yellow"),
("size_bar.over", "bold red"),
("size_bar.free", "bright_black"),
("size_bar.label", "bold"),
("size_bar.size", "none"),
];
fn theme_style(console: &Console, key: &str) -> Style {
if let Some(style) = console.theme().get(key) {
return style.clone();
}
STYLES
.iter()
.find(|(name, _)| *name == key)
.and_then(|(_, spec)| Style::parse(spec).ok())
.unwrap_or_default()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Units {
#[default]
Decimal,
Binary,
}
impl Units {
pub fn format(self, n: u64) -> String {
match self {
Units::Decimal => format::bytes(n),
Units::Binary => format::bytes_binary(n),
}
}
}
const UNICODE: [&str; 3] = ["█", "░", "▓"];
const ASCII: [&str; 3] = ["#", ".", "!"];
const GAP: &str = " ";
const MIN_BAR: usize = 4;
#[derive(Clone, Debug, PartialEq)]
pub struct SizeBar {
used: u64,
total: u64,
label: Option<String>,
units: Units,
bar_width: usize,
warn_at: Option<f64>,
show_sizes: bool,
show_percent: bool,
}
impl SizeBar {
pub fn new(used: u64, total: u64) -> Self {
SizeBar {
used,
total,
label: None,
units: Units::Decimal,
bar_width: 20,
warn_at: None,
show_sizes: true,
show_percent: true,
}
}
pub fn limit(used: u64, limit: u64) -> Self {
Self::new(used, limit).warn_at(0.9)
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn units(mut self, units: Units) -> Self {
self.units = units;
self
}
pub fn bar_width(mut self, width: usize) -> Self {
self.bar_width = width.max(1);
self
}
pub fn warn_at(mut self, ratio: f64) -> Self {
self.warn_at = Some(ratio);
self
}
pub fn show_sizes(mut self, show: bool) -> Self {
self.show_sizes = show;
self
}
pub fn show_percent(mut self, show: bool) -> Self {
self.show_percent = show;
self
}
pub fn ratio(&self) -> f64 {
match (self.used, self.total) {
(0, 0) => 0.0,
(_, 0) => f64::INFINITY,
(used, total) => used as f64 / total as f64,
}
}
pub fn is_over(&self) -> bool {
self.used > self.total
}
pub fn is_high(&self) -> bool {
!self.is_over() && self.warn_at.is_some_and(|at| self.ratio() >= at)
}
fn tail(&self) -> Vec<(String, &'static str)> {
let status = if self.is_over() {
"size_bar.over"
} else if self.is_high() {
"size_bar.high"
} else {
"size_bar.size"
};
let mut parts = Vec::new();
if self.show_sizes {
parts.push((
format!(
"{} / {}",
self.units.format(self.used),
self.units.format(self.total)
),
"size_bar.size",
));
}
if self.show_percent {
parts.push((format::percent(self.ratio(), 0), status));
}
if self.is_over() {
let over = self.units.format(self.used - self.total);
parts.push((format!("over by {over}"), "size_bar.over"));
}
parts
}
fn fixed_width(&self) -> usize {
let label = self.label.as_deref().map_or(0, |l| cell_len(l) + GAP.len());
let tail: usize = self
.tail()
.iter()
.map(|(text, _)| GAP.len() + cell_len(text))
.sum();
label + tail
}
fn cells(&self, width: usize) -> (usize, usize, usize) {
if self.is_over() {
let within = if self.total == 0 {
0
} else {
((width as f64) * self.total as f64 / self.used as f64).floor() as usize
};
let within = within.min(width.saturating_sub(1));
return (within, 0, width - within);
}
let mut used = (self.ratio() * width as f64).round() as usize;
if self.used > 0 {
used = used.max(1);
}
if self.used < self.total {
used = used.min(width.saturating_sub(1));
}
(used, width - used, 0)
}
fn line(&self, console: &Console, bar: usize) -> Text {
let glyphs = if console.ascii_only() { ASCII } else { UNICODE };
let used_style = if self.is_over() {
"size_bar.over"
} else if self.is_high() {
"size_bar.high"
} else {
"size_bar.used"
};
let mut text = Text::new("");
if let Some(label) = &self.label {
text.append(label, Some(theme_style(console, "size_bar.label").into()));
text.append(GAP, None);
}
let (used, free, over) = self.cells(bar);
for (glyph, count, key) in [
(glyphs[0], used, used_style),
(glyphs[1], free, "size_bar.free"),
(glyphs[2], over, "size_bar.over"),
] {
if count > 0 {
text.append(&glyph.repeat(count), Some(theme_style(console, key).into()));
}
}
for (part, key) in self.tail() {
text.append(GAP, None);
text.append(&part, Some(theme_style(console, key).into()));
}
text
}
}
impl Renderable for SizeBar {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let fixed = self.fixed_width();
let bar = self
.bar_width
.min(options.max_width.saturating_sub(fixed))
.max(MIN_BAR.min(self.bar_width));
self.line(console, bar)
.no_wrap(true)
.overflow(Overflow::Ellipsis)
.rich_render(console, options)
}
fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
let fixed = self.fixed_width();
Measurement::new(
(fixed + MIN_BAR.min(self.bar_width)).min(options.max_width),
(fixed + self.bar_width).min(options.max_width),
)
}
}