use std::io::{self, Write};
use std::thread;
use std::time::Duration;
use eye_declare::{
BorderType, Canvas, Elements, InlineRenderer, Markdown, Spinner, VStack, View, component,
element, props,
};
use ratatui_core::style::{Color, Modifier, Style};
use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
use ratatui_widgets::paragraph::Paragraph;
struct AppState {
thinking: bool,
messages: Vec<String>,
tool_running: Option<String>,
}
impl AppState {
fn new() -> Self {
Self {
thinking: false,
messages: Vec::new(),
tool_running: None,
}
}
}
#[props]
struct Card {
title: String,
}
#[component(props = Card, children = Elements)]
fn card(props: &Card, children: Elements) -> Elements {
element!(
View(
border: BorderType::Rounded,
border_style: Style::default().fg(Color::DarkGray),
title: props.title.clone(),
title_style: Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
padding_left: Some(eye_declare::Cells(1)),
padding_right: Some(eye_declare::Cells(1)),
) {
#(children)
}
)
}
#[props]
struct Badge {
label: String,
#[default(Color::Green)]
color: Color,
}
#[component(props = Badge)]
fn badge(props: &Badge) -> Elements {
let label = props.label.clone();
let color = props.color;
element!(
Canvas(render_fn: move |area: Rect, buf: &mut Buffer| {
let line = Line::styled(
format!(" {} ", label),
Style::default().fg(Color::Black).bg(color),
);
Paragraph::new(line).render(area, buf);
})
)
}
fn chat_view(state: &AppState) -> Elements {
element!(
#(for (i, msg) in state.messages.iter().enumerate() {
Card(key: format!("msg-{i}"), title: "Response") {
Markdown(key: format!("msg-{i}"), source: msg.clone())
}
})
#(if state.thinking {
Spinner(key: "thinking", label: "Thinking...")
})
#(if let Some(ref tool) = state.tool_running {
Spinner(key: "tool", label: format!("Running {}...", tool))
})
#(if !state.messages.is_empty() && state.tool_running.is_none() && !state.thinking {
Badge(key: "done", label: "Done", color: Color::Green)
})
)
}
fn main() -> io::Result<()> {
let (width, _) = crossterm::terminal::size()?;
let mut r = InlineRenderer::new(width);
let mut stdout = io::stdout();
let container = r.push(VStack);
let mut state = AppState::new();
state.thinking = true;
r.rebuild(container, chat_view(&state));
animate_while_active(&mut r, &mut stdout, Duration::from_millis(1500))?;
state.thinking = false;
state.messages.push(
"Here's a binary search implementation in Rust:\n\n\
```rust\n\
fn binary_search(arr: &[i32], target: i32) -> Option<usize> {\n\
\x20 let mut low = 0;\n\
\x20 let mut high = arr.len();\n\
\x20 while low < high {\n\
\x20 let mid = low + (high - low) / 2;\n\
\x20 match arr[mid].cmp(&target) {\n\
\x20 std::cmp::Ordering::Less => low = mid + 1,\n\
\x20 std::cmp::Ordering::Greater => high = mid,\n\
\x20 std::cmp::Ordering::Equal => return Some(mid),\n\
\x20 }\n\
\x20 }\n\
\x20 None\n\
}\n\
```"
.to_string(),
);
r.rebuild(container, chat_view(&state));
flush(&mut r, &mut stdout)?;
thread::sleep(Duration::from_millis(800));
state.tool_running = Some("cargo clippy".to_string());
r.rebuild(container, chat_view(&state));
animate_while_active(&mut r, &mut stdout, Duration::from_millis(2000))?;
state.tool_running = None;
state.messages.push(
"The implementation passes **clippy** with no warnings. \
The function takes a sorted slice and a target value, \
returning `Some(index)` if found or `None` otherwise."
.to_string(),
);
r.rebuild(container, chat_view(&state));
flush(&mut r, &mut stdout)?;
println!();
Ok(())
}
fn flush(r: &mut InlineRenderer, stdout: &mut impl Write) -> io::Result<()> {
let output = r.render();
if !output.is_empty() {
stdout.write_all(&output)?;
stdout.flush()?;
}
Ok(())
}
fn animate_while_active(
r: &mut InlineRenderer,
stdout: &mut impl Write,
max_duration: Duration,
) -> io::Result<()> {
let start = std::time::Instant::now();
while start.elapsed() < max_duration && r.has_active() {
r.tick();
flush(r, stdout)?;
thread::sleep(Duration::from_millis(50));
}
Ok(())
}