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<
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    #[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.0 == 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
163                                capture_to_idx.iter().enumerate().skip(1)
164                            {
165                                if let Some(col_idx) = col_idx_opt {
166                                    if let Some(m) = caps.get(group_idx) {
167                                        ranges[*col_idx] = (m.start(), m.end());
168                                    }
169                                }
170                            }
171                        }
172
173                        ranges
174                    })
175                } else if has_unnamed {
176                    log::debug!("Unnamed regex: {rg} with {} groups", capture_to_idx.len());
177
178                    // All unnamed capture groups → map in order
179                    Arc::new(move |s| {
180                        let s = &s.to_cow();
181                        let mut ranges = ArrayVec::from_iter(vec![(0, 0); col_count]);
182
183                        if let Some(caps) = rg.captures(s) {
184                            for (i, group) in caps.iter().skip(1).enumerate().take(col_count) {
185                                if let Some(m) = group {
186                                    ranges[i] = (m.start(), m.end());
187                                }
188                            }
189                        }
190
191                        ranges
192                    })
193                } else {
194                    log::debug!("Delimiter regex: {rg}");
195
196                    // No capture groups → normal delimiter split
197                    Arc::new(move |s| {
198                        let s = &s.to_cow();
199                        let mut ranges = ArrayVec::new();
200                        let mut last_end = 0;
201
202                        for m in rg.find_iter(s).take(col_count - 1) {
203                            ranges.push((last_end, m.start()));
204                            last_end = m.end();
205                        }
206
207                        ranges.push((last_end, s.len()));
208                        ranges
209                    })
210                }
211            }
212            // not recommended but its supported ig
213            Split::Regexes(ref rgs) => {
214                let rgs = rgs.clone(); // or Arc
215                Arc::new(move |s| {
216                    let s = &s.to_cow();
217                    let mut ranges = ArrayVec::new();
218
219                    for re in rgs.iter().take(col_count) {
220                        if let Some(m) = re.find(s) {
221                            ranges.push((m.start(), m.end()));
222                        } else {
223                            ranges.push((0, 0));
224                        }
225                    }
226                    ranges
227                })
228            }
229            Split::None => Arc::new(|s| ArrayVec::from_iter([(0, s.to_cow().len())])),
230        };
231        let injector = IndexedInjector::new_globally_indexed(injector);
232        let injector = SegmentedInjector::new(injector, splitter.clone());
233        let injector = AnsiInjector::new(injector, preprocess_config);
234
235        let selection_set = if render_config.results.multi {
236            Selector::new(Indexed::identifier)
237        } else {
238            Selector::new(Indexed::identifier).disabled()
239        };
240
241        let event_handlers = EventHandlers::new();
242        let interrupt_handlers = InterruptHandlers::new();
243
244        let new = Matchmaker {
245            worker,
246            render_config,
247            tui_config,
248            exit_config,
249            selector: selection_set,
250            event_handlers,
251            interrupt_handlers,
252        };
253
254        let misc = OddEnds {
255            splitter,
256            hidden_columns,
257            has_error,
258        };
259
260        (new, injector, misc)
261    }
262}
263
264impl<T: SSS, S: Selection> Matchmaker<T, S> {
265    pub fn new(worker: Worker<T>, selector: Selector<T, S>) -> Self {
266        Matchmaker {
267            worker,
268            render_config: RenderConfig::default(),
269            tui_config: TerminalConfig::default(),
270            exit_config: ExitConfig::default(),
271            selector,
272            event_handlers: EventHandlers::new(),
273            interrupt_handlers: InterruptHandlers::new(),
274        }
275    }
276
277    /// Configure the UI
278    pub fn config_render(&mut self, render: RenderConfig) -> &mut Self {
279        self.render_config = render;
280        self
281    }
282    /// Configure the TUI
283    pub fn config_tui(&mut self, tui: TerminalConfig) -> &mut Self {
284        self.tui_config = tui;
285        self
286    }
287    /// Configure exit conditions
288    pub fn config_exit(&mut self, exit: ExitConfig) -> &mut Self {
289        self.exit_config = exit;
290        self
291    }
292    /// Register a handler to listen on [`Event`]s
293    pub fn register_event_handler<F>(&mut self, event: Event, handler: F)
294    where
295        F: Fn(&mut MMState<'_, '_, T, S>, &Event) + 'static,
296    {
297        let boxed = Box::new(handler);
298        self.register_boxed_event_handler(event, boxed);
299    }
300    /// Register a boxed handler to listen on [`Event`]s
301    pub fn register_boxed_event_handler(
302        &mut self,
303        event: Event,
304        handler: DynamicMethod<T, S, Event>,
305    ) {
306        self.event_handlers.set(event, handler);
307    }
308    /// Register a handler to listen on [`Interrupt`]s
309    pub fn register_interrupt_handler<F>(&mut self, interrupt: Interrupt, handler: F)
310    where
311        F: Fn(&mut MMState<'_, '_, T, S>) + 'static,
312    {
313        let boxed = Box::new(handler);
314        self.register_boxed_interrupt_handler(interrupt, boxed);
315    }
316    /// Register a boxed handler to listen on [`Interrupt`]s
317    pub fn register_boxed_interrupt_handler(
318        &mut self,
319        variant: Interrupt,
320        handler: BoxedHandler<T, S>,
321    ) {
322        self.interrupt_handlers.set(variant, handler);
323    }
324
325    /// 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.
326    pub async fn pick<A: ActionExt>(self, builder: PickOptions<'_, T, S, A>) -> Result<Vec<S>> {
327        let PickOptions {
328            previewer,
329            ext_handler,
330            ext_aliaser,
331            #[cfg(feature = "bracketed-paste")]
332            paste_handler,
333            overlay_config,
334            hidden_columns,
335            initializer,
336            ..
337        } = builder;
338
339        if self.exit_config.select_1 && self.worker.counts().0 == 1 {
340            return Ok(self
341                .selector
342                .identify_to_vec([self.worker.get_nth(0).unwrap()]));
343        }
344
345        let mut event_loop = if let Some(e) = builder.event_loop {
346            e
347        } else if let Some(binds) = builder.binds {
348            EventLoop::with_binds(binds).with_tick_rate(self.render_config.tick_rate())
349        } else {
350            EventLoop::new()
351        };
352
353        let mut wait = false;
354        if let Some(path) = self.exit_config.last_key_path.clone()
355            && !path.is_empty()
356        {
357            event_loop.record_last_key(path);
358            wait = true;
359        }
360
361        let preview = match previewer {
362            Some(Either::Left(view)) => Some(view),
363            Some(Either::Right(mut previewer)) => {
364                let view = previewer.view();
365                previewer.connect_controller(event_loop.controller());
366
367                tokio::spawn(async move {
368                    let _ = previewer.run().await;
369                });
370
371                Some(view)
372            }
373            _ => None,
374        };
375
376        let (render_tx, render_rx) = builder
377            .channel
378            .unwrap_or_else(tokio::sync::mpsc::unbounded_channel);
379        event_loop.add_tx(render_tx.clone());
380
381        let mut tui =
382            tui::Tui::new(self.tui_config).map_err(|e| MatchError::TUIError(e.to_string()))?;
383        tui.enter()
384            .map_err(|e| MatchError::TUIError(e.to_string()))?;
385
386        // important to start after tui
387        let event_controller = event_loop.controller();
388        let event_loop_handle = tokio::spawn(async move {
389            let _ = event_loop.run().await;
390        });
391        log::debug!("event loop started");
392
393        let overlay_ui = if builder.overlays.is_empty() {
394            None
395        } else {
396            Some(OverlayUI::new(
397                builder.overlays.into_boxed_slice(),
398                overlay_config.unwrap_or_default(),
399            ))
400        };
401
402        // initial redraw to clear artifacts,
403        tui.redraw();
404
405        let matcher = if let Some(matcher) = builder.matcher {
406            matcher
407        } else {
408            &mut nucleo::Matcher::new(nucleo::Config::DEFAULT)
409        };
410
411        let (ui, picker, footer, preview) = UI::new(
412            self.render_config,
413            matcher,
414            self.worker,
415            self.selector,
416            preview,
417            &mut tui,
418            hidden_columns,
419        );
420
421        let ret = render::render_loop(
422            ui,
423            picker,
424            footer,
425            preview,
426            tui,
427            overlay_ui,
428            self.exit_config,
429            render_rx,
430            event_controller,
431            (self.event_handlers, self.interrupt_handlers),
432            ext_handler,
433            ext_aliaser,
434            initializer,
435            #[cfg(feature = "bracketed-paste")]
436            paste_handler,
437        )
438        .await;
439
440        if wait {
441            let _ = event_loop_handle.await;
442            log::debug!("event loop finished");
443        }
444
445        ret
446    }
447
448    pub async fn pick_default(self) -> Result<Vec<S>> {
449        self.pick::<NullActionExt>(PickOptions::new()).await
450    }
451}
452
453#[ext(MatchResultExt)]
454impl<T> Result<T> {
455    /// Return the first element
456    pub fn first<S>(self) -> Result<S>
457    where
458        T: IntoIterator<Item = S>,
459    {
460        match self {
461            Ok(v) => v.into_iter().next().ok_or(MatchError::NoMatch),
462            Err(e) => Err(e),
463        }
464    }
465
466    /// Handle [`MatchError::Abort`] using [`std::process::exit`]
467    pub fn abort(self) -> Result<T> {
468        match self {
469            Err(MatchError::Abort(x)) => std::process::exit(x),
470            _ => self,
471        }
472    }
473}
474
475// --------- BUILDER -------------
476
477/// Returns what should be pushed to input
478pub type PasteHandler<T, S> =
479    Box<dyn FnMut(String, &MMState<'_, '_, T, S>) -> String + Send + Sync + 'static>;
480
481pub type ActionExtHandler<T, S, A> =
482    Box<dyn FnMut(A, &mut MMState<'_, '_, T, S>) + Send + Sync + 'static>;
483
484pub type ActionAliaser<T, S, A> =
485    Box<dyn FnMut(Action<A>, &mut MMState<'_, '_, T, S>) -> Actions<A> + Send + Sync + 'static>;
486
487pub type Initializer<T, S> = Box<dyn FnOnce(&mut MMState<'_, '_, T, S>) + Send + Sync + 'static>;
488
489/// Used to configure [`Matchmaker::pick`] with additional options.
490pub struct PickOptions<'a, T: SSS, S: Selection, A: ActionExt = NullActionExt> {
491    matcher: Option<&'a mut nucleo::Matcher>,
492    matcher_config: nucleo::Config,
493
494    event_loop: Option<EventLoop<A>>,
495    binds: Option<BindMap<A>>,
496
497    ext_handler: Option<ActionExtHandler<T, S, A>>,
498    ext_aliaser: Option<ActionAliaser<T, S, A>>,
499    #[cfg(feature = "bracketed-paste")]
500    paste_handler: Option<PasteHandler<T, S>>,
501
502    overlays: Vec<Box<dyn Overlay<A = A>>>,
503    overlay_config: Option<OverlayConfig>,
504    previewer: Option<Either<Preview, Previewer>>,
505
506    hidden_columns: Vec<bool>,
507
508    // Initializing code, i.e. to setup state.
509    initializer: Option<Initializer<T, S>>,
510    pub channel: Option<(
511        RenderSender<A>,
512        tokio::sync::mpsc::UnboundedReceiver<crate::message::RenderCommand<A>>,
513    )>,
514}
515
516impl<'a, T: SSS, S: Selection, A: ActionExt> PickOptions<'a, T, S, A> {
517    pub const fn new() -> Self {
518        Self {
519            matcher: None,
520            event_loop: None,
521            previewer: None,
522            binds: None,
523            matcher_config: nucleo::Config::DEFAULT,
524            ext_handler: None,
525            ext_aliaser: None,
526            #[cfg(feature = "bracketed-paste")]
527            paste_handler: None,
528            overlay_config: None,
529            overlays: Vec::new(),
530            channel: None,
531            hidden_columns: Vec::new(),
532            initializer: None,
533        }
534    }
535
536    pub fn with_binds(binds: BindMap<A>) -> Self {
537        let mut ret = Self::new();
538        ret.binds = Some(binds);
539        ret
540    }
541
542    pub fn with_matcher(matcher: &'a mut nucleo::Matcher) -> Self {
543        let mut ret = Self::new();
544        ret.matcher = Some(matcher);
545        ret
546    }
547
548    pub fn binds(mut self, binds: BindMap<A>) -> Self {
549        self.binds = Some(binds);
550        self
551    }
552
553    pub fn event_loop(mut self, event_loop: EventLoop<A>) -> Self {
554        self.event_loop = Some(event_loop);
555        self
556    }
557
558    /// Use the given [`Previewer`] to provide a [`Preview`].
559    /// # Example
560    /// See [`make_previewer`] for how to create one.
561    pub fn previewer(mut self, previewer: Previewer) -> Self {
562        self.previewer = Some(Either::Right(previewer));
563        self
564    }
565
566    /// Set a [`Preview`].
567    /// Overrides [`Matchmaker::connect_preview`].
568    pub fn preview(mut self, preview: Preview) -> Self {
569        self.previewer = Some(Either::Left(preview));
570        self
571    }
572
573    pub fn matcher(mut self, matcher_config: nucleo::Config) -> Self {
574        self.matcher_config = matcher_config;
575        self
576    }
577
578    pub fn hidden_columns(mut self, hidden_columns: Vec<bool>) -> Self {
579        self.hidden_columns = hidden_columns;
580        self
581    }
582
583    pub fn ext_handler<F>(mut self, handler: F) -> Self
584    where
585        F: FnMut(A, &mut MMState<'_, '_, T, S>) + Send + Sync + 'static,
586    {
587        self.ext_handler = Some(Box::new(handler));
588        self
589    }
590
591    pub fn ext_aliaser<F>(mut self, aliaser: F) -> Self
592    where
593        F: FnMut(Action<A>, &mut MMState<'_, '_, T, S>) -> Actions<A> + Send + Sync + 'static,
594    {
595        self.ext_aliaser = Some(Box::new(aliaser));
596        self
597    }
598
599    pub fn initializer<F>(mut self, aliaser: F) -> Self
600    where
601        F: FnOnce(&mut MMState<'_, '_, T, S>) + Send + Sync + 'static,
602    {
603        self.initializer = Some(Box::new(aliaser));
604        self
605    }
606
607    #[cfg(feature = "bracketed-paste")]
608    pub fn paste_handler<F>(mut self, handler: F) -> Self
609    where
610        F: FnMut(String, &MMState<'_, '_, T, S>) -> String + Send + Sync + 'static,
611    {
612        self.paste_handler = Some(Box::new(handler));
613        self
614    }
615
616    pub fn overlay<O>(mut self, overlay: O) -> Self
617    where
618        O: Overlay<A = A> + 'static,
619    {
620        self.overlays.push(Box::new(overlay));
621        self
622    }
623
624    pub fn overlay_config(mut self, overlay: OverlayConfig) -> Self {
625        self.overlay_config = Some(overlay);
626        self
627    }
628
629    pub fn render_tx(&mut self) -> RenderSender<A> {
630        if let Some((s, _)) = &self.channel {
631            s.clone()
632        } else {
633            let channel = tokio::sync::mpsc::unbounded_channel();
634            let ret = channel.0.clone();
635            self.channel = Some(channel);
636            ret
637        }
638    }
639}
640
641impl<'a, T: SSS, S: Selection, A: ActionExt> Default for PickOptions<'a, T, S, A> {
642    fn default() -> Self {
643        Self::new()
644    }
645}
646
647// ----------- ATTACHMENTS ------------------
648
649pub type AttachmentFormatter<T, S> = Either<
650    Arc<RenderFn<T>>,
651    for<'a, 'b, 'c> fn(&'a MMState<'b, 'c, T, S>, &'a str, Option<&dyn Fn(String)>) -> String,
652>;
653
654// we could check if template is empty here to avoid allocating but feels like it might be a footgun
655pub fn use_formatter<T: SSS, S: Selection>(
656    formatter: &AttachmentFormatter<T, S>,
657    state: &MMState<'_, '_, T, S>,
658    template: &str,
659    repeat: Option<&dyn Fn(String)>,
660) -> String {
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
674impl<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                    "MM_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                    "MM_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                    "MM_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`]
801pub 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 | Event::Synced, move |state, _| {
813            if state.preview_visible() &&
814            let m = state.preview_payload().clone()
815            {
816                let cmd = use_formatter(&formatter, state, &m, None);
817                if cmd.is_empty() {
818                    return;
819                }
820                let mut envs = state.make_env_vars();
821                let extra = env_vars!(
822                    "COLUMNS" => state.previewer_area().map_or("0".to_string(), |r| r.width.to_string()),
823                    "LINES" => state.previewer_area().map_or("0".to_string(), |r| r.height.to_string()),
824                );
825                envs.extend(extra);
826
827                let msg = PreviewMessage::Run(cmd.clone(), envs);
828                if preview_tx.send(msg.clone()).is_err() {
829                    warn!("Failed to send to preview: {}", msg)
830                }
831
832                let target = state.preview_ui.as_ref().and_then(|p| p.config.initial.index.as_ref().and_then(|index_col| {
833                    state.current_raw().and_then(|item| {
834                        state.picker_ui.worker.format_with(item, index_col).and_then(|t| atoi::atoi(t.as_bytes()))
835                    })
836                }));
837
838                if let Some(p) = state.preview_ui {
839                    p.set_target(target);
840                };
841
842            } else if preview_tx.send(PreviewMessage::Stop).is_err() {
843                warn!("Failed to send to preview: stop")
844            }
845
846            state.preview_set_payload = None;
847        }
848    );
849
850    mm.register_event_handler(Event::PreviewSet, move |state, _event| {
851        if state.preview_visible() {
852            let msg = if let Some(m) = state.preview_set_payload() {
853                let m = if m.is_empty() && !help_str.lines.is_empty() {
854                    help_str.clone()
855                } else {
856                    Text::from(m)
857                };
858                PreviewMessage::Set(m)
859            } else {
860                PreviewMessage::Unset
861            };
862
863            if tx.send(msg.clone()).is_err() {
864                warn!("Failed to send: {}", msg)
865            }
866        }
867    });
868
869    previewer
870}
871
872fn maybe_tty() -> Stdio {
873    if let Ok(tty) = std::fs::File::open("/dev/tty") {
874        // let _ = std::io::Write::flush(&mut tty); // does nothing but seems logical
875        Stdio::from(tty)
876    } else {
877        log::error!("Failed to open /dev/tty");
878        Stdio::inherit()
879    }
880}
881
882// ------------ BOILERPLATE ---------------
883
884impl<T: SSS + Debug, S: Selection + Debug> Debug for Matchmaker<T, S> {
885    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
886        f.debug_struct("Matchmaker")
887            // omit `worker`
888            .field("render_config", &self.render_config)
889            .field("tui_config", &self.tui_config)
890            .field("selection_set", &self.selector)
891            .field("event_handlers", &self.event_handlers)
892            .field("interrupt_handlers", &self.interrupt_handlers)
893            .finish()
894    }
895}