matchmaker/
matchmaker.rs

1use std::{
2    fmt::{self, Debug, Formatter},
3    process::Stdio,
4    sync::Arc,
5};
6
7use arrayvec::ArrayVec;
8use cli_boilerplate_automation::{
9    _log,
10    broc::{exec_script, spawn_script},
11    env_vars, prints,
12};
13use easy_ext::ext;
14use log::{debug, info, warn};
15use ratatui::text::Text;
16
17use crate::{
18    MatchError, RenderFn, Result, SSS, Selection, Selector, SplitterFn,
19    action::{ActionAliaser, ActionExt, ActionExtHandler, NullActionExt},
20    binds::BindMap,
21    config::{ExitConfig, PreviewerConfig, RenderConfig, Split, TerminalConfig, WorkerConfig},
22    efx,
23    event::{EventLoop, RenderSender},
24    message::{Event, Interrupt},
25    nucleo::{
26        Indexed, Segmented, Worker,
27        injector::{IndexedInjector, Injector, SegmentedInjector, WorkerInjector},
28    },
29    preview::{
30        AppendOnly, Preview,
31        previewer::{PreviewMessage, Previewer},
32    },
33    render::{self, DynamicMethod, Effects, EventHandlers, InterruptHandlers, MMState},
34    tui,
35    ui::{Overlay, OverlayUI, UI},
36};
37
38/// The main entrypoint of the library. To use:
39/// 1. create your worker (T -> Columns)
40/// 2. Determine your identifier
41/// 3. Instantiate this with Matchmaker::new_from_raw(..)
42/// 4. Register your handlers
43///    4.5 Start and connect your previewer
44/// 5. Call mm.pick() or mm.pick_with_matcher(&mut matcher)
45pub struct Matchmaker<T: SSS, S: Selection = T> {
46    pub worker: Worker<T>,
47    pub render_config: RenderConfig,
48    pub tui_config: TerminalConfig,
49    pub exit_config: ExitConfig,
50    pub selector: Selector<T, S>,
51    pub event_handlers: EventHandlers<T, S>,
52    pub interrupt_handlers: InterruptHandlers<T, S>,
53    pub preview: Option<Preview>,
54}
55
56// ----------- MAIN -----------------------
57
58// defined for lack of a better way to expose these fns, i.e. to allow clients to request new injectors in case of worker restart
59pub struct OddEnds {
60    pub formatter: Arc<RenderFn<Indexed<Segmented<String>>>>,
61    pub splitter: SplitterFn<String>,
62}
63
64pub type ConfigInjector = SegmentedInjector<
65    String,
66    IndexedInjector<Segmented<String>, WorkerInjector<Indexed<Segmented<String>>>>,
67>;
68pub type ConfigMatchmaker = Matchmaker<Indexed<Segmented<String>>, Segmented<String>>;
69
70impl ConfigMatchmaker {
71    /// Creates a new Matchmaker from a config::BaseConfig.
72    pub fn new_from_config(
73        render_config: RenderConfig,
74        tui_config: TerminalConfig,
75        worker_config: WorkerConfig,
76    ) -> (Self, ConfigInjector, OddEnds) {
77        let cc = worker_config.columns;
78
79        let worker: Worker<Indexed<Segmented<String>>> = match cc.split {
80            Split::Delimiter(_) | Split::Regexes(_) => {
81                let names: Vec<Arc<str>> = if cc.names.is_empty() {
82                    (0..cc.max_cols())
83                        .map(|n| Arc::from(n.to_string()))
84                        .collect()
85                } else {
86                    cc.names
87                        .iter()
88                        .map(|s| Arc::from(s.name.as_str()))
89                        .collect()
90                };
91                Worker::new_indexable(names)
92            }
93            Split::None => Worker::new_indexable([""]),
94        };
95
96        let injector = worker.injector();
97
98        // the computed number of columns, <= cc.max_columns = MAX_COLUMNS
99        let col_count = worker.columns.len();
100
101        // Arc over box due to capturing
102        let splitter: SplitterFn<String> = match cc.split {
103            Split::Delimiter(ref rg) => {
104                let rg = rg.clone();
105                Arc::new(move |s| {
106                    let mut ranges = ArrayVec::new();
107                    let mut last_end = 0;
108                    for m in rg.find_iter(s).take(col_count) {
109                        ranges.push((last_end, m.start()));
110                        last_end = m.end();
111                    }
112                    ranges.push((last_end, s.len()));
113                    ranges
114                })
115            }
116            Split::Regexes(ref rgs) => {
117                let rgs = rgs.clone(); // or Arc
118                Arc::new(move |s| {
119                    let mut ranges = ArrayVec::new();
120                    for re in rgs.iter().take(col_count) {
121                        if let Some(m) = re.find(s) {
122                            ranges.push((m.start(), m.end()));
123                        } else {
124                            ranges.push((0, 0));
125                        }
126                    }
127                    ranges
128                })
129            }
130            Split::None => Arc::new(|s| ArrayVec::from_iter([(0, s.len())])),
131        };
132        let injector = IndexedInjector::new(injector, 0);
133        let injector = SegmentedInjector::new(injector, splitter.clone());
134
135        let selection_set = Selector::new(Indexed::identifier);
136
137        let event_handlers = EventHandlers::new();
138        let interrupt_handlers = InterruptHandlers::new();
139        let formatter = Arc::new(
140            worker.make_format_fn::<true>(|item| std::borrow::Cow::Borrowed(&item.inner.inner)),
141        );
142
143        let new = Matchmaker {
144            worker,
145            render_config,
146            tui_config,
147            exit_config: worker_config.exit,
148            selector: selection_set,
149            event_handlers,
150            interrupt_handlers,
151            preview: None,
152        };
153
154        let misc = OddEnds {
155            formatter,
156            splitter,
157        };
158
159        (new, injector, misc)
160    }
161}
162
163impl<T: SSS, S: Selection> Matchmaker<T, S> {
164    pub fn new(worker: Worker<T>, selector: Selector<T, S>) -> Self {
165        Matchmaker {
166            worker,
167            render_config: RenderConfig::default(),
168            tui_config: TerminalConfig::default(),
169            exit_config: ExitConfig::default(),
170            selector,
171            event_handlers: EventHandlers::new(),
172            interrupt_handlers: InterruptHandlers::new(),
173            preview: None,
174        }
175    }
176
177    // pub fn new_raw(worker: Worker<T>, identifier: Identifier<T, S>) -> Self {
178    //     Matchmaker {
179    //         worker,
180    //         render_config: RenderConfig::default(),
181    //         overlay_config: Default::default(),
182    //         tui_config: TerminalConfig::default(),
183    //         exit_config: ExitConfig::default(),
184    //         selection_set: SelectionSet::new(identifier),
185    //         event_handlers: EventHandlers::new(),
186    //         interrupt_handlers: InterruptHandlers::new(),
187    //         preview: None
188    //     }
189    // }
190
191    /// The contents of the preview are displayed in a pane when picking.
192    pub fn connect_preview(&mut self, preview: Preview) {
193        self.preview = Some(preview);
194    }
195
196    /// Configure the UI
197    pub fn config_render(&mut self, render: RenderConfig) -> &mut Self {
198        self.render_config = render;
199        self
200    }
201    /// Configure the TUI
202    pub fn config_tui(&mut self, tui: TerminalConfig) -> &mut Self {
203        self.tui_config = tui;
204        self
205    }
206    /// Configure exit conditions
207    pub fn config_exit(&mut self, exit: ExitConfig) -> &mut Self {
208        self.exit_config = exit;
209        self
210    }
211    /// Register a handler to listen on [`Event`]s
212    pub fn register_event_handler<F, I>(&mut self, events: I, handler: F)
213    where
214        F: Fn(&mut MMState<'_, T, S>, &Event) -> Effects + SSS,
215        I: IntoIterator<Item = Event>,
216    {
217        let boxed = Box::new(handler);
218        self.register_boxed_event_handler(events, boxed);
219    }
220    /// Register a boxed handler to listen on [`Event`]s
221    pub fn register_boxed_event_handler<I>(
222        &mut self,
223        events: I,
224        handler: DynamicMethod<T, S, Event>,
225    ) where
226        I: IntoIterator<Item = Event>,
227    {
228        let events_vec: Vec<_> = events.into_iter().collect();
229        self.event_handlers.set(events_vec, handler);
230    }
231    /// Register a handler to listen on [`Interrupt`]s
232    pub fn register_interrupt_handler<F>(&mut self, interrupt: Interrupt, handler: F)
233    where
234        F: Fn(&mut MMState<'_, T, S>, &Interrupt) -> Effects + SSS,
235    {
236        let boxed = Box::new(handler);
237        self.register_boxed_interrupt_handler(interrupt, boxed);
238    }
239    /// Register a boxed handler to listen on [`Interrupt`]s
240    pub fn register_boxed_interrupt_handler(
241        &mut self,
242        variant: Interrupt,
243        handler: DynamicMethod<T, S, Interrupt>,
244    ) {
245        self.interrupt_handlers.set(variant, handler);
246    }
247
248    /// The main method of the Matchmaker. It starts listening for events and renders the TUI with ratatui. It successfully returns with all the selected items selected when the Accept action is received.
249    pub async fn pick<A: ActionExt>(
250        mut self,
251        builder: PickOptions<'_, T, S, A>,
252    ) -> Result<Vec<S>, MatchError> {
253        let PickOptions {
254            previewer,
255            ext_handler,
256            ext_aliaser,
257            #[cfg(feature = "bracketed-paste")]
258            paste_handler,
259            ..
260        } = builder;
261
262        if self.exit_config.select_1 && self.worker.counts().0 == 1 {
263            return Ok(self
264                .selector
265                .identify_to_vec([self.worker.get_nth(0).unwrap()]));
266        }
267
268        let mut event_loop = if let Some(e) = builder.event_loop {
269            e
270        } else if let Some(binds) = builder.binds {
271            EventLoop::with_binds(binds).with_tick_rate(self.render_config.tick_rate())
272        } else {
273            EventLoop::new()
274        };
275
276        // note: this part is "crate-specific" since clients likely use their own previewer
277        if let Some(mut previewer) = previewer {
278            if self.preview.is_none() {
279                self.preview = Some(previewer.view());
280            }
281            previewer.connect_controller(event_loop.get_controller());
282            tokio::spawn(async move {
283                let _ = previewer.run().await;
284            });
285        }
286
287        let (render_tx, render_rx) = builder
288            .channel
289            .unwrap_or_else(tokio::sync::mpsc::unbounded_channel);
290        event_loop.add_tx(render_tx.clone());
291
292        let mut tui =
293            tui::Tui::new(self.tui_config).map_err(|e| MatchError::TUIError(e.to_string()))?;
294        tui.enter()
295            .map_err(|e| MatchError::TUIError(e.to_string()))?;
296
297        // important to start after tui
298        let event_controller = event_loop.get_controller();
299        tokio::spawn(async move {
300            let _ = event_loop.run().await;
301        });
302        log::debug!("event loop started");
303
304        let overlay_ui = if builder.overlays.is_empty() {
305            None
306        } else {
307            Some(OverlayUI::new(
308                builder.overlays.into_boxed_slice(),
309                self.render_config.overlay.take().unwrap_or_default(),
310            ))
311        };
312
313        // initial redraw to clear artifacts,
314        tui.redraw();
315
316        if let Some(matcher) = builder.matcher {
317            let (ui, picker, preview) = UI::new(
318                self.render_config,
319                matcher,
320                self.worker,
321                self.selector,
322                self.preview,
323                &mut tui,
324            );
325            render::render_loop(
326                ui,
327                picker,
328                preview,
329                tui,
330                overlay_ui,
331                self.exit_config,
332                render_rx,
333                event_controller,
334                (self.event_handlers, self.interrupt_handlers),
335                ext_handler,
336                ext_aliaser,
337                #[cfg(feature = "bracketed-paste")]
338                paste_handler,
339            )
340            .await
341        } else {
342            let mut matcher = nucleo::Matcher::new(nucleo::Config::DEFAULT);
343            let (ui, picker, preview) = UI::new(
344                self.render_config,
345                &mut matcher,
346                self.worker,
347                self.selector,
348                self.preview,
349                &mut tui,
350            );
351            render::render_loop(
352                ui,
353                picker,
354                preview,
355                tui,
356                overlay_ui,
357                self.exit_config,
358                render_rx,
359                event_controller,
360                (self.event_handlers, self.interrupt_handlers),
361                ext_handler,
362                ext_aliaser,
363                #[cfg(feature = "bracketed-paste")]
364                paste_handler,
365            )
366            .await
367        }
368    }
369
370    pub async fn pick_default(self) -> Result<Vec<S>, MatchError> {
371        self.pick::<NullActionExt>(PickOptions::new()).await
372    }
373}
374
375#[ext(MatchResultExt)]
376impl<T> Result<T, MatchError> {
377    /// Return the first element
378    pub fn first<S>(self) -> Result<S, MatchError>
379    where
380        T: IntoIterator<Item = S>,
381    {
382        match self {
383            Ok(v) => v.into_iter().next().ok_or(MatchError::NoMatch),
384            Err(e) => Err(e),
385        }
386    }
387
388    /// Handle [`MatchError::Abort`] using [`std::process::exit`]
389    pub fn abort(self) -> Result<T, MatchError> {
390        match self {
391            Err(MatchError::Abort(x)) => std::process::exit(x),
392            _ => self,
393        }
394    }
395}
396
397// --------- BUILDER -------------
398
399/// Returns what should be pushed to input
400pub type PasteHandler<T, S> = fn(String, &MMState<'_, T, S>) -> String;
401/// Used to configure [`Matchmaker::pick`] with additional options.
402pub struct PickOptions<'a, T: SSS, S: Selection, A: ActionExt = NullActionExt> {
403    matcher: Option<&'a mut nucleo::Matcher>,
404    matcher_config: nucleo::Config,
405
406    event_loop: Option<EventLoop<A>>,
407    binds: Option<BindMap<A>>,
408
409    ext_handler: Option<ActionExtHandler<T, S, A>>,
410    ext_aliaser: Option<ActionAliaser<T, S, A>>,
411    #[cfg(feature = "bracketed-paste")]
412    paste_handler: Option<PasteHandler<T, S>>,
413
414    overlays: Vec<Box<dyn Overlay<A = A>>>,
415    previewer: Option<Previewer>,
416
417    /// # Experimental
418    // pub signal_handler: Option<(&'static std::sync::atomic::AtomicUsize, SignalHandler<T, S>)>,
419    /// Initializing code, i.e. to setup context in the running thread. Since render_loop runs on the same thread this isn't actually necessary
420    /// but it seems a good idea to provide a standard way.
421    // initializer: Option<Box<dyn FnOnce()>>,
422    pub channel: Option<(
423        RenderSender<A>,
424        tokio::sync::mpsc::UnboundedReceiver<crate::message::RenderCommand<A>>,
425    )>,
426}
427
428// todo: support initializing render loop from (tx, event_loop, elcfg, nothing)
429impl<'a, T: SSS, S: Selection, A: ActionExt> PickOptions<'a, T, S, A> {
430    pub const fn new() -> Self {
431        Self {
432            matcher: None,
433            event_loop: None,
434            previewer: None,
435            binds: None,
436            matcher_config: nucleo::Config::DEFAULT,
437            ext_handler: None,
438            ext_aliaser: None,
439            #[cfg(feature = "bracketed-paste")]
440            paste_handler: None,
441            overlays: Vec::new(),
442            channel: None,
443        }
444    }
445
446    pub fn with_binds(binds: BindMap<A>) -> Self {
447        let mut ret = Self::new();
448        ret.binds = Some(binds);
449        ret
450    }
451
452    pub fn with_matcher(matcher: &'a mut nucleo::Matcher) -> Self {
453        let mut ret = Self::new();
454        ret.matcher = Some(matcher);
455        ret
456    }
457
458    pub fn binds(mut self, binds: BindMap<A>) -> Self {
459        self.binds = Some(binds);
460        self
461    }
462
463    pub fn event_loop(mut self, event_loop: EventLoop<A>) -> Self {
464        self.event_loop = Some(event_loop);
465        self
466    }
467
468    /// Set a [`Previewer`]
469    pub fn previewer(mut self, previewer: Previewer) -> Self {
470        self.previewer = Some(previewer);
471        self
472    }
473
474    pub fn matcher(mut self, matcher_config: nucleo::Config) -> Self {
475        self.matcher_config = matcher_config;
476        self
477    }
478
479    pub fn ext_handler(mut self, handler: ActionExtHandler<T, S, A>) -> Self {
480        self.ext_handler = Some(handler);
481        self
482    }
483
484    pub fn ext_aliaser(mut self, aliaser: ActionAliaser<T, S, A>) -> Self {
485        self.ext_aliaser = Some(aliaser);
486        self
487    }
488
489    #[cfg(feature = "bracketed-paste")]
490    pub fn paste_handler(mut self, handler: PasteHandler<T, S>) -> Self {
491        self.paste_handler = Some(handler);
492        self
493    }
494
495    pub fn overlay<O>(mut self, overlay: O) -> Self
496    where
497        O: Overlay<A = A> + 'static,
498    {
499        self.overlays.push(Box::new(overlay));
500        self
501    }
502
503    pub fn get_tx(&mut self) -> RenderSender<A> {
504        if let Some((s, _)) = &self.channel {
505            s.clone()
506        } else {
507            let channel = tokio::sync::mpsc::unbounded_channel();
508            let ret = channel.0.clone();
509            self.channel = Some(channel);
510            ret
511        }
512    }
513
514    // pub fn signal_handler(
515    //     mut self,
516    //     signal: &'static std::sync::atomic::AtomicUsize,
517    //     handler: SignalHandler<T, S>,
518    // ) -> Self {
519    //     self.signal_handler = Some((signal, handler));
520    //     self
521    // }
522
523    // pub fn initializer(
524    //     mut self,
525    //     initializer: impl FnOnce() + 'static,
526    // ) -> Self {
527    //     self.initializer = Some(Box::new(initializer));
528    //     self
529    // }
530}
531
532impl<'a, T: SSS, S: Selection, A: ActionExt> Default for PickOptions<'a, T, S, A> {
533    fn default() -> Self {
534        Self::new()
535    }
536}
537
538// ----------- ATTACHMENTS ------------------
539
540impl<T: SSS, S: Selection> Matchmaker<T, S> {
541    pub fn register_print_handler(
542        &mut self,
543        print_handle: AppendOnly<String>,
544        formatter: Arc<RenderFn<T>>,
545    ) {
546        self.register_interrupt_handler(Interrupt::Print("".into()), move |state, i| {
547            if let Interrupt::Print(template) = i
548                && let Some(t) = state.current_raw()
549            {
550                let s = formatter(t, template);
551                if atty::is(atty::Stream::Stdout) {
552                    print_handle.push(s);
553                } else {
554                    prints!(s);
555                }
556            };
557            efx![]
558        });
559    }
560
561    pub fn register_execute_handler(&mut self, formatter: Arc<RenderFn<T>>) {
562        let preview_formatter = formatter.clone();
563
564        self.register_interrupt_handler(Interrupt::Execute("".into()), move |state, interrupt| {
565            if let Interrupt::Execute(template) = interrupt
566                && !template.is_empty()
567                && let Some(t) = state.current_raw()
568            {
569                let cmd = formatter(t, template);
570                let mut vars = state.make_env_vars();
571                let preview_cmd = preview_formatter(t, state.preview_payload());
572                let extra = env_vars!(
573                    "FZF_PREVIEW_COMMAND" => preview_cmd,
574                );
575                vars.extend(extra);
576                let tty = maybe_tty();
577                if let Some(mut child) =
578                    spawn_script(&cmd, vars, tty, Stdio::inherit(), Stdio::inherit())
579                {
580                    match child.wait() {
581                        Ok(i) => {
582                            info!("Command [{cmd}] exited with {i}")
583                        }
584                        Err(e) => {
585                            info!("Failed to wait on command [{cmd}]: {e}")
586                        }
587                    }
588                }
589            };
590            efx![]
591        });
592    }
593
594    pub fn register_become_handler(&mut self, formatter: Arc<RenderFn<T>>) {
595        let preview_formatter = formatter.clone();
596
597        self.register_interrupt_handler(Interrupt::Become("".into()), move |state, interrupt| {
598            if let Interrupt::Become(template) = interrupt
599                && !template.is_empty()
600                && let Some(t) = state.current_raw()
601            {
602                let cmd = formatter(t, template);
603                let mut vars = state.make_env_vars();
604
605                let preview_cmd = preview_formatter(t, state.preview_payload());
606                let extra = env_vars!(
607                    "FZF_PREVIEW_COMMAND" => preview_cmd,
608                );
609                vars.extend(extra);
610                debug!("Becoming: {cmd}");
611                exec_script(&cmd, vars);
612            }
613            efx![]
614        });
615    }
616}
617
618pub fn make_previewer<T: SSS, S: Selection>(
619    mm: &mut Matchmaker<T, S>,
620    previewer_config: PreviewerConfig, // help_str is provided seperately so help_colors is ignored
621    formatter: Arc<RenderFn<T>>,
622    help_str: Text<'static>,
623) -> Previewer {
624    // initialize previewer
625    let (previewer, tx) = Previewer::new(previewer_config);
626    mm.connect_preview(previewer.view());
627    let preview_tx = tx.clone();
628
629    // preview handler
630    mm.register_event_handler([Event::CursorChange, Event::PreviewChange], move |state, _| {
631            if state.preview_show &&
632            let Some(t) = state.current_raw() &&
633            let m = state.preview_payload() &&
634            !m.is_empty()
635            {
636                let cmd = formatter(t, m);
637                let mut envs = state.make_env_vars();
638                let extra = env_vars!(
639                    "COLUMNS" => state.previewer_area().map_or("0".to_string(), |r| r.width.to_string()),
640                    "LINES" => state.previewer_area().map_or("0".to_string(), |r| r.height.to_string()),
641                );
642                envs.extend(extra);
643
644                let msg = PreviewMessage::Run(cmd.clone(), envs);
645                _log!("{cmd:?}");
646                if preview_tx.send(msg.clone()).is_err() {
647                    warn!("Failed to send to preview: {}", msg)
648                }
649            } else if preview_tx.send(PreviewMessage::Stop).is_err() {
650                warn!("Failed to send to preview: stop")
651            }
652
653            efx![render::Effect::ClearPreviewSet] //
654        });
655
656    mm.register_event_handler([Event::PreviewSet], move |state, _event| {
657        if state.preview_show {
658            let msg = if let Some(m) = state.preview_set_payload() {
659                let m = if m.is_empty() && !help_str.lines.is_empty() {
660                    help_str.clone()
661                } else {
662                    Text::from(m.clone())
663                };
664                PreviewMessage::Set(m.clone())
665            } else {
666                PreviewMessage::Unset
667            };
668
669            if tx.send(msg.clone()).is_err() {
670                warn!("Failed to send: {}", msg)
671            }
672        }
673        efx![]
674    });
675
676    previewer
677}
678
679fn maybe_tty() -> Stdio {
680    if let Ok(mut tty) = std::fs::File::open("/dev/tty") {
681        let _ = std::io::Write::flush(&mut tty); // does nothing but seems logical
682        Stdio::from(tty)
683    } else {
684        log::error!("Failed to open /dev/tty");
685        Stdio::inherit()
686    }
687}
688
689// ------------ BOILERPLATE ---------------
690
691impl<T: SSS + Debug, S: Selection + Debug> Debug for Matchmaker<T, S> {
692    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
693        f.debug_struct("Matchmaker")
694            // omit `worker`
695            .field("render_config", &self.render_config)
696            .field("tui_config", &self.tui_config)
697            .field("selection_set", &self.selector)
698            .field("event_handlers", &self.event_handlers)
699            .field("interrupt_handlers", &self.interrupt_handlers)
700            .field("previewer", &self.preview)
701            .finish()
702    }
703}