use ratatui::{ layout::{Constraint, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Clear, Paragraph}, Frame, }; use super::widgets::panel_contrast_fg; use crate::{ app::state::{Palette, ToastKind, ToastNotification}, detect::AgentState, }; pub(crate) fn toast_notification_rect( area: Rect, toast: &ToastNotification, offset_for_warning: bool, ) -> Rect { let content_width = (toast.title.len().max(toast.context.len()) as u16) + 4; let width = content_width.saturating_add(2).min(area.width); let height = 4u16.min(area.height); let x = area.x + area.width.saturating_sub(width); let y = area.y + area .height .saturating_sub(height + if offset_for_warning { 1 } else { 0 }); Rect::new(x, y, width, height) } pub(super) fn render_toast_notification( frame: &mut Frame, area: Rect, toast: &ToastNotification, offset_for_warning: bool, p: &Palette, ) { let dot_color = match toast.kind { ToastKind::NeedsAttention => p.red, ToastKind::Finished => p.blue, ToastKind::UpdateInstalled => p.accent, }; let toast_area = toast_notification_rect(area, toast, offset_for_warning); frame.render_widget(Clear, toast_area); let block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(p.overlay0)) .style(Style::default().bg(p.panel_bg)); let inner = block.inner(toast_area); frame.render_widget(block, toast_area); if inner.height < 2 { return; } let [title_row, context_row] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(inner); let title = Line::from(vec![ Span::styled("●", Style::default().fg(dot_color)), Span::raw(" "), Span::styled( &toast.title, Style::default().fg(p.text).add_modifier(Modifier::BOLD), ), ]); let context = Line::from(vec![ Span::styled(" ", Style::default().fg(p.overlay0)), Span::styled(&toast.context, Style::default().fg(p.overlay0)), ]); frame.render_widget(Paragraph::new(title), title_row); frame.render_widget(Paragraph::new(context), context_row); } pub(super) fn render_config_diagnostic(frame: &mut Frame, area: Rect, message: &str, p: &Palette) { let text = format!(" config warning: {message} "); let width = text.len() as u16 + 2; let notif_area = Rect::new( area.x + area.width.saturating_sub(width.min(area.width)), area.y, width.min(area.width), 1, ); frame.render_widget(Clear, notif_area); frame.render_widget( Paragraph::new(Span::styled( text, Style::default() .fg(panel_contrast_fg(p)) .bg(p.yellow) .add_modifier(Modifier::BOLD), )), notif_area, ); } pub(super) fn state_dot(state: AgentState, seen: bool, p: &Palette) -> (&'static str, Style) { match (state, seen) { (AgentState::Blocked, _) => ("●", Style::default().fg(p.red)), (AgentState::Working, _) => ("●", Style::default().fg(p.yellow)), (AgentState::Idle, false) => ("●", Style::default().fg(p.teal)), (AgentState::Idle, true) => ("○", Style::default().fg(p.green)), (AgentState::Unknown, _) => ("·", Style::default().fg(p.overlay0)), } } pub(super) fn agent_icon( state: AgentState, seen: bool, tick: u32, p: &Palette, ) -> (&'static str, Style) { match (state, seen) { (AgentState::Blocked, _) => ("◉", Style::default().fg(p.red)), (AgentState::Working, _) => (super::spinner_frame(tick), Style::default().fg(p.yellow)), (AgentState::Idle, false) => ("●", Style::default().fg(p.teal)), (AgentState::Idle, true) => ("✓", Style::default().fg(p.green)), (AgentState::Unknown, _) => ("○", Style::default().fg(p.overlay0)), } } pub(super) fn state_label(state: AgentState, seen: bool) -> &'static str { match (state, seen) { (AgentState::Blocked, _) => "blocked", (AgentState::Working, _) => "working", (AgentState::Idle, false) => "done", (AgentState::Idle, true) => "idle", (AgentState::Unknown, _) => "idle", } } pub(super) fn state_label_color(state: AgentState, seen: bool, p: &Palette) -> Color { match (state, seen) { (AgentState::Blocked, _) => p.red, (AgentState::Working, _) => p.yellow, (AgentState::Idle, false) => p.teal, (AgentState::Idle, true) => p.green, (AgentState::Unknown, _) => p.overlay0, } }