use std::collections::HashMap;
use std::time::SystemTime;
use pixtuoid_core::state::ActivityState;
use pixtuoid_core::SceneState;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::text::Span;
use ratatui::widgets::Paragraph;
use super::{to_color, TickerQueue};
use crate::tui::renderer::clip_widget_rect;
pub(in crate::tui) fn paint_theme_picker(
f: &mut ratatui::Frame<'_>,
selected: usize,
bounds: Rect,
theme: &crate::tui::theme::Theme,
) {
use crate::tui::theme;
use ratatui::style::Modifier;
use ratatui::text::{Line, Span as TSpan};
use ratatui::widgets::{Block, Borders, Clear};
let w = 30u16;
let h = (theme::ALL_THEMES.len() as u16 + 2).min(bounds.height);
let x = bounds.width.saturating_sub(w) / 2;
let y = bounds.height.saturating_sub(h) / 2;
let area = Rect {
x,
y,
width: w,
height: h,
};
f.render_widget(Clear, area);
let items: Vec<Line> = theme::ALL_THEMES
.iter()
.enumerate()
.map(|(i, t)| {
let prefix = if i == selected { "▸ " } else { " " };
let style = if i == selected {
Style::default()
.fg(to_color(theme.ui.neon_brand))
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(to_color(theme.ui.label_idle))
};
Line::from(TSpan::styled(format!("{prefix}{}", t.name), style))
})
.collect();
let block = Block::default()
.title(" Theme [↑↓/jk] Enter/Esc ")
.borders(Borders::ALL)
.border_style(Style::default().fg(to_color(theme.ui.neon_brand)))
.style(Style::default().bg(to_color(theme.ui.tooltip_bg)));
f.render_widget(Paragraph::new(items).block(block), area);
}
pub(in crate::tui) fn paint_footer(
f: &mut ratatui::Frame<'_>,
scene: &SceneState,
full_rect: Rect,
theme: &crate::tui::theme::Theme,
floor_info: Option<(usize, usize)>,
) {
let summary = build_status_summary(scene, full_rect.width, floor_info);
let footer = Paragraph::new(Span::raw(summary))
.style(Style::default().fg(to_color(theme.ui.label_idle)));
f.render_widget(
footer,
Rect {
x: full_rect.x,
y: full_rect.y + full_rect.height.saturating_sub(1),
width: full_rect.width,
height: 1,
},
);
}
pub(in crate::tui) fn build_status_summary(
scene: &SceneState,
term_width: u16,
floor_info: Option<(usize, usize)>,
) -> String {
let n = scene.agents.len();
let mut active = 0usize;
let mut waiting = 0usize;
let mut idle = 0usize;
let mut tool_counts: HashMap<&str, usize> = HashMap::new();
for slot in scene.agents.values() {
match &slot.state {
ActivityState::Idle => idle += 1,
ActivityState::Waiting { .. } => waiting += 1,
ActivityState::Active { detail, .. } => {
active += 1;
if let Some(d) = detail.as_deref() {
let token = d.split(|c: char| !c.is_alphanumeric()).next().unwrap_or("");
if !token.is_empty() {
*tool_counts.entry(token).or_insert(0) += 1;
}
}
}
}
}
let floor_suffix = match floor_info {
Some((current, total)) if total > 1 => format!(" F{current}/{total} [\u{2191}\u{2193}]"),
_ => String::new(),
};
let quit_base = " [p]ause [t]heme [q]uit ";
let quit = format!("{floor_suffix}{quit_base}");
let tools_str = {
let mut tools: Vec<(&&str, &usize)> = tool_counts.iter().collect();
tools.sort_by(|a, b| b.1.cmp(a.1).then(a.0.cmp(b.0)));
tools
.iter()
.take(4)
.map(|(name, count)| format!("{name}×{count}"))
.collect::<Vec<_>>()
.join(" ")
};
let stats_full = if n == 0 {
" 0 agents ".to_string()
} else {
let mut s = format!(" {n} agents · {active} active · {waiting} waiting · {idle} idle");
if !tools_str.is_empty() {
s.push_str(" · ");
s.push_str(&tools_str);
}
s.push(' ');
s
};
let stats_medium = format!(" {n}a · {active}A · {waiting}W · {idle}I ");
let stats_min = format!(" {n}a ");
let w = term_width as usize;
let q = quit.len();
for stats in [&stats_full, &stats_medium, &stats_min] {
if stats.len() + q <= w {
let pad = w.saturating_sub(stats.len() + q);
let mut out = String::with_capacity(w);
out.push_str(stats);
for _ in 0..pad {
out.push(' ');
}
out.push_str(&quit);
return out;
}
}
quit
}
#[allow(clippy::too_many_arguments)]
pub(in crate::tui) fn paint_wall_display(
f: &mut ratatui::Frame<'_>,
scene: &SceneState,
scene_rect: Rect,
now: SystemTime,
ticker: &TickerQueue,
theme: &crate::tui::theme::Theme,
floor_info: Option<(usize, usize)>,
) {
use ratatui::style::Modifier;
use ratatui::text::Line;
let cell_x = scene_rect.x + 2;
let cell_y = scene_rect.y + 1;
let live: Vec<&pixtuoid_core::AgentSlot> = scene
.agents
.values()
.filter(|a| a.exiting_at.is_none())
.collect();
let active = live
.iter()
.filter(|a| matches!(a.state, ActivityState::Active { .. }))
.count();
let waiting = live
.iter()
.filter(|a| matches!(a.state, ActivityState::Waiting { .. }))
.count();
let idle = live.len() - active - waiting;
let version = env!("CARGO_PKG_VERSION");
let mut top_spans = vec![
Span::styled(
format!("pixtuoid v{version}"),
Style::default()
.fg(to_color(theme.ui.neon_brand))
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(
"\u{2605} Star",
Style::default()
.fg(to_color(theme.ui.neon_star))
.add_modifier(Modifier::BOLD),
),
];
if let Some((current, total)) = floor_info {
if total > 1 {
top_spans.push(Span::raw(" "));
top_spans.push(Span::styled(
format!("Floor {current}/{total}"),
Style::default().fg(to_color(theme.ui.neon_brand)),
));
}
}
let top_line = Line::from(top_spans);
let oldest = live
.iter()
.filter_map(|a| now.duration_since(a.created_at).ok())
.max()
.unwrap_or_default();
let uptime_secs = oldest.as_secs();
let uptime_str = if uptime_secs >= 3600 {
format!(
"\u{2191}{}h{}m",
uptime_secs / 3600,
(uptime_secs % 3600) / 60
)
} else if uptime_secs >= 60 {
format!("\u{2191}{}m", uptime_secs / 60)
} else {
"\u{2191}<1m".to_string()
};
let bot_line = Line::from(vec![
Span::styled(
"\u{25cf}".repeat(active),
Style::default().fg(to_color(theme.ui.label_active)),
),
Span::styled(
"\u{25cf}".repeat(waiting),
Style::default().fg(to_color(theme.ui.label_waiting)),
),
Span::styled(
"\u{25cf}".repeat(idle),
Style::default().fg(to_color(theme.ui.label_idle)),
),
Span::raw(" "),
Span::styled(uptime_str, Style::default().fg(Color::DarkGray)),
]);
let ticker_width = 28usize;
let visible = ticker.visible(ticker_width, now);
let ticker_line = Line::from(Span::styled(
visible,
Style::default().fg(to_color(theme.ui.neon_ticker)),
));
let w = 30u16;
if let Some(r) = clip_widget_rect(
Rect {
x: cell_x,
y: cell_y,
width: w,
height: 3,
},
scene_rect,
) {
f.render_widget(Paragraph::new(vec![top_line, bot_line, ticker_line]), r);
}
}
pub(in crate::tui) fn paint_elevator_indicator(
f: &mut ratatui::Frame<'_>,
door: crate::tui::layout::Point,
current_floor: usize,
scene_rect: Rect,
theme: &crate::tui::theme::Theme,
) {
use ratatui::style::Modifier;
use ratatui::text::Line;
let label = format!(" \u{25b2} F{current_floor} \u{25bc} ");
let label_w = label.len() as u16;
let door_cell_x = door.x + 8u16.saturating_sub(label_w / 2);
let door_cell_y = door.y / 2;
let indicator_y = door_cell_y.saturating_sub(1);
if let Some(r) = crate::tui::renderer::clip_widget_rect(
Rect {
x: scene_rect.x + door_cell_x,
y: scene_rect.y + indicator_y,
width: label_w,
height: 1,
},
scene_rect,
) {
let style = Style::default()
.fg(to_color(theme.ui.neon_brand))
.bg(to_color(theme.ui.tooltip_bg))
.add_modifier(Modifier::BOLD);
f.render_widget(Paragraph::new(Line::from(Span::styled(label, style))), r);
}
}