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