use core::fmt::Write as _;
use super::{human_size, PhysicalDisk};
const PALETTE: [u8; 8] = [39, 208, 46, 201, 226, 51, 129, 214];
const GLYPHS: [char; 8] = ['#', '=', '+', '*', 'o', '~', 'x', '%'];
const FREE_ANSI: u8 = 240;
const FREE_GLYPH: char = '.';
fn push_slice(out: &mut String, color: bool, ansi: u8, ascii: char, w: usize) {
if w == 0 {
return;
}
if color {
let _ = write!(out, "\x1b[38;5;{ansi}m{}\x1b[0m", "█".repeat(w));
} else {
out.extend(std::iter::repeat_n(ascii, w));
}
}
fn swatch(color: bool, ansi: u8, ascii: char) -> String {
if color {
format!("\x1b[38;5;{ansi}m█\x1b[0m")
} else {
ascii.to_string()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Segment {
pub size_bytes: u64,
pub index: Option<usize>,
pub label: String,
}
pub(super) fn segments(disk: &PhysicalDisk) -> Vec<Segment> {
let mut sorted: Vec<&super::Partition> = disk.partitions.iter().collect();
sorted.sort_by_key(|p| p.start_offset);
let mut segs = Vec::with_capacity(sorted.len() * 2 + 1);
let mut cursor = 0u64;
for (i, p) in sorted.iter().enumerate() {
if p.start_offset > cursor {
segs.push(Segment {
size_bytes: p.start_offset - cursor,
index: None,
label: "free".to_string(),
});
}
let ty = p.partition_type.as_deref().unwrap_or("-");
segs.push(Segment {
size_bytes: p.size_bytes,
index: Some(i + 1),
label: format!("{} {ty}", p.name),
});
cursor = cursor.max(p.start_offset.saturating_add(p.size_bytes));
}
if disk.size_bytes > cursor {
segs.push(Segment {
size_bytes: disk.size_bytes - cursor,
index: None,
label: "free".to_string(),
});
}
segs
}
pub(super) fn allocate_widths(weights: &[u64], total: usize) -> Vec<usize> {
let n = weights.len();
let sum: u128 = weights.iter().map(|&w| u128::from(w)).sum();
if n == 0 || total == 0 || sum == 0 {
return vec![0; n];
}
let mut widths = vec![0usize; n];
let mut remainders = vec![0u128; n];
let mut allocated = 0usize;
for (i, &w) in weights.iter().enumerate() {
let exact = u128::from(w) * total as u128;
widths[i] = (exact / sum) as usize;
remainders[i] = exact % sum;
allocated += widths[i];
}
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| remainders[b].cmp(&remainders[a]));
let mut leftover = total - allocated;
for &i in &order {
if leftover == 0 {
break;
}
widths[i] += 1;
leftover -= 1;
}
for i in 0..n {
if weights[i] > 0 && widths[i] == 0 {
if let Some(j) = (0..n).filter(|&j| widths[j] > 1).max_by_key(|&j| widths[j]) {
widths[j] -= 1;
widths[i] += 1;
}
}
}
widths
}
#[must_use]
pub fn render_disk_bar(disk: &PhysicalDisk, width: usize, color: bool) -> String {
disk_bar(disk, width, color, 0)
}
pub(crate) fn disk_bar(disk: &PhysicalDisk, width: usize, color: bool, accent: usize) -> String {
let accent = accent % PALETTE.len();
let segs = segments(disk);
let weights: Vec<u64> = segs.iter().map(|s| s.size_bytes).collect();
let widths = allocate_widths(&weights, width);
let slots = partition_slots(&segs, accent);
let mut out = String::new();
out.push('[');
for (seg, &w) in segs.iter().zip(&widths) {
match seg.index {
Some(idx) => push_slice(&mut out, color, PALETTE[slots[idx]], GLYPHS[slots[idx]], w),
None => push_slice(&mut out, color, FREE_ANSI, FREE_GLYPH, w),
}
}
out.push(']');
out.push('\n');
let total = disk.size_bytes.max(1);
for seg in &segs {
let pct = seg.size_bytes as f64 * 100.0 / total as f64;
match seg.index {
Some(idx) => {
let _ = writeln!(
out,
" {} {idx:>2} {:<28} {:>10} {pct:>4.1}%",
swatch(color, PALETTE[slots[idx]], GLYPHS[slots[idx]]),
seg.label,
human_size(seg.size_bytes),
);
}
None => {
let _ = writeln!(
out,
" {} - {:<28} {:>10} {pct:>4.1}%",
swatch(color, FREE_ANSI, FREE_GLYPH),
"free (unallocated)",
human_size(seg.size_bytes),
);
}
}
}
out
}
fn partition_slots(segs: &[Segment], accent: usize) -> Vec<usize> {
let parts: Vec<(usize, u64)> = segs
.iter()
.filter_map(|s| s.index.map(|i| (i, s.size_bytes)))
.collect();
let largest = parts.iter().max_by_key(|(_, sz)| *sz).map(|(i, _)| *i);
let mut slots = vec![0usize; parts.len() + 1];
let mut next = 0usize;
for (i, _) in &parts {
slots[*i] = if Some(*i) == largest {
accent
} else {
while next % PALETTE.len() == accent {
next += 1;
}
let s = next % PALETTE.len();
next += 1;
s
};
}
slots
}
pub fn render_overview(disks: &[PhysicalDisk], width: usize, color: bool) -> String {
let physical: Vec<&PhysicalDisk> = disks.iter().filter(|d| !d.synthesized).collect();
if physical.len() < 2 {
return String::new();
}
let total: u64 = physical.iter().map(|d| d.size_bytes).sum();
let max = physical
.iter()
.map(|d| d.size_bytes)
.max()
.unwrap_or(0)
.max(1);
let name_w = physical
.iter()
.map(|d| d.name.chars().count())
.max()
.unwrap_or(0);
let mut out = String::new();
let _ = writeln!(
out,
"All storage ({} physical disks, {} total):",
physical.len(),
human_size(total)
);
for (i, d) in physical.iter().enumerate() {
let slot = i % PALETTE.len();
let mut fill = (u128::from(d.size_bytes) * width as u128 / u128::from(max)) as usize;
if d.size_bytes > 0 && fill == 0 {
fill = 1;
}
fill = fill.min(width);
let pct = d.size_bytes as f64 * 100.0 / total.max(1) as f64;
let mut bar = String::new();
push_slice(&mut bar, color, PALETTE[slot], GLYPHS[slot], fill);
bar.extend(std::iter::repeat_n(' ', width - fill));
let _ = writeln!(
out,
" {:<name_w$} [{bar}] {:>10} {pct:>4.1}%",
d.name,
human_size(d.size_bytes),
);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Partition;
fn part(name: &str, start: u64, size: u64, ty: &str) -> Partition {
Partition {
device_path: format!("/dev/{name}"),
name: name.to_string(),
start_offset: start,
size_bytes: size,
partition_type: Some(ty.to_string()),
mount_point: None,
filesystem: None,
label: None,
}
}
fn disk(size: u64, partitions: Vec<Partition>) -> PhysicalDisk {
PhysicalDisk {
device_path: "/dev/disk0".into(),
name: "disk0".into(),
size_bytes: size,
logical_sector_size: 512,
physical_sector_size: 512,
model: None,
serial: None,
removable: false,
read_only: false,
synthesized: false,
partitions,
}
}
#[test]
fn allocate_widths_sums_to_total() {
let w = allocate_widths(&[1, 1, 1], 64);
assert_eq!(w.iter().sum::<usize>(), 64);
assert_eq!(w, vec![22, 21, 21]);
}
#[test]
fn allocate_widths_is_proportional() {
let w = allocate_widths(&[900, 100], 100);
assert_eq!(w, vec![90, 10]);
}
#[test]
fn allocate_widths_gives_tiny_segment_at_least_one_column() {
let w = allocate_widths(&[1_000_000_000_000, 1], 50);
assert_eq!(w.iter().sum::<usize>(), 50);
assert!(w[1] >= 1, "tiny segment must be visible: {w:?}");
}
#[test]
fn allocate_widths_handles_all_zero_and_empty() {
assert_eq!(allocate_widths(&[], 10), Vec::<usize>::new());
assert_eq!(allocate_widths(&[0, 0], 10).iter().sum::<usize>(), 0);
}
#[test]
fn segments_inserts_unallocated_gaps() {
let d = disk(100, vec![part("p1", 10, 20, "A"), part("p2", 40, 20, "B")]);
let segs = segments(&d);
assert_eq!(segs.len(), 5);
assert_eq!(segs[0].index, None);
assert_eq!(segs[0].size_bytes, 10);
assert_eq!(segs[1].index, Some(1));
assert_eq!(segs[1].size_bytes, 20);
assert_eq!(segs[2].index, None); assert_eq!(segs[2].size_bytes, 10);
assert_eq!(segs[3].index, Some(2));
assert_eq!(segs[4].index, None); assert_eq!(segs[4].size_bytes, 40);
assert!(segs.last().unwrap().label.contains("free"));
}
#[test]
fn segments_no_gap_when_fully_covered() {
let d = disk(50, vec![part("p1", 0, 25, "A"), part("p2", 25, 25, "B")]);
let segs = segments(&d);
assert_eq!(segs.len(), 2);
assert!(segs.iter().all(|s| s.index.is_some()));
}
#[test]
fn render_bar_ascii_has_exact_width_and_legend() {
let d = disk(
100,
vec![part("p1", 0, 50, "TypeA"), part("p2", 50, 50, "TypeB")],
);
let out = render_disk_bar(&d, 40, false);
let bar_line = out.lines().next().unwrap();
let inner: String = bar_line
.trim_start_matches('[')
.trim_end_matches(']')
.to_string();
assert_eq!(inner.chars().count(), 40);
assert!(out.contains("p1"));
assert!(out.contains("p2"));
assert!(out.contains("TypeA"));
assert!(out.contains(&human_size(50)));
}
#[test]
fn render_bar_color_emits_ansi_escapes() {
let d = disk(100, vec![part("p1", 0, 100, "T")]);
let out = render_disk_bar(&d, 20, true);
assert!(out.contains("\x1b["), "color mode must emit ANSI escapes");
}
#[test]
fn disk_bar_paints_largest_partition_with_accent() {
let d = disk(100, vec![part("p1", 0, 10, "A"), part("p2", 10, 90, "B")]);
let out = disk_bar(&d, 40, false, 2);
let line = |name: &str| {
out.lines()
.find(|l| l.contains(name))
.unwrap()
.trim_start()
.chars()
.next()
.unwrap()
};
assert_eq!(
line("p2"),
GLYPHS[2],
"largest partition uses the accent glyph"
);
assert_ne!(line("p1"), GLYPHS[2], "non-largest avoids the accent glyph");
}
fn whole(name: &str, size: u64, synthesized: bool) -> PhysicalDisk {
let mut d = disk(size, vec![]);
d.name = name.into();
d.device_path = format!("/dev/{name}");
d.synthesized = synthesized;
d
}
fn bar_inner(line: &str) -> (usize, usize) {
let open = line.find('[').unwrap();
let close = line[open..].find(']').unwrap() + open;
let inner = &line[open + 1..close];
(
inner.chars().count(),
inner.chars().filter(|c| *c != ' ').count(),
)
}
#[test]
fn overview_is_a_per_disk_bar_chart_excluding_synthesized() {
let disks = vec![
whole("disk0", 4_000_000_000_000, false),
whole("disk3", 4_000_000_000_000, true),
whole("disk4", 2_000_000_000_000, false),
whole("disk5", 8_000_000_000_000, false),
];
let out = render_overview(&disks, 80, false);
let header = out.lines().next().unwrap();
assert!(header.contains("3 physical disks"), "{header}");
assert!(
header.contains("14.0 TB"),
"total excludes synthesized: {header}"
);
let line = |name: &str| out.lines().find(|l| l.contains(name)).unwrap();
let (w0, f0) = bar_inner(line("disk0"));
let (w4, f4) = bar_inner(line("disk4"));
let (w5, f5) = bar_inner(line("disk5"));
assert_eq!((w0, w4, w5), (80, 80, 80), "every bar spans the full width");
assert_eq!(f5, 80, "largest disk fills its bar");
assert_eq!(f0, 40, "4 TB is half of the 8 TB max");
assert_eq!(f4, 20, "2 TB is a quarter of the 8 TB max");
assert!(out.contains("57.1%")); assert!(
!out.contains("disk3"),
"synthesized disk excluded from overview"
);
}
#[test]
fn overview_empty_when_fewer_than_two_physical_disks() {
assert_eq!(render_overview(&[], 70, false), "");
assert_eq!(
render_overview(&[whole("disk0", 1_000_000_000_000, false)], 70, false),
""
);
let one_physical = vec![
whole("disk0", 1_000_000_000_000, false),
whole("disk1", 500_000_000, true),
];
assert_eq!(render_overview(&one_physical, 70, false), "");
}
}