matchmaker/
matchmaker.rs

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