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