matchmaker/
matchmaker.rs

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