1mod dynamic;
2mod state;
3
4use crossterm::event::{MouseButton, MouseEventKind};
5pub use dynamic::*;
6pub use state::*;
7use std::io::Write;
10
11use log::{info, warn};
12use ratatui::Frame;
13use ratatui::layout::{Position, Rect};
14use tokio::sync::mpsc;
15
16#[cfg(feature = "bracketed-paste")]
17use crate::PasteHandler;
18use crate::action::{Action, ActionExt};
19use crate::config::{CursorSetting, ExitConfig, RowConnectionStyle};
20use crate::message::{Event, Interrupt, RenderCommand};
21use crate::tui::Tui;
22use crate::ui::{DisplayUI, InputUI, OverlayUI, PickerUI, PreviewUI, ResultsUI, UI};
23use crate::{ActionAliaser, ActionExtHandler, MatchError, SSS, Selection};
24
25fn apply_aliases<T: SSS, S: Selection, A: ActionExt>(
26 buffer: &mut Vec<RenderCommand<A>>,
27 aliaser: ActionAliaser<T, S, A>,
28 dispatcher: &mut MMState<'_, '_, T, S>,
29) {
30 let mut out = Vec::new();
31
32 for cmd in buffer.drain(..) {
33 match cmd {
34 RenderCommand::Action(a) => out.extend(
35 aliaser(a, dispatcher)
36 .into_iter()
37 .map(RenderCommand::Action),
38 ),
39 other => out.push(other),
40 }
41 }
42
43 *buffer = out;
44}
45
46#[allow(clippy::too_many_arguments)]
47pub(crate) async fn render_loop<'a, W: Write, T: SSS, S: Selection, A: ActionExt>(
48 mut ui: UI,
49 mut picker_ui: PickerUI<'a, T, S>,
50 mut footer_ui: DisplayUI,
51 mut preview_ui: Option<PreviewUI>,
52 mut tui: Tui<W>,
53
54 mut overlay_ui: Option<OverlayUI<A>>,
55 exit_config: ExitConfig,
56
57 mut render_rx: mpsc::UnboundedReceiver<RenderCommand<A>>,
58 controller_tx: mpsc::UnboundedSender<Event>,
59
60 dynamic_handlers: DynamicHandlers<T, S>,
61 ext_handler: Option<ActionExtHandler<T, S, A>>,
62 ext_aliaser: Option<ActionAliaser<T, S, A>>,
63 #[cfg(feature = "bracketed-paste")] paste_handler: Option<PasteHandler<T, S>>,
64) -> Result<Vec<S>, MatchError> {
65 let mut buffer = Vec::with_capacity(256);
66
67 let mut state = State::new();
68 let mut click = Click::None;
69
70 if let Some(ref preview_ui) = preview_ui
72 && !preview_ui.command().is_empty()
73 {
74 state.update_preview(preview_ui.command());
75 }
76
77 while render_rx.recv_many(&mut buffer, 256).await > 0 {
78 let mut did_pause = false;
79 let mut did_exit = false;
80 let mut did_resize = false;
81
82 if let Some(aliaser) = ext_aliaser {
84 apply_aliases(
85 &mut buffer,
86 aliaser,
87 &mut state.dispatcher(&mut ui, &mut picker_ui, &mut footer_ui, &mut preview_ui),
88 )
89 };
91
92 if state.should_quit {
93 log::debug!("Exiting due to should_quit");
94 let ret = picker_ui.selector.output().collect::<Vec<S>>();
95 return if picker_ui.selector.is_disabled()
96 && let Some((_, item)) = get_current(&picker_ui)
97 {
98 Ok(vec![item])
99 } else if ret.is_empty() {
100 Err(MatchError::Abort(0))
101 } else {
102 Ok(ret)
103 };
104 } else if state.should_quit_nomatch {
105 log::debug!("Exiting due to should_quit_no_match");
106 return Err(MatchError::NoMatch);
107 }
108
109 for event in buffer.drain(..) {
110 state.clear_interrupt();
111
112 if !matches!(event, RenderCommand::Tick) {
113 info!("Recieved {event:?}");
114 }
115
116 match event {
117 RenderCommand::Action(Action::Input(c)) => {
118 if let Some(x) = overlay_ui.as_mut()
120 && x.handle_input(c)
121 {
122 continue;
123 }
124 picker_ui.input.push_char(c);
125 }
126 #[cfg(feature = "bracketed-paste")]
127 RenderCommand::Paste(content) => {
128 if let Some(handler) = paste_handler {
129 let content = {
130 handler(
131 content,
132 &state.dispatcher(
133 &mut ui,
134 &mut picker_ui,
135 &mut footer_ui,
136 &mut preview_ui,
137 ),
138 )
139 };
140 if !content.is_empty() {
141 picker_ui.input.push_str(&content);
142 }
143 }
144 }
145 RenderCommand::Resize(area) => {
146 tui.resize(area);
147 ui.area = area;
148 }
149 RenderCommand::Refresh => {
150 tui.redraw();
151 }
152 RenderCommand::HeaderColumns(columns) => {
153 picker_ui.header.header_columns(columns);
154 }
155 RenderCommand::Mouse(mouse) => {
156 let pos = Position::from((mouse.column, mouse.row));
158 let [preview, input, status, result] = state.layout;
159
160 match mouse.kind {
161 MouseEventKind::Down(MouseButton::Left) => {
162 if result.contains(pos) {
164 click = Click::ResultPos(mouse.row - result.top());
165 } else if input.contains(pos) {
166 let text_start_x = input.x
168 + picker_ui.input.prompt.width() as u16
169 + picker_ui.input.config.border.left();
170
171 if pos.x >= text_start_x {
172 let visual_offset = pos.x - text_start_x;
173 picker_ui.input.set_at_visual_offset(visual_offset);
174 } else {
175 picker_ui.input.set(None, 0);
176 }
177 } else if status.contains(pos) {
178 }
180 }
181 MouseEventKind::ScrollDown => {
182 if preview.contains(pos) {
183 if let Some(p) = preview_ui.as_mut() {
184 p.down(1)
185 }
186 } else {
187 picker_ui.results.cursor_next()
188 }
189 }
190 MouseEventKind::ScrollUp => {
191 if preview.contains(pos) {
192 if let Some(p) = preview_ui.as_mut() {
193 p.up(1)
194 }
195 } else {
196 picker_ui.results.cursor_prev()
197 }
198 }
199 MouseEventKind::ScrollLeft => {
200 }
202 MouseEventKind::ScrollRight => {
203 }
205 _ => {}
207 }
208 }
209 RenderCommand::QuitEmpty => {
210 return Ok(vec![]);
211 }
212 RenderCommand::Action(action) => {
213 if let Some(x) = overlay_ui.as_mut()
214 && x.handle_action(&action)
215 {
216 continue;
217 }
218 let PickerUI {
219 input,
220 results,
221 worker,
222 selector: selections,
223 header,
224 ..
225 } = &mut picker_ui;
226 match action {
227 Action::Select => {
228 if let Some(item) = worker.get_nth(results.index()) {
229 selections.sel(item);
230 }
231 }
232 Action::Deselect => {
233 if let Some(item) = worker.get_nth(results.index()) {
234 selections.desel(item);
235 }
236 }
237 Action::Toggle => {
238 if let Some(item) = worker.get_nth(results.index()) {
239 selections.toggle(item);
240 }
241 }
242 Action::CycleAll => {
243 selections.cycle_all_bg(worker.raw_results());
244 }
245 Action::ClearSelections => {
246 selections.clear();
247 }
248 Action::Accept => {
249 let ret = if selections.is_empty() {
250 if let Some(item) = get_current(&picker_ui) {
251 vec![item.1]
252 } else if exit_config.allow_empty {
253 vec![]
254 } else {
255 continue;
256 }
257 } else {
258 selections.output().collect::<Vec<S>>()
259 };
260 return Ok(ret);
261 }
262 Action::Quit(code) => {
263 return Err(MatchError::Abort(code));
264 }
265
266 Action::SetHeader(context) => {
268 if let Some(s) = context {
269 header.set(s, true);
270 } else {
271 header.clear(true);
272 }
273 }
274 Action::SetFooter(context) => {
275 if let Some(s) = context {
276 footer_ui.set(s, false);
277 } else {
278 footer_ui.clear(false);
279 }
280 }
281 Action::CyclePreview => {
283 if let Some(p) = preview_ui.as_mut() {
284 p.cycle_layout();
285 if !p.command().is_empty() {
286 state.update_preview(p.command());
287 }
288 }
289 }
290
291 Action::PreviewHScroll(x) | Action::PreviewScroll(x) => {
292 if let Some(p) = preview_ui.as_mut() {
293 p.scroll(matches!(action, Action::PreviewHScroll(_)), x);
294 }
295 }
296 Action::Preview(context) => {
297 if let Some(p) = preview_ui.as_mut() {
298 if !state.update_preview(context.as_str()) {
299 p.toggle_show()
300 } else {
301 p.show(true);
302 }
303 };
304 }
305 Action::Help(context) => {
306 if let Some(p) = preview_ui.as_mut() {
307 if !state.update_preview_set(context) {
309 state.update_preview_unset()
310 } else {
311 p.show(true);
312 }
313 };
314 }
315 Action::SwitchPreview(idx) => {
316 if let Some(p) = preview_ui.as_mut() {
317 if let Some(idx) = idx {
318 if !p.set_layout(idx) && !state.update_preview(p.command()) {
319 p.toggle_show();
320 }
321 } else {
322 p.toggle_show()
323 }
324 }
325 }
326 Action::SetPreview(idx) => {
327 if let Some(p) = preview_ui.as_mut() {
328 if let Some(idx) = idx {
329 p.set_layout(idx);
330 } else {
331 state.update_preview(p.command());
332 }
333 }
334 }
335 Action::ToggleWrap => {
336 results.wrap(!results.is_wrap());
337 }
338 Action::ToggleWrapPreview => {
339 if let Some(p) = preview_ui.as_mut() {
340 p.wrap(!p.is_wrap());
341 }
342 }
343
344 Action::Execute(payload) => {
346 state.set_interrupt(Interrupt::Execute, payload);
347 }
348 Action::Become(payload) => {
349 state.set_interrupt(Interrupt::Become, payload);
350 }
351 Action::Reload(payload) => {
352 state.set_interrupt(Interrupt::Reload, payload);
353 }
354 Action::Print(payload) => {
355 state.set_interrupt(Interrupt::Print, payload);
356 }
357
358 Action::SetInput(context) => {
359 input.set(context, u16::MAX);
360 }
361 Action::Column(context) => {
362 results.toggle_col(context);
363 }
364 Action::CycleColumn => {
365 results.cycle_col();
366 }
367 Action::ForwardChar => input.forward_char(),
369 Action::BackwardChar => input.backward_char(),
370 Action::ForwardWord => input.forward_word(),
371 Action::BackwardWord => input.backward_word(),
372 Action::DeleteChar => input.delete(),
373 Action::DeleteWord => input.delete_word(),
374 Action::DeleteLineStart => input.delete_line_start(),
375 Action::DeleteLineEnd => input.delete_line_end(),
376 Action::Cancel => input.cancel(),
377
378 Action::Up(x) | Action::Down(x) => {
380 let next = matches!(action, Action::Down(_)) ^ results.reverse();
381 for _ in 0..x.into() {
382 if next {
383 results.cursor_next();
384 } else {
385 results.cursor_prev();
386 }
387 }
388 }
389 Action::PreviewUp(n) => {
390 if let Some(p) = preview_ui.as_mut() {
391 p.up(n)
392 }
393 }
394 Action::PreviewDown(n) => {
395 if let Some(p) = preview_ui.as_mut() {
396 p.down(n)
397 }
398 }
399 Action::PreviewHalfPageUp => todo!(),
400 Action::PreviewHalfPageDown => todo!(),
401 Action::Pos(pos) => {
402 let pos = if pos >= 0 {
403 pos as u32
404 } else {
405 results.status.matched_count.saturating_sub((-pos) as u32)
406 };
407 results.cursor_jump(pos);
408 }
409 Action::InputPos(pos) => {
410 let pos = if pos >= 0 {
411 pos as u16
412 } else {
413 (input.len() as u16).saturating_sub((-pos) as u16)
414 };
415 input.set(None, pos);
416 }
417
418 Action::Redraw => {
420 tui.redraw();
421 }
422 Action::Overlay(index) => {
423 if let Some(x) = overlay_ui.as_mut() {
424 x.enable(index, &ui.area);
425 tui.redraw();
426 };
427 }
428 Action::Custom(e) => {
429 if let Some(handler) = ext_handler {
430 handler(
431 e,
432 &mut state.dispatcher(
433 &mut ui,
434 &mut picker_ui,
435 &mut footer_ui,
436 &mut preview_ui,
437 ),
438 );
439 }
440 }
441 _ => {}
442 }
443 }
444 _ => {}
445 }
446
447 let interrupt = state.interrupt();
448
449 match interrupt {
450 Interrupt::None => continue,
451 Interrupt::Execute => {
452 if controller_tx.send(Event::Pause).is_err() {
453 break;
454 }
455 tui.enter_execute();
456 did_exit = true;
457 did_pause = true;
458 }
459 Interrupt::Reload => {
460 picker_ui.worker.restart(false);
461 }
462 Interrupt::Become => {
463 tui.exit();
464 }
465 _ => {}
466 }
467 {
469 let mut dispatcher =
470 state.dispatcher(&mut ui, &mut picker_ui, &mut footer_ui, &mut preview_ui);
471 for h in dynamic_handlers.1.get(interrupt) {
472 h(&mut dispatcher);
473 }
474
475 if matches!(interrupt, Interrupt::Become) {
476 return Err(MatchError::Become(state.payload().clone()));
477 }
478 }
479
480 if state.should_quit {
481 log::debug!("Exiting due to should_quit");
482 let ret = picker_ui.selector.output().collect::<Vec<S>>();
483 return if picker_ui.selector.is_disabled()
484 && let Some((_, item)) = get_current(&picker_ui)
485 {
486 Ok(vec![item])
487 } else if ret.is_empty() {
488 Err(MatchError::Abort(0))
489 } else {
490 Ok(ret)
491 };
492 } else if state.should_quit_nomatch {
493 log::debug!("Exiting due to should_quit_nomatch");
494 return Err(MatchError::NoMatch);
495 }
496 }
497
498 picker_ui.update();
502 if exit_config.select_1
504 && picker_ui.results.status.matched_count == 1
505 && let Some((_, item)) = get_current(&picker_ui)
506 {
507 return Ok(vec![item]);
508 }
509
510 if did_exit {
512 tui.return_execute()
513 .map_err(|e| MatchError::TUIError(e.to_string()))?;
514 tui.redraw();
515 }
516
517 let mut overlay_ui_ref = overlay_ui.as_mut();
518 tui.terminal
519 .draw(|frame| {
520 let mut area = frame.area();
521
522 render_ui(frame, &mut area, &ui);
523
524 let mut _area = area;
525
526 let full_width_footer = footer_ui.single()
527 && footer_ui.config.row_connection_style == RowConnectionStyle::Full;
528
529 let mut footer =
530 if full_width_footer || preview_ui.as_ref().is_none_or(|p| !p.is_show()) {
531 split(&mut _area, footer_ui.height(), picker_ui.reverse())
532 } else {
533 Rect::default()
534 };
535
536 let [preview, picker_area, footer] = if let Some(preview_ui) = preview_ui.as_mut()
537 && let Some(layout) = preview_ui.layout()
538 {
539 let [preview, mut picker_area] = layout.split(_area);
540
541 if state.iterations == 0 && picker_area.width <= 5 {
542 warn!("UI too narrow, hiding preview");
543 preview_ui.show(false);
544
545 [Rect::default(), _area, footer]
546 } else {
547 if !full_width_footer {
548 footer =
549 split(&mut picker_area, footer_ui.height(), picker_ui.reverse());
550 }
551
552 [preview, picker_area, footer]
553 }
554 } else {
555 [Rect::default(), _area, footer]
556 };
557
558 let [input, status, header, results] = picker_ui.layout(picker_area);
559
560 did_resize = state.update_layout([preview, input, status, results]);
562
563 if did_resize {
564 picker_ui.results.update_dimensions(&results);
565 picker_ui.input.update_width(input.width);
566 footer_ui.update_width(
567 if footer_ui.config.row_connection_style == RowConnectionStyle::Capped {
568 area.width
569 } else {
570 footer.width
571 },
572 );
573 picker_ui.header.update_width(header.width);
574 ui.update_dimensions(area);
576 if let Some(x) = overlay_ui_ref.as_deref_mut() {
577 x.update_dimensions(&area);
578 }
579 };
580
581 render_input(frame, input, &mut picker_ui.input);
582 render_status(frame, status, &picker_ui.results);
583 render_results(frame, results, &mut picker_ui, &mut click);
584 render_display(frame, header, &mut picker_ui.header, &picker_ui.results);
585 render_display(frame, footer, &mut footer_ui, &picker_ui.results);
586 if let Some(preview_ui) = preview_ui.as_mut() {
587 state.update_preview_ui(preview_ui);
588 if did_resize {
589 preview_ui.update_dimensions(&preview);
590 }
591 render_preview(frame, preview, preview_ui);
592 }
593 if let Some(x) = overlay_ui_ref {
594 x.draw(frame);
595 }
596 })
597 .map_err(|e| MatchError::TUIError(e.to_string()))?;
598
599 if did_resize && tui.config.redraw_on_resize && !did_exit {
601 tui.redraw();
602 }
603 buffer.clear();
604
605 state.update(&picker_ui, &overlay_ui);
608 let events = state.events();
609
610 let mut dispatcher =
612 state.dispatcher(&mut ui, &mut picker_ui, &mut footer_ui, &mut preview_ui);
613 for e in events.iter() {
623 for h in dynamic_handlers.0.get(e) {
624 h(&mut dispatcher, &e)
625 }
626 }
627
628 for e in events.iter() {
631 controller_tx
632 .send(e)
633 .unwrap_or_else(|err| eprintln!("send failed: {:?}", err));
634 }
635 if did_pause {
638 log::debug!("Waiting for ack response to pause");
639 if controller_tx.send(Event::Resume).is_err() {
640 break;
641 };
642 while let Some(msg) = render_rx.recv().await {
644 if matches!(msg, RenderCommand::Ack) {
645 log::debug!("Recieved ack response to pause");
646 break;
647 }
648 }
649 }
650
651 click.process(&mut buffer);
652 }
653
654 Err(MatchError::EventLoopClosed)
655}
656
657pub enum Click {
660 None,
661 ResultPos(u16),
662 ResultIdx(u32),
663}
664
665impl Click {
666 fn process<A: ActionExt>(&mut self, buffer: &mut Vec<RenderCommand<A>>) {
667 match self {
668 Self::ResultIdx(u) => {
669 buffer.push(RenderCommand::Action(Action::Pos(*u as i32)));
670 }
671 _ => {
672 }
674 }
675 *self = Click::None
676 }
677}
678
679fn render_preview(frame: &mut Frame, area: Rect, ui: &mut PreviewUI) {
680 let widget = ui.make_preview();
688 frame.render_widget(widget, area);
689}
690
691fn render_results<T: SSS, S: Selection>(
692 frame: &mut Frame,
693 mut area: Rect,
694 ui: &mut PickerUI<T, S>,
695 click: &mut Click,
696) {
697 let cap = matches!(
698 ui.results.config.row_connection_style,
699 RowConnectionStyle::Capped
700 );
701 let (widget, table_width) = ui.make_table(click);
702
703 if cap {
704 area.width = area.width.min(table_width);
705 }
706
707 frame.render_widget(widget, area);
708}
709
710fn render_input(frame: &mut Frame, area: Rect, ui: &mut InputUI) {
711 ui.scroll_to_cursor();
712 let widget = ui.make_input();
713 if let CursorSetting::Default = ui.config.cursor {
714 frame.set_cursor_position(ui.cursor_offset(&area))
715 };
716
717 frame.render_widget(widget, area);
718}
719
720fn render_status(frame: &mut Frame, area: Rect, ui: &ResultsUI) {
721 if ui.config.status_show {
722 let widget = ui.make_status();
723 frame.render_widget(widget, area);
724 }
725}
726
727fn render_display(frame: &mut Frame, area: Rect, ui: &mut DisplayUI, results_ui: &ResultsUI) {
728 if !ui.show {
729 return;
730 }
731 let widget = ui.make_display(
732 results_ui.indentation() as u16,
733 results_ui.widths().to_vec(),
734 results_ui.config.column_spacing.0,
735 );
736
737 frame.render_widget(widget, area);
738
739 if ui.single() {
740 let widget = ui.make_full_width_row(results_ui.indentation() as u16);
741 frame.render_widget(widget, area);
742 }
743}
744
745fn render_ui(frame: &mut Frame, area: &mut Rect, ui: &UI) {
746 let widget = ui.make_ui();
747 frame.render_widget(widget, *area);
748 *area = ui.inner_area(area);
749}
750
751fn split(rect: &mut Rect, height: u16, cut_top: bool) -> Rect {
752 let h = height.min(rect.height);
753
754 if cut_top {
755 let offshoot = Rect {
756 x: rect.x,
757 y: rect.y,
758 width: rect.width,
759 height: h,
760 };
761
762 rect.y += h;
763 rect.height -= h;
764
765 offshoot
766 } else {
767 let offshoot = Rect {
768 x: rect.x,
769 y: rect.y + rect.height - h,
770 width: rect.width,
771 height: h,
772 };
773
774 rect.height -= h;
775
776 offshoot
777 }
778}
779
780#[cfg(test)]
783mod test {}
784
785