use ratatui::{
Frame,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, HighlightSpacing, List, ListItem},
};
use crate::tui::project_ctl::state::{Pane, ProjectCtlState, ProjectRow};
pub const LIVE_GLYPH: char = '●';
pub const IDLE_GLYPH: char = '○';
pub fn aggregate_glyph(row: &ProjectRow) -> char {
if row.live_count > 0 {
LIVE_GLYPH
} else {
IDLE_GLYPH
}
}
pub fn project_line(row: &ProjectRow) -> Line<'static> {
let glyph = aggregate_glyph(row);
let glyph_style = if row.live_count > 0 {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::DarkGray)
};
Line::from(vec![
Span::styled(format!("{glyph}{}", row.live_count), glyph_style),
Span::raw(" "),
Span::styled(
row.name.clone(),
Style::default().add_modifier(Modifier::BOLD),
),
])
}
pub fn render(frame: &mut Frame, area: Rect, state: &mut ProjectCtlState) {
let focused = state.focus == Pane::Projects;
let items: Vec<ListItem> = state
.projects
.iter()
.map(|p| ListItem::new(project_line(p)))
.collect();
let title =
Line::from(format!("PROJECTS ({})", state.projects.len())).style(title_style(focused));
let list = List::new(items)
.block(Block::default().borders(Borders::ALL).title(title))
.highlight_style(
Style::default()
.fg(Color::White)
.bg(Color::Blue)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("▸ ")
.highlight_spacing(HighlightSpacing::Always);
frame.render_stateful_widget(list, area, state.projects_nav.state_mut());
}
fn title_style(focused: bool) -> Style {
if focused {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default().add_modifier(Modifier::BOLD)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn row(live: usize) -> ProjectRow {
ProjectRow {
name: "widget".to_string(),
repo_url: "https://github.com/acme/widget".to_string(),
live_count: live,
total_count: live + 1,
}
}
fn line_text(line: &Line<'_>) -> String {
line.spans.iter().map(|s| s.content.as_ref()).collect()
}
#[test]
fn aggregate_glyph_reflects_live_count() {
assert_eq!(aggregate_glyph(&row(0)), IDLE_GLYPH);
assert_eq!(aggregate_glyph(&row(1)), LIVE_GLYPH);
}
#[test]
fn project_line_shows_glyph_count_and_name() {
let text = line_text(&project_line(&row(3)));
assert!(
text.starts_with("●3"),
"expected leading glyph+count: {text}"
);
assert!(text.contains("widget"), "missing name: {text}");
}
#[test]
fn project_line_idle_uses_hollow_glyph() {
let text = line_text(&project_line(&row(0)));
assert!(text.starts_with("○0"), "expected hollow glyph: {text}");
}
}