#![allow(dead_code)]
use crate::tui::theme::Theme;
use ratatui::{
style::{Modifier, Style},
text::{Line, Span},
};
pub fn bar(
label: &str,
value: u64,
max: u64,
bar_width: u16,
filled: bool,
theme: &Theme,
) -> Line<'static> {
let width = bar_width.max(1) as usize;
let fraction: f64 = if max == 0 {
0.0
} else {
(value.min(max) as f64) / (max as f64)
};
let filled_cells = ((fraction * width as f64).round() as usize).min(width);
let empty_cells = width - filled_cells;
let style_filled = Style::default()
.fg(theme.primary)
.add_modifier(Modifier::BOLD);
let style_muted = Style::default().fg(theme.text_muted);
let (filled_str, empty_str): (String, String) = if filled {
("█".repeat(filled_cells), "░".repeat(empty_cells))
} else {
(String::new(), "░".repeat(width))
};
let label_padded = format!("{:<14}", label);
let mut spans: Vec<Span> = Vec::with_capacity(6);
spans.push(Span::raw(label_padded));
spans.push(Span::raw(" ["));
if filled && !filled_str.is_empty() {
spans.push(Span::styled(filled_str, style_filled));
}
if !empty_str.is_empty() {
spans.push(Span::styled(empty_str, style_muted));
}
spans.push(Span::raw("] "));
spans.push(Span::styled(
value.to_string(),
Style::default().fg(theme.text_strong),
));
Line::from(spans)
}
pub fn dashboard(
title: &str,
rows: &[(String, u64)],
max: u64,
bar_width: u16,
theme: &Theme,
) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::with_capacity(rows.len() + 1);
lines.push(Line::from(Span::styled(
title.to_owned(),
Style::default()
.fg(theme.section_header)
.add_modifier(Modifier::BOLD),
)));
for (label, value) in rows {
lines.push(bar(label, *value, max, bar_width, true, theme));
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::theme::{theme, ColorDepth};
fn mk_theme() -> Theme {
theme(ColorDepth::Ansi16)
}
fn line_text(line: &Line) -> String {
line.spans.iter().map(|s| s.content.as_ref()).collect()
}
#[test]
fn bar_value_zero_sem_celulas_preenchidas() {
let t = mk_theme();
let line = bar("input", 0, 1000, 20, true, &t);
let text = line_text(&line);
assert!(
!text.contains('█'),
"value=0 não deve ter células preenchidas"
);
assert!(text.contains('░'), "value=0 deve ter células vazias");
}
#[test]
fn bar_value_igual_max_preenche_tudo() {
let t = mk_theme();
let line = bar("output", 500, 500, 10, true, &t);
let text = line_text(&line);
assert!(
text.contains('█'),
"value==max deve ter células preenchidas"
);
let barra = extrair_barra(&text);
assert!(
!barra.contains('░'),
"value==max não deve ter células vazias"
);
}
#[test]
fn bar_value_maior_que_max_clampado() {
let t = mk_theme();
let line = bar("cached", 9999, 100, 10, true, &t);
let text = line_text(&line);
let barra = extrair_barra(&text);
assert!(
!barra.contains('░'),
"value > max deve ser clampado — barra deve ser completamente preenchida"
);
}
#[test]
fn bar_max_zero_nao_causa_panic() {
let t = mk_theme();
let line = bar("total", 42, 0, 10, true, &t);
let text = line_text(&line);
assert!(text.contains('░'), "max=0 deve produzir barra vazia");
assert!(
!text.contains('█'),
"max=0 não deve ter células preenchidas"
);
}
#[test]
fn bar_filled_false_usa_apenas_pontilhado() {
let t = mk_theme();
let line = bar("ref", 750, 1000, 20, false, &t);
let text = line_text(&line);
let barra = extrair_barra(&text);
assert!(
!barra.contains('█'),
"filled=false não deve ter '█' na barra"
);
assert!(barra.contains('░'), "filled=false deve ter '░' na barra");
}
#[test]
fn bar_formato_contem_colchetes_e_valor() {
let t = mk_theme();
let line = bar("tokens", 123, 1000, 10, true, &t);
let text = line_text(&line);
assert!(text.contains('['), "deve conter '[' ");
assert!(text.contains(']'), "deve conter ']' ");
assert!(text.contains("123"), "deve conter o valor numérico");
}
#[test]
fn bar_largura_pequena_nao_causa_panic() {
let t = mk_theme();
let line = bar("x", 1, 10, 1, true, &t);
let text = line_text(&line);
assert!(!text.is_empty());
}
#[test]
fn bar_largura_zero_clampada_para_um() {
let t = mk_theme();
let line = bar("y", 0, 10, 0, true, &t);
let text = line_text(&line);
assert!(!text.is_empty());
}
#[test]
fn bar_label_curto_e_preenchido_com_espacos() {
let t = mk_theme();
let line = bar("ab", 0, 100, 5, true, &t);
let text = line_text(&line);
assert!(
text.starts_with("ab "),
"label deve ter padding de 14 chars: {:?}",
text
);
}
#[test]
fn dashboard_retorna_titulo_mais_linhas() {
let t = mk_theme();
let rows = vec![
("input".to_string(), 100u64),
("output".to_string(), 200u64),
("cache".to_string(), 50u64),
];
let lines = dashboard("Tokens Queimados", &rows, 500, 20, &t);
assert_eq!(lines.len(), 4, "deve retornar título + 3 linhas de dados");
}
#[test]
fn dashboard_titulo_esta_na_primeira_linha() {
let t = mk_theme();
let rows = vec![("x".to_string(), 10u64)];
let lines = dashboard("Meu Título", &rows, 100, 10, &t);
let titulo = line_text(&lines[0]);
assert!(titulo.contains("Meu Título"));
}
#[test]
fn dashboard_vazio_retorna_apenas_titulo() {
let t = mk_theme();
let lines = dashboard("Vazio", &[], 100, 10, &t);
assert_eq!(lines.len(), 1, "sem rows, deve retornar apenas o título");
}
#[test]
fn dashboard_com_max_zero_nao_causa_panic() {
let t = mk_theme();
let rows = vec![("a".to_string(), 0u64), ("b".to_string(), 5u64)];
let lines = dashboard("Zero", &rows, 0, 10, &t);
assert_eq!(lines.len(), 3);
}
fn extrair_barra(text: &str) -> &str {
if let (Some(a), Some(b)) = (text.find('['), text.find(']')) {
&text[a + 1..b]
} else {
""
}
}
}