matchmaker/
matchmaker.rs

1
2use std::{fmt::{self, Debug, Formatter}, process::Stdio, sync::Arc};
3
4use anyhow::bail;
5use log::{debug, error, info, warn};
6use tokio::sync::{watch::Sender};
7
8use crate::{
9    PickerItem, Result, Selection, SelectionSet, binds::BindMap, config::{
10        self,
11        ExitConfig,
12        PreviewerConfig,
13        RenderConfig,
14        Split,
15        TerminalConfig,
16    }, env_vars, event::{self}, message::{Interrupt, Event}, nucleo::{
17        injector::{
18            Indexed, IndexedInjector, Injector, Segmented, SegmentedInjector, WorkerInjector
19        },
20        worker::Worker,
21    }, render::{
22        self,
23        DynamicMethod,
24        EphemeralState,
25        EventHandlers,
26        InterruptHandlers,
27    }, spawn::{
28        exec, preview::{PreviewMessage, Previewer}, spawn, tty_or_null
29    }, tui::{self, map_chunks, map_reader_lines, read_to_chunks}, ui::UI
30};
31
32pub struct Matchmaker<T: PickerItem, S: Selection=T, C=()> {
33    pub matcher: Option<nucleo::Matcher>,
34    pub worker: Worker<T, C>,
35    render_config: RenderConfig,
36    bind_config: BindMap,
37    #[allow(dead_code)]
38    tui_config: TerminalConfig,
39    exit_config: ExitConfig,
40    selection_set: SelectionSet<T, S>,
41    context: Arc<C>,
42    event_handlers: EventHandlers<T, S, C>,
43    interrupt_handlers: InterruptHandlers<T, S, C>,
44    previewer: Option<Previewer>
45}
46
47
48// ----------- MAIN -----------------------
49
50// todo: a stopgap until i find some better way to expose these fns, i.e. to allow clients to request new injectors in case of worker restart
51pub struct MiscData {
52    pub formatter: Arc<Box<dyn Fn(&Indexed<Segmented<String>>, &str) -> String + Send + Sync>>,
53    pub splitter: Arc<dyn Fn(&String) -> Vec<(usize, usize)> + Send + Sync>
54}
55
56impl Matchmaker<Indexed<Segmented<String>>, Segmented<String>> {
57    pub fn new_from_config(config: config::Config) -> (Self, SegmentedInjector<String, IndexedInjector<Segmented<String>, WorkerInjector<Indexed<Segmented<String>>>>>, MiscData) {
58        let cc = config.matcher.columns;
59        
60        let worker: Worker<Indexed<Segmented<String>>> = match cc.split {
61            Split::Delimiter(_) | Split::Regexes(_) => {
62                let names: Vec<Arc<str>> = if cc.names.is_empty() {
63                    (0..cc.max_columns.0)
64                    .map(|n| Arc::from(n.to_string()))
65                    .collect()
66                } else {
67                    cc.names.iter().map(|s| Arc::from(s.name.as_str())).collect()
68                };
69                Worker::new_indexable(names)
70            },
71            Split::None => {
72                Worker::new_indexable([""])
73            }
74        };
75        
76        let injector = worker.injector();
77        
78        let col_count = worker.columns.len();
79        
80        // Arc over box due to capturing
81        let splitter: Arc<dyn Fn(&String) -> Vec<(usize, usize)> + Send + Sync> = match cc.split {
82            Split::Delimiter(ref rg) => {
83                let rg = rg.clone(); 
84                Arc::new(move |s| {
85                    let mut ranges = Vec::new();
86                    let mut last_end = 0;
87                    for (i, m) in rg.find_iter(s).enumerate() {
88                        if i >= col_count - 1 { break; }
89                        ranges.push((last_end, m.start()));
90                        last_end = m.end();
91                    }
92                    ranges.push((last_end, s.len()));
93                    ranges
94                })
95            }
96            Split::Regexes(ref rgs) => {
97                let rgs = rgs.clone(); // or Arc
98                Arc::new(move |s| {
99                    let mut ranges = Vec::new();
100                    for re in rgs.iter().take(col_count) {
101                        if let Some(m) = re.find(s) {
102                            ranges.push((m.start(), m.end()));
103                        } else {
104                            ranges.push((0, 0));
105                        }
106                    }
107                    ranges
108                })
109            }
110            Split::None => Arc::new(|s| vec![(0, s.len())]),
111        };
112        let injector= IndexedInjector::new(injector, ());
113        let injector= SegmentedInjector::new(injector, splitter.clone());
114        
115        let selection_set = SelectionSet::new(Indexed::identifier);
116        
117        let event_handlers = EventHandlers::new();
118        let interrupt_handlers = InterruptHandlers::new();
119        let formatter = Arc::new(worker.make_format_fn::<true>(|item| &item.inner.inner));
120        
121        let (previewer, tx) = Previewer::new(config.previewer);
122        
123        let mut new: Matchmaker<Indexed<Segmented<String>>, Segmented<String>> = Matchmaker {
124            matcher: Some(nucleo::Matcher::new(config.matcher.matcher.0)),
125            worker,
126            bind_config: config.binds,
127            render_config: config.render,
128            tui_config: config.tui,
129            exit_config: config.matcher.exit,
130            selection_set,
131            context: Arc::new(()),
132            event_handlers,
133            interrupt_handlers,
134            previewer: Some(previewer)
135        };
136        
137        // handlers
138        let preview_formatter = formatter.clone();
139        let execute_formatter = preview_formatter.clone();
140        let execute_preview_formatter = preview_formatter.clone();
141        let become_formatter = preview_formatter.clone();
142        let become_preview_formatter = preview_formatter.clone();
143        let reload_formatter = preview_formatter.clone();
144        
145        new.register_event_handler([Event::CursorChange, Event::PreviewChange], move |state, event| {
146            match event {
147                Event::CursorChange | Event::PreviewChange => {
148                    if state.preview_show &&
149                    let Some(t) = state.current_raw() &&
150                    !state.preview_payload().is_empty()
151                    {
152                        let cmd = preview_formatter.clone()(t, &state.preview_payload());
153                        let mut envs = state.make_env_vars();
154                        let extra = env_vars!(
155                            "COLUMNS" => state.previewer_area.map_or("0".to_string(), |r| r.width.to_string()),
156                            "LINES" => state.previewer_area.map_or("0".to_string(), |r| r.height.to_string()),
157                        );
158                        envs.extend(extra);
159                        
160                        let msg = PreviewMessage::Run(cmd.clone(), vec![]);
161                        if tx.send(msg.clone()).is_err() {
162                            warn!("Failed to send: {}", msg)
163                        }
164                    }
165                },
166                _ => {}
167            }
168        });
169        
170        new.register_interrupt_handler(Interrupt::Execute("".into()), move |state, interrupt| {
171            match interrupt {
172                Interrupt::Execute(template) => {
173                    if let Some(t) = state.current_raw() {
174                        let cmd = execute_formatter(t, template);
175                        let mut vars = state.make_env_vars();
176                        let preview_cmd = execute_preview_formatter(t, &state.preview_payload());
177                        let extra = env_vars!(
178                            "FZF_PREVIEW_COMMAND" => preview_cmd,
179                        );
180                        vars.extend(extra);
181                        let tty = tty_or_null();
182                        if let Some(mut child) = spawn(&cmd, vars, tty, Stdio::inherit(), Stdio::inherit()) {
183                            match child.wait() {
184                                Ok(i) => {
185                                    info!("Command [{cmd}] exited with {i}")
186                                },
187                                Err(e) => {
188                                    info!("Failed to wait on command [{cmd}]: {e}")
189                                }
190                            }
191                        }
192                    }
193                },
194                _ => {}
195            }
196        });
197        
198        new.register_interrupt_handler(Interrupt::Become("".into()), move |state, interrupt| {
199            match interrupt {
200                Interrupt::Become(template) => {
201                    if let Some(t) = state.current_raw() {
202                        let cmd = become_formatter(t, template);
203                        let mut vars = state.make_env_vars();
204                        let preview_cmd = become_preview_formatter(t, &state.preview_payload());
205                        let extra = env_vars!(
206                            "FZF_PREVIEW_COMMAND" => preview_cmd,
207                        );
208                        vars.extend(extra);
209                        debug!("Becoming: {cmd}");
210                        exec(&cmd, vars);
211                    }
212                },
213                _ => {}
214            }
215        });
216        
217        
218        let reload_splitter = splitter.clone();
219        new.register_interrupt_handler(Interrupt::Reload("".into()), move |state, interrupt| {
220            let injector = state.injector();
221            let injector= IndexedInjector::new(injector, ());
222            let injector= SegmentedInjector::new(injector, reload_splitter.clone());
223            
224            match interrupt {
225                Interrupt::Reload(template) => {
226                    if let Some(t) = state.current_raw() {
227                        let cmd = reload_formatter(t, template);
228                        let vars = state.make_env_vars();
229                        // let extra = env_vars!(
230                        //     "FZF_PREVIEW_COMMAND" => preview_cmd,
231                        // );
232                        // vars.extend(extra);
233                        debug!("Reloading: {cmd}");
234                        if let Some(mut child) = spawn(&cmd, vars, Stdio::null(), Stdio::piped(), Stdio::null()) {
235                            if let Some(stdout) = child.stdout.take() {
236                                let _handle = if let Some(delim) = config.matcher.delimiter {
237                                    tokio::spawn(async move {
238                                        map_chunks::<true>(read_to_chunks(stdout, delim), |line| injector.push(line).map_err(|e| e.into()))
239                                    })
240                                } else {
241                                    tokio::spawn(async move {
242                                        map_reader_lines::<true>(stdout, |line| injector.push(line).map_err(|e| e.into()))
243                                    })
244                                };
245                            } else {
246                                error!("Failed to capture stdout");
247                            }
248                        }
249                    }
250                },
251                _ => {}
252            }
253        });
254        
255        let misc = MiscData {
256            formatter,
257            splitter
258        };
259        
260        (new, injector, misc)
261    }
262}
263
264impl<T: PickerItem, S: Selection> Matchmaker<T, S> {
265    pub fn new(worker: Worker<T>, identifier: fn(&T) -> (u32, S)) -> Self {
266        let new = Matchmaker {
267            matcher: Some(nucleo::Matcher::new(nucleo::Config::DEFAULT)),
268            worker,
269            bind_config: BindMap::new(),
270            render_config: RenderConfig::default(),
271            tui_config: TerminalConfig::default(),
272            exit_config: ExitConfig::default(),
273            selection_set: SelectionSet::new(identifier),
274            context: Arc::new(()),
275            event_handlers: EventHandlers::new(),
276            interrupt_handlers: InterruptHandlers::new(),
277            previewer: None
278        };
279        
280        new
281    }
282}
283
284impl<T: PickerItem, S: Selection, C> Matchmaker<T, S, C>
285{
286    /// For library use:
287    /// 1. create your worker (T -> Columns)
288    /// 2. instantiate a matcher (i.e. globally with lazylock)
289    /// 3. Determine your identifier
290    /// 4. Call mm.pick_with_matcher(&mut matcher)
291    pub fn new_raw(worker: Worker<T, C>, identifier: fn(&T) -> (u32, S), context: Arc<C>) -> Self {
292        let new = Matchmaker {
293            matcher: None,
294            worker,
295            bind_config: BindMap::new(),
296            render_config: RenderConfig::default(),
297            tui_config: TerminalConfig::default(),
298            exit_config: ExitConfig::default(),
299            selection_set: SelectionSet::new(identifier),
300            context,
301            event_handlers: EventHandlers::new(),
302            interrupt_handlers: InterruptHandlers::new(),
303            previewer: None
304        };
305        
306        new
307    }
308    
309    // todo: accept static matcher
310    pub fn config_binds(&mut self, bind_config: BindMap) -> &mut Self {
311        self.bind_config = bind_config;
312        self
313    }
314    pub fn config_render(&mut self, render_config: RenderConfig) -> &mut Self {
315        self.render_config = render_config;
316        self
317    }
318    pub fn config_preview(&mut self, preview_config: PreviewerConfig) -> Sender<PreviewMessage> {
319        let (previewer, tx) = Previewer::new(preview_config);
320        self.previewer = Some(previewer);
321        tx
322    }
323    pub fn config_tui(&mut self, tui_config: TerminalConfig) -> &mut Self {
324        self.tui_config = tui_config;
325        self
326    }
327    pub fn config_exit(&mut self, exit_config: ExitConfig) -> &mut Self {
328        self.exit_config = exit_config;
329        self
330    }
331    
332    pub fn register_event_handler<F, I>(&mut self, events: I, handler: F)
333    where
334    F: Fn(EphemeralState<'_, T, S, C>, &Event) + Send + Sync + 'static,
335    I: IntoIterator<Item = Event>,
336    {
337        let boxed = Box::new(handler);
338        self.register_boxed_event_handler(events, boxed);
339    }
340    
341    pub fn register_boxed_event_handler<I>(
342        &mut self,
343        events: I,
344        handler: DynamicMethod<T, S, C, Event>,
345    )
346    where
347    I: IntoIterator<Item = Event>,
348    {
349        let events_vec: Vec<_> = events.into_iter().collect();
350        self.event_handlers.set(events_vec, handler);
351    }
352    
353    pub fn register_interrupt_handler<F>(
354        &mut self,
355        interrupt: Interrupt,
356        handler: F,
357    )
358    where
359    F: Fn(EphemeralState<'_, T, S, C>, &Interrupt) + Send + Sync + 'static,
360    {
361        let boxed = Box::new(handler);
362        self.register_boxed_interrupt_handler(interrupt, boxed);
363    }
364    
365    pub fn register_boxed_interrupt_handler(
366        &mut self,
367        variant: Interrupt,
368        handler: DynamicMethod<T, S, C, Interrupt>,
369    ) {
370        self.interrupt_handlers.set(variant, handler);
371    }
372    
373    
374    // Some repetition until i figure out if its possible to somehow be generic over owned or static mut references (i.e. to LazyLock Matcher)
375    pub async fn pick(mut self) -> Result<impl IntoIterator<Item = S>> {
376        if let Some(matcher) = self.matcher.as_mut() {
377            if self.exit_config.select_1 && self.worker.counts().0 == 1 {
378                return Ok(self.selection_set.map_to_vec([self.worker.get_nth(0).unwrap()]));
379            }
380            
381            let (render_tx, render_rx) = tokio::sync::mpsc::unbounded_channel();
382            let (mut event_loop, controller_tx) = event::EventLoop::new(vec![render_tx.clone()], self.render_config.tick_rate());
383            event_loop.binds(self.bind_config);
384            
385            let mut tui = tui::Tui::new(self.tui_config).expect("Failed to create TUI instance");
386            tui.enter()?;
387            
388            tokio::spawn(async move {
389                let _ = event_loop.run().await;
390            });
391            
392            let view = if let Some(mut previewer) = self.previewer {
393                previewer.connect_controller(controller_tx.clone());
394                let view = previewer.view();
395                tokio::spawn(async move {
396                    let _ = previewer.run().await;
397                });
398                
399                Some(view)
400            } else {
401                None
402            };
403            
404            let (ui, picker, preview) = UI::new(self.render_config, matcher, self.worker, self.selection_set, view, &mut tui);
405            
406            render::render_loop(ui, picker, preview, tui, render_rx, controller_tx, self.context, (self.event_handlers, self.interrupt_handlers), self.exit_config).await
407        } else {
408            bail!("No matcher")
409        }
410    }
411    
412    pub async fn pick_with_matcher(self, matcher: &mut nucleo::Matcher) -> Result<impl IntoIterator<Item = S>> {
413        if self.exit_config.select_1 && self.worker.counts().0 == 1 {
414            return Ok(self.selection_set.map_to_vec([self.worker.get_nth(0).unwrap()]));
415        }
416        
417        let (render_tx, render_rx) = tokio::sync::mpsc::unbounded_channel();
418        let (mut event_loop, controller_tx) = event::EventLoop::new(vec![render_tx.clone()], self.render_config.tick_rate());
419        event_loop.binds(self.bind_config);
420        
421        let mut tui = tui::Tui::new(self.tui_config).expect("Failed to create TUI instance");
422        tui.enter()?;
423        
424        tokio::spawn(async move {
425            let _ = event_loop.run().await;
426        });
427        
428        let view = if let Some(mut previewer) = self.previewer {
429            previewer.connect_controller(controller_tx.clone());
430            let view = previewer.view();
431            tokio::spawn(async move {
432                let _ = previewer.run().await;
433            });
434            
435            Some(view)
436        } else {
437            None
438        };
439        
440        let (ui, picker, preview) = UI::new(self.render_config, matcher, self.worker, self.selection_set, view, &mut tui);
441        
442        render::render_loop(ui, picker, preview, tui, render_rx, controller_tx, self.context, (self.event_handlers, self.interrupt_handlers), self.exit_config).await
443    }
444}
445
446
447// --------------------------------- BOILERPLATE -----------------------------------------------------------
448
449impl<T: PickerItem + Debug, S: Selection + Debug, C: Debug> Debug for Matchmaker<T, S, C> {
450    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
451        f.debug_struct("Matchmaker")
452        // omit `worker`
453        .field("matcher", &self.matcher)
454        .field("render_config", &self.render_config)
455        .field("bind_config", &self.bind_config)
456        .field("tui_config", &self.tui_config)
457        .field("selection_set", &self.selection_set)
458        .field("context", &self.context)
459        .field("event_handlers", &self.event_handlers)
460        .field("interrupt_handlers", &self.interrupt_handlers)
461        .field("previewer", &self.previewer)
462        .finish()
463    }
464}