use std::io::Write;
use crossterm::style::Color;
use crate::common::config::ThemeConfig;
use crate::ui::text::print_colored_text;
pub struct BarSegment {
pub value: f64,
pub color: Color,
pub label: Option<String>,
}
impl BarSegment {
pub fn new(value: f64, color: Color) -> Self {
Self {
value,
color,
label: None,
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
}
pub fn draw_bar<W: Write>(
stdout: &mut W,
label: &str,
value: f64,
max_value: f64,
width: usize,
show_text: Option<String>,
) {
let formatted_label = if label.len() > 5 {
label[..5].to_string()
} else {
format!("{label:<5}")
};
let available_bar_width = width.saturating_sub(9);
let fill_ratio = (value / max_value).min(1.0);
let filled_width = (available_bar_width as f64 * fill_ratio) as usize;
let color = ThemeConfig::progress_bar_color(fill_ratio);
let display_text = if let Some(text) = show_text {
if text.len() > 8 {
text[..8].to_string()
} else {
format!("{text:>8}") }
} else {
format!("{:>7.1}%", fill_ratio * 100.0) };
print_colored_text(stdout, &formatted_label, Color::White, None, None);
print_colored_text(stdout, ": [", Color::White, None, None);
let text_len = display_text.len();
let text_pos = available_bar_width.saturating_sub(text_len);
let filled_before_text = filled_width.min(text_pos);
if filled_before_text > 0 {
print_colored_text(stdout, &"▬".repeat(filled_before_text), color, None, None);
}
let empty_before_text = text_pos.saturating_sub(filled_width);
if empty_before_text > 0 {
print_colored_text(
stdout,
&"─".repeat(empty_before_text),
Color::DarkGrey,
None,
None,
);
}
if text_len > 0 {
print_colored_text(stdout, &display_text, Color::Grey, None, None);
}
let after_text_start = text_pos + text_len;
let filled_after_text = filled_width.saturating_sub(after_text_start);
if filled_after_text > 0 {
print_colored_text(stdout, &"▬".repeat(filled_after_text), color, None, None);
}
let total_used = after_text_start + filled_after_text;
let empty_after = available_bar_width.saturating_sub(total_used);
if empty_after > 0 {
print_colored_text(
stdout,
&"─".repeat(empty_after),
Color::DarkGrey,
None,
None,
);
}
print_colored_text(stdout, "]", Color::White, None, None);
}
pub fn draw_bar_multi<W: Write>(
stdout: &mut W,
label: &str,
segments: &[BarSegment],
max_value: f64,
width: usize,
show_text: Option<String>,
) {
let formatted_label = if label.len() > 5 {
label[..5].to_string()
} else {
format!("{label:<5}")
};
let available_bar_width = width.saturating_sub(9);
let total_value: f64 = segments.iter().map(|s| s.value).sum();
let total_ratio = (total_value / max_value).min(1.0);
let display_text = if let Some(text) = show_text {
if text.len() > 8 {
text[..8].to_string()
} else {
format!("{text:>8}")
}
} else {
format!("{:>7.1}%", total_ratio * 100.0)
};
print_colored_text(stdout, &formatted_label, Color::White, None, None);
print_colored_text(stdout, ": [", Color::White, None, None);
let text_len = display_text.len();
let text_pos = available_bar_width.saturating_sub(text_len);
let mut segment_positions = Vec::new();
let mut current_pos = 0;
for segment in segments {
let segment_ratio = segment.value / max_value;
let segment_width = (available_bar_width as f64 * segment_ratio).round() as usize;
segment_positions.push((current_pos, current_pos + segment_width, segment.color));
current_pos += segment_width;
}
let total_filled_width = (available_bar_width as f64 * total_ratio).round() as usize;
if current_pos > total_filled_width {
if let Some(last) = segment_positions.last_mut() {
last.1 = total_filled_width;
}
}
let text_end = text_pos + text_len;
let mut pos = 0;
while pos < available_bar_width {
if pos >= text_pos && pos < text_end {
print_colored_text(stdout, &display_text, Color::Grey, None, None);
pos = text_end;
continue;
}
let seg_match = segment_positions
.iter()
.find(|seg| pos >= seg.0 && pos < seg.1);
if let Some(&(_, end, color)) = seg_match {
let run_end = if text_pos > pos {
end.min(text_pos).min(available_bar_width)
} else {
end.min(available_bar_width)
};
let run_len = run_end.saturating_sub(pos);
if run_len > 0 {
print_colored_text(stdout, &"▬".repeat(run_len), color, None, None);
pos += run_len;
} else {
pos = end.max(pos + 1);
}
} else {
let next_boundary = segment_positions
.iter()
.filter_map(|seg| if seg.0 > pos { Some(seg.0) } else { None })
.min()
.unwrap_or(available_bar_width)
.min(text_pos)
.min(available_bar_width);
let run_len = next_boundary.saturating_sub(pos).max(1);
print_colored_text(stdout, &"─".repeat(run_len), Color::DarkGrey, None, None);
pos += run_len;
}
}
print_colored_text(stdout, "]", Color::White, None, None);
}
impl BarSegment {
#[allow(dead_code)]
pub fn cpu_low_priority(value: f64) -> Self {
Self::new(value, Color::Blue).with_label("low")
}
#[allow(dead_code)]
pub fn cpu_normal(value: f64) -> Self {
Self::new(value, Color::Green).with_label("normal")
}
#[allow(dead_code)]
pub fn cpu_kernel(value: f64) -> Self {
Self::new(value, Color::Red).with_label("kernel")
}
#[allow(dead_code)]
pub fn cpu_virtualized(value: f64) -> Self {
Self::new(value, Color::DarkBlue).with_label("virtual")
}
pub fn memory_used(value: f64) -> Self {
Self::new(value, Color::Green).with_label("used")
}
pub fn memory_buffers(value: f64) -> Self {
Self::new(value, Color::Blue).with_label("buffers")
}
pub fn memory_cache(value: f64) -> Self {
Self::new(value, Color::Yellow).with_label("cache")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::buffer::BufferWriter;
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' {
for nc in chars.by_ref() {
if nc == 'm' {
break;
}
}
} else {
out.push(c);
}
}
out
}
#[test]
fn test_draw_bar_zero_value_produces_output() {
let mut bw = BufferWriter::new();
draw_bar(&mut bw, "CPU", 0.0, 100.0, 40, None);
let raw = bw.get_buffer();
assert!(!raw.is_empty(), "draw_bar should produce non-empty output");
}
#[test]
fn test_draw_bar_full_value_produces_output() {
let mut bw = BufferWriter::new();
draw_bar(&mut bw, "MEM", 100.0, 100.0, 40, None);
let raw = bw.get_buffer();
assert!(!raw.is_empty());
}
#[test]
fn test_draw_bar_label_appears_in_output() {
let mut bw = BufferWriter::new();
draw_bar(&mut bw, "GPU", 50.0, 100.0, 40, None);
let visible = strip_ansi(bw.get_buffer());
assert!(
visible.contains("GPU"),
"Label 'GPU' should appear in bar output; got: {visible:?}"
);
}
#[test]
fn test_draw_bar_brackets_present() {
let mut bw = BufferWriter::new();
draw_bar(&mut bw, "CPU", 25.0, 100.0, 40, None);
let visible = strip_ansi(bw.get_buffer());
assert!(visible.contains('['), "Opening bracket missing");
assert!(visible.contains(']'), "Closing bracket missing");
}
#[test]
fn test_draw_bar_percentage_text_shown() {
let mut bw = BufferWriter::new();
draw_bar(&mut bw, "CPU", 50.0, 100.0, 40, None);
let visible = strip_ansi(bw.get_buffer());
assert!(
visible.contains("50.0%"),
"Percentage text missing; got: {visible:?}"
);
}
#[test]
fn test_draw_bar_custom_text_overrides_percent() {
let mut bw = BufferWriter::new();
draw_bar(&mut bw, "CPU", 50.0, 100.0, 40, Some("8.0GB".to_string()));
let visible = strip_ansi(bw.get_buffer());
assert!(
visible.contains("8.0GB"),
"Custom text missing; got: {visible:?}"
);
assert!(
!visible.contains('%'),
"Percentage should not appear when custom text is set"
);
}
#[test]
fn test_draw_bar_long_label_trimmed() {
let mut bw = BufferWriter::new();
draw_bar(&mut bw, "TOOLONG", 0.0, 100.0, 40, None);
let visible = strip_ansi(bw.get_buffer());
assert!(
visible.contains("TOOLO"),
"Trimmed label missing; got: {visible:?}"
);
assert!(
!visible.contains("TOOLONG"),
"Full untrimmed label should not appear; got: {visible:?}"
);
}
#[test]
fn test_draw_bar_batches_segments() {
let mut bw = BufferWriter::new();
draw_bar(&mut bw, "CPU", 50.0, 100.0, 120, None);
let raw = bw.get_buffer();
let esc_count = raw.chars().filter(|&c| c == '\x1b').count();
assert!(
esc_count <= 20,
"Too many escape sequences ({esc_count}); batching may not be working"
);
}
#[test]
fn test_draw_bar_multi_empty_segments_produces_output() {
let mut bw = BufferWriter::new();
draw_bar_multi(&mut bw, "MEM", &[], 100.0, 40, None);
let raw = bw.get_buffer();
assert!(!raw.is_empty());
}
#[test]
fn test_draw_bar_multi_single_segment() {
let mut bw = BufferWriter::new();
let segments = vec![BarSegment::memory_used(40.0)];
draw_bar_multi(&mut bw, "MEM", &segments, 100.0, 40, None);
let visible = strip_ansi(bw.get_buffer());
assert!(visible.contains("MEM"), "Label missing");
assert!(visible.contains('['), "Opening bracket missing");
assert!(visible.contains(']'), "Closing bracket missing");
}
#[test]
fn test_draw_bar_multi_multiple_segments() {
let mut bw = BufferWriter::new();
let segments = vec![
BarSegment::memory_used(30.0),
BarSegment::memory_buffers(10.0),
BarSegment::memory_cache(20.0),
];
draw_bar_multi(&mut bw, "MEM", &segments, 100.0, 40, None);
let raw = bw.get_buffer();
assert!(!raw.is_empty());
let visible = strip_ansi(raw);
assert!(visible.contains("MEM"));
}
#[test]
fn test_draw_bar_multi_batches_segments() {
let mut bw = BufferWriter::new();
let segments = vec![
BarSegment::memory_used(25.0),
BarSegment::memory_buffers(10.0),
BarSegment::memory_cache(15.0),
];
draw_bar_multi(&mut bw, "MEM", &segments, 100.0, 120, None);
let raw = bw.get_buffer();
let esc_count = raw.chars().filter(|&c| c == '\x1b').count();
assert!(
esc_count <= 30,
"Too many escape sequences ({esc_count}) in draw_bar_multi; batching may not be working"
);
}
#[test]
fn test_draw_bar_throughput() {
let mut bw = BufferWriter::new();
let start = std::time::Instant::now();
for i in 0..1000 {
bw.reset();
draw_bar(&mut bw, "CPU", (i % 101) as f64, 100.0, 80, None);
}
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 1000,
"draw_bar throughput too slow: {elapsed:?}"
);
}
#[test]
fn test_draw_bar_multi_throughput() {
let mut bw = BufferWriter::new();
let segments = vec![
BarSegment::memory_used(30.0),
BarSegment::memory_buffers(10.0),
BarSegment::memory_cache(20.0),
];
let start = std::time::Instant::now();
for _ in 0..1000 {
bw.reset();
draw_bar_multi(&mut bw, "MEM", &segments, 100.0, 80, None);
}
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 1000,
"draw_bar_multi throughput too slow: {elapsed:?}"
);
}
}