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