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