Skip to main content

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