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}