use ratatui::{
prelude::*,
widgets::{Paragraph, Widget},
};
use crate::session::TokenUsage;
#[derive(Debug, Clone, Copy)]
pub enum InputMode {
Normal,
Insert,
View,
Command,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum ActivityState {
#[default]
Idle,
Active(usize),
}
const BARCOLOR: Color = Color::Rgb(0, 122, 204);
impl InputMode {
fn label(self) -> &'static str {
match self {
Self::Normal | Self::Command => "NORMAL",
Self::Insert => "INSERT",
Self::View => "VIEW",
}
}
fn colors(self) -> (Color, Color) {
match self {
Self::Normal | Self::Command => (Color::White, Color::Blue),
Self::Insert => (Color::White, Color::Yellow),
Self::View => (Color::White, Color::Magenta),
}
}
}
#[derive(Debug, Clone)]
pub struct StatusBarConfig {
pub idle_symbol: String,
pub activity_symbols: Vec<String>,
pub input_tokens_symbol: String,
pub output_tokens_symbol: String,
pub leader_symbol: String,
pub separator_symbol: String,
pub reverse_separator_symbol: String,
}
#[derive(Debug, Clone)]
pub struct StatusBar {
pub acitivity_state: ActivityState,
pub config: StatusBarConfig,
pub input_mode: InputMode,
pub model: String,
pub context_used: u64,
pub context_total: u64,
pub current_line: u64,
pub total_lines: u64,
pub tokens: TokenUsage,
pub status_message: String,
}
impl Widget for StatusBar {
fn render(self, area: Rect, buf: &mut Buffer) {
let (mode_fg, mode_bg) = self.input_mode.colors();
let ctx_percent = (self.context_used * 100)
.checked_div(self.context_total)
.unwrap_or_default();
let left_spans = vec![
Span::styled(
format!(" {} ", self.input_mode.label()),
Style::default()
.fg(mode_fg)
.bg(mode_bg)
.add_modifier(Modifier::BOLD),
),
Span::styled(
&self.config.leader_symbol,
Style::default().fg(mode_bg).bg(BARCOLOR),
),
Span::styled(
format!(" {} ", self.model),
Style::default().fg(Color::White).bg(BARCOLOR),
),
Span::styled(
&self.config.separator_symbol,
Style::default().fg(Color::White).bg(BARCOLOR),
),
Span::styled(
format!(
"{:>3}% ({:>3}k of {:>3}k)",
ctx_percent,
self.context_used / 1000,
self.context_total / 1000
),
Style::default().fg(Color::White).bg(BARCOLOR),
),
Span::styled(
&self.config.separator_symbol,
Style::default().fg(Color::White).bg(BARCOLOR),
),
Span::styled(
format!(
" {}{} {}{} ",
self.config.input_tokens_symbol,
self.render_tokens(self.tokens.input),
self.config.output_tokens_symbol,
self.render_tokens(self.tokens.output)
),
Style::default().fg(Color::White).bg(BARCOLOR),
),
Span::styled(
&self.config.separator_symbol,
Style::default().fg(Color::White).bg(BARCOLOR),
),
Span::styled(
format!(" {} ", self.status_message),
Style::default().fg(Color::White).bg(BARCOLOR),
),
];
let right_spans = vec![
Span::styled(
format!(" {} ", self.acitivity_symbol()),
Style::default().fg(Color::White).bg(BARCOLOR),
),
Span::styled(
&self.config.reverse_separator_symbol,
Style::default().fg(Color::White).bg(BARCOLOR),
),
Span::styled(
format!(" {} of {} ", self.current_line, self.total_lines),
Style::default().fg(Color::White).bg(BARCOLOR),
),
];
let [left_area, right_area] =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(20)]).areas(area);
Paragraph::new(Line::from(left_spans))
.style(Style::default().bg(BARCOLOR))
.render(left_area, buf);
Paragraph::new(Line::from(right_spans))
.alignment(HorizontalAlignment::Right)
.style(Style::default().bg(BARCOLOR))
.render(right_area, buf);
}
}
impl StatusBar {
pub fn indicate_activity(&mut self) {
if self.config.activity_symbols.is_empty() {
self.acitivity_state = ActivityState::Idle;
} else {
match self.acitivity_state {
ActivityState::Idle => self.acitivity_state = ActivityState::Active(0),
ActivityState::Active(index) => {
if index + 1 >= self.config.activity_symbols.len() {
self.acitivity_state = ActivityState::Active(0)
} else {
self.acitivity_state = ActivityState::Active(index + 1);
}
}
}
}
}
pub fn indicate_idle(&mut self) {
self.acitivity_state = ActivityState::Idle;
}
fn acitivity_symbol(&self) -> &str {
match self.acitivity_state {
ActivityState::Idle => &self.config.idle_symbol,
ActivityState::Active(index) => self
.config
.activity_symbols
.get(index)
.unwrap_or(&self.config.idle_symbol),
}
}
fn render_tokens(&self, num_tokens: Option<u64>) -> String {
if let Some(num) = num_tokens {
if num > 1_000_000_000 {
format!("{:>3}B", num / 1_000_000_000)
} else if num > 1_000_000 {
format!("{:>3}M", num / 1_000_000)
} else if num > 1_000 {
format!("{:>3}k", num / 1000)
} else {
format!("{:>3}", num)
}
} else {
"-".to_string()
}
}
}