matchmaker/
matchmaker.rs

1
2use std::{fmt::{self, Debug, Formatter}, sync::Arc};
3
4use tokio::sync::{mpsc::UnboundedSender};
5
6use crate::{
7    MMItem, MatchError, RenderFn, Result, Selection, SelectionSet, SplitterFn, binds::BindMap, proc::
8    Preview, config::{
9        self, ExitConfig, MMConfig, RenderConfig, Split, TerminalConfig
10    }, event::EventLoop, message::{Event, Interrupt}, nucleo::{
11        Indexed, Segmented, Worker, injector::{
12            IndexedInjector, Injector, SegmentedInjector, WorkerInjector
13        }
14    }, render::{
15        self,
16        DynamicMethod,
17        EphemeralState,
18        EventHandlers,
19        InterruptHandlers,
20    }, tui::{self}, ui::UI
21};
22
23/// The main entrypoint of the library. To use:
24/// 1. create your worker (T -> Columns)
25/// 2. Determine your identifier
26/// 3. Instantiate this with Matchmaker::new_from_raw(..)
27/// 4. Register your handlers
28///    4.5 Start and connect your previewer
29/// 5. Call mm.pick() or mm.pick_with_matcher(&mut matcher)
30pub struct Matchmaker<T: MMItem, S: Selection=T, C=()> {
31    pub worker: Worker<T, C>,
32    render_config: RenderConfig,
33    bind_config: BindMap,
34    tui_config: TerminalConfig,
35    exit_config: ExitConfig,
36    selection_set: SelectionSet<T, S>,
37    event_loop: EventLoop,
38    context: Arc<C>,
39    event_handlers: EventHandlers<T, S, C>,
40    interrupt_handlers: InterruptHandlers<T, S, C>,
41    previewer: Option<Preview>
42}
43
44
45// ----------- MAIN -----------------------
46
47// defined for lack of a better way to expose these fns, i.e. to allow clients to request new injectors in case of worker restart
48pub struct OddEnds {
49    pub formatter: Arc<RenderFn<Indexed<Segmented<String>>>>,
50    pub splitter: SplitterFn<String>
51}
52
53pub type ConfigInjector = SegmentedInjector<String, IndexedInjector<Segmented<String>, WorkerInjector<Indexed<Segmented<String>>>>>;
54pub type ConfigMatchmaker = Matchmaker<Indexed<Segmented<String>>, Segmented<String>>;
55impl ConfigMatchmaker {
56    /// Creates a new Matchmaker from a config::Config.
57    pub fn new_from_config(config: config::Config, matcher_config: MMConfig) -> (Self, ConfigInjector, OddEnds) {
58        let cc = matcher_config.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)
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: SplitterFn<String> = 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 new: Matchmaker<Indexed<Segmented<String>>, Segmented<String>> = Matchmaker {
122            worker,
123            bind_config: config.binds,
124            render_config: config.render,
125            tui_config: config.tui,
126            exit_config: matcher_config.exit,
127            selection_set,
128            context: Arc::new(()),
129            event_loop: EventLoop::new(),
130            event_handlers,
131            interrupt_handlers,
132            previewer: None
133        };
134
135        let misc = OddEnds {
136            formatter,
137            splitter
138        };
139
140        (new, injector, misc)
141    }
142}
143
144impl<T: MMItem, S: Selection> Matchmaker<T, S> {
145    pub fn new(worker: Worker<T>, identifier: fn(&T) -> (u32, S)) -> Self {
146        Matchmaker {
147            worker,
148            bind_config: BindMap::new(),
149            render_config: RenderConfig::default(),
150            tui_config: TerminalConfig::default(),
151            exit_config: ExitConfig::default(),
152            selection_set: SelectionSet::new(identifier),
153            context: Arc::new(()),
154            event_loop: EventLoop::new(),
155            event_handlers: EventHandlers::new(),
156            interrupt_handlers: InterruptHandlers::new(),
157            previewer: None
158        }
159    }
160}
161
162impl<T: MMItem, S: Selection, C> Matchmaker<T, S, C>
163{
164    pub fn new_raw(worker: Worker<T, C>, identifier: fn(&T) -> (u32, S), context: Arc<C>) -> Self {
165        Matchmaker {
166            worker,
167            bind_config: BindMap::new(),
168            render_config: RenderConfig::default(),
169            tui_config: TerminalConfig::default(),
170            exit_config: ExitConfig::default(),
171            selection_set: SelectionSet::new(identifier),
172            context,
173            event_handlers: EventHandlers::new(),
174            event_loop: EventLoop::new(),
175            interrupt_handlers: InterruptHandlers::new(),
176            previewer: None
177        }
178    }
179
180    /// The controller can be used to influence the event loop and by proxy the render loop.
181    /// However, it's role is not yet solidified.
182    pub fn get_controller(&self) -> UnboundedSender<Event> {
183        self.event_loop.get_controller()
184    }
185    /// The contents of the preview are displayed in a pane when picking.
186    pub fn connect_preview(&mut self, preview: Preview) {
187        self.previewer = Some(preview);
188    }
189
190    /// Configure keybinds
191    pub fn config_binds(&mut self, bind_config: BindMap) -> &mut Self {
192        self.bind_config = bind_config;
193        self
194    }
195    /// Configure the UI
196    pub fn config_render(&mut self, render_config: RenderConfig) -> &mut Self {
197        self.render_config = render_config;
198        self
199    }
200    /// Configure the TUI
201    pub fn config_tui(&mut self, tui_config: TerminalConfig) -> &mut Self {
202        self.tui_config = tui_config;
203        self
204    }
205    /// Configure exit conditions
206    pub fn config_exit(&mut self, exit_config: ExitConfig) -> &mut Self {
207        self.exit_config = exit_config;
208        self
209    }
210
211    /// Register a handler to listen on [`Event`]s
212    pub fn register_event_handler<F, I>(&mut self, events: I, handler: F)
213    where
214    F: Fn(&mut EphemeralState<'_, T, S, C>, &Event) + Send + Sync + 'static,
215    I: IntoIterator<Item = Event>,
216    {
217        let boxed = Box::new(handler);
218        self.register_boxed_event_handler(events, boxed);
219    }
220    /// Register a boxed handler to listen on [`Event`]s
221    pub fn register_boxed_event_handler<I>(
222        &mut self,
223        events: I,
224        handler: DynamicMethod<T, S, C, Event>,
225    )
226    where
227    I: IntoIterator<Item = Event>,
228    {
229        let events_vec: Vec<_> = events.into_iter().collect();
230        self.event_handlers.set(events_vec, handler);
231    }
232    /// Register a handler to listen on [`Interrupt`]s
233    pub fn register_interrupt_handler<F>(
234        &mut self,
235        interrupt: Interrupt,
236        handler: F,
237    )
238    where
239    F: Fn(&mut EphemeralState<'_, T, S, C>, &Interrupt) + Send + Sync + 'static,
240    {
241        let boxed = Box::new(handler);
242        self.register_boxed_interrupt_handler(interrupt, boxed);
243    }
244    /// Register a boxed handler to listen on [`Interrupt`]s
245    pub fn register_boxed_interrupt_handler(
246        &mut self,
247        variant: Interrupt,
248        handler: DynamicMethod<T, S, C, Interrupt>,
249    ) {
250        self.interrupt_handlers.set(variant, handler);
251    }
252
253    /// 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.
254    pub async fn pick_with_matcher(mut self, matcher: &mut nucleo::Matcher) -> Result<Vec<S>, MatchError> {
255        if self.exit_config.select_1 && self.worker.counts().0 == 1 {
256            return Ok(self.selection_set.map_to_vec([self.worker.get_nth(0).unwrap()]));
257        }
258
259        let (render_tx, render_rx) = tokio::sync::mpsc::unbounded_channel();
260        self.event_loop.add_tx(render_tx.clone()).set_tick_rate(self.render_config.tick_rate());
261
262        let event_controller = self.event_loop.get_controller();
263        self.event_loop.binds(self.bind_config);
264
265        let mut tui = tui::Tui::new(self.tui_config).map_err(|e| MatchError::TUIError(e.to_string()))?;
266        tui.enter().map_err(|e| MatchError::TUIError(e.to_string()))?;
267
268        tokio::spawn(async move {
269            let _ = self.event_loop.run().await;
270        });
271
272        let (ui, picker, preview) = UI::new(self.render_config, matcher, self.worker, self.selection_set, self.previewer, &mut tui);
273
274        render::render_loop(ui, picker, preview, tui, render_rx, event_controller, self.context, (self.event_handlers, self.interrupt_handlers), self.exit_config).await
275    }
276
277    pub async fn pick(self) -> Result<Vec<S>, MatchError> {
278        let mut matcher=  nucleo::Matcher::new(nucleo::Config::DEFAULT);
279        self.pick_with_matcher(&mut matcher).await
280    }
281}
282// ------------ BOILERPLATE ---------------
283
284impl<T: MMItem + Debug, S: Selection + Debug, C: Debug> Debug for Matchmaker<T, S, C> {
285    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
286        f.debug_struct("Matchmaker")
287        // omit `worker`
288        .field("render_config", &self.render_config)
289        .field("bind_config", &self.bind_config)
290        .field("tui_config", &self.tui_config)
291        .field("selection_set", &self.selection_set)
292        .field("context", &self.context)
293        .field("event_handlers", &self.event_handlers)
294        .field("interrupt_handlers", &self.interrupt_handlers)
295        .field("previewer", &self.previewer)
296        .finish()
297    }
298}