1use std::{
2 fmt::{self, Debug, Formatter},
3 process::Stdio,
4 sync::Arc,
5};
6
7use arrayvec::ArrayVec;
8use cli_boilerplate_automation::{
9 _log,
10 broc::{exec_script, spawn_script},
11 env_vars, prints,
12};
13use easy_ext::ext;
14use log::{debug, info, warn};
15use ratatui::text::Text;
16
17use crate::{
18 MatchError, RenderFn, Result, SSS, Selection, Selector, SplitterFn,
19 action::{ActionAliaser, ActionExt, ActionExtHandler, NullActionExt},
20 binds::BindMap,
21 config::{ExitConfig, PreviewerConfig, RenderConfig, Split, TerminalConfig, WorkerConfig},
22 efx,
23 event::{EventLoop, RenderSender},
24 message::{Event, Interrupt},
25 nucleo::{
26 Indexed, Segmented, Worker,
27 injector::{IndexedInjector, Injector, SegmentedInjector, WorkerInjector},
28 },
29 preview::{
30 AppendOnly, Preview,
31 previewer::{PreviewMessage, Previewer},
32 },
33 render::{self, DynamicMethod, Effects, EventHandlers, InterruptHandlers, MMState},
34 tui,
35 ui::{Overlay, OverlayUI, UI},
36};
37
38pub struct Matchmaker<T: SSS, S: Selection = T> {
46 pub worker: Worker<T>,
47 pub render_config: RenderConfig,
48 pub tui_config: TerminalConfig,
49 pub exit_config: ExitConfig,
50 pub selector: Selector<T, S>,
51 pub event_handlers: EventHandlers<T, S>,
52 pub interrupt_handlers: InterruptHandlers<T, S>,
53 pub preview: Option<Preview>,
54}
55
56pub struct OddEnds {
60 pub formatter: Arc<RenderFn<Indexed<Segmented<String>>>>,
61 pub splitter: SplitterFn<String>,
62}
63
64pub type ConfigInjector = SegmentedInjector<
65 String,
66 IndexedInjector<Segmented<String>, WorkerInjector<Indexed<Segmented<String>>>>,
67>;
68pub type ConfigMatchmaker = Matchmaker<Indexed<Segmented<String>>, Segmented<String>>;
69
70impl ConfigMatchmaker {
71 pub fn new_from_config(
73 render_config: RenderConfig,
74 tui_config: TerminalConfig,
75 worker_config: WorkerConfig,
76 ) -> (Self, ConfigInjector, OddEnds) {
77 let cc = worker_config.columns;
78
79 let worker: Worker<Indexed<Segmented<String>>> = match cc.split {
80 Split::Delimiter(_) | Split::Regexes(_) => {
81 let names: Vec<Arc<str>> = if cc.names.is_empty() {
82 (0..cc.max_cols())
83 .map(|n| Arc::from(n.to_string()))
84 .collect()
85 } else {
86 cc.names
87 .iter()
88 .map(|s| Arc::from(s.name.as_str()))
89 .collect()
90 };
91 Worker::new_indexable(names)
92 }
93 Split::None => Worker::new_indexable([""]),
94 };
95
96 let injector = worker.injector();
97
98 let col_count = worker.columns.len();
100
101 let splitter: SplitterFn<String> = match cc.split {
103 Split::Delimiter(ref rg) => {
104 let rg = rg.clone();
105 Arc::new(move |s| {
106 let mut ranges = ArrayVec::new();
107 let mut last_end = 0;
108 for m in rg.find_iter(s).take(col_count) {
109 ranges.push((last_end, m.start()));
110 last_end = m.end();
111 }
112 ranges.push((last_end, s.len()));
113 ranges
114 })
115 }
116 Split::Regexes(ref rgs) => {
117 let rgs = rgs.clone(); Arc::new(move |s| {
119 let mut ranges = ArrayVec::new();
120 for re in rgs.iter().take(col_count) {
121 if let Some(m) = re.find(s) {
122 ranges.push((m.start(), m.end()));
123 } else {
124 ranges.push((0, 0));
125 }
126 }
127 ranges
128 })
129 }
130 Split::None => Arc::new(|s| ArrayVec::from_iter([(0, s.len())])),
131 };
132 let injector = IndexedInjector::new(injector, 0);
133 let injector = SegmentedInjector::new(injector, splitter.clone());
134
135 let selection_set = Selector::new(Indexed::identifier);
136
137 let event_handlers = EventHandlers::new();
138 let interrupt_handlers = InterruptHandlers::new();
139 let formatter = Arc::new(
140 worker.make_format_fn::<true>(|item| std::borrow::Cow::Borrowed(&item.inner.inner)),
141 );
142
143 let new = Matchmaker {
144 worker,
145 render_config,
146 tui_config,
147 exit_config: worker_config.exit,
148 selector: selection_set,
149 event_handlers,
150 interrupt_handlers,
151 preview: None,
152 };
153
154 let misc = OddEnds {
155 formatter,
156 splitter,
157 };
158
159 (new, injector, misc)
160 }
161}
162
163impl<T: SSS, S: Selection> Matchmaker<T, S> {
164 pub fn new(worker: Worker<T>, selector: Selector<T, S>) -> Self {
165 Matchmaker {
166 worker,
167 render_config: RenderConfig::default(),
168 tui_config: TerminalConfig::default(),
169 exit_config: ExitConfig::default(),
170 selector,
171 event_handlers: EventHandlers::new(),
172 interrupt_handlers: InterruptHandlers::new(),
173 preview: None,
174 }
175 }
176
177 pub fn connect_preview(&mut self, preview: Preview) {
193 self.preview = Some(preview);
194 }
195
196 pub fn config_render(&mut self, render: RenderConfig) -> &mut Self {
198 self.render_config = render;
199 self
200 }
201 pub fn config_tui(&mut self, tui: TerminalConfig) -> &mut Self {
203 self.tui_config = tui;
204 self
205 }
206 pub fn config_exit(&mut self, exit: ExitConfig) -> &mut Self {
208 self.exit_config = exit;
209 self
210 }
211 pub fn register_event_handler<F, I>(&mut self, events: I, handler: F)
213 where
214 F: Fn(&mut MMState<'_, T, S>, &Event) -> Effects + SSS,
215 I: IntoIterator<Item = Event>,
216 {
217 let boxed = Box::new(handler);
218 self.register_boxed_event_handler(events, boxed);
219 }
220 pub fn register_boxed_event_handler<I>(
222 &mut self,
223 events: I,
224 handler: DynamicMethod<T, S, Event>,
225 ) where
226 I: IntoIterator<Item = Event>,
227 {
228 let events_vec: Vec<_> = events.into_iter().collect();
229 self.event_handlers.set(events_vec, handler);
230 }
231 pub fn register_interrupt_handler<F>(&mut self, interrupt: Interrupt, handler: F)
233 where
234 F: Fn(&mut MMState<'_, T, S>, &Interrupt) -> Effects + SSS,
235 {
236 let boxed = Box::new(handler);
237 self.register_boxed_interrupt_handler(interrupt, boxed);
238 }
239 pub fn register_boxed_interrupt_handler(
241 &mut self,
242 variant: Interrupt,
243 handler: DynamicMethod<T, S, Interrupt>,
244 ) {
245 self.interrupt_handlers.set(variant, handler);
246 }
247
248 pub async fn pick<A: ActionExt>(
250 mut self,
251 builder: PickOptions<'_, T, S, A>,
252 ) -> Result<Vec<S>, MatchError> {
253 let PickOptions {
254 previewer,
255 ext_handler,
256 ext_aliaser,
257 #[cfg(feature = "bracketed-paste")]
258 paste_handler,
259 ..
260 } = builder;
261
262 if self.exit_config.select_1 && self.worker.counts().0 == 1 {
263 return Ok(self
264 .selector
265 .identify_to_vec([self.worker.get_nth(0).unwrap()]));
266 }
267
268 let mut event_loop = if let Some(e) = builder.event_loop {
269 e
270 } else if let Some(binds) = builder.binds {
271 EventLoop::with_binds(binds).with_tick_rate(self.render_config.tick_rate())
272 } else {
273 EventLoop::new()
274 };
275
276 if let Some(mut previewer) = previewer {
278 if self.preview.is_none() {
279 self.preview = Some(previewer.view());
280 }
281 previewer.connect_controller(event_loop.get_controller());
282 tokio::spawn(async move {
283 let _ = previewer.run().await;
284 });
285 }
286
287 let (render_tx, render_rx) = builder
288 .channel
289 .unwrap_or_else(tokio::sync::mpsc::unbounded_channel);
290 event_loop.add_tx(render_tx.clone());
291
292 let mut tui =
293 tui::Tui::new(self.tui_config).map_err(|e| MatchError::TUIError(e.to_string()))?;
294 tui.enter()
295 .map_err(|e| MatchError::TUIError(e.to_string()))?;
296
297 let event_controller = event_loop.get_controller();
299 tokio::spawn(async move {
300 let _ = event_loop.run().await;
301 });
302 log::debug!("event loop started");
303
304 let overlay_ui = if builder.overlays.is_empty() {
305 None
306 } else {
307 Some(OverlayUI::new(
308 builder.overlays.into_boxed_slice(),
309 self.render_config.overlay.take().unwrap_or_default(),
310 ))
311 };
312
313 tui.redraw();
315
316 if let Some(matcher) = builder.matcher {
317 let (ui, picker, preview) = UI::new(
318 self.render_config,
319 matcher,
320 self.worker,
321 self.selector,
322 self.preview,
323 &mut tui,
324 );
325 render::render_loop(
326 ui,
327 picker,
328 preview,
329 tui,
330 overlay_ui,
331 self.exit_config,
332 render_rx,
333 event_controller,
334 (self.event_handlers, self.interrupt_handlers),
335 ext_handler,
336 ext_aliaser,
337 #[cfg(feature = "bracketed-paste")]
338 paste_handler,
339 )
340 .await
341 } else {
342 let mut matcher = nucleo::Matcher::new(nucleo::Config::DEFAULT);
343 let (ui, picker, preview) = UI::new(
344 self.render_config,
345 &mut matcher,
346 self.worker,
347 self.selector,
348 self.preview,
349 &mut tui,
350 );
351 render::render_loop(
352 ui,
353 picker,
354 preview,
355 tui,
356 overlay_ui,
357 self.exit_config,
358 render_rx,
359 event_controller,
360 (self.event_handlers, self.interrupt_handlers),
361 ext_handler,
362 ext_aliaser,
363 #[cfg(feature = "bracketed-paste")]
364 paste_handler,
365 )
366 .await
367 }
368 }
369
370 pub async fn pick_default(self) -> Result<Vec<S>, MatchError> {
371 self.pick::<NullActionExt>(PickOptions::new()).await
372 }
373}
374
375#[ext(MatchResultExt)]
376impl<T> Result<T, MatchError> {
377 pub fn first<S>(self) -> Result<S, MatchError>
379 where
380 T: IntoIterator<Item = S>,
381 {
382 match self {
383 Ok(v) => v.into_iter().next().ok_or(MatchError::NoMatch),
384 Err(e) => Err(e),
385 }
386 }
387
388 pub fn abort(self) -> Result<T, MatchError> {
390 match self {
391 Err(MatchError::Abort(x)) => std::process::exit(x),
392 _ => self,
393 }
394 }
395}
396
397pub type PasteHandler<T, S> = fn(String, &MMState<'_, T, S>) -> String;
401pub struct PickOptions<'a, T: SSS, S: Selection, A: ActionExt = NullActionExt> {
403 matcher: Option<&'a mut nucleo::Matcher>,
404 matcher_config: nucleo::Config,
405
406 event_loop: Option<EventLoop<A>>,
407 binds: Option<BindMap<A>>,
408
409 ext_handler: Option<ActionExtHandler<T, S, A>>,
410 ext_aliaser: Option<ActionAliaser<T, S, A>>,
411 #[cfg(feature = "bracketed-paste")]
412 paste_handler: Option<PasteHandler<T, S>>,
413
414 overlays: Vec<Box<dyn Overlay<A = A>>>,
415 previewer: Option<Previewer>,
416
417 pub channel: Option<(
423 RenderSender<A>,
424 tokio::sync::mpsc::UnboundedReceiver<crate::message::RenderCommand<A>>,
425 )>,
426}
427
428impl<'a, T: SSS, S: Selection, A: ActionExt> PickOptions<'a, T, S, A> {
430 pub const fn new() -> Self {
431 Self {
432 matcher: None,
433 event_loop: None,
434 previewer: None,
435 binds: None,
436 matcher_config: nucleo::Config::DEFAULT,
437 ext_handler: None,
438 ext_aliaser: None,
439 #[cfg(feature = "bracketed-paste")]
440 paste_handler: None,
441 overlays: Vec::new(),
442 channel: None,
443 }
444 }
445
446 pub fn with_binds(binds: BindMap<A>) -> Self {
447 let mut ret = Self::new();
448 ret.binds = Some(binds);
449 ret
450 }
451
452 pub fn with_matcher(matcher: &'a mut nucleo::Matcher) -> Self {
453 let mut ret = Self::new();
454 ret.matcher = Some(matcher);
455 ret
456 }
457
458 pub fn binds(mut self, binds: BindMap<A>) -> Self {
459 self.binds = Some(binds);
460 self
461 }
462
463 pub fn event_loop(mut self, event_loop: EventLoop<A>) -> Self {
464 self.event_loop = Some(event_loop);
465 self
466 }
467
468 pub fn previewer(mut self, previewer: Previewer) -> Self {
470 self.previewer = Some(previewer);
471 self
472 }
473
474 pub fn matcher(mut self, matcher_config: nucleo::Config) -> Self {
475 self.matcher_config = matcher_config;
476 self
477 }
478
479 pub fn ext_handler(mut self, handler: ActionExtHandler<T, S, A>) -> Self {
480 self.ext_handler = Some(handler);
481 self
482 }
483
484 pub fn ext_aliaser(mut self, aliaser: ActionAliaser<T, S, A>) -> Self {
485 self.ext_aliaser = Some(aliaser);
486 self
487 }
488
489 #[cfg(feature = "bracketed-paste")]
490 pub fn paste_handler(mut self, handler: PasteHandler<T, S>) -> Self {
491 self.paste_handler = Some(handler);
492 self
493 }
494
495 pub fn overlay<O>(mut self, overlay: O) -> Self
496 where
497 O: Overlay<A = A> + 'static,
498 {
499 self.overlays.push(Box::new(overlay));
500 self
501 }
502
503 pub fn get_tx(&mut self) -> RenderSender<A> {
504 if let Some((s, _)) = &self.channel {
505 s.clone()
506 } else {
507 let channel = tokio::sync::mpsc::unbounded_channel();
508 let ret = channel.0.clone();
509 self.channel = Some(channel);
510 ret
511 }
512 }
513
514 }
531
532impl<'a, T: SSS, S: Selection, A: ActionExt> Default for PickOptions<'a, T, S, A> {
533 fn default() -> Self {
534 Self::new()
535 }
536}
537
538impl<T: SSS, S: Selection> Matchmaker<T, S> {
541 pub fn register_print_handler(
542 &mut self,
543 print_handle: AppendOnly<String>,
544 formatter: Arc<RenderFn<T>>,
545 ) {
546 self.register_interrupt_handler(Interrupt::Print("".into()), move |state, i| {
547 if let Interrupt::Print(template) = i
548 && let Some(t) = state.current_raw()
549 {
550 let s = formatter(t, template);
551 if atty::is(atty::Stream::Stdout) {
552 print_handle.push(s);
553 } else {
554 prints!(s);
555 }
556 };
557 efx![]
558 });
559 }
560
561 pub fn register_execute_handler(&mut self, formatter: Arc<RenderFn<T>>) {
562 let preview_formatter = formatter.clone();
563
564 self.register_interrupt_handler(Interrupt::Execute("".into()), move |state, interrupt| {
565 if let Interrupt::Execute(template) = interrupt
566 && !template.is_empty()
567 && let Some(t) = state.current_raw()
568 {
569 let cmd = formatter(t, template);
570 let mut vars = state.make_env_vars();
571 let preview_cmd = preview_formatter(t, state.preview_payload());
572 let extra = env_vars!(
573 "FZF_PREVIEW_COMMAND" => preview_cmd,
574 );
575 vars.extend(extra);
576 let tty = maybe_tty();
577 if let Some(mut child) =
578 spawn_script(&cmd, vars, tty, Stdio::inherit(), Stdio::inherit())
579 {
580 match child.wait() {
581 Ok(i) => {
582 info!("Command [{cmd}] exited with {i}")
583 }
584 Err(e) => {
585 info!("Failed to wait on command [{cmd}]: {e}")
586 }
587 }
588 }
589 };
590 efx![]
591 });
592 }
593
594 pub fn register_become_handler(&mut self, formatter: Arc<RenderFn<T>>) {
595 let preview_formatter = formatter.clone();
596
597 self.register_interrupt_handler(Interrupt::Become("".into()), move |state, interrupt| {
598 if let Interrupt::Become(template) = interrupt
599 && !template.is_empty()
600 && let Some(t) = state.current_raw()
601 {
602 let cmd = formatter(t, template);
603 let mut vars = state.make_env_vars();
604
605 let preview_cmd = preview_formatter(t, state.preview_payload());
606 let extra = env_vars!(
607 "FZF_PREVIEW_COMMAND" => preview_cmd,
608 );
609 vars.extend(extra);
610 debug!("Becoming: {cmd}");
611 exec_script(&cmd, vars);
612 }
613 efx![]
614 });
615 }
616}
617
618pub fn make_previewer<T: SSS, S: Selection>(
619 mm: &mut Matchmaker<T, S>,
620 previewer_config: PreviewerConfig, formatter: Arc<RenderFn<T>>,
622 help_str: Text<'static>,
623) -> Previewer {
624 let (previewer, tx) = Previewer::new(previewer_config);
626 mm.connect_preview(previewer.view());
627 let preview_tx = tx.clone();
628
629 mm.register_event_handler([Event::CursorChange, Event::PreviewChange], move |state, _| {
631 if state.preview_show &&
632 let Some(t) = state.current_raw() &&
633 let m = state.preview_payload() &&
634 !m.is_empty()
635 {
636 let cmd = formatter(t, m);
637 let mut envs = state.make_env_vars();
638 let extra = env_vars!(
639 "COLUMNS" => state.previewer_area().map_or("0".to_string(), |r| r.width.to_string()),
640 "LINES" => state.previewer_area().map_or("0".to_string(), |r| r.height.to_string()),
641 );
642 envs.extend(extra);
643
644 let msg = PreviewMessage::Run(cmd.clone(), envs);
645 _log!("{cmd:?}");
646 if preview_tx.send(msg.clone()).is_err() {
647 warn!("Failed to send to preview: {}", msg)
648 }
649 } else if preview_tx.send(PreviewMessage::Stop).is_err() {
650 warn!("Failed to send to preview: stop")
651 }
652
653 efx![render::Effect::ClearPreviewSet] });
655
656 mm.register_event_handler([Event::PreviewSet], move |state, _event| {
657 if state.preview_show {
658 let msg = if let Some(m) = state.preview_set_payload() {
659 let m = if m.is_empty() && !help_str.lines.is_empty() {
660 help_str.clone()
661 } else {
662 Text::from(m.clone())
663 };
664 PreviewMessage::Set(m.clone())
665 } else {
666 PreviewMessage::Unset
667 };
668
669 if tx.send(msg.clone()).is_err() {
670 warn!("Failed to send: {}", msg)
671 }
672 }
673 efx![]
674 });
675
676 previewer
677}
678
679fn maybe_tty() -> Stdio {
680 if let Ok(mut tty) = std::fs::File::open("/dev/tty") {
681 let _ = std::io::Write::flush(&mut tty); Stdio::from(tty)
683 } else {
684 log::error!("Failed to open /dev/tty");
685 Stdio::inherit()
686 }
687}
688
689impl<T: SSS + Debug, S: Selection + Debug> Debug for Matchmaker<T, S> {
692 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
693 f.debug_struct("Matchmaker")
694 .field("render_config", &self.render_config)
696 .field("tui_config", &self.tui_config)
697 .field("selection_set", &self.selector)
698 .field("event_handlers", &self.event_handlers)
699 .field("interrupt_handlers", &self.interrupt_handlers)
700 .field("previewer", &self.preview)
701 .finish()
702 }
703}