Skip to main content

chatty_rs/app/
initializer.rs

1#[cfg(test)]
2#[path = "initializer_test.rs"]
3mod tests;
4
5#[allow(unused_imports)]
6use crate::config;
7
8use crate::models::NoticeKind;
9use crate::models::NoticeMessage;
10use crate::{
11    config::constants::FRAME_DURATION,
12    models::task::{Task, TaskEvent},
13};
14use crossterm::event::Event as CrosstermEvent;
15use crossterm::event::EventStream;
16use eyre::Result;
17use futures::FutureExt;
18use once_cell::sync::OnceCell;
19use ratatui::Terminal;
20use ratatui::prelude::Backend;
21use ratatui::prelude::CrosstermBackend;
22use ratatui::style::Stylize;
23use ratatui::text::Line;
24use ratatui::widgets::Block;
25use ratatui::widgets::Paragraph;
26use ratatui::widgets::Widget;
27use ratatui_macros::span;
28use tokio::{sync::mpsc, time};
29use tui_textarea::Input;
30use tui_textarea::Key;
31
32use super::destruct_terminal;
33use super::init_terminal;
34use super::services::CrosstermStream;
35use super::ui::utils;
36
37static SENDER: OnceCell<mpsc::UnboundedSender<TaskEvent>> = OnceCell::new();
38
39#[derive(Debug)]
40enum Event {
41    Task {
42        inner: Task,
43        created_at: chrono::DateTime<chrono::Utc>,
44        completed_at: Option<chrono::DateTime<chrono::Utc>>,
45        success: bool,
46    },
47    Notice {
48        inner: NoticeMessage,
49    },
50}
51
52#[macro_export]
53macro_rules! task_success {
54    ($id:expr) => {
55        $crate::app::Initializer::complete_task($id, None, true)
56    };
57    ($id:expr, $msg:expr) => {
58        $crate::app::Initializer::complete_task($id, Some($msg), true)
59    };
60}
61
62#[macro_export]
63macro_rules! task_failure {
64    ($id:expr) => {
65        $crate::app::Initializer::complete_task($id, None, false)
66    };
67    ($id:expr, $msg:expr) => {
68        $crate::app::Initializer::complete_task($id, Some($msg), false)
69    };
70}
71
72pub struct Initializer {
73    crossterm_events: Box<dyn CrosstermStream>,
74    task_rx: mpsc::UnboundedReceiver<TaskEvent>,
75    messages: Vec<Event>,
76    complete: bool,
77}
78
79impl Initializer {
80    pub fn ready() -> bool {
81        SENDER.get().is_some()
82    }
83
84    pub fn add_notice(msg: NoticeMessage) {
85        let task = TaskEvent::AddNotice(msg);
86        if let Some(sender) = SENDER.get() {
87            let _ = sender.send(task);
88        }
89    }
90
91    pub fn add_task(id: &str, name: &str) {
92        log::debug!("Initializing task: {}", name);
93        let task = TaskEvent::AddTask(Task {
94            id: id.to_string(),
95            message: name.to_string(),
96        });
97        if let Some(sender) = SENDER.get() {
98            let _ = sender.send(task);
99        }
100    }
101
102    pub fn complete() {
103        let task = TaskEvent::Complete;
104        if let Some(sender) = SENDER.get() {
105            let _ = sender.send(task);
106        }
107    }
108
109    pub fn is_complete(&self) -> bool {
110        self.messages.iter().any(|t| {
111            matches!(
112                t,
113                Event::Task {
114                    completed_at: Some(_),
115                    ..
116                }
117            )
118        })
119    }
120
121    pub fn complete_task(id: &str, message: Option<String>, success: bool) {
122        let task = TaskEvent::CompleteTask {
123            id: id.to_string(),
124            suffix_message: message,
125            success,
126        };
127        if let Some(sender) = SENDER.get() {
128            let _ = sender.send(task);
129        }
130    }
131
132    pub async fn run(mut self) -> Result<Self> {
133        init_terminal()?;
134        let term_backend = CrosstermBackend::new(std::io::stdout());
135        let mut terminal = Terminal::new(term_backend)?;
136        let result = self.start_loop(&mut terminal).await;
137        destruct_terminal();
138        terminal.show_cursor()?;
139        if let Err(err) = result {
140            log::error!("Error in initializer loop: {}", err);
141            return Err(err);
142        }
143        Ok(self)
144    }
145
146    async fn start_loop<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
147        loop {
148            let event = self.next_event().await;
149            if let TaskEvent::UiTick = event {
150                self.render(terminal)?;
151                continue;
152            }
153            if self.handle_event(event) {
154                return Ok(());
155            }
156        }
157    }
158
159    async fn next_event(&mut self) -> TaskEvent {
160        loop {
161            let event = tokio::select! {
162                event = self.task_rx.recv() =>event,
163                event = self.crossterm_events.next().fuse() => match event {
164                    Some(Ok(CrosstermEvent::Key(key_event))) => Some(TaskEvent::CrosstermKey(key_event)),
165                    Some(Err(_)) => None,
166                    _ => None
167                },
168                _ = time::sleep(FRAME_DURATION) => Some(TaskEvent::UiTick)
169            };
170
171            if let Some(event) = event {
172                return event;
173            }
174        }
175    }
176
177    fn handle_event(&mut self, event: TaskEvent) -> bool {
178        if let TaskEvent::UiTick = event {
179            return false;
180        }
181        match event {
182            TaskEvent::AddTask(task) => {
183                let mut task = task;
184                if !task.message.ends_with("...") {
185                    task.message = format!("{}...", task.message);
186                }
187                self.messages.push(Event::Task {
188                    inner: task,
189                    created_at: chrono::Utc::now(),
190                    completed_at: None,
191                    success: false,
192                });
193            }
194            TaskEvent::AddNotice(notice) => {
195                self.messages.push(Event::Notice { inner: notice });
196            }
197            TaskEvent::CompleteTask {
198                id,
199                suffix_message,
200                success,
201            } => self.handle_task_complete(&id, suffix_message, success),
202            TaskEvent::Complete => return self.handle_complete(),
203            TaskEvent::CrosstermKey(key) => {
204                let input: Input = key.into();
205                if input.ctrl && (input.key == Key::Char('c') || input.key == Key::Char('q')) {
206                    destruct_terminal();
207                    std::process::exit(0);
208                }
209
210                if input.ctrl || input.alt || input.shift {
211                    return false;
212                }
213
214                if input.key == Key::Enter && self.complete {
215                    return true;
216                }
217            }
218            _ => {}
219        }
220        false
221    }
222
223    fn handle_task_complete(&mut self, task_id: &str, message: Option<String>, task_success: bool) {
224        let task = self
225            .messages
226            .iter_mut()
227            .find(|t| matches!(t, Event::Task { inner, .. } if inner.id == task_id));
228
229        let task = match task {
230            Some(task) => task,
231            None => return,
232        };
233
234        if let Event::Task {
235            inner,
236            completed_at,
237            success,
238            ..
239        } = task
240        {
241            *completed_at = Some(chrono::Utc::now());
242            if let Some(message) = message {
243                inner.message = format!("{} {}", inner.message, message);
244            }
245            *success = task_success;
246            log::debug!(
247                "Completing task: {} (success: {})",
248                inner.message,
249                task_success
250            );
251        }
252    }
253
254    fn render<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
255        terminal.draw(|f| {
256            let area = f.area();
257            let popup_area = utils::popup_area(area, 60, 70);
258
259            let mut tasks = self
260                .messages
261                .iter()
262                .map(|t| t.to_line())
263                .collect::<Vec<_>>();
264
265            if self.complete {
266                tasks.push(Line::from(vec![
267                    span!("[i] ").fg(NoticeKind::Info.text_color()),
268                    span!("Initialize completed! Press ")
269                        .white()
270                        .bold()
271                        .italic(),
272                    span!("Enter").green().bold().italic(),
273                    span!(" to start!").white().italic(),
274                ]));
275            }
276
277            let paragraph = Paragraph::new(tasks).block(Block::default());
278            paragraph.render(popup_area, f.buffer_mut());
279        })?;
280        Ok(())
281    }
282
283    #[cfg(not(test))]
284    fn handle_complete(&mut self) -> bool {
285        self.complete = true;
286        config::instance().general.auto_start.unwrap_or_default()
287    }
288
289    #[cfg(test)]
290    fn handle_complete(&mut self) -> bool {
291        self.complete = true;
292        true
293    }
294}
295
296impl Default for Initializer {
297    fn default() -> Self {
298        let (task_tx, task_rx) = mpsc::unbounded_channel::<TaskEvent>();
299        SENDER.set(task_tx).unwrap();
300
301        Self {
302            crossterm_events: Box::new(EventStream::new()),
303            task_rx,
304            messages: vec![],
305            complete: false,
306        }
307    }
308}
309
310impl Event {
311    fn to_line<'b>(&self) -> Line<'b> {
312        match self {
313            Event::Task {
314                inner,
315                completed_at,
316                created_at,
317                success,
318                ..
319            } => {
320                let mut spans = vec![];
321                let mut message = inner.message.clone();
322                if let Some(completed_at) = completed_at {
323                    if *success {
324                        spans.push(span!("✓ ").green());
325                    } else {
326                        spans.push(span!("✗ ").red());
327                    }
328                    let took = completed_at
329                        .signed_duration_since(*created_at)
330                        .to_std()
331                        .unwrap_or_default();
332                    message.push_str(
333                        format!(" completed! (took {:.2}s)", took.as_secs_f64()).as_str(),
334                    );
335                } else {
336                    spans.push(span!("? ").yellow());
337                }
338                spans.push(span!(message));
339                Line::from(spans)
340            }
341            Event::Notice { inner, .. } => {
342                let mut spans = vec![];
343                match inner.kind() {
344                    NoticeKind::Info => {
345                        spans.push(span!("[i] ").fg(inner.kind().text_color()));
346                    }
347                    NoticeKind::Warning => {
348                        spans.push(span!("[!] ").fg(inner.kind().text_color()));
349                    }
350                    NoticeKind::Error => {
351                        spans.push(span!("[x] ").fg(inner.kind().text_color()));
352                    }
353                }
354                spans.push(span!(inner.message().to_string()));
355                Line::from(spans)
356            }
357        }
358    }
359}