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