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