declarative/
declarative.rs1use std::io::{self, Write};
14use std::thread;
15use std::time::Duration;
16
17use eye_declare::{
18 BorderType, Canvas, Elements, InlineRenderer, Markdown, Spinner, VStack, View, component,
19 element, props,
20};
21use ratatui_core::style::{Color, Modifier, Style};
22use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
23use ratatui_widgets::paragraph::Paragraph;
24
25struct AppState {
30 thinking: bool,
31 messages: Vec<String>,
32 tool_running: Option<String>,
33}
34
35impl AppState {
36 fn new() -> Self {
37 Self {
38 thinking: false,
39 messages: Vec::new(),
40 tool_running: None,
41 }
42 }
43}
44
45#[props]
51struct Card {
52 title: String,
53}
54
55#[component(props = Card, children = Elements)]
56fn card(props: &Card, children: Elements) -> Elements {
57 element!(
58 View(
59 border: BorderType::Rounded,
60 border_style: Style::default().fg(Color::DarkGray),
61 title: props.title.clone(),
62 title_style: Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
63 padding_left: Some(eye_declare::Cells(1)),
64 padding_right: Some(eye_declare::Cells(1)),
65 ) {
66 #(children)
67 }
68 )
69}
70
71#[props]
77struct Badge {
78 label: String,
79 #[default(Color::Green)]
80 color: Color,
81}
82
83#[component(props = Badge)]
84fn badge(props: &Badge) -> Elements {
85 let label = props.label.clone();
86 let color = props.color;
87
88 element!(
89 Canvas(render_fn: move |area: Rect, buf: &mut Buffer| {
90 let line = Line::styled(
91 format!(" {} ", label),
92 Style::default().fg(Color::Black).bg(color),
93 );
94 Paragraph::new(line).render(area, buf);
95 })
96 )
97}
98
99fn chat_view(state: &AppState) -> Elements {
104 element!(
105 #(for (i, msg) in state.messages.iter().enumerate() {
106 Card(key: format!("msg-{i}"), title: "Response") {
107 Markdown(key: format!("msg-{i}"), source: msg.clone())
108 }
109 })
110
111 #(if state.thinking {
112 Spinner(key: "thinking", label: "Thinking...")
113 })
114
115 #(if let Some(ref tool) = state.tool_running {
116 Spinner(key: "tool", label: format!("Running {}...", tool))
117 })
118
119 #(if !state.messages.is_empty() && state.tool_running.is_none() && !state.thinking {
120 Badge(key: "done", label: "Done", color: Color::Green)
121 })
122 )
123}
124
125fn main() -> io::Result<()> {
130 let (width, _) = crossterm::terminal::size()?;
131 let mut r = InlineRenderer::new(width);
132 let mut stdout = io::stdout();
133
134 let container = r.push(VStack);
135 let mut state = AppState::new();
136
137 state.thinking = true;
139 r.rebuild(container, chat_view(&state));
140 animate_while_active(&mut r, &mut stdout, Duration::from_millis(1500))?;
142
143 state.thinking = false;
145 state.messages.push(
146 "Here's a binary search implementation in Rust:\n\n\
147 ```rust\n\
148 fn binary_search(arr: &[i32], target: i32) -> Option<usize> {\n\
149 \x20 let mut low = 0;\n\
150 \x20 let mut high = arr.len();\n\
151 \x20 while low < high {\n\
152 \x20 let mid = low + (high - low) / 2;\n\
153 \x20 match arr[mid].cmp(&target) {\n\
154 \x20 std::cmp::Ordering::Less => low = mid + 1,\n\
155 \x20 std::cmp::Ordering::Greater => high = mid,\n\
156 \x20 std::cmp::Ordering::Equal => return Some(mid),\n\
157 \x20 }\n\
158 \x20 }\n\
159 \x20 None\n\
160 }\n\
161 ```"
162 .to_string(),
163 );
164 r.rebuild(container, chat_view(&state));
165 flush(&mut r, &mut stdout)?;
166 thread::sleep(Duration::from_millis(800));
167
168 state.tool_running = Some("cargo clippy".to_string());
170 r.rebuild(container, chat_view(&state));
171 animate_while_active(&mut r, &mut stdout, Duration::from_millis(2000))?;
173
174 state.tool_running = None;
176 state.messages.push(
177 "The implementation passes **clippy** with no warnings. \
178 The function takes a sorted slice and a target value, \
179 returning `Some(index)` if found or `None` otherwise."
180 .to_string(),
181 );
182 r.rebuild(container, chat_view(&state));
183 flush(&mut r, &mut stdout)?;
184
185 println!();
186 Ok(())
187}
188
189fn flush(r: &mut InlineRenderer, stdout: &mut impl Write) -> io::Result<()> {
194 let output = r.render();
195 if !output.is_empty() {
196 stdout.write_all(&output)?;
197 stdout.flush()?;
198 }
199 Ok(())
200}
201
202fn animate_while_active(
204 r: &mut InlineRenderer,
205 stdout: &mut impl Write,
206 max_duration: Duration,
207) -> io::Result<()> {
208 let start = std::time::Instant::now();
209 while start.elapsed() < max_duration && r.has_active() {
210 r.tick();
211 flush(r, stdout)?;
212 thread::sleep(Duration::from_millis(50));
213 }
214 Ok(())
215}