use ratatui::{
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
Frame,
};
use unicode_width::UnicodeWidthStr;
use super::TuiPalette;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum StatusMode {
#[default]
Normal,
Plan,
Auto,
Yolo,
}
impl StatusMode {
pub fn label(&self) -> &'static str {
match self {
StatusMode::Normal => "NORMAL",
StatusMode::Plan => "PLAN",
StatusMode::Auto => "AUTO",
StatusMode::Yolo => "YOLO",
}
}
pub fn style(&self) -> Style {
match self {
StatusMode::Normal => Style::default().fg(TuiPalette::SAGE),
StatusMode::Plan => Style::default()
.fg(TuiPalette::AMBER)
.add_modifier(Modifier::BOLD),
StatusMode::Auto => Style::default()
.fg(TuiPalette::COPPER)
.add_modifier(Modifier::BOLD),
StatusMode::Yolo => Style::default()
.fg(TuiPalette::error())
.add_modifier(Modifier::BOLD),
}
}
}
impl From<crate::config::ExecutionMode> for StatusMode {
fn from(mode: crate::config::ExecutionMode) -> Self {
match mode {
crate::config::ExecutionMode::Normal => StatusMode::Normal,
crate::config::ExecutionMode::AutoEdit => StatusMode::Auto,
crate::config::ExecutionMode::Yolo => StatusMode::Yolo,
crate::config::ExecutionMode::Daemon => StatusMode::Yolo,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StatusLine {
pub model: String,
pub tokens_used: (usize, usize),
pub context_percent: f32,
pub mode: StatusMode,
pub session_id: String,
pub connected: bool,
pub status_message: Option<String>,
}
impl Default for StatusLine {
fn default() -> Self {
Self {
model: "Unknown".into(),
tokens_used: (0, 0),
context_percent: 0.0,
mode: StatusMode::Normal,
session_id: "-".into(),
connected: false,
status_message: None,
}
}
}
impl StatusLine {
pub fn new(model: &str) -> Self {
Self {
model: model.into(),
..Default::default()
}
}
pub fn with_session(model: &str) -> Self {
let session_id = format!("{:.8}", uuid::Uuid::new_v4());
Self {
model: model.into(),
session_id,
..Default::default()
}
}
pub fn total_tokens(&self) -> usize {
self.tokens_used.0 + self.tokens_used.1
}
pub fn reset_usage(&mut self) {
self.tokens_used = (0, 0);
self.context_percent = 0.0;
}
fn fmt_tokens(n: usize) -> String {
if n >= 1_000_000 {
format!("{:.1}M", n as f64 / 1_000_000.0)
} else if n >= 1_000 {
format!("{:.1}K", n as f64 / 1_000.0)
} else {
n.to_string()
}
}
fn context_style(&self) -> Style {
if self.context_percent >= 90.0 {
TuiPalette::error_style()
} else if self.context_percent >= 75.0 {
TuiPalette::warning_style()
} else {
TuiPalette::success_style()
}
}
pub fn render(&self, frame: &mut Frame, area: Rect) {
let connection_icon = if self.connected { "●" } else { "○" };
let connection_style = if self.connected {
TuiPalette::success_style()
} else {
TuiPalette::error_style()
};
let total = Self::fmt_tokens(self.total_tokens());
let input = Self::fmt_tokens(self.tokens_used.0);
let output = Self::fmt_tokens(self.tokens_used.1);
let left = vec![
Span::styled(format!(" {} ", connection_icon), connection_style),
Span::styled(
format!("{} ", self.model),
Style::default()
.fg(TuiPalette::AMBER)
.add_modifier(Modifier::BOLD),
),
Span::styled("[", TuiPalette::muted_style()),
Span::styled(self.mode.label(), self.mode.style()),
Span::styled("] ", TuiPalette::muted_style()),
];
let center = vec![
Span::styled("Tokens: ", TuiPalette::muted_style()),
Span::styled(
format!("{} ", total),
Style::default().fg(TuiPalette::COPPER),
),
Span::styled(
format!("(in:{} out:{}) ", input, output),
TuiPalette::muted_style(),
),
Span::styled("Ctx: ", TuiPalette::muted_style()),
Span::styled(
format!("{:.0}%", self.context_percent),
self.context_style(),
),
];
let mut right = vec![
Span::styled("Session: ", TuiPalette::muted_style()),
Span::styled(&self.session_id, Style::default().fg(TuiPalette::SAGE)),
];
if let Some(ref msg) = self.status_message {
right.push(Span::styled(" | ", TuiPalette::muted_style()));
right.push(Span::styled(msg, TuiPalette::muted_style()));
}
let left_text: String = left.iter().map(|s| s.content.clone()).collect();
let right_text: String = right.iter().map(|s| s.content.clone()).collect();
let center_text: String = center.iter().map(|s| s.content.clone()).collect();
let left_width = UnicodeWidthStr::width(left_text.as_str()) as u16;
let right_width = UnicodeWidthStr::width(right_text.as_str()) as u16;
let center_width = UnicodeWidthStr::width(center_text.as_str()) as u16;
let available = area.width.saturating_sub(left_width + right_width);
let padding = available.saturating_sub(center_width) / 2;
let mut spans = left;
spans.push(Span::raw(" ".repeat(padding as usize)));
spans.extend(center);
let right_padding = area
.width
.saturating_sub(left_width + padding + center_width + right_width);
spans.push(Span::raw(" ".repeat(right_padding as usize)));
spans.extend(right);
let paragraph = Paragraph::new(Line::from(spans));
frame.render_widget(paragraph, area);
}
}
#[cfg(test)]
#[path = "../../../tests/unit/ui/tui/status_line/status_line_test.rs"]
mod tests;