1mod dynamic;
2mod state;
3mod state_effects;
4
5pub use dynamic::*;
6pub use state::*;
7pub use state_effects::*;
8use std::io::Write;
11
12use anyhow::Result;
13use log::{info, warn};
14use ratatui::Frame;
15use ratatui::layout::Rect;
16use tokio::sync::mpsc;
17
18#[cfg(feature = "bracketed-paste")]
19use crate::PasteHandler;
20use crate::action::{Action, ActionAliaser, ActionExt, ActionExtHandler};
21use crate::config::{CursorSetting, ExitConfig};
22use crate::message::{Event, Interrupt, RenderCommand};
23use crate::tui::Tui;
24use crate::ui::{DisplayUI, InputUI, OverlayUI, PickerUI, PreviewUI, ResultsUI, UI};
25use crate::{MatchError, SSS, Selection};
26
27fn apply_aliases<T: SSS, S: Selection, A: ActionExt>(
29 buffer: &mut Vec<RenderCommand<A>>,
30 aliaser: ActionAliaser<T, S, A>,
31 state: &MMState<'_, T, S>,
32) {
33 let mut out = Vec::new();
34
35 for cmd in buffer.drain(..) {
36 match cmd {
37 RenderCommand::Action(a) => {
38 out.extend(aliaser(a, state).0.into_iter().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 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<S> = State::new();
68
69 if let Some(ref preview_ui) = preview_ui
71 && !preview_ui.command().is_empty()
72 {
73 state.update_preview(preview_ui.command());
74 }
75
76 while render_rx.recv_many(&mut buffer, 256).await > 0 {
77 let mut did_pause = false;
78 let mut did_exit = false;
79 let mut did_resize = false;
80
81 let mut effects = Effects::new();
82 if let Some(aliaser) = ext_aliaser {
84 let state = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
85 apply_aliases(&mut buffer, aliaser, &state)
86 };
88
89 for event in buffer.drain(..) {
91 let mut interrupt = Interrupt::None;
92
93 if !matches!(event, RenderCommand::Tick) {
94 info!("Recieved {event:?}");
95 }
96
97 match event {
98 RenderCommand::Action(Action::Input(c)) => {
99 if let Some(x) = overlay_ui.as_mut()
101 && x.handle_input(c)
102 {
103 continue;
104 }
105 picker_ui
106 .input
107 .input
108 .insert(picker_ui.input.cursor as usize, c);
109 picker_ui.input.cursor += 1;
110 }
111 #[cfg(feature = "bracketed-paste")]
112 RenderCommand::Paste(content) => {
113 if let Some(handler) = paste_handler {
114 let content = {
115 let dispatcher = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
116 handler(content, &dispatcher)
117 };
118 if !content.is_empty() {
119 use unicode_segmentation::UnicodeSegmentation;
120
121 use crate::utils::text::grapheme_index_to_byte_index;
122
123 let byte_idx = grapheme_index_to_byte_index(
124 &picker_ui.input.input,
125 picker_ui.input.cursor,
126 );
127
128 picker_ui.input.input.insert_str(byte_idx, &content);
129 picker_ui.input.cursor += content.graphemes(true).count() as u16;
130 }
131 }
132 }
133 RenderCommand::Resize(area) => {
134 picker_ui.footer.update_width(area.width);
135 picker_ui.header.update_width(area.width);
136 tui.resize(area);
137 ui.area = area;
138 }
139 RenderCommand::Refresh => {
140 tui.redraw();
141 }
142 RenderCommand::Effect(e) => {
143 match e {
144 Effect::Reload => {
145 interrupt = Interrupt::Reload("".into());
148 }
149 _ => {
150 effects.insert(e);
151 }
152 }
153 }
154 RenderCommand::Action(action) => {
155 if let Some(x) = overlay_ui.as_mut()
156 && x.handle_action(&action)
157 {
158 continue;
159 }
160 let PickerUI {
161 input,
162 results,
163 worker,
164 selections,
165 header,
166 footer,
167 ..
168 } = &mut picker_ui;
169 match action {
171 Action::Select => {
172 if let Some(item) = worker.get_nth(results.index()) {
173 selections.sel(item);
174 }
175 }
176 Action::Deselect => {
177 if let Some(item) = worker.get_nth(results.index()) {
178 selections.desel(item);
179 }
180 }
181 Action::Toggle => {
182 if let Some(item) = worker.get_nth(results.index()) {
183 selections.toggle(item);
184 }
185 }
186 Action::CycleAll => {
187 selections.cycle_all_bg(worker.raw_results());
188 }
189 Action::ClearAll => {
190 selections.clear();
191 }
192 Action::Accept => {
193 let ret = if selections.is_empty() {
194 if let Some(item) = state.current {
195 vec![item.1]
196 } else if exit_config.allow_empty {
197 vec![]
198 } else {
199 continue;
200 }
201 } else {
202 selections.output().collect::<Vec<S>>()
203 };
204 return Ok(ret);
205 }
206 Action::Quit(code) => {
207 return Err(MatchError::Abort(code.0));
208 }
209
210 Action::SetHeader(context) => {
212 if let Some(s) = context {
213 header.set(s);
214 } else {
215 todo!()
216 }
217 }
218 Action::SetFooter(context) => {
219 if let Some(s) = context {
220 footer.set(s);
221 } else {
222 todo!()
223 }
224 }
225 Action::CyclePreview => {
227 if let Some(p) = preview_ui.as_mut() {
228 p.cycle_layout();
229 if !p.command().is_empty() {
230 state.update_preview(p.command());
231 }
232 }
233 }
234 Action::Preview(context) => {
235 if let Some(p) = preview_ui.as_mut() {
236 if !state.update_preview(context.as_str()) {
237 p.toggle_show()
238 } else {
239 p.show::<true>();
240 }
241 };
242 }
243 Action::Help(context) => {
244 if let Some(p) = preview_ui.as_mut() {
245 if !state.update_preview_set(context) {
247 state.update_preview_unset()
248 } else {
249 p.show::<true>();
250 }
251 };
252 }
253 Action::SwitchPreview(idx) => {
254 if let Some(p) = preview_ui.as_mut() {
255 if let Some(idx) = idx {
256 if !p.set_idx(idx) && !state.update_preview(p.command()) {
257 p.toggle_show();
258 }
259 } else {
260 p.toggle_show()
261 }
262 }
263 }
264 Action::SetPreview(idx) => {
265 if let Some(p) = preview_ui.as_mut() {
266 if let Some(idx) = idx {
267 p.set_idx(idx);
268 } else {
269 state.update_preview(p.command());
270 }
271 }
272 }
273 Action::ToggleWrap => {
274 results.wrap(!results.is_wrap());
275 }
276 Action::ToggleWrapPreview => {
277 if let Some(p) = preview_ui.as_mut() {
278 p.wrap(!p.is_wrap());
279 }
280 }
281
282 Action::Execute(context) => {
284 interrupt = Interrupt::Execute(context);
285 }
286 Action::Become(context) => {
287 interrupt = Interrupt::Become(context);
288 }
289 Action::Reload(context) => {
290 interrupt = Interrupt::Reload(context);
291 }
292 Action::Print(context) => {
293 interrupt = Interrupt::Print(context);
294 }
295
296 Action::SetInput(context) => {
297 input.set(context, u16::MAX);
298 }
299 Action::Column(context) => {
300 results.toggle_col(context);
301 }
302 Action::CycleColumn => {
303 results.cycle_col();
304 }
305 Action::ForwardChar => input.forward_char(),
307 Action::BackwardChar => input.backward_char(),
308 Action::ForwardWord => input.forward_word(),
309 Action::BackwardWord => input.backward_word(),
310 Action::DeleteChar => input.delete(),
311 Action::DeleteWord => input.delete_word(),
312 Action::DeleteLineStart => input.delete_line_start(),
313 Action::DeleteLineEnd => input.delete_line_end(),
314 Action::Cancel => input.cancel(),
315
316 Action::Up(x) | Action::Down(x) => {
318 let next = matches!(action, Action::Down(_)) ^ results.reverse();
319 for _ in 0..x.into() {
320 if next {
321 results.cursor_next();
322 } else {
323 results.cursor_prev();
324 }
325 }
326 }
327 Action::PreviewUp(n) => {
328 if let Some(p) = preview_ui.as_mut() {
329 p.up(n.into())
330 }
331 }
332 Action::PreviewDown(n) => {
333 if let Some(p) = preview_ui.as_mut() {
334 p.down(n.into())
335 }
336 }
337 Action::PreviewHalfPageUp => todo!(),
338 Action::PreviewHalfPageDown => todo!(),
339 Action::Pos(pos) => {
340 let pos = if pos >= 0 {
341 pos as u32
342 } else {
343 results.status.matched_count.saturating_sub((-pos) as u32)
344 };
345 results.cursor_jump(pos);
346 }
347 Action::InputPos(pos) => {
348 let pos = if pos >= 0 {
349 pos as u16
350 } else {
351 (input.len() as u16).saturating_sub((-pos) as u16)
352 };
353 input.cursor = pos;
354 }
355
356 Action::Redraw => {
358 tui.redraw();
359 }
360 Action::Overlay(index) => {
361 if let Some(x) = overlay_ui.as_mut() {
362 x.enable(index, &ui.area);
363 tui.redraw();
364 };
365 }
366 Action::Custom(e) => {
367 if let Some(handler) = ext_handler {
368 let dispatcher =
369 state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
370 let effects = handler(e, &dispatcher);
371 state.apply_effects(
372 effects,
373 &mut ui,
374 &mut picker_ui,
375 &mut preview_ui,
376 );
377 }
378 }
379 _ => {}
380 }
381 }
382 _ => {}
383 }
384
385 match interrupt {
386 Interrupt::None => continue,
387 Interrupt::Execute(_) => {
388 if controller_tx.send(Event::Pause).is_err() {
389 break;
390 }
391 did_exit = true;
392 tui.enter_execute();
393 did_pause = true;
394 }
395 Interrupt::Reload(_) => {
396 picker_ui.worker.restart(false);
397 }
398 Interrupt::Become(_) => {
399 tui.exit();
400 }
401 _ => {}
402 }
403
404 state.update_current(&picker_ui);
405 {
407 let mut effects = Effects::new();
408 let mut dispatcher = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
409 for h in dynamic_handlers.1.get(&interrupt) {
410 effects.append(h(&mut dispatcher, &interrupt))
411 }
412
413 if let Interrupt::Become(context) = interrupt {
414 return Err(MatchError::Become(context));
415 }
416 state.apply_effects(effects, &mut ui, &mut picker_ui, &mut preview_ui);
417 }
418 }
419
420 picker_ui.update();
424 if exit_config.select_1 && picker_ui.results.status.matched_count == 1 {
426 return Ok(state.take_current().into_iter().collect());
427 }
428
429 if did_exit {
431 tui.return_execute()
432 .map_err(|e| MatchError::TUIError(e.to_string()))?;
433 tui.redraw();
434 }
435
436 let mut overlay_ui_ref = overlay_ui.as_mut();
437 tui.terminal
438 .draw(|frame| {
439 let mut area = frame.area();
440
441 render_ui(frame, &mut area, &ui);
442
443 let [preview, picker_area] = if let Some(preview_ui) = preview_ui.as_mut()
444 && let Some(layout) = preview_ui.layout()
445 {
446 let ret = layout.split(area);
447 if state.iterations == 0 && ret[1].width <= 5 {
448 warn!("UI too narrow, hiding preview");
449 preview_ui.show::<false>();
450 [Rect::default(), area]
451 } else {
452 ret
453 }
454 } else {
455 [Rect::default(), area]
456 };
457
458 let [input, status, header, results, footer] = picker_ui.layout(picker_area);
459
460 did_resize = state.update_layout([preview, input, status, results]);
462
463 if did_resize {
464 picker_ui.results.update_dimensions(&results);
465 ui.update_dimensions(area);
467 if let Some(x) = overlay_ui_ref.as_deref_mut() {
468 x.update_dimensions(&area);
469 }
470 };
471
472 render_input(frame, input, &picker_ui.input);
473 render_status(frame, status, &picker_ui.results);
474 render_results(frame, results, &mut picker_ui);
475 render_display(
476 frame,
477 header,
478 &picker_ui.header,
479 picker_ui.results.indentation(),
480 );
481 render_display(
482 frame,
483 footer,
484 &picker_ui.footer,
485 picker_ui.results.indentation(),
486 );
487 if let Some(preview_ui) = preview_ui.as_mut() {
488 state.update_preview_ui(preview_ui);
489 if did_resize {
490 preview_ui.update_dimensions(&preview);
491 }
492 render_preview(frame, preview, preview_ui);
493 }
494 if let Some(x) = overlay_ui_ref {
495 x.draw(frame);
496 }
497 })
498 .map_err(|e| MatchError::TUIError(e.to_string()))?;
499
500 if did_resize && tui.config.redraw_on_resize && !did_exit {
502 tui.redraw();
503 }
504 buffer.clear();
505
506 state.update(&picker_ui, &overlay_ui);
509 let events = state.events();
510
511 let mut dispatcher = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
513 for e in events.iter() {
523 for h in dynamic_handlers.0.get(e) {
524 effects.append(h(&mut dispatcher, e))
525 }
526 }
527
528 state.apply_effects(effects, &mut ui, &mut picker_ui, &mut preview_ui);
530
531 for e in events {
534 controller_tx
535 .send(e)
536 .unwrap_or_else(|err| eprintln!("send failed: {:?}", err));
537 }
538 if did_pause {
541 log::debug!("Waiting for ack response to pause");
542 if controller_tx.send(Event::Resume).is_err() {
543 break;
544 };
545 while let Some(msg) = render_rx.recv().await {
547 if matches!(msg, RenderCommand::Ack) {
548 log::debug!("Recieved ack response to pause");
549 break;
550 }
551 }
552 }
553 }
554
555 Err(MatchError::EventLoopClosed)
556}
557
558fn render_preview(frame: &mut Frame, area: Rect, ui: &mut PreviewUI) {
560 let widget = ui.make_preview();
568 frame.render_widget(widget, area);
569}
570
571fn render_results<T: SSS, S: Selection>(frame: &mut Frame, area: Rect, ui: &mut PickerUI<T, S>) {
572 let widget = ui.make_table();
573
574 frame.render_widget(widget, area);
575}
576
577fn render_input(frame: &mut Frame, area: Rect, ui: &InputUI) {
578 let widget = ui.make_input();
579 if let CursorSetting::Default = ui.config.cursor {
580 frame.set_cursor_position(ui.cursor_offset(&area))
581 };
582
583 frame.render_widget(widget, area);
584}
585
586fn render_status(frame: &mut Frame, area: Rect, ui: &ResultsUI) {
587 let widget = ui.make_status();
588
589 frame.render_widget(widget, area);
590}
591
592fn render_display(frame: &mut Frame, area: Rect, ui: &DisplayUI, result_indentation: usize) {
593 let widget = ui.make_display(result_indentation);
594
595 frame.render_widget(widget, area);
596}
597
598fn render_ui(frame: &mut Frame, area: &mut Rect, ui: &UI) {
599 let widget = ui.make_ui();
600 frame.render_widget(widget, *area);
601 *area = ui.inner_area(area);
602}
603
604#[cfg(test)]
607mod test {}
608
609