declarative/
declarative.rs1use std::io::{self, Write};
11use std::thread;
12use std::time::Duration;
13
14use eye_declare::{Elements, InlineRenderer, Markdown, Spinner, TextBlock, VStack};
15use ratatui_core::style::{Color, Style};
16
17struct AppState {
22 thinking: bool,
23 messages: Vec<String>,
24 tool_running: Option<String>,
25}
26
27impl AppState {
28 fn new() -> Self {
29 Self {
30 thinking: false,
31 messages: Vec::new(),
32 tool_running: None,
33 }
34 }
35}
36
37fn chat_view(state: &AppState) -> Elements {
42 let mut els = Elements::new();
43
44 for (i, msg) in state.messages.iter().enumerate() {
46 els.add(Markdown::new(msg)).key(format!("msg-{i}"));
47 }
48
49 if state.thinking {
51 els.add(Spinner::new("Thinking...")).key("thinking");
52 }
53
54 if let Some(ref tool) = state.tool_running {
56 els.add(Spinner::new(format!("Running {}...", tool)))
57 .key("tool");
58 }
59
60 if !state.messages.is_empty() || state.thinking || state.tool_running.is_some() {
62 els.add(TextBlock::new().line("---", Style::default().fg(Color::DarkGray)));
63 }
64
65 els
66}
67
68fn main() -> io::Result<()> {
73 let (width, _) = crossterm::terminal::size()?;
74 let mut r = InlineRenderer::new(width);
75 let mut stdout = io::stdout();
76
77 let container = r.push(VStack);
78 let mut state = AppState::new();
79
80 state.thinking = true;
82 r.rebuild(container, chat_view(&state));
83 animate_while_active(&mut r, &mut stdout, Duration::from_millis(1500))?;
85
86 state.thinking = false;
88 state.messages.push(
89 "Here's a binary search implementation in Rust:\n\n\
90 ```rust\n\
91 fn binary_search(arr: &[i32], target: i32) -> Option<usize> {\n\
92 \x20 let mut low = 0;\n\
93 \x20 let mut high = arr.len();\n\
94 \x20 while low < high {\n\
95 \x20 let mid = low + (high - low) / 2;\n\
96 \x20 match arr[mid].cmp(&target) {\n\
97 \x20 std::cmp::Ordering::Less => low = mid + 1,\n\
98 \x20 std::cmp::Ordering::Greater => high = mid,\n\
99 \x20 std::cmp::Ordering::Equal => return Some(mid),\n\
100 \x20 }\n\
101 \x20 }\n\
102 \x20 None\n\
103 }\n\
104 ```"
105 .to_string(),
106 );
107 r.rebuild(container, chat_view(&state));
108 flush(&mut r, &mut stdout)?;
109 thread::sleep(Duration::from_millis(800));
110
111 state.tool_running = Some("cargo clippy".to_string());
113 r.rebuild(container, chat_view(&state));
114 animate_while_active(&mut r, &mut stdout, Duration::from_millis(2000))?;
116
117 state.tool_running = None;
119 state.messages.push(
120 "The implementation passes **clippy** with no warnings. \
121 The function takes a sorted slice and a target value, \
122 returning `Some(index)` if found or `None` otherwise."
123 .to_string(),
124 );
125 r.rebuild(container, chat_view(&state));
126 flush(&mut r, &mut stdout)?;
127
128 println!();
129 Ok(())
130}
131
132fn flush(r: &mut InlineRenderer, stdout: &mut impl Write) -> io::Result<()> {
137 let output = r.render();
138 if !output.is_empty() {
139 stdout.write_all(&output)?;
140 stdout.flush()?;
141 }
142 Ok(())
143}
144
145fn animate_while_active(
147 r: &mut InlineRenderer,
148 stdout: &mut impl Write,
149 max_duration: Duration,
150) -> io::Result<()> {
151 let start = std::time::Instant::now();
152 while start.elapsed() < max_duration && r.has_active() {
153 r.tick();
154 flush(r, stdout)?;
155 thread::sleep(Duration::from_millis(50));
156 }
157 Ok(())
158}