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