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 cba::{bath::PathExt, broc::CommandExt, ebog, 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        ColumnsConfig, ExitConfig, OverlayConfig, PreviewerConfig, RenderConfig, Split,
19        TerminalConfig, 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
58pub struct OddEnds {
59    pub splitter: SplitterFn<Either<String, Text<'static>>>,
60    pub hidden_columns: Vec<bool>,
61    pub has_error: bool
62}
63
64pub type ConfigInjector = AnsiInjector<
65SegmentedInjector<
66Either<String, Text<'static>>,
67IndexedInjector<Segmented<Either<String, Text<'static>>>, WorkerInjector<ConfigMMItem>>,
68>,
69>;
70pub type ConfigMatchmaker = Matchmaker<ConfigMMItem, Segmented<Either<String, Text<'static>>>>;
71pub type ConfigMMInnerItem = Segmented<Either<String, Text<'static>>>;
72pub type ConfigMMItem = Indexed<ConfigMMInnerItem>;
73
74impl ConfigMatchmaker {
75    #[allow(unused)]
76    /// Creates a new Matchmaker from a config::BaseConfig.
77    pub fn new_from_config(
78        render_config: RenderConfig,
79        tui_config: TerminalConfig,
80        worker_config: WorkerConfig,
81        columns_config: ColumnsConfig,
82        exit_config: ExitConfig,
83        preprocess_config: PreprocessOptions,
84    ) -> (Self, ConfigInjector, OddEnds) {
85        let mut has_error = false;
86
87        let cc = columns_config;
88        let hidden_columns = cc.names.iter().map(|x| x.hidden).collect();
89        // "hack" because we cannot make the results stable in the worker as our current hack uses the identifier
90        let init = !cc.names_from_zero as usize;
91        let mut worker: Worker<ConfigMMItem> = match cc.split {
92            Split::Delimiter(_) | Split::Regexes(_) => {
93                let names: Vec<Arc<str>> = if cc.names.is_empty() {
94                    (init..(cc.max_cols() + init))
95                    .map(|n| Arc::from(n.to_string()))
96                    .collect()
97                } else {
98                    cc.names
99                    .iter()
100                    .take(cc.max_cols())
101                    .map(|s| Arc::from(s.name.as_str()))
102                    .collect()
103                };
104                Worker::new_indexable(names, cc.default.as_ref().map(|x| x.0.as_str()))
105            }
106            Split::None => Worker::new_indexable([""], None),
107        };
108        
109        #[cfg(feature = "experimental")]
110        worker.reverse_items(worker_config.reverse);
111        #[cfg(feature = "experimental")]
112        worker.set_stability(worker_config.sort_threshold);
113        
114        let injector = worker.injector();
115        
116        // the computed number of columns, <= cc.max_columns = MAX_COLUMNS
117        let col_count = worker.columns.len();
118        
119        // Arc over box due to capturing
120        let splitter: SplitterFn<Either<String, Text>> = match cc.split {
121            Split::Delimiter(ref rg) => {
122                let rg = rg.clone();
123                let names = cc.names.clone();
124                let col_count = worker.columns.len();
125                let mut has_named_group = false;
126                
127                // Map named captures to column indices
128                let capture_to_idx: Vec<Option<usize>> = rg
129                .capture_names()
130                .enumerate()
131                .map(|(i, name_opt)| {
132                    if i == 0 {
133                        None
134                    } else {
135                        name_opt.and_then(|name| {
136                            has_named_group = true;
137                            names.iter().position(|n| n.name == name)
138                        })
139                    }
140                })
141                .collect();
142                
143                // Determine the mode:
144                // 1. Named captures → capture_to_idx has at least one Some
145                // 2. All unnamed → capture_to_idx has at least one None beyond index 0
146                // 3. No capture groups → captures_len() == 1
147                let has_unnamed = rg.captures_len() > 1 && !has_named_group;
148                
149                if has_named_group {
150                    log::debug!("Named regex: {rg} with {} groups", capture_to_idx.len());
151                    if capture_to_idx.iter().all(|x| x.is_none()) {
152                        ebog!("No capture group matches a column name");
153                        has_error = true;
154                    }
155                    
156                    // Named capture groups
157                    Arc::new(move |s| {
158                        let s = &s.to_cow();
159                        let mut ranges = ArrayVec::from_iter(vec![(0, 0); col_count]);
160                        
161                        if let Some(caps) = rg.captures(s) {
162                            for (group_idx, col_idx_opt) in capture_to_idx.iter().enumerate().skip(1) {
163                                if let Some(col_idx) = col_idx_opt {
164                                    if let Some(m) = caps.get(group_idx) {
165                                        ranges[*col_idx] = (m.start(), m.end());
166                                    }
167                                }
168                            }
169                        }
170
171                        ranges
172                    })
173                } else if has_unnamed {
174                    log::debug!("Unnamed regex: {rg} with {} groups", capture_to_idx.len());
175
176                    // All unnamed capture groups → map in order
177                    Arc::new(move |s| {
178                        let s = &s.to_cow();
179                        let mut ranges = ArrayVec::from_iter(vec![(0, 0); col_count]);
180                        
181                        if let Some(caps) = rg.captures(s) {
182                            for (i, group) in caps.iter().skip(1).enumerate().take(col_count) {
183                                if let Some(m) = group {
184                                    ranges[i] = (m.start(), m.end());
185                                }
186                            }
187                        }
188
189                        ranges
190                    })
191                } else {
192                    log::debug!("Delimiter regex: {rg}");
193
194                    // No capture groups → normal delimiter split
195                    Arc::new(move |s| {
196                        let s = &s.to_cow();
197                        let mut ranges = ArrayVec::new();
198                        let mut last_end = 0;
199                        
200                        for m in rg.find_iter(s).take(col_count - 1) {
201                            ranges.push((last_end, m.start()));
202                            last_end = m.end();
203                        }
204                        
205                        ranges.push((last_end, s.len()));
206                        ranges
207                    })
208                }
209            }
210            // not recommended but its supported ig
211            Split::Regexes(ref rgs) => {
212                let rgs = rgs.clone(); // or Arc
213                Arc::new(move |s| {
214                    let s = &s.to_cow();
215                    let mut ranges = ArrayVec::new();
216                    
217                    for re in rgs.iter().take(col_count) {
218                        if let Some(m) = re.find(s) {
219                            ranges.push((m.start(), m.end()));
220                        } else {
221                            ranges.push((0, 0));
222                        }
223                    }
224                    ranges
225                })
226            }
227            Split::None => Arc::new(|s| ArrayVec::from_iter([(0, s.to_cow().len())])),
228        };
229        let injector = IndexedInjector::new_globally_indexed(injector);
230        let injector = SegmentedInjector::new(injector, splitter.clone());
231        let injector = AnsiInjector::new(injector, preprocess_config);
232        
233        let selection_set = if render_config.results.multi {
234            Selector::new(Indexed::identifier)
235        } else {
236            Selector::new(Indexed::identifier).disabled()
237        };
238        
239        let event_handlers = EventHandlers::new();
240        let interrupt_handlers = InterruptHandlers::new();
241        
242        let new = Matchmaker {
243            worker,
244            render_config,
245            tui_config,
246            exit_config,
247            selector: selection_set,
248            event_handlers,
249            interrupt_handlers,
250        };
251        
252        let misc = OddEnds {
253            splitter,
254            hidden_columns,
255            has_error
256        };
257        
258        (new, injector, misc)
259    }
260}
261
262impl<T: SSS, S: Selection> Matchmaker<T, S> {
263    pub fn new(worker: Worker<T>, selector: Selector<T, S>) -> Self {
264        Matchmaker {
265            worker,
266            render_config: RenderConfig::default(),
267            tui_config: TerminalConfig::default(),
268            exit_config: ExitConfig::default(),
269            selector,
270            event_handlers: EventHandlers::new(),
271            interrupt_handlers: InterruptHandlers::new(),
272        }
273    }
274    
275    /// Configure the UI
276    pub fn config_render(&mut self, render: RenderConfig) -> &mut Self {
277        self.render_config = render;
278        self
279    }
280    /// Configure the TUI
281    pub fn config_tui(&mut self, tui: TerminalConfig) -> &mut Self {
282        self.tui_config = tui;
283        self
284    }
285    /// Configure exit conditions
286    pub fn config_exit(&mut self, exit: ExitConfig) -> &mut Self {
287        self.exit_config = exit;
288        self
289    }
290    /// Register a handler to listen on [`Event`]s
291    pub fn register_event_handler<F>(&mut self, event: Event, handler: F)
292    where
293    F: Fn(&mut MMState<'_, '_, T, S>, &Event) + 'static,
294    {
295        let boxed = Box::new(handler);
296        self.register_boxed_event_handler(event, boxed);
297    }
298    /// Register a boxed handler to listen on [`Event`]s
299    pub fn register_boxed_event_handler(
300        &mut self,
301        event: Event,
302        handler: DynamicMethod<T, S, Event>,
303    ) {
304        self.event_handlers.set(event, handler);
305    }
306    /// Register a handler to listen on [`Interrupt`]s
307    pub fn register_interrupt_handler<F>(&mut self, interrupt: Interrupt, handler: F)
308    where
309    F: Fn(&mut MMState<'_, '_, T, S>) + 'static,
310    {
311        let boxed = Box::new(handler);
312        self.register_boxed_interrupt_handler(interrupt, boxed);
313    }
314    /// Register a boxed handler to listen on [`Interrupt`]s
315    pub fn register_boxed_interrupt_handler(
316        &mut self,
317        variant: Interrupt,
318        handler: BoxedHandler<T, S>,
319    ) {
320        self.interrupt_handlers.set(variant, handler);
321    }
322    
323    /// 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.
324    pub async fn pick<A: ActionExt>(self, builder: PickOptions<'_, T, S, A>) -> Result<Vec<S>> {
325        let PickOptions {
326            previewer,
327            ext_handler,
328            ext_aliaser,
329            #[cfg(feature = "bracketed-paste")]
330            paste_handler,
331            overlay_config,
332            hidden_columns,
333            initializer,
334            ..
335        } = builder;
336        
337        if self.exit_config.select_1 && self.worker.counts().0 == 1 {
338            return Ok(self
339                .selector
340                .identify_to_vec([self.worker.get_nth(0).unwrap()]));
341            }
342            
343            let mut event_loop = if let Some(e) = builder.event_loop {
344                e
345            } else if let Some(binds) = builder.binds {
346                EventLoop::with_binds(binds).with_tick_rate(self.render_config.tick_rate())
347            } else {
348                EventLoop::new()
349            };
350            
351            let mut wait = false;
352            if let Some(path) = self.exit_config.last_key_path.clone()
353            && !path.is_empty()
354            {
355                event_loop.record_last_key(path);
356                wait = true;
357            }
358            
359            let preview = match previewer {
360                Some(Either::Left(view)) => Some(view),
361                Some(Either::Right(mut previewer)) => {
362                    let view = previewer.view();
363                    previewer.connect_controller(event_loop.controller());
364                    
365                    tokio::spawn(async move {
366                        let _ = previewer.run().await;
367                    });
368                    
369                    Some(view)
370                }
371                _ => None,
372            };
373            
374            let (render_tx, render_rx) = builder
375            .channel
376            .unwrap_or_else(tokio::sync::mpsc::unbounded_channel);
377            event_loop.add_tx(render_tx.clone());
378            
379            let mut tui =
380            tui::Tui::new(self.tui_config).map_err(|e| MatchError::TUIError(e.to_string()))?;
381            tui.enter()
382            .map_err(|e| MatchError::TUIError(e.to_string()))?;
383            
384            // important to start after tui
385            let event_controller = event_loop.controller();
386            let event_loop_handle = tokio::spawn(async move {
387                let _ = event_loop.run().await;
388            });
389            log::debug!("event loop started");
390            
391            let overlay_ui = if builder.overlays.is_empty() {
392                None
393            } else {
394                Some(OverlayUI::new(
395                    builder.overlays.into_boxed_slice(),
396                    overlay_config.unwrap_or_default(),
397                ))
398            };
399            
400            // initial redraw to clear artifacts,
401            tui.redraw();
402            
403            let matcher = if let Some(matcher) = builder.matcher {
404                matcher
405            } else {
406                &mut nucleo::Matcher::new(nucleo::Config::DEFAULT)
407            };
408            
409            let (ui, picker, footer, preview) = UI::new(
410                self.render_config,
411                matcher,
412                self.worker,
413                self.selector,
414                preview,
415                &mut tui,
416                hidden_columns,
417            );
418            
419            let ret = render::render_loop(
420                ui,
421                picker,
422                footer,
423                preview,
424                tui,
425                overlay_ui,
426                self.exit_config,
427                render_rx,
428                event_controller,
429                (self.event_handlers, self.interrupt_handlers),
430                ext_handler,
431                ext_aliaser,
432                initializer,
433                #[cfg(feature = "bracketed-paste")]
434                paste_handler,
435            )
436            .await;
437            
438            if wait {
439                let _ = event_loop_handle.await;
440                log::debug!("event loop finished");
441            }
442            
443            ret
444        }
445        
446        pub async fn pick_default(self) -> Result<Vec<S>> {
447            self.pick::<NullActionExt>(PickOptions::new()).await
448        }
449    }
450    
451    #[ext(MatchResultExt)]
452    impl<T> Result<T> {
453        /// Return the first element
454        pub fn first<S>(self) -> Result<S>
455        where
456        T: IntoIterator<Item = S>,
457        {
458            match self {
459                Ok(v) => v.into_iter().next().ok_or(MatchError::NoMatch),
460                Err(e) => Err(e),
461            }
462        }
463        
464        /// Handle [`MatchError::Abort`] using [`std::process::exit`]
465        pub fn abort(self) -> Result<T> {
466            match self {
467                Err(MatchError::Abort(x)) => std::process::exit(x),
468                _ => self,
469            }
470        }
471    }
472    
473    // --------- BUILDER -------------
474    
475    /// Returns what should be pushed to input
476    pub type PasteHandler<T, S> =
477    Box<dyn FnMut(String, &MMState<'_, '_, T, S>) -> String + Send + Sync + 'static>;
478    
479    pub type ActionExtHandler<T, S, A> =
480    Box<dyn FnMut(A, &mut MMState<'_, '_, T, S>) + Send + Sync + 'static>;
481    
482    pub type ActionAliaser<T, S, A> =
483    Box<dyn FnMut(Action<A>, &mut MMState<'_, '_, T, S>) -> Actions<A> + Send + Sync + 'static>;
484    
485    pub type Initializer<T, S> = Box<dyn FnOnce(&mut MMState<'_, '_, T, S>) + Send + Sync + 'static>;
486    
487    /// Used to configure [`Matchmaker::pick`] with additional options.
488    pub struct PickOptions<'a, T: SSS, S: Selection, A: ActionExt = NullActionExt> {
489        matcher: Option<&'a mut nucleo::Matcher>,
490        matcher_config: nucleo::Config,
491        
492        event_loop: Option<EventLoop<A>>,
493        binds: Option<BindMap<A>>,
494        
495        ext_handler: Option<ActionExtHandler<T, S, A>>,
496        ext_aliaser: Option<ActionAliaser<T, S, A>>,
497        #[cfg(feature = "bracketed-paste")]
498        paste_handler: Option<PasteHandler<T, S>>,
499        
500        overlays: Vec<Box<dyn Overlay<A = A>>>,
501        overlay_config: Option<OverlayConfig>,
502        previewer: Option<Either<Preview, Previewer>>,
503        
504        hidden_columns: Vec<bool>,
505        
506        // Initializing code, i.e. to setup state.
507        initializer: Option<Initializer<T, S>>,
508        pub channel: Option<(
509            RenderSender<A>,
510            tokio::sync::mpsc::UnboundedReceiver<crate::message::RenderCommand<A>>,
511        )>,
512    }
513    
514    impl<'a, T: SSS, S: Selection, A: ActionExt> PickOptions<'a, T, S, A> {
515        pub const fn new() -> Self {
516            Self {
517                matcher: None,
518                event_loop: None,
519                previewer: None,
520                binds: None,
521                matcher_config: nucleo::Config::DEFAULT,
522                ext_handler: None,
523                ext_aliaser: None,
524                #[cfg(feature = "bracketed-paste")]
525                paste_handler: None,
526                overlay_config: None,
527                overlays: Vec::new(),
528                channel: None,
529                hidden_columns: Vec::new(),
530                initializer: None,
531            }
532        }
533        
534        pub fn with_binds(binds: BindMap<A>) -> Self {
535            let mut ret = Self::new();
536            ret.binds = Some(binds);
537            ret
538        }
539        
540        pub fn with_matcher(matcher: &'a mut nucleo::Matcher) -> Self {
541            let mut ret = Self::new();
542            ret.matcher = Some(matcher);
543            ret
544        }
545        
546        pub fn binds(mut self, binds: BindMap<A>) -> Self {
547            self.binds = Some(binds);
548            self
549        }
550        
551        pub fn event_loop(mut self, event_loop: EventLoop<A>) -> Self {
552            self.event_loop = Some(event_loop);
553            self
554        }
555        
556        /// Use the given [`Previewer`] to provide a [`Preview`].
557        /// # Example
558        /// See [`make_previewer`] for how to create one.
559        pub fn previewer(mut self, previewer: Previewer) -> Self {
560            self.previewer = Some(Either::Right(previewer));
561            self
562        }
563        
564        /// Set a [`Preview`].
565        /// Overrides [`Matchmaker::connect_preview`].
566        pub fn preview(mut self, preview: Preview) -> Self {
567            self.previewer = Some(Either::Left(preview));
568            self
569        }
570        
571        pub fn matcher(mut self, matcher_config: nucleo::Config) -> Self {
572            self.matcher_config = matcher_config;
573            self
574        }
575        
576        pub fn hidden_columns(mut self, hidden_columns: Vec<bool>) -> Self {
577            self.hidden_columns = hidden_columns;
578            self
579        }
580        
581        pub fn ext_handler<F>(mut self, handler: F) -> Self
582        where
583        F: FnMut(A, &mut MMState<'_, '_, T, S>) + Send + Sync + 'static,
584        {
585            self.ext_handler = Some(Box::new(handler));
586            self
587        }
588        
589        pub fn ext_aliaser<F>(mut self, aliaser: F) -> Self
590        where
591        F: FnMut(Action<A>, &mut MMState<'_, '_, T, S>) -> Actions<A> + Send + Sync + 'static,
592        {
593            self.ext_aliaser = Some(Box::new(aliaser));
594            self
595        }
596        
597        pub fn initializer<F>(mut self, aliaser: F) -> Self
598        where
599        F: FnOnce(&mut MMState<'_, '_, T, S>) + Send + Sync + 'static,
600        {
601            self.initializer = Some(Box::new(aliaser));
602            self
603        }
604        
605        #[cfg(feature = "bracketed-paste")]
606        pub fn paste_handler<F>(mut self, handler: F) -> Self
607        where
608        F: FnMut(String, &MMState<'_, '_, T, S>) -> String + Send + Sync + 'static,
609        {
610            self.paste_handler = Some(Box::new(handler));
611            self
612        }
613        
614        pub fn overlay<O>(mut self, overlay: O) -> Self
615        where
616        O: Overlay<A = A> + 'static,
617        {
618            self.overlays.push(Box::new(overlay));
619            self
620        }
621        
622        pub fn overlay_config(mut self, overlay: OverlayConfig) -> Self {
623            self.overlay_config = Some(overlay);
624            self
625        }
626        
627        pub fn render_tx(&mut self) -> RenderSender<A> {
628            if let Some((s, _)) = &self.channel {
629                s.clone()
630            } else {
631                let channel = tokio::sync::mpsc::unbounded_channel();
632                let ret = channel.0.clone();
633                self.channel = Some(channel);
634                ret
635            }
636        }
637    }
638    
639    impl<'a, T: SSS, S: Selection, A: ActionExt> Default for PickOptions<'a, T, S, A> {
640        fn default() -> Self {
641            Self::new()
642        }
643    }
644    
645    // ----------- ATTACHMENTS ------------------
646    
647    pub type AttachmentFormatter<T, S> = Either<
648    Arc<RenderFn<T>>,
649    for<'a, 'b, 'c> fn(&'a MMState<'b, 'c, T, S>, &'a str, Option<&dyn Fn(String)>) -> String,
650    >;
651    
652    pub fn use_formatter<T: SSS, S: Selection>(
653        formatter: &AttachmentFormatter<T, S>,
654        state: &MMState<'_, '_, T, S>,
655        template: &str,
656        repeat: Option<&dyn Fn(String)>,
657    ) -> String {
658        if template.is_empty() {
659            return String::new();
660        }
661        match formatter {
662            Either::Left(f) => {
663                if let Some(t) = state.current_raw() {
664                    f(t, template)
665                } else {
666                    String::new()
667                }
668            }
669            Either::Right(f) => f(state, template, repeat),
670        }
671    }
672    
673    // todo: this static bound shouldn't be necessary on S i don't know why its needed
674    impl<T: SSS, S: Selection + 'static> Matchmaker<T, S> {
675        // technically we don't need concurrency but the cost should be negligable
676        /// Causes [`Action::Print`] to print to stdout.
677        pub fn register_print_handler(
678            &mut self,
679            print_handle: AppendOnly<String>,
680            output_separator: String,
681            formatter: AttachmentFormatter<T, S>,
682        ) {
683            self.register_interrupt_handler(Interrupt::Print, move |state| {
684                let template = state.payload().clone();
685                let repeat = |s: String| {
686                    if atty::is(atty::Stream::Stdout) {
687                        print_handle.push(s);
688                    } else {
689                        print!("{}{}", s, output_separator);
690                    }
691                };
692                let s = use_formatter(&formatter, state, &template, Some(&repeat));
693                if !s.is_empty() {
694                    repeat(s)
695                }
696            });
697        }
698        
699        /// Causes [`Action::Execute`] to cause the program to execute the program specified by its payload.
700        /// Note:
701        /// - not intended for direct use.
702        /// - Assumes preview and cmd formatter are the same.
703        pub fn register_execute_handler(&mut self, formatter: AttachmentFormatter<T, S>) {
704            let _formatter = formatter.clone();
705            self.register_interrupt_handler(Interrupt::Execute, move |state| {
706                let template = state.payload().clone();
707                if !template.is_empty() {
708                    let cmd = use_formatter(&formatter, state, &template, None);
709                    if cmd.is_empty() {
710                        return;
711                    }
712                    let mut vars = state.make_env_vars();
713                    
714                    let preview_template = state.preview_payload().clone();
715                    let preview_cmd = use_formatter(&formatter, state, &preview_template, None);
716                    let extra = env_vars!(
717                        "FZF_PREVIEW_COMMAND" => preview_cmd,
718                    );
719                    vars.extend(extra);
720                    
721                    if let Some(mut child) = Command::from_script(&cmd)
722                    .envs(vars)
723                    .stdin(maybe_tty())
724                    ._spawn()
725                    {
726                        match child.wait() {
727                            Ok(i) => {
728                                info!("Command [{cmd}] exited with {i}")
729                            }
730                            Err(e) => {
731                                info!("Failed to wait on command [{cmd}]: {e}")
732                            }
733                        }
734                    }
735                };
736            });
737            self.register_interrupt_handler(Interrupt::ExecuteSilent, move |state| {
738                let template = state.payload().clone();
739                if !template.is_empty() {
740                    let cmd = use_formatter(&_formatter, state, &template, None);
741                    if cmd.is_empty() {
742                        return;
743                    }
744                    let mut vars = state.make_env_vars();
745                    
746                    let preview_template = state.preview_payload().clone();
747                    let preview_cmd = use_formatter(&_formatter, state, &preview_template, None);
748                    let extra = env_vars!(
749                        "FZF_PREVIEW_COMMAND" => preview_cmd,
750                    );
751                    vars.extend(extra);
752                    
753                    if let Some(mut child) = Command::from_script(&cmd)
754                    .envs(vars)
755                    .stdin(maybe_tty())
756                    ._spawn()
757                    {
758                        match child.wait() {
759                            Ok(i) => {
760                                info!("Command [{cmd}] exited with {i}")
761                            }
762                            Err(e) => {
763                                info!("Failed to wait on command [{cmd}]: {e}")
764                            }
765                        }
766                    }
767                };
768            });
769        }
770        
771        /// Causes [`Action::Become`] to cause the program to become the program specified by its payload.
772        /// Note:
773        /// - not intended for direct use.
774        /// - Assumes preview and cmd formatter are the same.
775        pub fn register_become_handler(&mut self, formatter: AttachmentFormatter<T, S>) {
776            self.register_interrupt_handler(Interrupt::Become, move |state| {
777                let template = state.payload().clone();
778                if !template.is_empty() {
779                    let cmd = use_formatter(&formatter, state, &template, None);
780                    if cmd.is_empty() {
781                        return;
782                    }
783                    let mut vars = state.make_env_vars();
784                    
785                    let preview_template = state.preview_payload().clone();
786                    let preview_cmd = use_formatter(&formatter, state, &preview_template, None);
787                    let extra = env_vars!(
788                        "FZF_PREVIEW_COMMAND" => preview_cmd,
789                    );
790                    vars.extend(extra);
791                    debug!("Becoming: {cmd}");
792                    
793                    Command::from_script(&cmd).envs(vars)._exec()
794                }
795            });
796        }
797    }
798    
799    /// Causes the program to display a preview of the active result.
800    /// The Previewer can be connected to [`Matchmaker`] using [`PickOptions::previewer`]
801    pub fn make_previewer<T: SSS, S: Selection + 'static>(
802        mm: &mut Matchmaker<T, S>,
803        previewer_config: PreviewerConfig, // note: help_str is provided separately so help_colors is ignored
804        formatter: AttachmentFormatter<T, S>,
805        help_str: Text<'static>,
806    ) -> Previewer {
807        // initialize previewer
808        let (previewer, tx) = Previewer::new(previewer_config);
809        let preview_tx = tx.clone();
810        
811        // preview handler
812        mm.register_event_handler(Event::CursorChange | Event::PreviewChange, move |state, _| {
813            if state.preview_visible() &&
814            let m = state.preview_payload().clone() &&
815            !m.is_empty()
816            {
817                let cmd = use_formatter(&formatter, state, &m, None);
818                if cmd.is_empty() {
819                    return;
820                }
821                let mut envs = state.make_env_vars();
822                let extra = env_vars!(
823                    "COLUMNS" => state.previewer_area().map_or("0".to_string(), |r| r.width.to_string()),
824                    "LINES" => state.previewer_area().map_or("0".to_string(), |r| r.height.to_string()),
825                );
826                envs.extend(extra);
827                
828                let msg = PreviewMessage::Run(cmd.clone(), envs);
829                if preview_tx.send(msg.clone()).is_err() {
830                    warn!("Failed to send to preview: {}", msg)
831                }
832                
833                let target = state.preview_ui.as_ref().and_then(|p| p.config.initial.index.as_ref().and_then(|index_col| {
834                    state.current_raw().and_then(|item| {
835                        state.picker_ui.worker.format_with(item, index_col).and_then(|t| atoi::atoi(t.as_bytes()))
836                    })
837                }));
838                
839                if let Some(p) = state.preview_ui {
840                    p.set_target(target);
841                };
842                
843            } else if preview_tx.send(PreviewMessage::Stop).is_err() {
844                warn!("Failed to send to preview: stop")
845            }
846            
847            state.preview_set_payload = None;
848        }
849    );
850    
851    mm.register_event_handler(Event::PreviewSet, move |state, _event| {
852        if state.preview_visible() {
853            let msg = if let Some(m) = state.preview_set_payload() {
854                let m = if m.is_empty() && !help_str.lines.is_empty() {
855                    help_str.clone()
856                } else {
857                    Text::from(m)
858                };
859                PreviewMessage::Set(m)
860            } else {
861                PreviewMessage::Unset
862            };
863            
864            if tx.send(msg.clone()).is_err() {
865                warn!("Failed to send: {}", msg)
866            }
867        }
868    });
869    
870    previewer
871}
872
873fn maybe_tty() -> Stdio {
874    if let Ok(tty) = std::fs::File::open("/dev/tty") {
875        // let _ = std::io::Write::flush(&mut tty); // does nothing but seems logical
876        Stdio::from(tty)
877    } else {
878        log::error!("Failed to open /dev/tty");
879        Stdio::inherit()
880    }
881}
882
883// ------------ BOILERPLATE ---------------
884
885impl<T: SSS + Debug, S: Selection + Debug> Debug for Matchmaker<T, S> {
886    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
887        f.debug_struct("Matchmaker")
888        // omit `worker`
889        .field("render_config", &self.render_config)
890        .field("tui_config", &self.tui_config)
891        .field("selection_set", &self.selector)
892        .field("event_handlers", &self.event_handlers)
893        .field("interrupt_handlers", &self.interrupt_handlers)
894        .finish()
895    }
896}