1use std::io::{self, Write};
10use std::thread;
11use std::time::Duration;
12
13use eye_declare::{Component, Elements, Hooks, InlineRenderer, Spinner, Text, VStack};
14use ratatui_core::{
15 buffer::Buffer,
16 layout::Rect,
17 style::{Color, Modifier, Style},
18 text::{Line, Span},
19 widgets::Widget,
20};
21use ratatui_widgets::paragraph::Paragraph;
22
23struct StatusLog {
29 name: String,
30}
31
32impl StatusLog {
33 fn new(name: impl Into<String>) -> Self {
34 Self { name: name.into() }
35 }
36}
37
38#[derive(Default)]
39struct StatusLogState {
40 entries: Vec<(String, Style)>,
41}
42
43impl StatusLogState {
44 fn log(&mut self, msg: impl Into<String>, style: Style) {
45 self.entries.push((msg.into(), style));
46 }
47}
48
49impl Component for StatusLog {
50 type State = StatusLogState;
51
52 fn render(&self, area: Rect, buf: &mut Buffer, state: &Self::State) {
53 let lines: Vec<Line> = state
54 .entries
55 .iter()
56 .map(|(text, style)| Line::from(Span::styled(text.as_str(), *style)))
57 .collect();
58 Paragraph::new(lines).render(area, buf);
59 }
60
61 fn initial_state(&self) -> Option<StatusLogState> {
62 let mut state = StatusLogState {
63 entries: Vec::new(),
64 };
65 if !self.name.is_empty() {
66 state.log(
67 format!(" {} created", self.name),
68 Style::default().fg(Color::DarkGray),
69 );
70 }
71 Some(state)
72 }
73
74 fn lifecycle(&self, hooks: &mut Hooks<Self, StatusLogState>, _state: &StatusLogState) {
75 if !self.name.is_empty() {
76 let mount_name = self.name.clone();
77 hooks.use_mount(move |_props, state| {
78 state.log(
79 format!(" {} mounted", mount_name),
80 Style::default()
81 .fg(Color::Green)
82 .add_modifier(Modifier::ITALIC),
83 );
84 });
85
86 let unmount_name = self.name.clone();
87 hooks.use_unmount(move |_props, state| {
88 state.log(
89 format!(" {} unmounted", unmount_name),
90 Style::default()
91 .fg(Color::Red)
92 .add_modifier(Modifier::ITALIC),
93 );
94 });
95 }
96 }
97}
98
99struct AppState {
104 tasks: Vec<String>,
105 processing: bool,
106}
107
108fn task_view(state: &AppState) -> Elements {
109 let mut els = Elements::new();
110
111 els.add(Text::styled(
112 format!("Tasks ({})", state.tasks.len()),
113 Style::default()
114 .fg(Color::White)
115 .add_modifier(Modifier::BOLD),
116 ));
117
118 for task in &state.tasks {
119 els.add(StatusLog::new(task)).key(task.clone());
120 }
121
122 if state.processing {
123 els.add(Spinner::new("Processing...")).key("spinner");
124 }
125
126 els.add(Text::styled("---", Style::default().fg(Color::DarkGray)));
127
128 els
129}
130
131fn main() -> io::Result<()> {
136 let (width, _) = crossterm::terminal::size()?;
137 let mut r = InlineRenderer::new(width);
138 let mut stdout = io::stdout();
139
140 let container = r.push(VStack);
141 let mut state = AppState {
142 tasks: vec!["Alpha".into(), "Beta".into(), "Gamma".into()],
143 processing: false,
144 };
145
146 r.rebuild(container, task_view(&state));
148 flush(&mut r, &mut stdout)?;
149 thread::sleep(Duration::from_millis(1000));
150
151 state.tasks.retain(|t| t != "Beta");
153 r.rebuild(container, task_view(&state));
154 flush(&mut r, &mut stdout)?;
155 thread::sleep(Duration::from_millis(1000));
156
157 state.tasks.push("Delta".into());
159 r.rebuild(container, task_view(&state));
160 flush(&mut r, &mut stdout)?;
161 thread::sleep(Duration::from_millis(1000));
162
163 state.processing = true;
165 r.rebuild(container, task_view(&state));
166 let start = std::time::Instant::now();
168 while start.elapsed() < Duration::from_millis(1500) && r.has_active() {
169 r.tick();
170 flush(&mut r, &mut stdout)?;
171 thread::sleep(Duration::from_millis(50));
172 }
173
174 state.tasks.clear();
176 state.processing = false;
177 r.rebuild(container, task_view(&state));
178 flush(&mut r, &mut stdout)?;
179
180 println!();
181 Ok(())
182}
183
184fn flush(r: &mut InlineRenderer, stdout: &mut impl Write) -> io::Result<()> {
185 let output = r.render();
186 if !output.is_empty() {
187 stdout.write_all(&output)?;
188 stdout.flush()?;
189 }
190 Ok(())
191}