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
39pub 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
56pub struct OddEnds {
59 pub formatter: Arc<RenderFn<ConfigMMItem>>,
60 pub splitter: SplitterFn<Either<String, Text<'static>>>,
61 pub hidden_columns: Vec<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 pub fn new_from_config(
77 render_config: RenderConfig,
78 tui_config: TerminalConfig,
79 worker_config: WorkerConfig,
80 exit_config: ExitConfig,
81 preprocess_config: PreprocessOptions,
82 ) -> (Self, ConfigInjector, OddEnds) {
83 let cc = worker_config.columns;
84 let hidden_columns = cc.names.iter().map(|x| x.hidden).collect();
85 #[allow(unused_mut)]
87 let mut worker: Worker<ConfigMMItem> = match cc.split {
88 Split::Delimiter(_) | Split::Regexes(_) => {
89 let names: Vec<Arc<str>> = if cc.names.is_empty() {
90 (0..cc.max_cols())
91 .map(|n| Arc::from(n.to_string()))
92 .collect()
93 } else {
94 cc.names
95 .iter()
96 .map(|s| Arc::from(s.name.as_str()))
97 .collect()
98 };
99 Worker::new_indexable(names)
100 }
101 Split::None => Worker::new_indexable([""]),
102 };
103
104 #[cfg(feature = "experimental")]
105 worker.reverse_items(worker_config.reverse);
106 #[cfg(feature = "experimental")]
107 worker.set_stability(worker_config.sort_threshold);
108
109 let injector = worker.injector();
110
111 let col_count = worker.columns.len();
113
114 let splitter: SplitterFn<Either<String, Text>> = match cc.split {
116 Split::Delimiter(ref rg) => {
117 let rg = rg.clone();
118 Arc::new(move |s| {
119 let s = &s.to_cow();
120
121 let mut ranges = ArrayVec::new();
122 let mut last_end = 0;
123 for m in rg.find_iter(s).take(col_count) {
124 ranges.push((last_end, m.start()));
125 last_end = m.end();
126 }
127 ranges.push((last_end, s.len()));
128 ranges
129 })
130 }
131 Split::Regexes(ref rgs) => {
132 let rgs = rgs.clone(); Arc::new(move |s| {
134 let s = &s.to_cow();
135 let mut ranges = ArrayVec::new();
136
137 for re in rgs.iter().take(col_count) {
138 if let Some(m) = re.find(s) {
139 ranges.push((m.start(), m.end()));
140 } else {
141 ranges.push((0, 0));
142 }
143 }
144 ranges
145 })
146 }
147 Split::None => Arc::new(|s| ArrayVec::from_iter([(0, s.to_cow().len())])),
148 };
149 let injector = IndexedInjector::new_globally_indexed(injector);
150 let injector = SegmentedInjector::new(injector, splitter.clone());
151 let injector = AnsiInjector::new(injector, preprocess_config);
152
153 let selection_set = if render_config.results.multi {
154 Selector::new(Indexed::identifier)
155 } else {
156 Selector::new(Indexed::identifier).disabled()
157 };
158
159 let event_handlers = EventHandlers::new();
160 let interrupt_handlers = InterruptHandlers::new();
161 let formatter = Arc::new(worker.default_format_fn::<true>(|item| item.to_cow()));
162
163 let new = Matchmaker {
164 worker,
165 render_config,
166 tui_config,
167 exit_config,
168 selector: selection_set,
169 event_handlers,
170 interrupt_handlers,
171 };
172
173 let misc = OddEnds {
174 formatter,
175 splitter,
176 hidden_columns,
177 };
178
179 (new, injector, misc)
180 }
181}
182
183impl<T: SSS, S: Selection> Matchmaker<T, S> {
184 pub fn new(worker: Worker<T>, selector: Selector<T, S>) -> Self {
185 Matchmaker {
186 worker,
187 render_config: RenderConfig::default(),
188 tui_config: TerminalConfig::default(),
189 exit_config: ExitConfig::default(),
190 selector,
191 event_handlers: EventHandlers::new(),
192 interrupt_handlers: InterruptHandlers::new(),
193 }
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>(&mut self, event: Event, handler: F)
213 where
214 F: Fn(&mut MMState<'_, '_, T, S>, &Event) + 'static,
215 {
216 let boxed = Box::new(handler);
217 self.register_boxed_event_handler(event, boxed);
218 }
219 pub fn register_boxed_event_handler(
221 &mut self,
222 event: Event,
223 handler: DynamicMethod<T, S, Event>,
224 ) {
225 self.event_handlers.set(event, handler);
226 }
227 pub fn register_interrupt_handler<F>(&mut self, interrupt: Interrupt, handler: F)
229 where
230 F: Fn(&mut MMState<'_, '_, T, S>) + 'static,
231 {
232 let boxed = Box::new(handler);
233 self.register_boxed_interrupt_handler(interrupt, boxed);
234 }
235 pub fn register_boxed_interrupt_handler(
237 &mut self,
238 variant: Interrupt,
239 handler: BoxedHandler<T, S>,
240 ) {
241 self.interrupt_handlers.set(variant, handler);
242 }
243
244 pub async fn pick<A: ActionExt>(self, builder: PickOptions<'_, T, S, A>) -> Result<Vec<S>> {
246 let PickOptions {
247 previewer,
248 ext_handler,
249 ext_aliaser,
250 #[cfg(feature = "bracketed-paste")]
251 paste_handler,
252 overlay_config,
253 hidden_columns,
254 initializer,
255 ..
256 } = builder;
257
258 if self.exit_config.select_1 && self.worker.counts().0 == 1 {
259 return Ok(self
260 .selector
261 .identify_to_vec([self.worker.get_nth(0).unwrap()]));
262 }
263
264 let mut event_loop = if let Some(e) = builder.event_loop {
265 e
266 } else if let Some(binds) = builder.binds {
267 EventLoop::with_binds(binds).with_tick_rate(self.render_config.tick_rate())
268 } else {
269 EventLoop::new()
270 };
271
272 let mut wait = false;
273 if let Some(path) = self.exit_config.last_key_path.clone()
274 && !path.is_empty()
275 {
276 event_loop.record_last_key(path);
277 wait = true;
278 }
279
280 let preview = match previewer {
281 Some(Either::Left(view)) => Some(view),
282 Some(Either::Right(mut previewer)) => {
283 let view = previewer.view();
284 previewer.connect_controller(event_loop.controller());
285
286 tokio::spawn(async move {
287 let _ = previewer.run().await;
288 });
289
290 Some(view)
291 }
292 _ => None,
293 };
294
295 let (render_tx, render_rx) = builder
296 .channel
297 .unwrap_or_else(tokio::sync::mpsc::unbounded_channel);
298 event_loop.add_tx(render_tx.clone());
299
300 let mut tui =
301 tui::Tui::new(self.tui_config).map_err(|e| MatchError::TUIError(e.to_string()))?;
302 tui.enter()
303 .map_err(|e| MatchError::TUIError(e.to_string()))?;
304
305 let event_controller = event_loop.controller();
307 let event_loop_handle = tokio::spawn(async move {
308 let _ = event_loop.run().await;
309 });
310 log::debug!("event loop started");
311
312 let overlay_ui = if builder.overlays.is_empty() {
313 None
314 } else {
315 Some(OverlayUI::new(
316 builder.overlays.into_boxed_slice(),
317 overlay_config.unwrap_or_default(),
318 ))
319 };
320
321 tui.redraw();
323
324 let matcher = if let Some(matcher) = builder.matcher {
325 matcher
326 } else {
327 &mut nucleo::Matcher::new(nucleo::Config::DEFAULT)
328 };
329
330 let (ui, picker, footer, preview) = UI::new(
331 self.render_config,
332 matcher,
333 self.worker,
334 self.selector,
335 preview,
336 &mut tui,
337 hidden_columns,
338 );
339
340 let ret = render::render_loop(
341 ui,
342 picker,
343 footer,
344 preview,
345 tui,
346 overlay_ui,
347 self.exit_config,
348 render_rx,
349 event_controller,
350 (self.event_handlers, self.interrupt_handlers),
351 ext_handler,
352 ext_aliaser,
353 initializer,
354 #[cfg(feature = "bracketed-paste")]
355 paste_handler,
356 )
357 .await;
358
359 if wait {
360 let _ = event_loop_handle.await;
361 log::debug!("event loop finished");
362 }
363
364 ret
365 }
366
367 pub async fn pick_default(self) -> Result<Vec<S>> {
368 self.pick::<NullActionExt>(PickOptions::new()).await
369 }
370}
371
372#[ext(MatchResultExt)]
373impl<T> Result<T> {
374 pub fn first<S>(self) -> Result<S>
376 where
377 T: IntoIterator<Item = S>,
378 {
379 match self {
380 Ok(v) => v.into_iter().next().ok_or(MatchError::NoMatch),
381 Err(e) => Err(e),
382 }
383 }
384
385 pub fn abort(self) -> Result<T> {
387 match self {
388 Err(MatchError::Abort(x)) => std::process::exit(x),
389 _ => self,
390 }
391 }
392}
393
394pub type PasteHandler<T, S> =
398 Box<dyn FnMut(String, &MMState<'_, '_, T, S>) -> String + Send + Sync + 'static>;
399
400pub type ActionExtHandler<T, S, A> =
401 Box<dyn FnMut(A, &mut MMState<'_, '_, T, S>) + Send + Sync + 'static>;
402
403pub type ActionAliaser<T, S, A> =
404 Box<dyn FnMut(Action<A>, &mut MMState<'_, '_, T, S>) -> Actions<A> + Send + Sync + 'static>;
405
406pub type Initializer<T, S> = Box<dyn FnOnce(&mut MMState<'_, '_, T, S>) + Send + Sync + 'static>;
407
408pub struct PickOptions<'a, T: SSS, S: Selection, A: ActionExt = NullActionExt> {
410 matcher: Option<&'a mut nucleo::Matcher>,
411 matcher_config: nucleo::Config,
412
413 event_loop: Option<EventLoop<A>>,
414 binds: Option<BindMap<A>>,
415
416 ext_handler: Option<ActionExtHandler<T, S, A>>,
417 ext_aliaser: Option<ActionAliaser<T, S, A>>,
418 #[cfg(feature = "bracketed-paste")]
419 paste_handler: Option<PasteHandler<T, S>>,
420
421 overlays: Vec<Box<dyn Overlay<A = A>>>,
422 overlay_config: Option<OverlayConfig>,
423 previewer: Option<Either<Preview, Previewer>>,
424
425 hidden_columns: Vec<bool>,
426
427 initializer: Option<Initializer<T, S>>,
429 pub channel: Option<(
430 RenderSender<A>,
431 tokio::sync::mpsc::UnboundedReceiver<crate::message::RenderCommand<A>>,
432 )>,
433}
434
435impl<'a, T: SSS, S: Selection, A: ActionExt> PickOptions<'a, T, S, A> {
436 pub const fn new() -> Self {
437 Self {
438 matcher: None,
439 event_loop: None,
440 previewer: None,
441 binds: None,
442 matcher_config: nucleo::Config::DEFAULT,
443 ext_handler: None,
444 ext_aliaser: None,
445 #[cfg(feature = "bracketed-paste")]
446 paste_handler: None,
447 overlay_config: None,
448 overlays: Vec::new(),
449 channel: None,
450 hidden_columns: Vec::new(),
451 initializer: None,
452 }
453 }
454
455 pub fn with_binds(binds: BindMap<A>) -> Self {
456 let mut ret = Self::new();
457 ret.binds = Some(binds);
458 ret
459 }
460
461 pub fn with_matcher(matcher: &'a mut nucleo::Matcher) -> Self {
462 let mut ret = Self::new();
463 ret.matcher = Some(matcher);
464 ret
465 }
466
467 pub fn binds(mut self, binds: BindMap<A>) -> Self {
468 self.binds = Some(binds);
469 self
470 }
471
472 pub fn event_loop(mut self, event_loop: EventLoop<A>) -> Self {
473 self.event_loop = Some(event_loop);
474 self
475 }
476
477 pub fn previewer(mut self, previewer: Previewer) -> Self {
481 self.previewer = Some(Either::Right(previewer));
482 self
483 }
484
485 pub fn preview(mut self, preview: Preview) -> Self {
488 self.previewer = Some(Either::Left(preview));
489 self
490 }
491
492 pub fn matcher(mut self, matcher_config: nucleo::Config) -> Self {
493 self.matcher_config = matcher_config;
494 self
495 }
496
497 pub fn hidden_columns(mut self, hidden_columns: Vec<bool>) -> Self {
498 self.hidden_columns = hidden_columns;
499 self
500 }
501
502 pub fn ext_handler<F>(mut self, handler: F) -> Self
503 where
504 F: FnMut(A, &mut MMState<'_, '_, T, S>) + Send + Sync + 'static,
505 {
506 self.ext_handler = Some(Box::new(handler));
507 self
508 }
509
510 pub fn ext_aliaser<F>(mut self, aliaser: F) -> Self
511 where
512 F: FnMut(Action<A>, &mut MMState<'_, '_, T, S>) -> Actions<A> + Send + Sync + 'static,
513 {
514 self.ext_aliaser = Some(Box::new(aliaser));
515 self
516 }
517
518 pub fn initializer<F>(mut self, aliaser: F) -> Self
519 where
520 F: FnOnce(&mut MMState<'_, '_, T, S>) + Send + Sync + 'static,
521 {
522 self.initializer = Some(Box::new(aliaser));
523 self
524 }
525
526 #[cfg(feature = "bracketed-paste")]
527 pub fn paste_handler<F>(mut self, handler: F) -> Self
528 where
529 F: FnMut(String, &MMState<'_, '_, T, S>) -> String + Send + Sync + 'static,
530 {
531 self.paste_handler = Some(Box::new(handler));
532 self
533 }
534
535 pub fn overlay<O>(mut self, overlay: O) -> Self
536 where
537 O: Overlay<A = A> + 'static,
538 {
539 self.overlays.push(Box::new(overlay));
540 self
541 }
542
543 pub fn overlay_config(mut self, overlay: OverlayConfig) -> Self {
544 self.overlay_config = Some(overlay);
545 self
546 }
547
548 pub fn render_tx(&mut self) -> RenderSender<A> {
549 if let Some((s, _)) = &self.channel {
550 s.clone()
551 } else {
552 let channel = tokio::sync::mpsc::unbounded_channel();
553 let ret = channel.0.clone();
554 self.channel = Some(channel);
555 ret
556 }
557 }
558}
559
560impl<'a, T: SSS, S: Selection, A: ActionExt> Default for PickOptions<'a, T, S, A> {
561 fn default() -> Self {
562 Self::new()
563 }
564}
565
566impl<T: SSS, S: Selection> Matchmaker<T, S> {
569 pub fn register_print_handler(
572 &mut self,
573 print_handle: AppendOnly<String>,
574 output_separator: String,
575 formatter: Arc<RenderFn<T>>,
576 ) {
577 self.register_interrupt_handler(Interrupt::Print, move |state| {
578 if let Some(t) = state.current_raw() {
579 let s = formatter(t, state.payload());
580 if atty::is(atty::Stream::Stdout) {
581 print_handle.push(s);
582 } else {
583 print!("{}{}", s, output_separator);
584 }
585 };
586 });
587 }
588
589 pub fn register_execute_handler(&mut self, formatter: Arc<RenderFn<T>>) {
594 self.register_interrupt_handler(Interrupt::Execute, move |state| {
595 let template = state.payload();
596 if !template.is_empty()
597 && let Some(t) = state.current_raw()
598 {
599 let cmd = formatter(t, template);
600 let mut vars = state.make_env_vars();
601
602 let preview_cmd = formatter(t, state.preview_payload());
603 let extra = env_vars!(
604 "FZF_PREVIEW_COMMAND" => preview_cmd,
605 );
606 vars.extend(extra);
607
608 if let Some(mut child) = Command::from_script(&cmd)
609 .envs(vars)
610 .stdin(maybe_tty())
611 ._spawn()
612 {
613 match child.wait() {
614 Ok(i) => {
615 info!("Command [{cmd}] exited with {i}")
616 }
617 Err(e) => {
618 info!("Failed to wait on command [{cmd}]: {e}")
619 }
620 }
621 }
622 };
623 });
624 }
625
626 pub fn register_become_handler(&mut self, formatter: Arc<RenderFn<T>>) {
631 self.register_interrupt_handler(Interrupt::Become, move |state| {
632 let template = state.payload();
633 if !template.is_empty()
634 && let Some(t) = state.current_raw()
635 {
636 let cmd = formatter(t, template);
637 let mut vars = state.make_env_vars();
638
639 let preview_cmd = formatter(t, state.preview_payload());
640 let extra = env_vars!(
641 "FZF_PREVIEW_COMMAND" => preview_cmd,
642 );
643 vars.extend(extra);
644 debug!("Becoming: {cmd}");
645
646 Command::from_script(&cmd).envs(vars)._exec()
647 }
648 });
649 }
650}
651
652pub fn make_previewer<T: SSS, S: Selection>(
655 mm: &mut Matchmaker<T, S>,
656 previewer_config: PreviewerConfig, formatter: Arc<RenderFn<T>>,
658 help_str: Text<'static>,
659) -> Previewer {
660 let (previewer, tx) = Previewer::new(previewer_config);
662 let preview_tx = tx.clone();
663
664 mm.register_event_handler(Event::CursorChange | Event::PreviewChange, move |state, _| {
666 if state.preview_visible() &&
667 let Some(t) = state.current_raw() &&
668 let m = state.preview_payload() &&
669 !m.is_empty()
670 {
671 let cmd = formatter(t, m);
672 let mut envs = state.make_env_vars();
673 let extra = env_vars!(
674 "COLUMNS" => state.previewer_area().map_or("0".to_string(), |r| r.width.to_string()),
675 "LINES" => state.previewer_area().map_or("0".to_string(), |r| r.height.to_string()),
676 );
677 envs.extend(extra);
678
679 let msg = PreviewMessage::Run(cmd.clone(), envs);
680 if preview_tx.send(msg.clone()).is_err() {
681 warn!("Failed to send to preview: {}", msg)
682 }
683
684 let target = state.preview_ui.as_ref().and_then(|p| p.config.scroll.index.as_ref().and_then(|index_col| {
685 state.current_raw().and_then(|item| {
686 state.picker_ui.worker.format_with(item, index_col).and_then(|t| t.parse::<isize>().ok())
687 })
688 }));
689
690 if let Some(p) = state.preview_ui {
691 p.set_target(target);
692 };
693
694 } else if preview_tx.send(PreviewMessage::Stop).is_err() {
695 warn!("Failed to send to preview: stop")
696 }
697
698 state.preview_set_payload = None;
699 }
700 );
701
702 mm.register_event_handler(Event::PreviewSet, move |state, _event| {
703 if state.preview_visible() {
704 let msg = if let Some(m) = state.preview_set_payload() {
705 let m = if m.is_empty() && !help_str.lines.is_empty() {
706 help_str.clone()
707 } else {
708 Text::from(m)
709 };
710 PreviewMessage::Set(m)
711 } else {
712 PreviewMessage::Unset
713 };
714
715 if tx.send(msg.clone()).is_err() {
716 warn!("Failed to send: {}", msg)
717 }
718 }
719 });
720
721 previewer
722}
723
724fn maybe_tty() -> Stdio {
725 if let Ok(tty) = std::fs::File::open("/dev/tty") {
726 Stdio::from(tty)
728 } else {
729 log::error!("Failed to open /dev/tty");
730 Stdio::inherit()
731 }
732}
733
734impl<T: SSS + Debug, S: Selection + Debug> Debug for Matchmaker<T, S> {
737 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
738 f.debug_struct("Matchmaker")
739 .field("render_config", &self.render_config)
741 .field("tui_config", &self.tui_config)
742 .field("selection_set", &self.selector)
743 .field("event_handlers", &self.event_handlers)
744 .field("interrupt_handlers", &self.interrupt_handlers)
745 .finish()
746 }
747}