1mod dynamic;
2mod state;
3
4pub use dynamic::*;
5pub use state::*;
6
7use std::io::Write;
10use std::sync::Arc;
11
12use anyhow::Result;
13use log::{info, warn};
14use ratatui::Frame;
15use ratatui::layout::Rect;
16use tokio::sync::mpsc;
17
18use crate::action::Action;
19use crate::config::{CursorSetting, ExitConfig};
20use crate::message::{Event, Interrupt, RenderCommand};
21use crate::tui::Tui;
22use crate::ui::{DisplayUI, InputUI, PickerUI, PreviewUI, ResultsUI, UI};
23use crate::{MatchError, MMItem, Selection};
24
25#[allow(clippy::too_many_arguments)]
26pub async fn render_loop<'a, W: Write, T: MMItem, S: Selection, C>(
27 mut ui: UI,
28 mut picker_ui: PickerUI<'a, T, S, C>,
29 mut preview_ui: Option<PreviewUI>,
30 mut tui: Tui<W>,
31 mut render_rx: mpsc::UnboundedReceiver<RenderCommand>,
32 controller_tx: mpsc::UnboundedSender<Event>,
33 context: Arc<C>,
34 dynamic_handlers: DynamicHandlers<T, S, C>,
35 exit_config: ExitConfig,
36) -> Result<Vec<S>, MatchError> {
37 let mut buffer = Vec::with_capacity(256);
38 let mut state: State<S, C> = State::new(context);
39 if let Some(ref preview_ui) = preview_ui
40 && !preview_ui.command().is_empty()
41 {
42 state.update_preview(preview_ui.command());
43 }
44
45 while render_rx.recv_many(&mut buffer, 256).await > 0 {
46 let mut did_pause = false;
47 let mut did_exit = false;
48
49 for event in &buffer {
50 let mut interrupt = Interrupt::None;
51
52 let PickerUI {
53 input,
54 results,
55 worker,
56 selections,
57 header,
58 footer,
59 ..
60 } = &mut picker_ui;
61
62 if !matches!(event, RenderCommand::Tick) {
63 info!("Recieved {event:?}");
64 }
65
66 match event {
67 RenderCommand::Input(c) => {
68 input.input.insert(input.cursor as usize, *c);
69 input.cursor += 1;
70 }
71 RenderCommand::Resize(area) => {
72 tui.resize(*area);
73 ui.area = *area;
74 }
75 RenderCommand::Refresh => {
76 tui.redraw();
77 }
78 RenderCommand::Action(action) => {
79 match action {
80 Action::Select => {
81 if let Some(item) = worker.get_nth(results.index()) {
82 selections.sel(item);
83 }
84 }
85 Action::Deselect => {
86 if let Some(item) = worker.get_nth(results.index()) {
87 selections.desel(item);
88 }
89 }
90 Action::Toggle => {
91 if let Some(item) = worker.get_nth(results.index()) {
92 selections.toggle(item);
93 }
94 }
95 Action::CycleAll => {
96 selections.cycle_all_bg(worker.raw_results());
97 }
98 Action::Accept => {
99 if selections.is_empty() {
100 if let Some(item) = worker.get_nth(results.index())
101 {
102 selections.sel(item);
103 } else if !exit_config.allow_empty {
104 continue;
105 }
106 }
107 return Ok(selections.output().collect::<Vec<S>>());
108 }
109 Action::Quit(code) => {
110 return Err(MatchError::Abort(code.into()));
111 }
112
113 Action::SetHeader(context) => {
115 if let Some(s) = context {
116 header.set(s.into());
117 } else {
118 todo!()
119 }
120 }
121 Action::SetFooter(context) => {
122 if let Some(s) = context {
123 footer.set(s.into());
124 } else {
125 todo!()
126 }
127 }
128
129 Action::CyclePreview => {
130 if let Some(p) = preview_ui.as_mut() {
131 p.cycle_layout();
132 if !p.command().is_empty() {
133 state.update_preview(p.command().as_str());
134 }
135 }
136 }
137 Action::Preview(context) => {
138 if let Some(p) = preview_ui.as_mut() {
139 if !state.update_preview(context.as_str()) {
140 p.toggle_show()
141 } else {
142 p.show::<true>();
143 }
144 };
145 }
146 Action::Help(context) => {
147 if let Some(p) = preview_ui.as_mut() {
148 if !state.update_preview_set(context) {
150 state.update_preview_unset()
151 } else {
152 p.show::<true>();
153 }
154 };
155 }
156
157 Action::SwitchPreview(idx) => {
158 if let Some(p) = preview_ui.as_mut() {
159 if let Some(idx) = idx {
160 if !p.set_idx(*idx) && !state.update_preview(p.command()) {
161 p.toggle_show();
162 }
163 } else {
164 p.toggle_show()
165 }
166 }
167 }
168 Action::SetPreview(idx) => {
169 if let Some(p) = preview_ui.as_mut() {
170 if let Some(idx) = idx {
171 p.set_idx(*idx);
172 } else {
173 state.update_preview(p.command());
174 }
175 }
176 }
177 Action::ToggleWrap => {
178 results.wrap(!results.is_wrap());
179 }
180 Action::ToggleWrapPreview => {
181 if let Some(p) = preview_ui.as_mut() {
182 p.wrap(!p.is_wrap());
183 }
184 }
185
186 Action::Execute(context) => {
188 interrupt = Interrupt::Execute(context.into());
189 }
190 Action::Become(context) => {
191 interrupt = Interrupt::Become(context.into());
192 }
193 Action::Reload(context) => {
194 interrupt = Interrupt::Reload(context.into());
195 }
196 Action::Print(context) => {
197 interrupt = Interrupt::Print(context.into());
198 }
199
200 Action::SetInput(context) => {
201 input.set_input(context.into(), u16::MAX);
202 }
203 Action::Column(context) => {
204 results.toggle_col(*context);
205 }
206 Action::CycleColumn => {
207 results.cycle_col();
208 }
209 Action::ForwardChar => input.forward_char(),
211 Action::BackwardChar => input.backward_char(),
212 Action::ForwardWord => input.forward_word(),
213 Action::BackwardWord => input.backward_word(),
214 Action::DeleteChar => input.delete(),
215 Action::DeleteWord => input.delete_word(),
216 Action::DeleteLineStart => input.delete_line_start(),
217 Action::DeleteLineEnd => input.delete_line_end(),
218 Action::Cancel => input.cancel(),
219
220 Action::Up(x) | Action::Down(x) => {
222 let next = matches!(action, Action::Down(_)) ^ results.reverse();
223 for _ in 0..x.into() {
224 if next {
225 results.cursor_next();
226 } else {
227 results.cursor_prev();
228 }
229 }
230 }
231 Action::PreviewUp(n) => {
232 if let Some(p) = preview_ui.as_mut() {
233 p.up(n.into())
234 }
235 }
236 Action::PreviewDown(n) => {
237 if let Some(p) = preview_ui.as_mut() {
238 p.down(n.into())
239 }
240 }
241 Action::PreviewHalfPageUp => todo!(),
242 Action::PreviewHalfPageDown => todo!(),
243 Action::Pos(pos) => {
244 let pos = if *pos >= 0 {
245 *pos as u32
246 } else {
247 results.status.matched_count.saturating_sub((-*pos) as u32)
248 };
249 results.cursor_jump(pos);
250 }
251 Action::InputPos(pos) => {
252 let pos = if *pos >= 0 {
253 *pos as u16
254 } else {
255 (input.len() as u16).saturating_sub((-*pos) as u16)
256 };
257 input.cursor = pos;
258 }
259
260 Action::Redraw => {
262 tui.redraw();
263 }
264 _ => {}
265 }
266 }
267 _ => {}
268 }
269
270 if !matches!(interrupt, Interrupt::None) {
271 match interrupt {
272 Interrupt::Execute(_) => {
273 if controller_tx.send(Event::Pause).is_err() {
274 break
275 }
276 did_exit = true;
277 tui.enter_execute();
278 did_pause = true;
279 while let Some(msg) = render_rx.recv().await {
280 if matches!(msg, RenderCommand::Ack) {
281 break;
282 }
283 }
284 }
285 Interrupt::Reload(_) => {
286 picker_ui.worker.restart(false);
287 }
288 Interrupt::Become(_) => {
289 tui.exit();
290 }
291 _ => {}
292 }
293
294 state.update(&picker_ui);
295 let (dispatcher, mut effects) = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
296
297 for h in dynamic_handlers.1.get(&interrupt) {
298 dispatcher.dispatch(h, &interrupt, &mut effects);
299 }
300
301 if let Interrupt::Become(context) = interrupt {
302 return Err(MatchError::Become(context));
303 }
304 state.process_effects(effects);
305 };
306 }
307
308 picker_ui.update();
312
313 if did_exit {
314 tui.return_execute().map_err(|e| MatchError::TUIError(e.to_string()))?
315 }
316
317 let mut resized = false;
318 tui.terminal
319 .draw(|frame| {
320 let mut area = frame.area();
321
322 render_ui(frame, &mut area, &ui);
323
324 let [preview, picker_area] = if let Some(preview_ui) = preview_ui.as_mut()
325 && preview_ui.is_show()
326 {
327 let ret = preview_ui.layout().split(area);
328 if state.iterations == 0 && ret[1].width <= 5 {
329 warn!("UI too narrow, hiding preview");
330 preview_ui.show::<false>();
331 [Rect::default(), area]
332 } else {
333 ret
334 }
335 } else {
336 [Rect::default(), area]
337 };
338
339
340
341 let [input, status, header, results, footer] = picker_ui.layout(picker_area);
342
343 resized = state.update_layout([preview, input, status, results]);
344
345 if resized {
347 picker_ui.results.update_dimensions(&results);
348 ui.update_dimensions(area);
349 };
350
351 render_input(frame, input, &picker_ui.input);
352 render_status(frame, status, &picker_ui.results);
353 render_results(frame, results, &mut picker_ui);
354 render_display(frame, header, &picker_ui.header, picker_ui.results.indentation());
355 render_display(frame, footer, &picker_ui.footer, picker_ui.results.indentation());
356
357 if let Some(preview_ui) = preview_ui.as_mut() {
358 state.update_preview_ui(preview_ui);
359 if resized {
360 preview_ui.update_dimensions(&preview);
361 }
362 render_preview(frame, preview, preview_ui);
363 }
364 })
365 .map_err(|e| MatchError::TUIError(e.to_string()))?;
366 if resized {
368 tui.redraw();
369 }
370
371 if state.iterations == 0 {
373 state.insert(Event::Start);
374 }
375 state.update(&picker_ui);
376 let events = state.events();
377 let (dispatcher, mut effects) = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
378
379 if exit_config.select_1 && dispatcher.status().matched_count == 1 {
381 return Ok(vec![state.take_current().unwrap()]);
382 }
383
384 for e in events.iter() {
386 for h in dynamic_handlers.0.get(e) {
387 dispatcher.dispatch(h, e, &mut effects);
388 }
389 }
390 for e in events {
392 controller_tx
393 .send(e)
394 .unwrap_or_else(|err| eprintln!("send failed: {:?}", err));
395 }
396 state.process_effects(effects);
398
399 buffer.clear();
400
401 if did_pause {
402 if controller_tx.send(Event::Resume).is_err() {
403 break
404 };
405 while let Some(msg) = render_rx.recv().await {
407 if matches!(msg, RenderCommand::Ack) {
408 break;
409 }
410 }
411 }
412 }
413
414 Err(MatchError::EventLoopClosed)
415}
416
417fn render_preview(frame: &mut Frame, area: Rect, ui: &mut PreviewUI) {
419 let widget = ui.make_preview();
427 frame.render_widget(widget, area);
428}
429
430fn render_results<T: MMItem, S: Selection, C>(
431 frame: &mut Frame,
432 area: Rect,
433 ui: &mut PickerUI<T, S, C>,
434) {
435 let widget = ui.make_table();
436
437 frame.render_widget(widget, area);
438}
439
440fn render_input(frame: &mut Frame, area: Rect, ui: &InputUI) {
441 let widget = ui.make_input();
442 if let CursorSetting::Default = ui.config.cursor {
443 frame.set_cursor_position(ui.cursor_offset(&area))
444 };
445
446 frame.render_widget(widget, area);
447}
448
449fn render_status(frame: &mut Frame, area: Rect, ui: &ResultsUI) {
450 let widget = ui.make_status();
451
452 frame.render_widget(widget, area);
453}
454
455fn render_display(frame: &mut Frame, area: Rect, ui: &DisplayUI, result_indentation: usize) {
456 let widget = ui.make_display(result_indentation);
457
458 frame.render_widget(widget, area);
459}
460
461fn render_ui(frame: &mut Frame, area: &mut Rect, ui: &UI) {
462 let widget = ui.make_ui();
463 frame.render_widget(widget, *area);
464 *area = ui.inner_area(area);
465}
466
467#[cfg(test)]
470mod test {}
471
472