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