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::HeaderTable(columns) => {
153 picker_ui.header.header_table(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::CycleSort => {
268 #[cfg(feature = "experimental")]
269 {
270 let threshold = match picker_ui.worker.get_stability() {
271 0 => 6,
272 u32::MAX => 0,
273 _ => u32::MAX,
274 };
275 picker_ui.worker.set_stability(threshold);
276 }
277 }
278 Action::SetHeader(context) => {
279 if let Some(s) = context {
280 header.set(s);
281 } else {
282 header.clear(true);
283 }
284 }
285 Action::SetFooter(context) => {
286 if let Some(s) = context {
287 footer_ui.set(s);
288 } else {
289 footer_ui.clear(false);
290 }
291 }
292 Action::CyclePreview => {
294 if let Some(p) = preview_ui.as_mut() {
295 p.cycle_layout();
296 if !p.command().is_empty() {
297 state.update_preview(p.command());
298 }
299 }
300 }
301
302 Action::PreviewHScroll(x) | Action::PreviewScroll(x) => {
303 if let Some(p) = preview_ui.as_mut() {
304 p.scroll(matches!(action, Action::PreviewHScroll(_)), x);
305 }
306 }
307 Action::Preview(context) => {
308 if let Some(p) = preview_ui.as_mut() {
309 if !state.update_preview(context.as_str()) {
310 p.toggle_show()
311 } else {
312 p.show(true);
313 }
314 };
315 }
316 Action::Help(context) => {
317 if let Some(p) = preview_ui.as_mut() {
318 if !state.update_preview_set(context) {
320 state.update_preview_unset()
321 } else {
322 p.show(true);
323 }
324 };
325 }
326 Action::SwitchPreview(idx) => {
327 if let Some(p) = preview_ui.as_mut() {
328 if let Some(idx) = idx {
329 if !p.set_layout(idx) && !state.update_preview(p.command()) {
330 p.toggle_show();
331 }
332 } else {
333 p.toggle_show()
334 }
335 }
336 }
337 Action::SetPreview(idx) => {
338 if let Some(p) = preview_ui.as_mut() {
339 if let Some(idx) = idx {
340 p.set_layout(idx);
341 } else {
342 state.update_preview(p.command());
343 }
344 }
345 }
346 Action::ToggleWrap => {
347 results.wrap(!results.is_wrap());
348 }
349 Action::ToggleWrapPreview => {
350 if let Some(p) = preview_ui.as_mut() {
351 p.wrap(!p.is_wrap());
352 }
353 }
354
355 Action::Execute(payload) => {
357 state.set_interrupt(Interrupt::Execute, payload);
358 }
359 Action::Become(payload) => {
360 state.set_interrupt(Interrupt::Become, payload);
361 }
362 Action::Reload(payload) => {
363 state.set_interrupt(Interrupt::Reload, payload);
364 }
365 Action::Print(payload) => {
366 state.set_interrupt(Interrupt::Print, payload);
367 }
368
369 Action::SetInput(context) => {
370 input.set(context, u16::MAX);
371 }
372 Action::Column(context) => {
373 results.toggle_col(context);
374 }
375 Action::CycleColumn => {
376 results.cycle_col();
377 }
378 Action::ForwardChar => input.forward_char(),
380 Action::BackwardChar => input.backward_char(),
381 Action::ForwardWord => input.forward_word(),
382 Action::BackwardWord => input.backward_word(),
383 Action::DeleteChar => input.delete(),
384 Action::DeleteWord => input.delete_word(),
385 Action::DeleteLineStart => input.delete_line_start(),
386 Action::DeleteLineEnd => input.delete_line_end(),
387 Action::Cancel => input.cancel(),
388
389 Action::Up(x) | Action::Down(x) => {
391 let next = matches!(action, Action::Down(_)) ^ results.reverse();
392 for _ in 0..x.into() {
393 if next {
394 results.cursor_next();
395 } else {
396 results.cursor_prev();
397 }
398 }
399 }
400 Action::PreviewUp(n) => {
401 if let Some(p) = preview_ui.as_mut() {
402 p.up(n)
403 }
404 }
405 Action::PreviewDown(n) => {
406 if let Some(p) = preview_ui.as_mut() {
407 p.down(n)
408 }
409 }
410 Action::PreviewHalfPageUp => todo!(),
411 Action::PreviewHalfPageDown => todo!(),
412 Action::Pos(pos) => {
413 let pos = if pos >= 0 {
414 pos as u32
415 } else {
416 results.status.matched_count.saturating_sub((-pos) as u32)
417 };
418 results.cursor_jump(pos);
419 }
420 Action::InputPos(pos) => {
421 let pos = if pos >= 0 {
422 pos as u16
423 } else {
424 (input.len() as u16).saturating_sub((-pos) as u16)
425 };
426 input.set(None, pos);
427 }
428
429 Action::Redraw => {
431 tui.redraw();
432 }
433 Action::Overlay(index) => {
434 if let Some(x) = overlay_ui.as_mut() {
435 x.enable(index, &ui.area);
436 tui.redraw();
437 };
438 }
439 Action::Custom(e) => {
440 if let Some(handler) = ext_handler {
441 handler(
442 e,
443 &mut state.dispatcher(
444 &mut ui,
445 &mut picker_ui,
446 &mut footer_ui,
447 &mut preview_ui,
448 ),
449 );
450 }
451 }
452 _ => {}
453 }
454 }
455 _ => {}
456 }
457
458 let interrupt = state.interrupt();
459
460 match interrupt {
461 Interrupt::None => continue,
462 Interrupt::Execute => {
463 if controller_tx.send(Event::Pause).is_err() {
464 break;
465 }
466 tui.enter_execute();
467 did_exit = true;
468 did_pause = true;
469 }
470 Interrupt::Reload => {
471 picker_ui.worker.restart(false);
472 }
473 Interrupt::Become => {
474 tui.exit();
475 }
476 _ => {}
477 }
478 {
480 let mut dispatcher =
481 state.dispatcher(&mut ui, &mut picker_ui, &mut footer_ui, &mut preview_ui);
482 for h in dynamic_handlers.1.get(interrupt) {
483 h(&mut dispatcher);
484 }
485
486 if matches!(interrupt, Interrupt::Become) {
487 return Err(MatchError::Become(state.payload().clone()));
488 }
489 }
490
491 if state.should_quit {
492 log::debug!("Exiting due to should_quit");
493 let ret = picker_ui.selector.output().collect::<Vec<S>>();
494 return if picker_ui.selector.is_disabled()
495 && let Some((_, item)) = get_current(&picker_ui)
496 {
497 Ok(vec![item])
498 } else if ret.is_empty() {
499 Err(MatchError::Abort(0))
500 } else {
501 Ok(ret)
502 };
503 } else if state.should_quit_nomatch {
504 log::debug!("Exiting due to should_quit_nomatch");
505 return Err(MatchError::NoMatch);
506 }
507 }
508
509 picker_ui.update();
513 if exit_config.select_1
515 && picker_ui.results.status.matched_count == 1
516 && let Some((_, item)) = get_current(&picker_ui)
517 {
518 return Ok(vec![item]);
519 }
520
521 if did_exit {
523 tui.return_execute()
524 .map_err(|e| MatchError::TUIError(e.to_string()))?;
525 tui.redraw();
526 }
527
528 let mut overlay_ui_ref = overlay_ui.as_mut();
529 tui.terminal
530 .draw(|frame| {
531 let mut area = frame.area();
532
533 render_ui(frame, &mut area, &ui);
534
535 let mut _area = area;
536
537 let full_width_footer = footer_ui.single()
538 && footer_ui.config.row_connection_style == RowConnectionStyle::Full;
539
540 let mut footer =
541 if full_width_footer || preview_ui.as_ref().is_none_or(|p| !p.is_show()) {
542 split(&mut _area, footer_ui.height(), picker_ui.reverse())
543 } else {
544 Rect::default()
545 };
546
547 let [preview, picker_area, footer] = if let Some(preview_ui) = preview_ui.as_mut()
548 && let Some(layout) = preview_ui.layout()
549 {
550 let [preview, mut picker_area] = layout.split(_area);
551
552 if state.iterations == 0 && picker_area.width <= 5 {
553 warn!("UI too narrow, hiding preview");
554 preview_ui.show(false);
555
556 [Rect::default(), _area, footer]
557 } else {
558 if !full_width_footer {
559 footer =
560 split(&mut picker_area, footer_ui.height(), picker_ui.reverse());
561 }
562
563 [preview, picker_area, footer]
564 }
565 } else {
566 [Rect::default(), _area, footer]
567 };
568
569 let [input, status, header, results] = picker_ui.layout(picker_area);
570
571 did_resize = state.update_layout([preview, input, status, results]);
573
574 if did_resize {
575 picker_ui.results.update_dimensions(&results);
576 picker_ui.input.update_width(input.width);
577 footer_ui.update_width(
578 if footer_ui.config.row_connection_style == RowConnectionStyle::Capped {
579 area.width
580 } else {
581 footer.width
582 },
583 );
584 picker_ui.header.update_width(header.width);
585 ui.update_dimensions(area);
587 if let Some(x) = overlay_ui_ref.as_deref_mut() {
588 x.update_dimensions(&area);
589 }
590 };
591
592 render_input(frame, input, &mut picker_ui.input);
593 render_status(frame, status, &picker_ui.results);
594 render_results(frame, results, &mut picker_ui, &mut click);
595 render_display(frame, header, &mut picker_ui.header, &picker_ui.results);
596 render_display(frame, footer, &mut footer_ui, &picker_ui.results);
597 if let Some(preview_ui) = preview_ui.as_mut() {
598 state.update_preview_ui(preview_ui);
599 if did_resize {
600 preview_ui.update_dimensions(&preview);
601 }
602 render_preview(frame, preview, preview_ui);
603 }
604 if let Some(x) = overlay_ui_ref {
605 x.draw(frame);
606 }
607 })
608 .map_err(|e| MatchError::TUIError(e.to_string()))?;
609
610 if did_resize && tui.config.redraw_on_resize && !did_exit {
612 tui.redraw();
613 }
614 buffer.clear();
615
616 state.update(&picker_ui, &overlay_ui);
619 let events = state.events();
620
621 let mut dispatcher =
623 state.dispatcher(&mut ui, &mut picker_ui, &mut footer_ui, &mut preview_ui);
624 for e in events.iter() {
634 for h in dynamic_handlers.0.get(e) {
635 h(&mut dispatcher, &e)
636 }
637 }
638
639 for e in events.iter() {
642 controller_tx
643 .send(e)
644 .unwrap_or_else(|err| eprintln!("send failed: {:?}", err));
645 }
646 if did_pause {
649 log::debug!("Waiting for ack response to pause");
650 if controller_tx.send(Event::Resume).is_err() {
651 break;
652 };
653 while let Some(msg) = render_rx.recv().await {
655 if matches!(msg, RenderCommand::Ack) {
656 log::debug!("Recieved ack response to pause");
657 break;
658 }
659 }
660 }
661
662 click.process(&mut buffer);
663 }
664
665 Err(MatchError::EventLoopClosed)
666}
667
668pub enum Click {
671 None,
672 ResultPos(u16),
673 ResultIdx(u32),
674}
675
676impl Click {
677 fn process<A: ActionExt>(&mut self, buffer: &mut Vec<RenderCommand<A>>) {
678 match self {
679 Self::ResultIdx(u) => {
680 buffer.push(RenderCommand::Action(Action::Pos(*u as i32)));
681 }
682 _ => {
683 }
685 }
686 *self = Click::None
687 }
688}
689
690fn render_preview(frame: &mut Frame, area: Rect, ui: &mut PreviewUI) {
691 let widget = ui.make_preview();
699 frame.render_widget(widget, area);
700}
701
702fn render_results<T: SSS, S: Selection>(
703 frame: &mut Frame,
704 mut area: Rect,
705 ui: &mut PickerUI<T, S>,
706 click: &mut Click,
707) {
708 let cap = matches!(
709 ui.results.config.row_connection_style,
710 RowConnectionStyle::Capped
711 );
712 let (widget, table_width) = ui.make_table(click);
713
714 if cap {
715 area.width = area.width.min(table_width);
716 }
717
718 frame.render_widget(widget, area);
719}
720
721fn render_input(frame: &mut Frame, area: Rect, ui: &mut InputUI) {
722 ui.scroll_to_cursor();
723 let widget = ui.make_input();
724 if let CursorSetting::Default = ui.config.cursor {
725 frame.set_cursor_position(ui.cursor_offset(&area))
726 };
727
728 frame.render_widget(widget, area);
729}
730
731fn render_status(frame: &mut Frame, area: Rect, ui: &ResultsUI) {
732 if ui.status_config.show {
733 let widget = ui.make_status();
734 frame.render_widget(widget, area);
735 }
736}
737
738fn render_display(frame: &mut Frame, area: Rect, ui: &mut DisplayUI, results_ui: &ResultsUI) {
739 if !ui.show {
740 return;
741 }
742 let widget = ui.make_display(
743 results_ui.indentation() as u16,
744 results_ui.widths().to_vec(),
745 results_ui.config.column_spacing.0,
746 );
747
748 frame.render_widget(widget, area);
749
750 if ui.single() {
751 let widget = ui.make_full_width_row(results_ui.indentation() as u16);
752 frame.render_widget(widget, area);
753 }
754}
755
756fn render_ui(frame: &mut Frame, area: &mut Rect, ui: &UI) {
757 let widget = ui.make_ui();
758 frame.render_widget(widget, *area);
759 *area = ui.inner_area(area);
760}
761
762fn split(rect: &mut Rect, height: u16, cut_top: bool) -> Rect {
763 let h = height.min(rect.height);
764
765 if cut_top {
766 let offshoot = Rect {
767 x: rect.x,
768 y: rect.y,
769 width: rect.width,
770 height: h,
771 };
772
773 rect.y += h;
774 rect.height -= h;
775
776 offshoot
777 } else {
778 let offshoot = Rect {
779 x: rect.x,
780 y: rect.y + rect.height - h,
781 width: rect.width,
782 height: h,
783 };
784
785 rect.height -= h;
786
787 offshoot
788 }
789}
790
791#[cfg(test)]
794mod test {}
795
796