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 #[allow(warnings)]
144 match e {
145 Effect::Reload => {
146 interrupt = Interrupt::Reload("".into());
149 }
150 _ => {
151 effects.insert(e);
152 }
153 }
154 }
155 RenderCommand::Action(action) => {
156 if let Some(x) = overlay_ui.as_mut()
157 && x.handle_action(&action)
158 {
159 continue;
160 }
161 let PickerUI {
162 input,
163 results,
164 worker,
165 selections,
166 header,
167 footer,
168 ..
169 } = &mut picker_ui;
170 match action {
172 Action::Select => {
173 if let Some(item) = worker.get_nth(results.index()) {
174 selections.sel(item);
175 }
176 }
177 Action::Deselect => {
178 if let Some(item) = worker.get_nth(results.index()) {
179 selections.desel(item);
180 }
181 }
182 Action::Toggle => {
183 if let Some(item) = worker.get_nth(results.index()) {
184 selections.toggle(item);
185 }
186 }
187 Action::CycleAll => {
188 selections.cycle_all_bg(worker.raw_results());
189 }
190 Action::ClearAll => {
191 selections.clear();
192 }
193 Action::Accept => {
194 if selections.is_empty() {
195 if let Some(item) = worker.get_nth(results.index()) {
196 selections.sel(item);
197 } else if !exit_config.allow_empty {
198 continue;
199 }
200 }
201 return Ok(selections.output().collect::<Vec<S>>());
202 }
203 Action::Quit(code) => {
204 return Err(MatchError::Abort(code.0));
205 }
206
207 Action::SetHeader(context) => {
209 if let Some(s) = context {
210 header.set(s);
211 } else {
212 todo!()
213 }
214 }
215 Action::SetFooter(context) => {
216 if let Some(s) = context {
217 footer.set(s);
218 } else {
219 todo!()
220 }
221 }
222 Action::CyclePreview => {
224 if let Some(p) = preview_ui.as_mut() {
225 p.cycle_layout();
226 if !p.command().is_empty() {
227 state.update_preview(p.command());
228 }
229 }
230 }
231 Action::Preview(context) => {
232 if let Some(p) = preview_ui.as_mut() {
233 if !state.update_preview(context.as_str()) {
234 p.toggle_show()
235 } else {
236 p.show::<true>();
237 }
238 };
239 }
240 Action::Help(context) => {
241 if let Some(p) = preview_ui.as_mut() {
242 if !state.update_preview_set(context) {
244 state.update_preview_unset()
245 } else {
246 p.show::<true>();
247 }
248 };
249 }
250 Action::SwitchPreview(idx) => {
251 if let Some(p) = preview_ui.as_mut() {
252 if let Some(idx) = idx {
253 if !p.set_idx(idx) && !state.update_preview(p.command()) {
254 p.toggle_show();
255 }
256 } else {
257 p.toggle_show()
258 }
259 }
260 }
261 Action::SetPreview(idx) => {
262 if let Some(p) = preview_ui.as_mut() {
263 if let Some(idx) = idx {
264 p.set_idx(idx);
265 } else {
266 state.update_preview(p.command());
267 }
268 }
269 }
270 Action::ToggleWrap => {
271 results.wrap(!results.is_wrap());
272 }
273 Action::ToggleWrapPreview => {
274 if let Some(p) = preview_ui.as_mut() {
275 p.wrap(!p.is_wrap());
276 }
277 }
278
279 Action::Execute(context) => {
281 interrupt = Interrupt::Execute(context);
282 }
283 Action::Become(context) => {
284 interrupt = Interrupt::Become(context);
285 }
286 Action::Reload(context) => {
287 interrupt = Interrupt::Reload(context);
288 }
289 Action::Print(context) => {
290 interrupt = Interrupt::Print(context);
291 }
292
293 Action::SetInput(context) => {
294 input.set(context, u16::MAX);
295 }
296 Action::Column(context) => {
297 results.toggle_col(context);
298 }
299 Action::CycleColumn => {
300 results.cycle_col();
301 }
302 Action::ForwardChar => input.forward_char(),
304 Action::BackwardChar => input.backward_char(),
305 Action::ForwardWord => input.forward_word(),
306 Action::BackwardWord => input.backward_word(),
307 Action::DeleteChar => input.delete(),
308 Action::DeleteWord => input.delete_word(),
309 Action::DeleteLineStart => input.delete_line_start(),
310 Action::DeleteLineEnd => input.delete_line_end(),
311 Action::Cancel => input.cancel(),
312
313 Action::Up(x) | Action::Down(x) => {
315 let next = matches!(action, Action::Down(_)) ^ results.reverse();
316 for _ in 0..x.into() {
317 if next {
318 results.cursor_next();
319 } else {
320 results.cursor_prev();
321 }
322 }
323 }
324 Action::PreviewUp(n) => {
325 if let Some(p) = preview_ui.as_mut() {
326 p.up(n.into())
327 }
328 }
329 Action::PreviewDown(n) => {
330 if let Some(p) = preview_ui.as_mut() {
331 p.down(n.into())
332 }
333 }
334 Action::PreviewHalfPageUp => todo!(),
335 Action::PreviewHalfPageDown => todo!(),
336 Action::Pos(pos) => {
337 let pos = if pos >= 0 {
338 pos as u32
339 } else {
340 results.status.matched_count.saturating_sub((-pos) as u32)
341 };
342 results.cursor_jump(pos);
343 }
344 Action::InputPos(pos) => {
345 let pos = if pos >= 0 {
346 pos as u16
347 } else {
348 (input.len() as u16).saturating_sub((-pos) as u16)
349 };
350 input.cursor = pos;
351 }
352
353 Action::Redraw => {
355 tui.redraw();
356 }
357 Action::Overlay(index) => {
358 if let Some(x) = overlay_ui.as_mut() {
359 x.enable(index, &ui.area);
360 tui.redraw();
361 };
362 }
363 Action::Custom(e) => {
364 if let Some(handler) = ext_handler {
365 let dispatcher =
366 state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
367 let effects = handler(e, &dispatcher);
368 state.apply_effects(
369 effects,
370 &mut ui,
371 &mut picker_ui,
372 &mut preview_ui,
373 );
374 }
375 }
376 _ => {}
377 }
378 }
379 _ => {}
380 }
381
382 match interrupt {
383 Interrupt::None => continue,
384 Interrupt::Execute(_) => {
385 if controller_tx.send(Event::Pause).is_err() {
386 break;
387 }
388 did_exit = true;
389 tui.enter_execute();
390 did_pause = true;
391 }
392 Interrupt::Reload(_) => {
393 picker_ui.worker.restart(false);
394 }
395 Interrupt::Become(_) => {
396 tui.exit();
397 }
398 _ => {}
399 }
400
401 state.update_current(&picker_ui);
402 {
404 let mut effects = Effects::new();
405 let mut dispatcher = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
406 for h in dynamic_handlers.1.get(&interrupt) {
407 effects.append(h(&mut dispatcher, &interrupt))
408 }
409
410 if let Interrupt::Become(context) = interrupt {
411 return Err(MatchError::Become(context));
412 }
413 state.apply_effects(effects, &mut ui, &mut picker_ui, &mut preview_ui);
414 }
415 }
416
417 picker_ui.update();
421 if exit_config.select_1 && picker_ui.results.status.matched_count == 1 {
423 return Ok(state.take_current().into_iter().collect());
424 }
425
426 if did_exit {
428 tui.return_execute()
429 .map_err(|e| MatchError::TUIError(e.to_string()))?;
430 tui.redraw();
431 }
432
433 let mut overlay_ui_ref = overlay_ui.as_mut();
434 tui.terminal
435 .draw(|frame| {
436 let mut area = frame.area();
437
438 render_ui(frame, &mut area, &ui);
439
440 let [preview, picker_area] = if let Some(preview_ui) = preview_ui.as_mut()
441 && let Some(layout) = preview_ui.layout()
442 {
443 let ret = layout.split(area);
444 if state.iterations == 0 && ret[1].width <= 5 {
445 warn!("UI too narrow, hiding preview");
446 preview_ui.show::<false>();
447 [Rect::default(), area]
448 } else {
449 ret
450 }
451 } else {
452 [Rect::default(), area]
453 };
454
455 let [input, status, header, results, footer] = picker_ui.layout(picker_area);
456
457 did_resize = state.update_layout([preview, input, status, results]);
459
460 if did_resize {
461 picker_ui.results.update_dimensions(&results);
462 ui.update_dimensions(area);
464 if let Some(x) = overlay_ui_ref.as_deref_mut() {
465 x.update_dimensions(&area);
466 }
467 };
468
469 render_input(frame, input, &picker_ui.input);
470 render_status(frame, status, &picker_ui.results);
471 render_results(frame, results, &mut picker_ui);
472 render_display(
473 frame,
474 header,
475 &picker_ui.header,
476 picker_ui.results.indentation(),
477 );
478 render_display(
479 frame,
480 footer,
481 &picker_ui.footer,
482 picker_ui.results.indentation(),
483 );
484 if let Some(preview_ui) = preview_ui.as_mut() {
485 state.update_preview_ui(preview_ui);
486 if did_resize {
487 preview_ui.update_dimensions(&preview);
488 }
489 render_preview(frame, preview, preview_ui);
490 }
491 if let Some(x) = overlay_ui_ref {
492 x.draw(frame);
493 }
494 })
495 .map_err(|e| MatchError::TUIError(e.to_string()))?;
496
497 if did_resize && tui.config.redraw_on_resize && !did_exit {
499 tui.redraw();
500 }
501 buffer.clear();
502
503 state.update(&picker_ui, &overlay_ui);
506 let events = state.events();
507
508 let mut dispatcher = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
510 for e in events.iter() {
520 for h in dynamic_handlers.0.get(e) {
521 effects.append(h(&mut dispatcher, e))
522 }
523 }
524
525 state.apply_effects(effects, &mut ui, &mut picker_ui, &mut preview_ui);
527
528 for e in events {
531 controller_tx
532 .send(e)
533 .unwrap_or_else(|err| eprintln!("send failed: {:?}", err));
534 }
535 if did_pause {
538 log::debug!("Waiting for ack response to pause");
539 if controller_tx.send(Event::Resume).is_err() {
540 break;
541 };
542 while let Some(msg) = render_rx.recv().await {
544 if matches!(msg, RenderCommand::Ack) {
545 log::debug!("Recieved ack response to pause");
546 break;
547 }
548 }
549 }
550 }
551
552 Err(MatchError::EventLoopClosed)
553}
554
555fn render_preview(frame: &mut Frame, area: Rect, ui: &mut PreviewUI) {
557 let widget = ui.make_preview();
565 frame.render_widget(widget, area);
566}
567
568fn render_results<T: SSS, S: Selection>(frame: &mut Frame, area: Rect, ui: &mut PickerUI<T, S>) {
569 let widget = ui.make_table();
570
571 frame.render_widget(widget, area);
572}
573
574fn render_input(frame: &mut Frame, area: Rect, ui: &InputUI) {
575 let widget = ui.make_input();
576 if let CursorSetting::Default = ui.config.cursor {
577 frame.set_cursor_position(ui.cursor_offset(&area))
578 };
579
580 frame.render_widget(widget, area);
581}
582
583fn render_status(frame: &mut Frame, area: Rect, ui: &ResultsUI) {
584 let widget = ui.make_status();
585
586 frame.render_widget(widget, area);
587}
588
589fn render_display(frame: &mut Frame, area: Rect, ui: &DisplayUI, result_indentation: usize) {
590 let widget = ui.make_display(result_indentation);
591
592 frame.render_widget(widget, area);
593}
594
595fn render_ui(frame: &mut Frame, area: &mut Rect, ui: &UI) {
596 let widget = ui.make_ui();
597 frame.render_widget(widget, *area);
598 *area = ui.inner_area(area);
599}
600
601#[cfg(test)]
604mod test {}
605
606