//! Status bar rendering for the TUI — modern segmented bar design. //! //! Flow: `draw_status_bar` reads live connection/turn state off //! `AppStateRest` every frame and paints a single-line bar at the //! bottom of the screen with three visual segments: //! [app name + status badge] [spinner + info] [provider · model · tokens] //! //! Design: the status bar uses a dark background with carefully //! spaced segments so information is scannable at a glance. use ratatui::layout::Rect; use ratatui::style::{Style, Modifier}; use ratatui::text::{Line, Span}; use ratatui::widgets::Block; use ratatui::Frame; use super::theme::Theme; /// Render the single-line status bar. /// /// Layout (left-to-right, space-filling): /// LEFT: [zesdex] + status indicator (READY/PROG/NOAPI) /// CENTER: spinner + optional contextual info /// RIGHT: provider · model · ↑tokens_in ↓tokens_out pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) { let spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; // ── Agent status badge ──────────────────────────────────────────────── let (status_text, badge_bg, status_fg) = if state.turn_in_flight() { let f = spinner_frames[(state.misc.tick_count as usize / 2) % spinner_frames.len()]; (format!(" {} PROG ", f), Theme::MODE_YOLO, Theme::BG) } else if state.misc.api_connected { (" READY ".to_string(), Theme::MODE_AUTO, Theme::BG) } else { (" NOAPI ".to_string(), Theme::TEXT_DIM, Theme::BG) }; let status_badge = Span::styled( status_text, Style::default() .fg(status_fg) .bg(badge_bg) .add_modifier(Modifier::BOLD), ); // ── Left segment: app name ──────────────────────────────────────────── let left_spans = vec![ Span::styled( " ⚡zesdex ", Style::default() .fg(Theme::TEXT) .add_modifier(Modifier::BOLD), ), status_badge, ]; // ── Right segment: metadata ─────────────────────────────────────────── let right_str = if let Some(ref rt) = state.session_runtime { let max_tokens = state.app_config.model_roles.values() .find(|role| role.provider == state.settings.provider && role.model == state.settings.model) .and_then(|role| role.context_window); let total_chars: usize = rt.messages.iter() .filter_map(|m| m.content.as_deref()) .map(|c| c.len()) .sum(); let current_tokens = total_chars / 4; let mut parts = Vec::new(); if rt.usage.last_tokens_in > 0 || rt.usage.last_tokens_out > 0 { parts.push(format!("↑{} ↓{}", rt.usage.last_tokens_in, rt.usage.last_tokens_out)); } let max_str = max_tokens.map(|v| v.to_string()).unwrap_or_else(|| "?".to_string()); parts.push(format!("{}/{}", current_tokens, max_str)); parts.push(state.settings.provider.clone()); parts.push(state.settings.model.clone()); format!(" {} ", parts.join(" · ")) } else { let max_tokens = state.app_config.model_roles.values() .find(|role| role.provider == state.settings.provider && role.model == state.settings.model) .and_then(|role| role.context_window); let max_str = max_tokens.map(|v| v.to_string()).unwrap_or_else(|| "?".to_string()); format!(" 0/{} · {} · {} ", max_str, state.settings.provider, state.settings.model) }; // ── Combine everything ──────────────────────────────────────────────── let left_line = Line::from(left_spans); let right_line = Line::from(Span::styled( right_str, Style::default().fg(Theme::TEXT_MUTED), )); // Render the bar using two columns use ratatui::layout::{Constraint, Direction, Layout}; let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Length(25), Constraint::Min(10), ]) .split(area); let block = Block::default() .style( Style::default() .bg(Theme::STATUS_BAR_BG) .fg(Theme::TEXT), ); // Left part let left_para = ratatui::widgets::Paragraph::new(left_line).block(block.clone()); frame.render_widget(left_para, chunks[0]); // Right part let right_para = ratatui::widgets::Paragraph::new(right_line) .block(block) .alignment(ratatui::layout::Alignment::Right); frame.render_widget(right_para, chunks[1]); }