use agcodex_core::modes::ModeColor;
use agcodex_core::modes::OperatingMode;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::Block;
use ratatui::widgets::Borders;
use ratatui::widgets::Widget;
use ratatui::widgets::WidgetRef;
use std::time::Duration;
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct ModeIndicator {
mode: OperatingMode,
focused: bool,
transition_start: Option<Instant>,
previous_mode: Option<OperatingMode>,
}
impl ModeIndicator {
pub const fn new(mode: OperatingMode) -> Self {
Self {
mode,
focused: false,
transition_start: None,
previous_mode: None,
}
}
pub fn with_transition(mode: OperatingMode, previous_mode: OperatingMode) -> Self {
Self {
mode,
focused: false,
transition_start: Some(Instant::now()),
previous_mode: Some(previous_mode),
}
}
pub const fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
fn transition_progress(&self) -> f32 {
if let Some(start) = self.transition_start {
let elapsed = start.elapsed();
let duration = Duration::from_millis(300); let progress = elapsed.as_secs_f32() / duration.as_secs_f32();
progress.min(1.0)
} else {
1.0
}
}
fn get_transition_color(&self) -> Color {
let progress = self.transition_progress();
if progress >= 1.0 {
let visuals = self.mode.visuals();
Self::mode_color_to_ratatui(visuals.color)
} else if let Some(prev_mode) = self.previous_mode {
let prev_visuals = prev_mode.visuals();
let curr_visuals = self.mode.visuals();
if progress < 0.5 {
Self::mode_color_to_ratatui(prev_visuals.color)
} else {
Self::mode_color_to_ratatui(curr_visuals.color)
}
} else {
let visuals = self.mode.visuals();
Self::mode_color_to_ratatui(visuals.color)
}
}
const fn mode_color_to_ratatui(mode_color: ModeColor) -> Color {
match mode_color {
ModeColor::Blue => Color::Blue,
ModeColor::Green => Color::Green,
ModeColor::Yellow => Color::Yellow,
}
}
fn get_style(&self) -> Style {
let color = self.get_transition_color();
let progress = self.transition_progress();
let mut style = Style::default()
.fg(Color::Black)
.bg(color)
.add_modifier(Modifier::BOLD);
if progress < 1.0 {
style = style.add_modifier(Modifier::ITALIC);
}
style
}
fn get_border_style(&self) -> Style {
let color = self.get_transition_color();
let progress = self.transition_progress();
let mut style = Style::default().fg(color);
if progress < 1.0 {
style = style.add_modifier(Modifier::BOLD);
}
style
}
fn get_indicator_text(&self) -> String {
let visuals = self.mode.visuals();
let progress = self.transition_progress();
if progress < 1.0 {
let anim_chars = ["⬤", "◉", "◎", "○"];
let index = ((1.0 - progress) * anim_chars.len() as f32) as usize;
let anim = anim_chars.get(index).unwrap_or(&anim_chars[0]);
format!("{} {}", anim, visuals.indicator)
} else {
visuals.indicator.to_string()
}
}
}
impl Widget for ModeIndicator {
fn render(self, area: Rect, buf: &mut Buffer) {
WidgetRef::render_ref(&self, area, buf);
}
}
impl WidgetRef for ModeIndicator {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let visuals = self.mode.visuals();
let style = self.get_style();
let border_style = self.get_border_style();
let indicator_text = self.get_indicator_text();
let indicator_span = Span::styled(indicator_text, style);
let indicator_line = Line::from(vec![indicator_span]);
let borders = if self.transition_progress() < 1.0 {
Borders::ALL | Borders::NONE } else {
Borders::ALL
};
let block = Block::default().borders(borders).border_style(border_style);
if self.focused && area.height > 3 {
let lines = [
indicator_line,
Line::from(Span::styled(
if self.transition_progress() < 1.0 {
format!("➜ {}", visuals.description)
} else {
visuals.description.to_string()
},
Style::default().fg(border_style.fg.unwrap_or(Color::White)),
)),
];
let inner_area = block.inner(area);
block.render(area, buf);
for (i, line) in lines.iter().enumerate() {
if i < inner_area.height as usize {
line.render(
Rect {
x: inner_area.x,
y: inner_area.y + i as u16,
width: inner_area.width,
height: 1,
},
buf,
);
}
}
} else {
let inner_area = block.inner(area);
block.render(area, buf);
indicator_line.render(inner_area, buf);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
#[test]
fn test_mode_indicator_creation() {
let indicator = ModeIndicator::new(OperatingMode::Plan);
assert_eq!(indicator.mode, OperatingMode::Plan);
assert!(!indicator.focused);
}
#[test]
fn test_mode_indicator_focused() {
let indicator = ModeIndicator::new(OperatingMode::Build).focused(true);
assert!(indicator.focused);
}
#[test]
fn test_mode_color_conversion() {
assert_eq!(
ModeIndicator::mode_color_to_ratatui(ModeColor::Blue),
Color::Blue
);
assert_eq!(
ModeIndicator::mode_color_to_ratatui(ModeColor::Green),
Color::Green
);
assert_eq!(
ModeIndicator::mode_color_to_ratatui(ModeColor::Yellow),
Color::Yellow
);
}
#[test]
fn test_widget_renders() {
let indicator = ModeIndicator::new(OperatingMode::Plan);
let area = Rect::new(0, 0, 20, 3);
let mut buf = Buffer::empty(area);
indicator.render(area, &mut buf);
assert!(!buf.content().is_empty());
}
}