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