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