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