use super::TuiPalette;
use ratatui::{
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Gauge, Paragraph, Widget},
};
pub struct GardenSpinner {
frame: usize,
message: String,
}
impl GardenSpinner {
pub fn new(message: &str) -> Self {
Self {
frame: 0,
message: message.into(),
}
}
pub fn tick(&mut self) {
self.frame = (self.frame + 1) % 4;
}
fn spinner_char(&self) -> &'static str {
match self.frame {
0 => "🌱",
1 => "🌿",
2 => "🍃",
_ => "🌳",
}
}
}
impl Widget for GardenSpinner {
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
let text = format!("{} {}", self.spinner_char(), self.message);
let paragraph = Paragraph::new(text).style(Style::default().fg(TuiPalette::AMBER));
paragraph.render(area, buf);
}
}
pub struct GrowthGauge {
ratio: f64,
label: String,
}
impl GrowthGauge {
pub fn new(ratio: f64, label: &str) -> Self {
Self {
ratio: ratio.clamp(0.0, 1.0),
label: label.into(),
}
}
fn growth_stage(&self) -> &'static str {
match (self.ratio * 100.0) as u8 {
0..=25 => "Seedling",
26..=50 => "Sprouting",
51..=75 => "Growing",
76..=99 => "Flourishing",
_ => "Mature",
}
}
#[allow(dead_code)]
fn bar_chars(&self, width: usize) -> String {
let filled = ((self.ratio * width as f64) as usize).min(width);
let empty = width - filled;
format!("{}{}", "█".repeat(filled), "░".repeat(empty))
}
}
impl Widget for GrowthGauge {
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
let gauge = Gauge::default()
.block(Block::default())
.gauge_style(Style::default().fg(TuiPalette::GARDEN_GREEN))
.ratio(self.ratio)
.label(format!(
"{} {} ({:.0}%)",
self.growth_stage(),
self.label,
self.ratio * 100.0
));
gauge.render(area, buf);
}
}
pub struct StatusIndicator {
status: StatusType,
label: String,
}
#[derive(Debug, Clone, Copy)]
pub enum StatusType {
Success,
Warning,
Error,
Info,
Loading,
}
impl StatusIndicator {
pub fn new(status: StatusType, label: &str) -> Self {
Self {
status,
label: label.into(),
}
}
fn icon(&self) -> &'static str {
match self.status {
StatusType::Success => "✿",
StatusType::Warning => "🥀",
StatusType::Error => "❄️",
StatusType::Info => "📋",
StatusType::Loading => "⏳",
}
}
fn style(&self) -> Style {
match self.status {
StatusType::Success => TuiPalette::success_style(),
StatusType::Warning => TuiPalette::warning_style(),
StatusType::Error => TuiPalette::error_style(),
StatusType::Info => TuiPalette::muted_style(),
StatusType::Loading => Style::default().fg(TuiPalette::AMBER),
}
}
}
impl Widget for StatusIndicator {
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
let text = format!("{} {}", self.icon(), self.label);
let paragraph = Paragraph::new(text).style(self.style());
paragraph.render(area, buf);
}
}
pub struct ToolOutput {
tool_name: String,
output: String,
success: bool,
}
impl ToolOutput {
pub fn new(tool_name: &str, output: &str, success: bool) -> Self {
Self {
tool_name: tool_name.into(),
output: output.into(),
success,
}
}
}
impl Widget for ToolOutput {
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
let status_icon = if self.success { "✓" } else { "✗" };
let status_style = if self.success {
TuiPalette::success_style()
} else {
TuiPalette::error_style()
};
let title = Line::from(vec![
Span::styled(format!("{} ", status_icon), status_style),
Span::styled(
&self.tool_name,
Style::default()
.fg(TuiPalette::COPPER)
.add_modifier(Modifier::BOLD),
),
]);
let block = Block::default()
.borders(Borders::ALL)
.border_style(TuiPalette::border_style())
.title(title);
let inner = block.inner(area);
block.render(area, buf);
let output = Paragraph::new(self.output.as_str()).style(TuiPalette::muted_style());
output.render(inner, buf);
}
}
#[cfg(test)]
#[path = "../../../tests/unit/ui/tui/widgets/widgets_test.rs"]
mod tests;