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