use crate::theme::theme;
use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
Frame,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentStatusKind {
Running,
Idle,
Ended,
Error,
}
impl AgentStatusKind {
fn symbol(&self) -> &'static str {
match self {
AgentStatusKind::Running => "\u{25cf}", AgentStatusKind::Idle => "\u{25cb}", AgentStatusKind::Ended => "\u{25cb}", AgentStatusKind::Error => "\u{2717}", }
}
fn color(&self) -> Color {
match self {
AgentStatusKind::Running => theme().status_running(),
AgentStatusKind::Idle => theme().status_idle(),
AgentStatusKind::Ended => theme().status_ended(),
AgentStatusKind::Error => theme().status_error(),
}
}
fn text(&self) -> &'static str {
match self {
AgentStatusKind::Running => "Running",
AgentStatusKind::Idle => "Idle",
AgentStatusKind::Ended => "Ended",
AgentStatusKind::Error => "Error",
}
}
}
#[derive(Debug, Clone)]
pub struct AgentStatusInfo {
pub id: usize,
pub name: String,
pub status: AgentStatusKind,
pub is_active: bool,
}
impl AgentStatusInfo {
pub fn new(id: usize, name: impl Into<String>, status: AgentStatusKind, is_active: bool) -> Self {
Self {
id,
name: name.into(),
status,
is_active,
}
}
}
pub struct StatusBar {
agents: Vec<AgentStatusInfo>,
}
impl StatusBar {
pub fn new() -> Self {
Self { agents: Vec::new() }
}
pub fn update(&mut self, agents: Vec<AgentStatusInfo>) {
self.agents = agents;
}
pub fn agent_count(&self) -> usize {
self.agents.len()
}
pub fn agents_with_status(&self, status: AgentStatusKind) -> impl Iterator<Item = &AgentStatusInfo> {
self.agents.iter().filter(move |a| a.status == status)
}
pub fn render(&self, f: &mut Frame, area: Rect) {
let t = theme();
if self.agents.is_empty() {
let empty = Paragraph::new(Line::from(vec![
Span::styled(
"\u{2500}".repeat(3), Style::default().fg(t.border_secondary()),
),
Span::styled(
" No agents running ",
Style::default().fg(t.text_muted()),
),
Span::styled(
"\u{2500}".repeat(area.width.saturating_sub(25) as usize),
Style::default().fg(t.border_secondary()),
),
]));
f.render_widget(empty, area);
return;
}
let mut spans: Vec<Span> = Vec::new();
spans.push(Span::styled(
"\u{2500} ", Style::default().fg(t.border_secondary()),
));
for (idx, agent) in self.agents.iter().enumerate() {
if idx > 0 {
spans.push(Span::styled(
" ",
Style::default().fg(t.border_secondary()),
));
}
let bracket_style = if agent.is_active {
Style::default()
.fg(t.neon_cyan())
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(t.text_muted())
};
spans.push(Span::styled("[", bracket_style));
spans.push(Span::styled(agent.id.to_string(), bracket_style));
spans.push(Span::styled("] ", bracket_style));
let name_style = if agent.is_active {
Style::default()
.fg(t.text_primary())
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(t.text_secondary())
};
let max_name_len = 15;
let display_name = if agent.name.len() > max_name_len {
format!("{}...", &agent.name[..max_name_len - 3])
} else {
agent.name.clone()
};
spans.push(Span::styled(display_name, name_style));
spans.push(Span::raw(" "));
spans.push(Span::styled(
agent.status.symbol(),
Style::default().fg(agent.status.color()),
));
spans.push(Span::raw(" "));
spans.push(Span::styled(
agent.status.text(),
Style::default().fg(agent.status.color()),
));
}
let content_len: usize = spans.iter().map(|s| s.content.len()).sum();
let remaining = area.width.saturating_sub(content_len as u16 + 1);
if remaining > 0 {
spans.push(Span::styled(
format!(" {}", "\u{2500}".repeat(remaining as usize)),
Style::default().fg(t.border_secondary()),
));
}
let statusbar = Paragraph::new(Line::from(spans));
f.render_widget(statusbar, area);
}
}
impl Default for StatusBar {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_statusbar_new() {
let statusbar = StatusBar::new();
assert_eq!(statusbar.agent_count(), 0);
}
#[test]
fn test_statusbar_update() {
let mut statusbar = StatusBar::new();
statusbar.update(vec![
AgentStatusInfo::new(1, "feat/auth", AgentStatusKind::Running, true),
AgentStatusInfo::new(2, "fix/api", AgentStatusKind::Idle, false),
]);
assert_eq!(statusbar.agent_count(), 2);
}
#[test]
fn test_agent_status_info_new() {
let info = AgentStatusInfo::new(1, "test-branch", AgentStatusKind::Running, true);
assert_eq!(info.id, 1);
assert_eq!(info.name, "test-branch");
assert_eq!(info.status, AgentStatusKind::Running);
assert!(info.is_active);
}
#[test]
fn test_status_kind_symbol() {
assert_eq!(AgentStatusKind::Running.symbol(), "\u{25cf}");
assert_eq!(AgentStatusKind::Idle.symbol(), "\u{25cb}");
assert_eq!(AgentStatusKind::Ended.symbol(), "\u{25cb}");
assert_eq!(AgentStatusKind::Error.symbol(), "\u{2717}");
}
#[test]
fn test_status_kind_color() {
let t = theme();
assert_eq!(AgentStatusKind::Running.color(), t.status_running());
assert_eq!(AgentStatusKind::Idle.color(), t.status_idle());
assert_eq!(AgentStatusKind::Ended.color(), t.status_ended());
assert_eq!(AgentStatusKind::Error.color(), t.status_error());
}
#[test]
fn test_status_kind_text() {
assert_eq!(AgentStatusKind::Running.text(), "Running");
assert_eq!(AgentStatusKind::Idle.text(), "Idle");
assert_eq!(AgentStatusKind::Ended.text(), "Ended");
assert_eq!(AgentStatusKind::Error.text(), "Error");
}
#[test]
fn test_agents_with_status() {
let mut statusbar = StatusBar::new();
statusbar.update(vec![
AgentStatusInfo::new(1, "agent1", AgentStatusKind::Running, true),
AgentStatusInfo::new(2, "agent2", AgentStatusKind::Idle, false),
AgentStatusInfo::new(3, "agent3", AgentStatusKind::Running, false),
AgentStatusInfo::new(4, "agent4", AgentStatusKind::Ended, false),
]);
let running: Vec<_> = statusbar.agents_with_status(AgentStatusKind::Running).collect();
assert_eq!(running.len(), 2);
let idle: Vec<_> = statusbar.agents_with_status(AgentStatusKind::Idle).collect();
assert_eq!(idle.len(), 1);
let ended: Vec<_> = statusbar.agents_with_status(AgentStatusKind::Ended).collect();
assert_eq!(ended.len(), 1);
}
#[test]
fn test_statusbar_default() {
let statusbar = StatusBar::default();
assert_eq!(statusbar.agent_count(), 0);
}
}