#![warn(missing_docs)]
pub mod ansi;
pub mod detect;
pub mod glyphs;
pub mod render;
#[cfg(any(test, feature = "html"))]
pub mod html;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rgb(pub u8, pub u8, pub u8);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Capability {
Ascii,
EighthBlock,
}
impl Capability {
pub fn sub_positions(self) -> u32 {
match self {
Capability::Ascii => 1,
Capability::EighthBlock => 8,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Theme {
pub primary: Rgb,
pub secondary: Rgb,
pub empty: Rgb,
pub overflow: Rgb,
}
impl Default for Theme {
fn default() -> Self {
Self {
primary: Rgb(88, 166, 255),
secondary: Rgb(60, 90, 160),
empty: Rgb(33, 38, 45),
overflow: Rgb(248, 81, 73),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OverflowPolicy {
#[default]
Swap,
Clamp,
Distinct,
}
pub use render::{Cell, CellKind};
pub use detect::{detect, detect_color};
use crate::render::classify;
#[derive(Clone, Debug)]
pub struct Bar {
width: usize,
primary: f64,
secondary: f64,
theme: Theme,
capability: Capability,
color: bool,
min_visible: bool,
overflow: OverflowPolicy,
}
impl Bar {
pub fn new(width: usize) -> Self {
Self {
width,
primary: 0.0,
secondary: 0.0,
theme: Theme::default(),
capability: detect::detect(),
color: detect::detect_color(),
min_visible: false,
overflow: OverflowPolicy::default(),
}
}
pub fn primary(mut self, v: f64) -> Self { self.primary = v; self }
pub fn secondary(mut self, v: f64) -> Self { self.secondary = v; self }
pub fn theme(mut self, t: Theme) -> Self { self.theme = t; self }
pub fn capability(mut self, c: Capability) -> Self { self.capability = c; self }
pub fn color(mut self, on: bool) -> Self { self.color = on; self }
pub fn auto_color(self) -> Self {
let c = detect::detect_color();
self.color(c)
}
pub fn min_visible(mut self, on: bool) -> Self { self.min_visible = on; self }
pub fn overflow(mut self, p: OverflowPolicy) -> Self { self.overflow = p; self }
#[cfg(test)]
fn sanitized(&self) -> (f64, f64) {
let (lo, hi, _) = self.resolved();
(lo, hi)
}
fn resolved(&self) -> (f64, f64, bool) {
let s = |x: f64| if x.is_nan() { 0.0 } else { x.clamp(0.0, 1.0) };
let p = s(self.primary);
let q = s(self.secondary);
let (mut lo, mut hi, is_overflow) = match self.overflow {
OverflowPolicy::Swap => (p.min(q), p.max(q), false),
OverflowPolicy::Clamp => (p.min(q), q, false),
OverflowPolicy::Distinct => {
if p > q { (q, p, true) } else { (p, q, false) }
}
};
if self.min_visible {
let total = (self.width as u32)
.saturating_mul(self.capability.sub_positions())
.max(1) as f64;
let floor = 1.0 / total;
if lo > 0.0 && lo < floor { lo = floor; }
if hi > 0.0 && hi < floor { hi = floor; }
if hi < lo { hi = lo; }
}
(lo, hi, is_overflow)
}
pub fn cells(&self) -> Vec<Cell> {
let (lo, hi, is_overflow) = self.resolved();
let mut cells = classify(self.width, lo, hi, self.capability);
if is_overflow {
for c in cells.iter_mut() {
c.kind = match c.kind {
CellKind::SecondaryFull => CellKind::OverflowFull,
CellKind::PrimaryBoundary => CellKind::OverflowInnerBoundary,
CellKind::SecondaryBoundary => CellKind::OverflowOuterBoundary,
other => other,
};
}
}
cells
}
pub fn render(&self) -> String {
if self.color {
ansi::encode(&self.cells(), &self.theme, self.capability)
} else {
ansi::encode_plain(&self.cells(), self.capability)
}
}
pub fn render_plain(&self) -> String {
ansi::encode_plain(&self.cells(), self.capability)
}
}
#[cfg(test)]
mod bar_tests {
use super::*;
#[test] fn clamps_out_of_range() {
let (p1, p2) = Bar::new(10).primary(-1.0).secondary(2.0).sanitized();
assert_eq!(p1, 0.0);
assert_eq!(p2, 1.0);
}
#[test] fn swaps_when_primary_above_secondary() {
let (p1, p2) = Bar::new(10).primary(0.9).secondary(0.1).sanitized();
assert_eq!(p1, 0.1);
assert_eq!(p2, 0.9);
}
#[test] fn nan_becomes_zero() {
let (p1, _) = Bar::new(10).primary(f64::NAN).sanitized();
assert_eq!(p1, 0.0);
}
#[test] fn zero_width_no_cells() {
assert!(Bar::new(0).primary(0.5).secondary(0.7).cells().is_empty());
}
#[test] fn render_is_non_empty_for_nonzero_width() {
let s = Bar::new(8).primary(0.5).secondary(0.7).render();
assert!(!s.is_empty());
}
#[test] fn min_visible_off_lets_tiny_pct_round_to_zero() {
let cells = Bar::new(8)
.capability(Capability::Ascii)
.primary(0.05).secondary(0.05)
.cells();
assert!(cells.iter().all(|c| c.kind == CellKind::Empty));
}
#[test] fn min_visible_on_bumps_tiny_pct_to_one_cell() {
let cells = Bar::new(8)
.capability(Capability::Ascii)
.primary(0.05).secondary(0.05)
.min_visible(true)
.cells();
assert_eq!(cells[0].kind, CellKind::PrimaryFull);
assert!(cells[1..].iter().all(|c| c.kind == CellKind::Empty));
}
#[test] fn min_visible_does_not_bump_zero() {
let cells = Bar::new(8)
.capability(Capability::Ascii)
.primary(0.0).secondary(0.0)
.min_visible(true)
.cells();
assert!(cells.iter().all(|c| c.kind == CellKind::Empty));
}
#[test] fn overflow_swap_is_default_and_matches_legacy() {
let b = Bar::new(10).primary(0.9).secondary(0.1);
let (lo, hi, ov) = b.resolved();
assert_eq!((lo, hi, ov), (0.1, 0.9, false));
}
#[test] fn overflow_clamp_caps_primary_at_secondary() {
let b = Bar::new(10).primary(0.9).secondary(0.1).overflow(OverflowPolicy::Clamp);
let (lo, hi, ov) = b.resolved();
assert_eq!((lo, hi, ov), (0.1, 0.1, false));
}
#[test] fn overflow_distinct_preserves_order_and_sets_flag() {
let b = Bar::new(10).primary(0.9).secondary(0.1).overflow(OverflowPolicy::Distinct);
let (lo, hi, ov) = b.resolved();
assert_eq!((lo, hi, ov), (0.1, 0.9, true));
}
#[test] fn overflow_distinct_when_primary_le_secondary_no_overflow() {
let b = Bar::new(10).primary(0.3).secondary(0.7).overflow(OverflowPolicy::Distinct);
let (_, _, ov) = b.resolved();
assert!(!ov);
}
#[test] fn overflow_distinct_produces_overflow_kinds() {
let cells = Bar::new(13)
.primary(0.67).secondary(0.33)
.capability(Capability::EighthBlock)
.overflow(OverflowPolicy::Distinct)
.cells();
assert!(cells.iter().any(|c| c.kind == CellKind::OverflowFull));
assert_eq!(cells[4].kind, CellKind::OverflowInnerBoundary);
assert_eq!(cells[8].kind, CellKind::OverflowOuterBoundary);
}
#[test] fn min_visible_bumps_secondary_independently() {
let cells = Bar::new(8)
.capability(Capability::EighthBlock)
.primary(0.0).secondary(0.05)
.min_visible(true)
.cells();
assert!(matches!(
cells[0].kind,
CellKind::SecondaryFull | CellKind::SecondaryBoundary
));
}
}