matchmaker/render/
mod.rs

1mod dynamic;
2mod state;
3
4pub use dynamic::*;
5pub use state::*;
6
7// ------------------------------
8
9use std::io::Write;
10use std::sync::Arc;
11
12use anyhow::Result;
13use log::{info, warn};
14use ratatui::Frame;
15use ratatui::layout::Rect;
16use tokio::sync::mpsc;
17
18use crate::action::Action;
19use crate::config::{CursorSetting, ExitConfig};
20use crate::message::{Event, Interrupt, RenderCommand};
21use crate::tui::Tui;
22use crate::ui::{DisplayUI, InputUI, PickerUI, PreviewUI, ResultsUI, UI};
23use crate::{MatchError, MMItem, Selection};
24
25#[allow(clippy::too_many_arguments)]
26pub async fn render_loop<'a, W: Write, T: MMItem, S: Selection, C>(
27    mut ui: UI,
28    mut picker_ui: PickerUI<'a, T, S, C>,
29    mut preview_ui: Option<PreviewUI>,
30    mut tui: Tui<W>,
31    mut render_rx: mpsc::UnboundedReceiver<RenderCommand>,
32    controller_tx: mpsc::UnboundedSender<Event>,
33    context: Arc<C>,
34    dynamic_handlers: DynamicHandlers<T, S, C>,
35    exit_config: ExitConfig,
36) -> Result<Vec<S>, MatchError> {
37    let mut buffer = Vec::with_capacity(256);
38    let mut state: State<S, C> = State::new(context);
39    if let Some(ref preview_ui) = preview_ui
40    && !preview_ui.command().is_empty()
41    {
42        state.update_preview(preview_ui.command());
43    }
44
45    while render_rx.recv_many(&mut buffer, 256).await > 0 {
46        let mut did_pause = false;
47        let mut did_exit = false;
48
49        for event in &buffer {
50            let mut interrupt = Interrupt::None;
51
52            let PickerUI {
53                input,
54                results,
55                worker,
56                selections,
57                header,
58                footer,
59                ..
60            } = &mut picker_ui;
61
62            if !matches!(event, RenderCommand::Tick) {
63                info!("Recieved {event:?}");
64            }
65
66            match event {
67                RenderCommand::Input(c) => {
68                    input.input.insert(input.cursor as usize, *c);
69                    input.cursor += 1;
70                }
71                RenderCommand::Resize(area) => {
72                    tui.resize(*area);
73                    ui.area = *area;
74                }
75                RenderCommand::Refresh => {
76                    tui.redraw();
77                }
78                RenderCommand::Action(action) => {
79                    match action {
80                        Action::Select => {
81                            if let Some(item) = worker.get_nth(results.index()) {
82                                selections.sel(item);
83                            }
84                        }
85                        Action::Deselect => {
86                            if let Some(item) = worker.get_nth(results.index()) {
87                                selections.desel(item);
88                            }
89                        }
90                        Action::Toggle => {
91                            if let Some(item) = worker.get_nth(results.index()) {
92                                selections.toggle(item);
93                            }
94                        }
95                        Action::CycleAll => {
96                            selections.cycle_all_bg(worker.raw_results());
97                        }
98                        Action::Accept => {
99                            if selections.is_empty() {
100                                if let Some(item) = worker.get_nth(results.index())
101                                {
102                                    selections.sel(item);
103                                } else if !exit_config.allow_empty {
104                                    continue;
105                                }
106                            }
107                            return Ok(selections.output().collect::<Vec<S>>());
108                        }
109                        Action::Quit(code) => {
110                            return Err(MatchError::Abort(code.into()));
111                        }
112
113                        // UI
114                        Action::SetHeader(context) => {
115                            if let Some(s) = context {
116                                header.set(s.into());
117                            } else {
118                                todo!()
119                            }
120                        }
121                        Action::SetFooter(context) => {
122                            if let Some(s) = context {
123                                footer.set(s.into());
124                            } else {
125                                todo!()
126                            }
127                        }
128
129                        Action::CyclePreview => {
130                            if let Some(p) = preview_ui.as_mut() {
131                                p.cycle_layout();
132                                if !p.command().is_empty() {
133                                    state.update_preview(p.command().as_str());
134                                }
135                            }
136                        }
137                        Action::Preview(context) => {
138                            if let Some(p) = preview_ui.as_mut() {
139                                if !state.update_preview(context.as_str()) {
140                                    p.toggle_show()
141                                } else {
142                                    p.show::<true>();
143                                }
144                            };
145                        }
146                        Action::Help(context) => {
147                            if let Some(p) = preview_ui.as_mut() {
148                                // empty payload signifies help
149                                if !state.update_preview_set(context) {
150                                    state.update_preview_unset()
151                                } else {
152                                    p.show::<true>();
153                                }
154                            };
155                        }
156
157                        Action::SwitchPreview(idx) => {
158                            if let Some(p) = preview_ui.as_mut() {
159                                if let Some(idx) = idx {
160                                    if !p.set_idx(*idx) && !state.update_preview(p.command()) {
161                                        p.toggle_show();
162                                    }
163                                } else {
164                                    p.toggle_show()
165                                }
166                            }
167                        }
168                        Action::SetPreview(idx) => {
169                            if let Some(p) = preview_ui.as_mut() {
170                                if let Some(idx) = idx {
171                                    p.set_idx(*idx);
172                                } else {
173                                    state.update_preview(p.command());
174                                }
175                            }
176                        }
177                        Action::ToggleWrap => {
178                            results.wrap(!results.is_wrap());
179                        }
180                        Action::ToggleWrapPreview => {
181                            if let Some(p) = preview_ui.as_mut() {
182                                p.wrap(!p.is_wrap());
183                            }
184                        }
185
186                        // Programmable
187                        Action::Execute(context) => {
188                            interrupt = Interrupt::Execute(context.into());
189                        }
190                        Action::Become(context) => {
191                            interrupt = Interrupt::Become(context.into());
192                        }
193                        Action::Reload(context) => {
194                            interrupt = Interrupt::Reload(context.into());
195                        }
196                        Action::Print(context) => {
197                            interrupt = Interrupt::Print(context.into());
198                        }
199
200                        Action::SetInput(context) => {
201                            input.set_input(context.into(), u16::MAX);
202                        }
203                        Action::Column(context) => {
204                            results.toggle_col(*context);
205                        }
206                        Action::CycleColumn => {
207                            results.cycle_col();
208                        }
209                        // Edit
210                        Action::ForwardChar => input.forward_char(),
211                        Action::BackwardChar => input.backward_char(),
212                        Action::ForwardWord => input.forward_word(),
213                        Action::BackwardWord => input.backward_word(),
214                        Action::DeleteChar => input.delete(),
215                        Action::DeleteWord => input.delete_word(),
216                        Action::DeleteLineStart => input.delete_line_start(),
217                        Action::DeleteLineEnd => input.delete_line_end(),
218                        Action::Cancel => input.cancel(),
219
220                        // Navigation
221                        Action::Up(x) | Action::Down(x) => {
222                            let next = matches!(action, Action::Down(_)) ^ results.reverse();
223                            for _ in 0..x.into() {
224                                if next {
225                                    results.cursor_next();
226                                } else {
227                                    results.cursor_prev();
228                                }
229                            }
230                        }
231                        Action::PreviewUp(n) => {
232                            if let Some(p) = preview_ui.as_mut() {
233                                p.up(n.into())
234                            }
235                        }
236                        Action::PreviewDown(n) => {
237                            if let Some(p) = preview_ui.as_mut() {
238                                p.down(n.into())
239                            }
240                        }
241                        Action::PreviewHalfPageUp => todo!(),
242                        Action::PreviewHalfPageDown => todo!(),
243                        Action::Pos(pos) => {
244                            let pos = if *pos >= 0 {
245                                *pos as u32
246                            } else {
247                                results.status.matched_count.saturating_sub((-*pos) as u32)
248                            };
249                            results.cursor_jump(pos);
250                        }
251                        Action::InputPos(pos) => {
252                            let pos = if *pos >= 0 {
253                                *pos as u16
254                            } else {
255                                (input.len() as u16).saturating_sub((-*pos) as u16)
256                            };
257                            input.cursor = pos;
258                        }
259
260                        // Experimental/Debugging
261                        Action::Redraw => {
262                            tui.redraw();
263                        }
264                        _ => {}
265                    }
266                }
267                _ => {}
268            }
269
270            if !matches!(interrupt, Interrupt::None) {
271                match interrupt {
272                    Interrupt::Execute(_) => {
273                        if controller_tx.send(Event::Pause).is_err() {
274                            break
275                        }
276                        did_exit = true;
277                        tui.enter_execute();
278                        did_pause = true;
279                        while let Some(msg) = render_rx.recv().await {
280                            if matches!(msg, RenderCommand::Ack) {
281                                break;
282                            }
283                        }
284                    }
285                    Interrupt::Reload(_) => {
286                        picker_ui.worker.restart(false);
287                    }
288                    Interrupt::Become(_) => {
289                        tui.exit();
290                    }
291                    _ => {}
292                }
293
294                state.update(&picker_ui);
295                let (dispatcher, mut effects) = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
296
297                for h in dynamic_handlers.1.get(&interrupt) {
298                    dispatcher.dispatch(h, &interrupt, &mut effects);
299                }
300
301                if let Interrupt::Become(context) = interrupt {
302                    return Err(MatchError::Become(context));
303                }
304                state.process_effects(effects);
305            };
306        }
307
308        // debug!("{state:?}");
309
310        // ------------- update state + render ------------------------
311        picker_ui.update();
312
313        if did_exit {
314            tui.return_execute().map_err(|e| MatchError::TUIError(e.to_string()))?
315        }
316
317        let mut resized = false;
318        tui.terminal
319        .draw(|frame| {
320            let mut area = frame.area();
321
322            render_ui(frame, &mut area, &ui);
323
324            let [preview, picker_area] = if let Some(preview_ui) = preview_ui.as_mut()
325            && preview_ui.is_show()
326            {
327                let ret = preview_ui.layout().split(area);
328                if state.iterations == 0 && ret[1].width <= 5 {
329                    warn!("UI too narrow, hiding preview");
330                    preview_ui.show::<false>();
331                    [Rect::default(), area]
332                } else {
333                    ret
334                }
335            } else {
336                [Rect::default(), area]
337            };
338
339
340
341            let [input, status, header, results, footer] = picker_ui.layout(picker_area);
342
343            resized = state.update_layout([preview, input, status, results]);
344
345            // might be more efficient to always update, but logically this feels better
346            if resized {
347                picker_ui.results.update_dimensions(&results);
348                ui.update_dimensions(area);
349            };
350
351            render_input(frame, input, &picker_ui.input);
352            render_status(frame, status, &picker_ui.results);
353            render_results(frame, results, &mut picker_ui);
354            render_display(frame, header, &picker_ui.header, picker_ui.results.indentation());
355            render_display(frame, footer, &picker_ui.footer, picker_ui.results.indentation());
356
357            if let Some(preview_ui) = preview_ui.as_mut() {
358                state.update_preview_ui(preview_ui);
359                if resized {
360                    preview_ui.update_dimensions(&preview);
361                }
362                render_preview(frame, preview, preview_ui);
363            }
364        })
365        .map_err(|e| MatchError::TUIError(e.to_string()))?;
366        // usefult o clear artifacts
367        if resized {
368            tui.redraw();
369        }
370
371        // update state
372        if state.iterations == 0 {
373            state.insert(Event::Start);
374        }
375        state.update(&picker_ui);
376        let events = state.events();
377        let (dispatcher, mut effects) = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
378
379        // process exit conditions
380        if exit_config.select_1 && dispatcher.status().matched_count == 1 {
381            return Ok(vec![state.take_current().unwrap()]);
382        }
383
384        // ping handlers with events
385        for e in events.iter() {
386            for h in dynamic_handlers.0.get(e) {
387                dispatcher.dispatch(h, e, &mut effects);
388            }
389        }
390        // send events to controller
391        for e in events {
392            controller_tx
393            .send(e)
394            .unwrap_or_else(|err| eprintln!("send failed: {:?}", err));
395        }
396        // process effects
397        state.process_effects(effects);
398
399        buffer.clear();
400
401        if did_pause {
402            if controller_tx.send(Event::Resume).is_err() {
403                break
404            };
405            // due to control flow, this does nothing, but is a useful safeguard anyway
406            while let Some(msg) = render_rx.recv().await {
407                if matches!(msg, RenderCommand::Ack) {
408                    break;
409                }
410            }
411        }
412    }
413
414    Err(MatchError::EventLoopClosed)
415}
416
417// ------------------------- HELPERS ----------------------------
418fn render_preview(frame: &mut Frame, area: Rect, ui: &mut PreviewUI) {
419    // if ui.view.changed() {
420    // doesn't work, use resize
421    //     frame.render_widget(Clear, area);
422    // } else {
423    //     let widget = ui.make_preview();
424    //     frame.render_widget(widget, area);
425    // }
426    let widget = ui.make_preview();
427    frame.render_widget(widget, area);
428}
429
430fn render_results<T: MMItem, S: Selection, C>(
431    frame: &mut Frame,
432    area: Rect,
433    ui: &mut PickerUI<T, S, C>,
434) {
435    let widget = ui.make_table();
436
437    frame.render_widget(widget, area);
438}
439
440fn render_input(frame: &mut Frame, area: Rect, ui: &InputUI) {
441    let widget = ui.make_input();
442    if let CursorSetting::Default = ui.config.cursor {
443        frame.set_cursor_position(ui.cursor_offset(&area))
444    };
445
446    frame.render_widget(widget, area);
447}
448
449fn render_status(frame: &mut Frame, area: Rect, ui: &ResultsUI) {
450    let widget = ui.make_status();
451
452    frame.render_widget(widget, area);
453}
454
455fn render_display(frame: &mut Frame, area: Rect, ui: &DisplayUI, result_indentation: usize) {
456    let widget = ui.make_display(result_indentation);
457
458    frame.render_widget(widget, area);
459}
460
461fn render_ui(frame: &mut Frame, area: &mut Rect, ui: &UI) {
462    let widget = ui.make_ui();
463    frame.render_widget(widget, *area);
464    *area = ui.inner_area(area);
465}
466
467// -----------------------------------------------------------------------------------
468
469#[cfg(test)]
470mod test {}
471
472// #[cfg(test)]
473// async fn send_every_second(tx: mpsc::UnboundedSender<RenderCommand>) {
474//     let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
475
476//     loop {
477//         interval.tick().await;
478//         if tx.send(RenderCommand::quit()).is_err() {
479//             break;
480//         }
481//     }
482// }