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