Skip to main content

matchmaker/render/
mod.rs

1mod dynamic;
2mod state;
3
4use crossterm::event::{MouseButton, MouseEventKind};
5pub use dynamic::*;
6pub use state::*;
7// ------------------------------
8
9use std::io::Write;
10
11use log::{info, warn};
12use ratatui::Frame;
13use ratatui::layout::{Position, Rect};
14use tokio::sync::mpsc;
15
16#[cfg(feature = "bracketed-paste")]
17use crate::PasteHandler;
18use crate::action::{Action, ActionExt};
19use crate::config::{CursorSetting, ExitConfig, RowConnectionStyle};
20use crate::message::{Event, Interrupt, RenderCommand};
21use crate::tui::Tui;
22use crate::ui::{DisplayUI, InputUI, OverlayUI, PickerUI, PreviewUI, ResultsUI, UI};
23use crate::{ActionAliaser, ActionExtHandler, MatchError, SSS, Selection};
24
25fn apply_aliases<T: SSS, S: Selection, A: ActionExt>(
26    buffer: &mut Vec<RenderCommand<A>>,
27    aliaser: ActionAliaser<T, S, A>,
28    dispatcher: &mut MMState<'_, '_, T, S>,
29) {
30    let mut out = Vec::new();
31
32    for cmd in buffer.drain(..) {
33        match cmd {
34            RenderCommand::Action(a) => out.extend(
35                aliaser(a, dispatcher)
36                    .into_iter()
37                    .map(RenderCommand::Action),
38            ),
39            other => out.push(other),
40        }
41    }
42
43    *buffer = out;
44}
45
46#[allow(clippy::too_many_arguments)]
47pub(crate) async fn render_loop<'a, W: Write, T: SSS, S: Selection, A: ActionExt>(
48    mut ui: UI,
49    mut picker_ui: PickerUI<'a, T, S>,
50    mut footer_ui: DisplayUI,
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::new();
68    let mut click = Click::None;
69
70    // place the initial command in the state where the preview listener can access
71    if let Some(ref preview_ui) = preview_ui
72        && !preview_ui.command().is_empty()
73    {
74        state.update_preview(preview_ui.command());
75    }
76
77    while render_rx.recv_many(&mut buffer, 256).await > 0 {
78        let mut did_pause = false;
79        let mut did_exit = false;
80        let mut did_resize = false;
81
82        // todo: why exactly can we not borrow the picker_ui mutably?
83        if let Some(aliaser) = ext_aliaser {
84            apply_aliases(
85                &mut buffer,
86                aliaser,
87                &mut state.dispatcher(&mut ui, &mut picker_ui, &mut footer_ui, &mut preview_ui),
88            )
89            // effects could be moved out for efficiency, but it seems more logical to add them as they come so that we can trigger interrupts
90        };
91
92        if state.should_quit {
93            let ret = picker_ui.selector.output().collect::<Vec<S>>();
94            return if picker_ui.selector.is_disabled()
95                && let Some((_, item)) = get_current(&picker_ui)
96            {
97                Ok(vec![item])
98            } else if ret.is_empty() {
99                Err(MatchError::Abort(0))
100            } else {
101                Ok(ret)
102            };
103        } else if state.should_quit_nomatch {
104            return Err(MatchError::NoMatch);
105        }
106
107        for event in buffer.drain(..) {
108            state.clear_interrupt();
109
110            if !matches!(event, RenderCommand::Tick) {
111                info!("Recieved {event:?}");
112            }
113
114            match event {
115                RenderCommand::Action(Action::Input(c)) => {
116                    // btw, why can't we do let input = picker_ui.input without running into issues?
117                    if let Some(x) = overlay_ui.as_mut()
118                        && x.handle_input(c)
119                    {
120                        continue;
121                    }
122                    picker_ui.input.push_char(c);
123                }
124                #[cfg(feature = "bracketed-paste")]
125                RenderCommand::Paste(content) => {
126                    if let Some(handler) = paste_handler {
127                        let content = {
128                            handler(
129                                content,
130                                &state.dispatcher(
131                                    &mut ui,
132                                    &mut picker_ui,
133                                    &mut footer_ui,
134                                    &mut preview_ui,
135                                ),
136                            )
137                        };
138                        if !content.is_empty() {
139                            picker_ui.input.push_str(&content);
140                        }
141                    }
142                }
143                RenderCommand::Resize(area) => {
144                    tui.resize(area);
145                    ui.area = area;
146                }
147                RenderCommand::Refresh => {
148                    tui.redraw();
149                }
150                RenderCommand::HeaderColumns(columns) => {
151                    picker_ui.header.header_columns(columns);
152                }
153                RenderCommand::Mouse(mouse) => {
154                    // we could also impl this in the aliasing step
155                    let pos = Position::from((mouse.column, mouse.row));
156                    let [preview, input, status, result] = state.layout;
157
158                    match mouse.kind {
159                        MouseEventKind::Down(MouseButton::Left) => {
160                            // todo: clickable column headers, clickable results, also, grouping?
161                            if result.contains(pos) {
162                                click = Click::ResultPos(mouse.row - result.top());
163                            } else if input.contains(pos) {
164                                // The X offset of the start of the visible text relative to the terminal
165                                let text_start_x = input.x
166                                    + picker_ui.input.prompt.width() as u16
167                                    + picker_ui.input.config.border.left();
168
169                                if pos.x >= text_start_x {
170                                    let visual_offset = pos.x - text_start_x;
171                                    picker_ui.input.set_at_visual_offset(visual_offset);
172                                } else {
173                                    picker_ui.input.set(None, 0);
174                                }
175                            } else if status.contains(pos) {
176                                // todo
177                            }
178                        }
179                        MouseEventKind::ScrollDown => {
180                            if preview.contains(pos) {
181                                if let Some(p) = preview_ui.as_mut() {
182                                    p.down(1)
183                                }
184                            } else {
185                                picker_ui.results.cursor_next()
186                            }
187                        }
188                        MouseEventKind::ScrollUp => {
189                            if preview.contains(pos) {
190                                if let Some(p) = preview_ui.as_mut() {
191                                    p.up(1)
192                                }
193                            } else {
194                                picker_ui.results.cursor_prev()
195                            }
196                        }
197                        MouseEventKind::ScrollLeft => {
198                            // todo
199                        }
200                        MouseEventKind::ScrollRight => {
201                            // todo
202                        }
203                        // Drag tracking: todo
204                        _ => {}
205                    }
206                }
207                RenderCommand::QuitEmpty => {
208                    return Ok(vec![]);
209                }
210                RenderCommand::Action(action) => {
211                    if let Some(x) = overlay_ui.as_mut()
212                        && x.handle_action(&action)
213                    {
214                        continue;
215                    }
216                    let PickerUI {
217                        input,
218                        results,
219                        worker,
220                        selector: selections,
221                        header,
222                        ..
223                    } = &mut picker_ui;
224                    match action {
225                        Action::Select => {
226                            if let Some(item) = worker.get_nth(results.index()) {
227                                selections.sel(item);
228                            }
229                        }
230                        Action::Deselect => {
231                            if let Some(item) = worker.get_nth(results.index()) {
232                                selections.desel(item);
233                            }
234                        }
235                        Action::Toggle => {
236                            if let Some(item) = worker.get_nth(results.index()) {
237                                selections.toggle(item);
238                            }
239                        }
240                        Action::CycleAll => {
241                            selections.cycle_all_bg(worker.raw_results());
242                        }
243                        Action::ClearSelections => {
244                            selections.clear();
245                        }
246                        Action::Accept => {
247                            let ret = if selections.is_empty() {
248                                if let Some(item) = get_current(&picker_ui) {
249                                    vec![item.1]
250                                } else if exit_config.allow_empty {
251                                    vec![]
252                                } else {
253                                    continue;
254                                }
255                            } else {
256                                selections.output().collect::<Vec<S>>()
257                            };
258                            return Ok(ret);
259                        }
260                        Action::Quit(code) => {
261                            return Err(MatchError::Abort(code));
262                        }
263
264                        // UI
265                        Action::SetHeader(context) => {
266                            if let Some(s) = context {
267                                header.set(s);
268                            } else {
269                                header.clear();
270                            }
271                        }
272                        Action::SetFooter(context) => {
273                            if let Some(s) = context {
274                                footer_ui.set(s);
275                            } else {
276                                footer_ui.clear();
277                            }
278                        }
279                        // this sometimes aborts the viewer on some files, why?
280                        Action::CyclePreview => {
281                            if let Some(p) = preview_ui.as_mut() {
282                                p.cycle_layout();
283                                if !p.command().is_empty() {
284                                    state.update_preview(p.command());
285                                }
286                            }
287                        }
288
289                        Action::PreviewHScroll(x) | Action::PreviewScroll(x) => {
290                            if let Some(p) = preview_ui.as_mut() {
291                                p.scroll(matches!(action, Action::PreviewHScroll(_)), x);
292                            }
293                        }
294                        Action::Preview(context) => {
295                            if let Some(p) = preview_ui.as_mut() {
296                                if !state.update_preview(context.as_str()) {
297                                    p.toggle_show()
298                                } else {
299                                    p.show(true);
300                                }
301                            };
302                        }
303                        Action::Help(context) => {
304                            if let Some(p) = preview_ui.as_mut() {
305                                // empty payload signifies help
306                                if !state.update_preview_set(context) {
307                                    state.update_preview_unset()
308                                } else {
309                                    p.show(true);
310                                }
311                            };
312                        }
313                        Action::SwitchPreview(idx) => {
314                            if let Some(p) = preview_ui.as_mut() {
315                                if let Some(idx) = idx {
316                                    if !p.set_layout(idx) && !state.update_preview(p.command()) {
317                                        p.toggle_show();
318                                    }
319                                } else {
320                                    p.toggle_show()
321                                }
322                            }
323                        }
324                        Action::SetPreview(idx) => {
325                            if let Some(p) = preview_ui.as_mut() {
326                                if let Some(idx) = idx {
327                                    p.set_layout(idx);
328                                } else {
329                                    state.update_preview(p.command());
330                                }
331                            }
332                        }
333                        Action::ToggleWrap => {
334                            results.wrap(!results.is_wrap());
335                        }
336                        Action::ToggleWrapPreview => {
337                            if let Some(p) = preview_ui.as_mut() {
338                                p.wrap(!p.is_wrap());
339                            }
340                        }
341
342                        // Programmable
343                        Action::Execute(payload) => {
344                            state.set_interrupt(Interrupt::Execute, payload);
345                        }
346                        Action::Become(payload) => {
347                            state.set_interrupt(Interrupt::Become, payload);
348                        }
349                        Action::Reload(payload) => {
350                            state.set_interrupt(Interrupt::Reload, payload);
351                        }
352                        Action::Print(payload) => {
353                            state.set_interrupt(Interrupt::Print, payload);
354                        }
355
356                        Action::SetInput(context) => {
357                            input.set(context, u16::MAX);
358                        }
359                        Action::Column(context) => {
360                            results.toggle_col(context);
361                        }
362                        Action::CycleColumn => {
363                            results.cycle_col();
364                        }
365                        // Edit
366                        Action::ForwardChar => input.forward_char(),
367                        Action::BackwardChar => input.backward_char(),
368                        Action::ForwardWord => input.forward_word(),
369                        Action::BackwardWord => input.backward_word(),
370                        Action::DeleteChar => input.delete(),
371                        Action::DeleteWord => input.delete_word(),
372                        Action::DeleteLineStart => input.delete_line_start(),
373                        Action::DeleteLineEnd => input.delete_line_end(),
374                        Action::Cancel => input.cancel(),
375
376                        // Navigation
377                        Action::Up(x) | Action::Down(x) => {
378                            let next = matches!(action, Action::Down(_)) ^ results.reverse();
379                            for _ in 0..x.into() {
380                                if next {
381                                    results.cursor_next();
382                                } else {
383                                    results.cursor_prev();
384                                }
385                            }
386                        }
387                        Action::PreviewUp(n) => {
388                            if let Some(p) = preview_ui.as_mut() {
389                                p.up(n)
390                            }
391                        }
392                        Action::PreviewDown(n) => {
393                            if let Some(p) = preview_ui.as_mut() {
394                                p.down(n)
395                            }
396                        }
397                        Action::PreviewHalfPageUp => todo!(),
398                        Action::PreviewHalfPageDown => todo!(),
399                        Action::Pos(pos) => {
400                            let pos = if pos >= 0 {
401                                pos as u32
402                            } else {
403                                results.status.matched_count.saturating_sub((-pos) as u32)
404                            };
405                            results.cursor_jump(pos);
406                        }
407                        Action::InputPos(pos) => {
408                            let pos = if pos >= 0 {
409                                pos as u16
410                            } else {
411                                (input.len() as u16).saturating_sub((-pos) as u16)
412                            };
413                            input.set(None, pos);
414                        }
415
416                        // Experimental/Debugging
417                        Action::Redraw => {
418                            tui.redraw();
419                        }
420                        Action::Overlay(index) => {
421                            if let Some(x) = overlay_ui.as_mut() {
422                                x.enable(index, &ui.area);
423                                tui.redraw();
424                            };
425                        }
426                        Action::Custom(e) => {
427                            if let Some(handler) = ext_handler {
428                                handler(
429                                    e,
430                                    &mut state.dispatcher(
431                                        &mut ui,
432                                        &mut picker_ui,
433                                        &mut footer_ui,
434                                        &mut preview_ui,
435                                    ),
436                                );
437                            }
438                        }
439                        _ => {}
440                    }
441                }
442                _ => {}
443            }
444
445            let interrupt = state.interrupt();
446
447            match interrupt {
448                Interrupt::None => continue,
449                Interrupt::Execute => {
450                    if controller_tx.send(Event::Pause).is_err() {
451                        break;
452                    }
453                    tui.enter_execute();
454                    did_exit = true;
455                    did_pause = true;
456                }
457                Interrupt::Reload => {
458                    picker_ui.worker.restart(false);
459                }
460                Interrupt::Become => {
461                    tui.exit();
462                }
463                _ => {}
464            }
465            // Apply interrupt effect
466            {
467                let mut dispatcher =
468                    state.dispatcher(&mut ui, &mut picker_ui, &mut footer_ui, &mut preview_ui);
469                for h in dynamic_handlers.1.get(interrupt) {
470                    h(&mut dispatcher);
471                }
472
473                if matches!(interrupt, Interrupt::Become) {
474                    return Err(MatchError::Become(state.payload().clone()));
475                }
476            }
477
478            if state.should_quit {
479                let ret = picker_ui.selector.output().collect::<Vec<S>>();
480                return if picker_ui.selector.is_disabled()
481                    && let Some((_, item)) = get_current(&picker_ui)
482                {
483                    Ok(vec![item])
484                } else if ret.is_empty() {
485                    Err(MatchError::Abort(0))
486                } else {
487                    Ok(ret)
488                };
489            } else if state.should_quit_nomatch {
490                return Err(MatchError::NoMatch);
491            }
492        }
493
494        // debug!("{state:?}");
495
496        // ------------- update state + render ------------------------
497        picker_ui.update();
498        // process exit conditions
499        if exit_config.select_1
500            && picker_ui.results.status.matched_count == 1
501            && let Some((_, item)) = get_current(&picker_ui)
502        {
503            return Ok(vec![item]);
504        }
505
506        // resume tui
507        if did_exit {
508            tui.return_execute()
509                .map_err(|e| MatchError::TUIError(e.to_string()))?;
510            tui.redraw();
511        }
512
513        let mut overlay_ui_ref = overlay_ui.as_mut();
514        tui.terminal
515            .draw(|frame| {
516                let mut area = frame.area();
517
518                render_ui(frame, &mut area, &ui);
519
520                let mut _area = area;
521
522                let full_width_footer = footer_ui.single()
523                    && footer_ui.config.row_connection_style == RowConnectionStyle::Full;
524                let mut footer =
525                    if full_width_footer || preview_ui.as_ref().is_none_or(|p| !p.is_show()) {
526                        split(&mut _area, footer_ui.height(), picker_ui.reverse())
527                    } else {
528                        Rect::default()
529                    };
530
531                let [preview, picker_area, footer] = if let Some(preview_ui) = preview_ui.as_mut()
532                    && let Some(layout) = preview_ui.layout()
533                {
534                    let [preview, mut picker_area] = layout.split(_area);
535
536                    if state.iterations == 0 && picker_area.width <= 5 {
537                        warn!("UI too narrow, hiding preview");
538                        preview_ui.show(false);
539
540                        [Rect::default(), _area, footer]
541                    } else {
542                        if !full_width_footer {
543                            footer =
544                                split(&mut picker_area, footer_ui.height(), picker_ui.reverse());
545                        }
546
547                        [preview, picker_area, footer]
548                    }
549                } else {
550                    [Rect::default(), _area, footer]
551                };
552
553                let [input, status, header, results] = picker_ui.layout(picker_area);
554
555                // compare and save dimensions
556                did_resize = state.update_layout([preview, input, status, results]);
557
558                if did_resize {
559                    picker_ui.results.update_dimensions(&results);
560                    picker_ui.input.update_width(input.width);
561                    footer_ui.update_width(
562                        if footer_ui.config.row_connection_style == RowConnectionStyle::Capped {
563                            area.width
564                        } else {
565                            footer.width
566                        },
567                    );
568                    picker_ui.header.update_width(header.width);
569                    // although these only want update when the whole ui change
570                    ui.update_dimensions(area);
571                    if let Some(x) = overlay_ui_ref.as_deref_mut() {
572                        x.update_dimensions(&area);
573                    }
574                };
575
576                render_input(frame, input, &mut picker_ui.input);
577                render_status(frame, status, &picker_ui.results);
578                render_results(frame, results, &mut picker_ui, &mut click);
579                render_display(frame, header, &mut picker_ui.header, &picker_ui.results);
580                render_header_lines(frame, header, &picker_ui.header, &picker_ui.results);
581                render_display(frame, footer, &mut footer_ui, &picker_ui.results);
582                if let Some(preview_ui) = preview_ui.as_mut() {
583                    state.update_preview_ui(preview_ui);
584                    if did_resize {
585                        preview_ui.update_dimensions(&preview);
586                    }
587                    render_preview(frame, preview, preview_ui);
588                }
589                if let Some(x) = overlay_ui_ref {
590                    x.draw(frame);
591                }
592            })
593            .map_err(|e| MatchError::TUIError(e.to_string()))?;
594
595        // useful to clear artifacts
596        if did_resize && tui.config.redraw_on_resize && !did_exit {
597            tui.redraw();
598        }
599        buffer.clear();
600
601        // note: the remainder could be scoped by a conditional on having run?
602        // ====== Event handling ==========
603        state.update(&picker_ui, &overlay_ui);
604        let events = state.events();
605
606        // ---- Invoke handlers -------
607        let mut dispatcher =
608            state.dispatcher(&mut ui, &mut picker_ui, &mut footer_ui, &mut preview_ui);
609        // if let Some((signal, handler)) = signal_handler &&
610        // let s = signal.load(std::sync::atomic::Ordering::Acquire) &&
611        // s > 0
612        // {
613        //     handler(s, &mut dispatcher);
614        //     signal.store(0, std::sync::atomic::Ordering::Release);
615        // };
616
617        // ping handlers with events
618        for e in events.iter() {
619            for h in dynamic_handlers.0.get(e) {
620                h(&mut dispatcher, &e)
621            }
622        }
623
624        // ------------------------------
625        // send events into controller
626        for e in events.iter() {
627            controller_tx
628                .send(e)
629                .unwrap_or_else(|err| eprintln!("send failed: {:?}", err));
630        }
631        // =================================
632
633        if did_pause {
634            log::debug!("Waiting for ack response to pause");
635            if controller_tx.send(Event::Resume).is_err() {
636                break;
637            };
638            // due to control flow, this does nothing, but is anyhow a useful safeguard to guarantee the pause
639            while let Some(msg) = render_rx.recv().await {
640                if matches!(msg, RenderCommand::Ack) {
641                    log::debug!("Recieved ack response to pause");
642                    break;
643                }
644            }
645        }
646
647        click.process(&mut buffer);
648    }
649
650    Err(MatchError::EventLoopClosed)
651}
652
653// ------------------------- HELPERS ----------------------------
654
655pub enum Click {
656    None,
657    ResultPos(u16),
658    ResultIdx(u32),
659}
660
661impl Click {
662    fn process<A: ActionExt>(&mut self, buffer: &mut Vec<RenderCommand<A>>) {
663        match self {
664            Self::ResultIdx(u) => {
665                buffer.push(RenderCommand::Action(Action::Pos(*u as i32)));
666            }
667            _ => {
668                // todo
669            }
670        }
671        *self = Click::None
672    }
673}
674
675fn render_preview(frame: &mut Frame, area: Rect, ui: &mut PreviewUI) {
676    // if ui.view.changed() {
677    // doesn't work, use resize
678    //     frame.render_widget(Clear, area);
679    // } else {
680    //     let widget = ui.make_preview();
681    //     frame.render_widget(widget, area);
682    // }
683    let widget = ui.make_preview();
684    frame.render_widget(widget, area);
685}
686
687fn render_results<T: SSS, S: Selection>(
688    frame: &mut Frame,
689    mut area: Rect,
690    ui: &mut PickerUI<T, S>,
691    click: &mut Click,
692) {
693    let cap = matches!(
694        ui.results.config.row_connection_style,
695        RowConnectionStyle::Capped
696    );
697    let (widget, table_width) = ui.make_table(click);
698
699    if cap {
700        area.width = area.width.min(table_width);
701    }
702
703    frame.render_widget(widget, area);
704}
705
706fn render_input(frame: &mut Frame, area: Rect, ui: &mut InputUI) {
707    ui.scroll_to_cursor();
708    let widget = ui.make_input();
709    if let CursorSetting::Default = ui.config.cursor {
710        frame.set_cursor_position(ui.cursor_offset(&area))
711    };
712
713    frame.render_widget(widget, area);
714}
715
716fn render_status(frame: &mut Frame, area: Rect, ui: &ResultsUI) {
717    if ui.config.status_show {
718        let widget = ui.make_status();
719        frame.render_widget(widget, area);
720    }
721}
722
723fn render_display(frame: &mut Frame, area: Rect, ui: &mut DisplayUI, results_ui: &ResultsUI) {
724    if !ui.show {
725        return;
726    }
727    let widget = ui.make_display(
728        results_ui.indentation() as u16,
729        results_ui.widths().to_vec(),
730        results_ui.config.column_spacing.0,
731    );
732
733    log::debug!("{widget:?}");
734    frame.render_widget(widget, area);
735}
736
737fn render_header_lines(frame: &mut Frame, area: Rect, ui: &DisplayUI, results_ui: &ResultsUI) {
738    if !ui.show || !ui.single() {
739        return;
740    }
741    let widget = ui.make_full_width_row(results_ui.indentation() as u16);
742
743    frame.render_widget(widget, area);
744}
745
746fn render_ui(frame: &mut Frame, area: &mut Rect, ui: &UI) {
747    let widget = ui.make_ui();
748    frame.render_widget(widget, *area);
749    *area = ui.inner_area(area);
750}
751
752fn split(rect: &mut Rect, height: u16, cut_top: bool) -> Rect {
753    let h = height.min(rect.height);
754
755    if cut_top {
756        let offshoot = Rect {
757            x: rect.x,
758            y: rect.y,
759            width: rect.width,
760            height: h,
761        };
762
763        rect.y += h;
764        rect.height -= h;
765
766        offshoot
767    } else {
768        let offshoot = Rect {
769            x: rect.x,
770            y: rect.y + rect.height - h,
771            width: rect.width,
772            height: h,
773        };
774
775        rect.height -= h;
776
777        offshoot
778    }
779}
780
781// -----------------------------------------------------------------------------------
782
783#[cfg(test)]
784mod test {}
785
786// #[cfg(test)]
787// async fn send_every_second(tx: mpsc::UnboundedSender<RenderCommand>) {
788//     let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
789
790//     loop {
791//         interval.tick().await;
792//         if tx.send(RenderCommand::quit()).is_err() {
793//             break;
794//         }
795//     }
796// }