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