matchmaker/render/
mod.rs

1mod dynamic;
2mod state;
3mod state_effects;
4
5pub use dynamic::*;
6pub use state::*;
7pub use state_effects::*;
8// ------------------------------
9
10use std::io::Write;
11
12use anyhow::Result;
13use log::{info, warn};
14use ratatui::Frame;
15use ratatui::layout::Rect;
16use tokio::sync::mpsc;
17
18use crate::action::{Action, ActionExt, ActionAliaser, ActionExtHandler};
19use crate::config::{CursorSetting, ExitConfig};
20use crate::message::{Event, Interrupt, RenderCommand};
21use crate::tui::Tui;
22use crate::ui::{DisplayUI, InputUI, OverlayUI, PickerUI, PreviewUI, ResultsUI, UI};
23use crate::{MatchError, SSS, Selection};
24
25// todo: we can make it return a stack allocated smallvec ig
26fn apply_aliases<T: SSS, S: Selection, A: ActionExt>(
27    buffer: &mut Vec<RenderCommand<A>>,
28    aliaser: ActionAliaser<T, S, A>,
29    state: &MMState<'_, T, S>
30) {
31    let mut out = Vec::new();
32
33    for cmd in buffer.drain(..) {
34        match cmd {
35            RenderCommand::Action(a) => {
36                out.extend(aliaser(a, state).0.into_iter().map(RenderCommand::Action))
37            },
38            other => out.push(other),
39        }
40    }
41
42    *buffer = out;
43}
44
45#[allow(clippy::too_many_arguments)]
46pub(crate) async fn render_loop<'a, W: Write, T: SSS, S: Selection, A: ActionExt>(
47    mut ui: UI,
48    mut picker_ui: PickerUI<'a, T, S>,
49    mut preview_ui: Option<PreviewUI>,
50    mut tui: Tui<W>,
51
52    mut overlay_ui: Option<OverlayUI<A>>,
53    exit_config: ExitConfig,
54
55    mut render_rx: mpsc::UnboundedReceiver<RenderCommand<A>>,
56    controller_tx: mpsc::UnboundedSender<Event>,
57
58    dynamic_handlers: DynamicHandlers<T, S>,
59    ext_handler: Option<ActionExtHandler<T, S, A>>,
60    ext_aliaser: Option<ActionAliaser<T, S, A>>,
61    initializer: Option<Box<dyn FnOnce()>>
62    // signal_handler: Option<(&'static std::sync::atomic::AtomicUsize, SignalHandler<T, S>)>,
63) -> Result<Vec<S>, MatchError> {
64    if let Some(initializer) = initializer {
65        initializer()
66    }
67    let mut buffer = Vec::with_capacity(256);
68
69    let mut state: State<S> = State::new();
70
71    // place the initial command in the state where the preview listener can access
72    if let Some(ref preview_ui) = preview_ui
73    && !preview_ui.command().is_empty()
74    {
75        state.update_preview(preview_ui.command());
76    }
77
78    while render_rx.recv_many(&mut buffer, 256).await > 0 {
79        let mut did_pause = false;
80        let mut did_exit = false;
81        let mut did_resize = false;
82
83        let mut effects = Effects::new();
84        // todo: why exactly can we not borrow the picker_ui mutably?
85        if let Some(aliaser) = ext_aliaser {
86            let state = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
87            apply_aliases(&mut buffer, aliaser, &state)
88            // effects could be moved out for efficiency, but it seems more logical to add them as they come so that we can trigger interrupts
89        };
90
91        // todo: benchmark vs drain
92        for event in buffer.drain(..) {
93            let mut interrupt = Interrupt::None;
94
95            let PickerUI {
96                input,
97                results,
98                worker,
99                selections,
100                header,
101                footer,
102                ..
103            } = &mut picker_ui;
104
105            if !matches!(event, RenderCommand::Tick) {
106                info!("Recieved {event:?}");
107            }
108
109            match event {
110                RenderCommand::Input(c) => {
111                    if let Some(x) = overlay_ui.as_mut() && x.handle_input(c) {
112                        continue;
113                    }
114                    input.input.insert(input.cursor as usize, c);
115                    input.cursor += 1;
116                }
117                RenderCommand::Resize(area) => {
118                    tui.resize(area);
119                    ui.area = area;
120                }
121                RenderCommand::Refresh => {
122                    tui.redraw();
123                }
124                RenderCommand::Effect(e) => {
125                    #[allow(warnings)]
126                    match e {
127                        Effect::Reload => {
128                            // its jank but the Reload effect triggers the reload handler in this unique case.
129                            // Its useful for when the reload action can't be used when overlay is in effect.
130                            interrupt = Interrupt::Reload("".into());
131                        }
132                        _ => {
133                            effects.insert(e);
134                        }
135                    }
136
137                }
138                RenderCommand::Action(action) => {
139                    if let Some(x) = overlay_ui.as_mut() && x.handle_action(&action) {
140                        continue;
141                    }
142                    match action {
143                        Action::Select => {
144                            if let Some(item) = worker.get_nth(results.index()) {
145                                selections.sel(item);
146                            }
147                        }
148                        Action::Deselect => {
149                            if let Some(item) = worker.get_nth(results.index()) {
150                                selections.desel(item);
151                            }
152                        }
153                        Action::Toggle => {
154                            if let Some(item) = worker.get_nth(results.index()) {
155                                selections.toggle(item);
156                            }
157                        }
158                        Action::CycleAll => {
159                            selections.cycle_all_bg(worker.raw_results());
160                        }
161                        Action::ClearAll => {
162                            selections.clear();
163                        }
164                        Action::Accept => {
165                            if selections.is_empty() {
166                                if let Some(item) = worker.get_nth(results.index())
167                                {
168                                    selections.sel(item);
169                                } else if !exit_config.allow_empty {
170                                    continue;
171                                }
172                            }
173                            return Ok(selections.output().collect::<Vec<S>>());
174                        }
175                        Action::Quit(code) => {
176                            return Err(MatchError::Abort(code.0));
177                        }
178
179                        // UI
180                        Action::SetHeader(context) => {
181                            if let Some(s) = context {
182                                header.set(s);
183                            } else {
184                                todo!()
185                            }
186                        }
187                        Action::SetFooter(context) => {
188                            if let Some(s) = context {
189                                footer.set(s);
190                            } else {
191                                todo!()
192                            }
193                        }
194                        // this sometimes aborts the viewer on some files, why?
195                        Action::CyclePreview => {
196                            if let Some(p) = preview_ui.as_mut() {
197                                p.cycle_layout();
198                                if !p.command().is_empty() {
199                                    state.update_preview(p.command());
200                                }
201                            }
202                        }
203                        Action::Preview(context) => {
204                            if let Some(p) = preview_ui.as_mut() {
205                                if !state.update_preview(context.as_str()) {
206                                    p.toggle_show()
207                                } else {
208                                    p.show::<true>();
209                                }
210                            };
211                        }
212                        Action::Help(context) => {
213                            if let Some(p) = preview_ui.as_mut() {
214                                // empty payload signifies help
215                                if !state.update_preview_set(context) {
216                                    state.update_preview_unset()
217                                } else {
218                                    p.show::<true>();
219                                }
220                            };
221                        }
222                        Action::SwitchPreview(idx) => {
223                            if let Some(p) = preview_ui.as_mut() {
224                                if let Some(idx) = idx {
225                                    if !p.set_idx(idx) && !state.update_preview(p.command()) {
226                                        p.toggle_show();
227                                    }
228                                } else {
229                                    p.toggle_show()
230                                }
231                            }
232                        }
233                        Action::SetPreview(idx) => {
234                            if let Some(p) = preview_ui.as_mut() {
235                                if let Some(idx) = idx {
236                                    p.set_idx(idx);
237                                } else {
238                                    state.update_preview(p.command());
239                                }
240                            }
241                        }
242                        Action::ToggleWrap => {
243                            results.wrap(!results.is_wrap());
244                        }
245                        Action::ToggleWrapPreview => {
246                            if let Some(p) = preview_ui.as_mut() {
247                                p.wrap(!p.is_wrap());
248                            }
249                        }
250
251                        // Programmable
252                        Action::Execute(context) => {
253                            interrupt = Interrupt::Execute(context);
254                        }
255                        Action::Become(context) => {
256                            interrupt = Interrupt::Become(context);
257                        }
258                        Action::Reload(context) => {
259                            interrupt = Interrupt::Reload(context);
260                        }
261                        Action::Print(context) => {
262                            interrupt = Interrupt::Print(context);
263                        }
264
265                        Action::SetInput(context) => {
266                            input.set(context, u16::MAX);
267                        }
268                        Action::Column(context) => {
269                            results.toggle_col(context);
270                        }
271                        Action::CycleColumn => {
272                            results.cycle_col();
273                        }
274                        // Edit
275                        Action::ForwardChar => input.forward_char(),
276                        Action::BackwardChar => input.backward_char(),
277                        Action::ForwardWord => input.forward_word(),
278                        Action::BackwardWord => input.backward_word(),
279                        Action::DeleteChar => input.delete(),
280                        Action::DeleteWord => input.delete_word(),
281                        Action::DeleteLineStart => input.delete_line_start(),
282                        Action::DeleteLineEnd => input.delete_line_end(),
283                        Action::Cancel => input.cancel(),
284
285                        // Navigation
286                        Action::Up(x) | Action::Down(x) => {
287                            let next = matches!(action, Action::Down(_)) ^ results.reverse();
288                            for _ in 0..x.into() {
289                                if next {
290                                    results.cursor_next();
291                                } else {
292                                    results.cursor_prev();
293                                }
294                            }
295                        }
296                        Action::PreviewUp(n) => {
297                            if let Some(p) = preview_ui.as_mut() {
298                                p.up(n.into())
299                            }
300                        }
301                        Action::PreviewDown(n) => {
302                            if let Some(p) = preview_ui.as_mut() {
303                                p.down(n.into())
304                            }
305                        }
306                        Action::PreviewHalfPageUp => todo!(),
307                        Action::PreviewHalfPageDown => todo!(),
308                        Action::Pos(pos) => {
309                            let pos = if pos >= 0 {
310                                pos as u32
311                            } else {
312                                results.status.matched_count.saturating_sub((-pos) as u32)
313                            };
314                            results.cursor_jump(pos);
315                        }
316                        Action::InputPos(pos) => {
317                            let pos = if pos >= 0 {
318                                pos as u16
319                            } else {
320                                (input.len() as u16).saturating_sub((-pos) as u16)
321                            };
322                            input.cursor = pos;
323                        }
324
325                        // Experimental/Debugging
326                        Action::Redraw => {
327                            tui.redraw();
328                        }
329                        Action::Overlay(index) => {
330                            if let Some(x) = overlay_ui.as_mut() {
331                                x.enable(index, &ui.area);
332                                tui.redraw();
333                            };
334                        }
335                        Action::Custom(e) => {
336                            if let Some(handler) = ext_handler {
337                                let mut dispatcher = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
338                                let effects = handler(e, &mut dispatcher);
339                                state.apply_effects(effects, &mut ui, &mut picker_ui, &mut preview_ui);
340                            }
341                        }
342                        _ => {}
343                    }
344                }
345                _ => {}
346            }
347
348            match interrupt {
349                Interrupt::None => continue,
350                Interrupt::Execute(_) => {
351                    if controller_tx.send(Event::Pause).is_err() {
352                        break
353                    }
354                    did_exit = true;
355                    tui.enter_execute();
356                    did_pause = true;
357                }
358                Interrupt::Reload(_) => {
359                    picker_ui.worker.restart(false);
360                }
361                Interrupt::Become(_) => {
362                    tui.exit();
363                }
364                _ => {}
365            }
366
367            state.update_current(&picker_ui);
368            // Apply interrupt effect
369            {
370                let mut effects = Effects::new();
371                let mut dispatcher = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
372                for h in dynamic_handlers.1.get(&interrupt) {
373                    effects.append(
374                        h(&mut dispatcher, &interrupt)
375                    )
376                }
377
378                if let Interrupt::Become(context) = interrupt {
379                    return Err(MatchError::Become(context));
380                }
381                state.apply_effects(effects, &mut ui, &mut picker_ui, &mut preview_ui);
382            }
383        }
384
385        // debug!("{state:?}");
386
387        // ------------- update state + render ------------------------
388        picker_ui.update();
389        // process exit conditions
390        if exit_config.select_1 && picker_ui.results.status.matched_count == 1 {
391            return Ok(vec![state.take_current().unwrap()]);
392        }
393
394        // resume tui
395        if did_exit {
396            tui.return_execute().map_err(|e| MatchError::TUIError(e.to_string()))?
397        }
398
399        let mut overlay_ui_ref = overlay_ui.as_mut();
400        tui.terminal
401        .draw(|frame| {
402            let mut area = frame.area();
403
404            render_ui(frame, &mut area, &ui);
405
406            let [preview, picker_area] = if let Some(preview_ui) = preview_ui.as_mut()
407            && let Some(layout) = preview_ui.layout()
408            {
409                let ret = layout.split(area);
410                if state.iterations == 0 && ret[1].width <= 5 {
411                    warn!("UI too narrow, hiding preview");
412                    preview_ui.show::<false>();
413                    [Rect::default(), area]
414                } else {
415                    ret
416                }
417            } else {
418                [Rect::default(), area]
419            };
420
421            let [input, status, header, results, footer] = picker_ui.layout(picker_area);
422
423            // compare and save dimensions
424            did_resize = state.update_layout([preview, input, status, results]);
425
426            if did_resize {
427                picker_ui.results.update_dimensions(&results);
428                // although these only want update when the whole ui change
429                ui.update_dimensions(area);
430                if let Some(x) = overlay_ui_ref.as_deref_mut() {
431                    x.update_dimensions(&area);
432                }
433            };
434
435            render_input(frame, input, &picker_ui.input);
436            render_status(frame, status, &picker_ui.results);
437            render_results(frame, results, &mut picker_ui);
438            render_display(frame, header, &picker_ui.header, picker_ui.results.indentation());
439            render_display(frame, footer, &picker_ui.footer, picker_ui.results.indentation());
440            if let Some(preview_ui) = preview_ui.as_mut() {
441                state.update_preview_ui(preview_ui);
442                if did_resize {
443                    preview_ui.update_dimensions(&preview);
444                }
445                render_preview(frame, preview, preview_ui);
446            }
447            if let Some(x) = overlay_ui_ref {
448                x.draw(frame);
449            }
450        })
451        .map_err(|e| MatchError::TUIError(e.to_string()))?;
452
453        // useful to clear artifacts
454        if did_resize && tui.config.redraw_on_resize {
455            tui.redraw();
456        }
457        buffer.clear();
458
459        // note: the remainder could be scoped by a conditional on having run?
460        // ====== Event handling ==========
461        state.update(&picker_ui, &overlay_ui);
462        let events = state.events();
463
464        // ---- Invoke handlers -------
465        let mut dispatcher = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
466        // if let Some((signal, handler)) = signal_handler &&
467        // let s = signal.load(std::sync::atomic::Ordering::Acquire) &&
468        // s > 0
469        // {
470        //     handler(s, &mut dispatcher);
471        //     signal.store(0, std::sync::atomic::Ordering::Release);
472        // };
473
474        // ping handlers with events
475        for e in events.iter() {
476            for h in dynamic_handlers.0.get(e) {
477                effects.append(
478                    h(&mut dispatcher, e)
479                )
480            }
481        }
482
483        // apply effects
484        state.apply_effects(effects, &mut ui, &mut picker_ui, &mut preview_ui);
485
486        // ------------------------------
487        // send events into controller
488        for e in events {
489            controller_tx
490            .send(e)
491            .unwrap_or_else(|err| eprintln!("send failed: {:?}", err));
492        }
493        // =================================
494
495        if did_pause {
496            if controller_tx.send(Event::Resume).is_err() {
497                break
498            };
499            // due to control flow, this does nothing, but is anyhow a useful safeguard to guarantee the pause
500            while let Some(msg) = render_rx.recv().await {
501                if matches!(msg, RenderCommand::Ack) {
502                    break;
503                }
504            }
505        }
506    }
507
508    Err(MatchError::EventLoopClosed)
509}
510
511// ------------------------- HELPERS ----------------------------
512fn render_preview(frame: &mut Frame, area: Rect, ui: &mut PreviewUI) {
513    // if ui.view.changed() {
514    // doesn't work, use resize
515    //     frame.render_widget(Clear, area);
516    // } else {
517    //     let widget = ui.make_preview();
518    //     frame.render_widget(widget, area);
519    // }
520    let widget = ui.make_preview();
521    frame.render_widget(widget, area);
522}
523
524fn render_results<T: SSS, S: Selection>(
525    frame: &mut Frame,
526    area: Rect,
527    ui: &mut PickerUI<T, S>,
528) {
529    let widget = ui.make_table();
530
531    frame.render_widget(widget, area);
532}
533
534fn render_input(frame: &mut Frame, area: Rect, ui: &InputUI) {
535    let widget = ui.make_input();
536    if let CursorSetting::Default = ui.config.cursor {
537        frame.set_cursor_position(ui.cursor_offset(&area))
538    };
539
540    frame.render_widget(widget, area);
541}
542
543fn render_status(frame: &mut Frame, area: Rect, ui: &ResultsUI) {
544    let widget = ui.make_status();
545
546    frame.render_widget(widget, area);
547}
548
549fn render_display(frame: &mut Frame, area: Rect, ui: &DisplayUI, result_indentation: usize) {
550    let widget = ui.make_display(result_indentation);
551
552    frame.render_widget(widget, area);
553}
554
555fn render_ui(frame: &mut Frame, area: &mut Rect, ui: &UI) {
556    let widget = ui.make_ui();
557    frame.render_widget(widget, *area);
558    *area = ui.inner_area(area);
559}
560
561// -----------------------------------------------------------------------------------
562
563#[cfg(test)]
564mod test {}
565
566// #[cfg(test)]
567// async fn send_every_second(tx: mpsc::UnboundedSender<RenderCommand>) {
568//     let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
569
570//     loop {
571//         interval.tick().await;
572//         if tx.send(RenderCommand::quit()).is_err() {
573//             break;
574//         }
575//     }
576// }