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