matchmaker/render/
render.rs

1use std::io::Write;
2use std::sync::Arc;
3
4use anyhow::Result;
5use log::{debug, info};
6use ratatui::Frame;
7use ratatui::layout::Rect;
8use ratatui::widgets::Clear;
9use tokio::sync::mpsc;
10
11use super::{DynamicHandlers, InterruptHandlers, State};
12use crate::action::{Action, Exit};
13use crate::config::{CursorSetting, ExitConfig, PreviewSetting, Side};
14use crate::message::{Interrupt, Event, RenderCommand};
15use crate::tui::Tui;
16use crate::ui::{InputUI, PickerUI, PreviewUI, ResultsUI, UI};
17use crate::{MatchmakerError, PickerItem, Selection};
18
19pub async fn render_loop<'a, W: Write, T: PickerItem, S: Selection, C>(
20    mut ui: UI,
21    mut picker_ui: PickerUI<'a, T, S, C>,
22    mut preview_ui: Option<PreviewUI>,
23    mut tui: Tui<W>,
24    mut render_rx: mpsc::UnboundedReceiver<RenderCommand>,
25    controller_tx: mpsc::UnboundedSender<Event>,
26    context: Arc<C>,
27    dynamic_handlers: DynamicHandlers<T, S, C>,
28    exit_config: ExitConfig
29) -> Result<Vec<S>> {
30    let mut buffer = Vec::with_capacity(256);
31    let mut state: State<S, C> = State::new(context);
32    if let Some(ref preview_ui) = preview_ui
33    && !preview_ui.command().is_empty()
34    {
35        state.update_preview(preview_ui.command());
36    }
37    
38    'rendering: while render_rx.recv_many(&mut buffer, 256).await > 0 {
39        let mut did_pause = false;
40        let mut did_exit = false;
41        
42        for event in &buffer {
43            let mut interrupt = Interrupt::None;
44            
45            let PickerUI {
46                input,
47                results,
48                worker,
49                selections,
50                ..
51            } = &mut picker_ui;
52            
53            if !matches!(event, RenderCommand::Tick) {
54                info!("Recieved {event:?}");
55            }
56            
57            match event {
58                RenderCommand::Input(c) => {
59                    input.input.insert(input.cursor as usize, *c);
60                    input.cursor += 1;
61                }
62                RenderCommand::Resize(area) => {
63                    tui.terminal.resize(area.clone());
64                }
65                RenderCommand::Refresh => {
66                    tui.terminal.resize(tui.area);
67                }
68                RenderCommand::Action(action) => {
69                    match action {
70                        Action::Select => {
71                            if let Some(item) = worker.get_nth(results.index()) {
72                                selections.sel(item);
73                            }
74                        }
75                        Action::Deselect => {
76                            if let Some(item) = worker.get_nth(results.index()) {
77                                selections.desel(item);
78                            }
79                        }
80                        Action::Toggle => {
81                            if let Some(item) = worker.get_nth(results.index()) {
82                                selections.toggle(item);
83                            }
84                        }
85                        Action::CycleAll => {
86                            selections.cycle_all_bg(worker.raw_results());
87                        }
88                        Action::Accept => {
89                            if selections.is_empty()
90                            && let Some(item) = worker.get_nth(results.index())
91                            {
92                                selections.sel(item);
93                            }
94                            return Ok(selections.output().collect::<Vec<S>>());
95                        }
96                        Action::Quit(code) => {
97                            return Err(MatchmakerError::Abort(code.into()).into());
98                        }
99                        
100                        // UI
101                        Action::ChangeHeader(context) => {
102                            todo!()
103                        }
104                        
105                        
106                        Action::CyclePreview => {
107                            if let Some(p) = preview_ui.as_mut() {
108                                p.cycle_layout();
109                                if !p.command().is_empty() {
110                                    state.update_preview(p.command().as_str());
111                                }
112                            }
113                        }
114                        Action::Preview(context) => {
115                            if let Some(p) = preview_ui.as_mut() {
116                                if !state.update_preview(context.as_str()) {
117                                    p.toggle_show()
118                                } else {
119                                    p.show::<true>();
120                                }
121                            };
122                        }
123                        Action::SwitchPreview(idx) => {
124                            if let Some(p) = preview_ui.as_mut() {
125                                if let Some(idx) = idx {
126                                    if !p.set_idx(*idx) && !state.update_preview(p.command()) {
127                                        p.toggle_show();
128                                    }
129                                } else {
130                                    p.toggle_show()
131                                }
132                            }
133                        }
134                        Action::SetPreview(idx) => {
135                            if let Some(p) = preview_ui.as_mut()
136                            {
137                                if let Some(idx) = idx {
138                                    p.set_idx(*idx);
139                                } else {
140                                    state.update_preview(p.command());
141                                }
142                            }
143                        }
144                        
145                        // Programmable
146                        Action::Execute(context) => {
147                            interrupt = Interrupt::Execute(context.into());
148                        }
149                        Action::Become(context) => {
150                            interrupt = Interrupt::Become(context.into());
151                        }
152                        Action::Reload(context) => {
153                            interrupt = Interrupt::Reload(context.into());
154                        }
155                        Action::Print(context) => {
156                            interrupt = Interrupt::Print(context.into());
157                        }
158                        
159                        
160                        Action::SetInput(context) => {
161                            input.set_input(context.into(), u16::MAX);
162                        }
163                        Action::Column(context) => {
164                            results.toggle_col(*context);
165                        }
166                        // Edit
167                        Action::ForwardChar => input.forward_char(),
168                        Action::BackwardChar => input.backward_char(),
169                        Action::ForwardWord => input.forward_word(),
170                        Action::BackwardWord => input.backward_word(),
171                        Action::DeleteChar => input.delete(),
172                        Action::DeleteWord => input.delete_word(),
173                        Action::DeleteLineStart => input.delete_line_start(),
174                        Action::DeleteLineEnd => input.delete_line_end(),
175                        Action::Cancel => input.cancel(),
176                        
177                        // Navigation
178                        Action::Up(x) | Action::Down(x) => {
179                            let next = matches!(action, Action::Down(_)) ^ results.reverse();
180                            for _ in 0..x.into() {
181                                if next {
182                                    results.cursor_next();
183                                } else {
184                                    results.cursor_prev();
185                                }
186                            }
187                        }
188                        Action::PreviewUp(n) => {
189                            preview_ui.as_mut().map(|p| p.up(n.into()));
190                        }
191                        Action::PreviewDown(n) => {
192                            preview_ui.as_mut().map(|p| p.down(n.into()));
193                        }
194                        Action::PreviewHalfPageUp => todo!(),
195                        Action::PreviewHalfPageDown => todo!(),
196                        Action::Pos(pos) => {
197                            let pos = if *pos >= 0 {
198                                *pos as u32
199                            } else {
200                                results.status.matched_count.saturating_sub((-*pos) as u32)
201                            };
202                            results.cursor_jump(pos);
203                        }
204                        
205                        // Experimental/Debugging
206                        Action::Redraw => {
207                            tui.terminal.resize(tui.area);
208                        }
209                        _ => {}
210                    }
211                }
212                _ => {}
213            }
214            
215            if !matches!(interrupt, Interrupt::None) {
216                match interrupt {
217                    Interrupt::Execute(_) => {
218                        controller_tx.send(Event::Pause);
219                        did_exit = true;
220                        tui.enter_execute();
221                        did_pause = true;
222                        while let Some(msg) = render_rx.recv().await {
223                            if matches!(msg, RenderCommand::Ack) {
224                                break
225                            }
226                        }
227                    }
228                    Interrupt::Reload(_) => {
229                        picker_ui.worker.restart(false);
230                    }
231                    Interrupt::Become(_) => {
232                        tui.exit();
233                    }
234                    _ => {}
235                }
236                
237                state.update(&picker_ui);
238                let dispatcher = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
239                
240                for h in dynamic_handlers.1.get(&interrupt) {
241                    (h)(dispatcher.clone(), &interrupt);
242                };
243                
244                match interrupt {
245                    Interrupt::Become(context) => return Err(MatchmakerError::Become(context).into()),
246                    _ => {}
247                }
248            };
249        }
250        
251        // debug!("{state:?}");
252        // debug!("{:?}", picker_ui.results.widths());
253        debug!("{:?}", picker_ui.results.widths());
254        
255        // ------------- update state + render ------------------------
256        picker_ui.update();
257        
258        if did_exit {
259            tui.return_execute();
260        }
261        
262        let mut resized = false;
263        tui.terminal.draw(|frame| {
264            let area = frame.area();
265            
266            let [preview, picker_area] = if let Some(preview_ui) = preview_ui.as_ref()
267            && preview_ui.is_show()
268            {
269                preview_ui.layout().split(area)
270            } else {
271                [Rect::default(), area]
272            };
273            
274            let [input, status, results] = picker_ui.layout(picker_area);
275            
276            resized = state.update_layout([preview, input, status, results]);
277            
278            // might be more efficient to always update, but logically this feels better
279            if resized {
280                picker_ui.results.update_dimensions(&results);
281                ui.update_dimensions(area);
282            };
283            
284            render_input(frame, input, &picker_ui.input);
285            render_status(frame, status, &picker_ui.results);
286            render_results(frame, results, &mut picker_ui);
287            
288            if let Some(preview_ui) = preview_ui.as_mut() {
289                state.update_preview_ui(&preview_ui);
290                if resized {
291                    preview_ui.update_dimensions(&preview);
292                }
293                render_preview(frame, preview, preview_ui);
294            }
295        })?;
296        if resized {
297            tui.terminal.resize(tui.area);
298        }
299        
300        if state.iterations == 0 {
301            state.insert(Event::Start);
302        }
303        state.update(&picker_ui);
304        let events = state.events();
305        let dispatcher = state.dispatcher(&ui, &picker_ui, preview_ui.as_ref());
306        
307        if exit_config.select_1 && dispatcher.status().matched_count == 1 {
308            return Ok(vec![state.current().unwrap()]);
309        }
310        // todo: sync, and whatever else may be needed
311        
312        for e in events.iter() {
313            for h in dynamic_handlers.0.get(e) {
314                (h)(dispatcher.clone(), e)
315            }
316        }
317        for e in events {
318            controller_tx
319            .send(e)
320            .unwrap_or_else(|err| eprintln!("send failed: {:?}", err));
321        }
322        
323        buffer.clear();
324        
325        if did_pause {
326            controller_tx.send(Event::Resume);
327            // due to control flow, this does nothing, but is a useful safeguard anyway
328            while let Some(msg) = render_rx.recv().await {
329                if matches!(msg, RenderCommand::Ack) {
330                    break
331                }
332            }
333        }
334    }
335    
336    Err(MatchmakerError::EventLoopClosed.into())
337}
338
339// ------------------------------- HELPERS --------------------------------------------------------
340fn render_preview(frame: &mut Frame, area: Rect, ui: &mut PreviewUI) {
341    // note: this fixes previewer garbage at the cost of some delay but what is the actual cause?
342    if ui.view.changed() {
343        frame.render_widget(Clear, area);
344    } else {
345        let widget = ui.make_preview();
346        frame.render_widget(widget, area);
347    }
348}
349
350fn render_results<T: PickerItem, S: Selection, C>(
351    frame: &mut Frame,
352    area: Rect,
353    ui: &mut PickerUI<T, S, C>,
354) {
355    let widget = ui.make_table();
356    
357    frame.render_widget(widget, area);
358}
359
360fn render_input(frame: &mut Frame, area: Rect, ui: &InputUI) {
361    let widget = ui.make_input();
362    match ui.config.cursor {
363        CursorSetting::Default => frame.set_cursor_position(ui.cursor_offset(&area)),
364        _ => {}
365    };
366    
367    frame.render_widget(widget, area);
368}
369
370fn render_status(frame: &mut Frame, area: Rect, ui: &ResultsUI) {
371    let widget = ui.make_status();
372    
373    frame.render_widget(widget, area);
374}
375
376// -----------------------------------------------------------------------------------
377
378#[cfg(test)]
379mod test {}
380
381// #[cfg(test)]
382// async fn send_every_second(tx: mpsc::UnboundedSender<RenderCommand>) {
383//     let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
384
385//     loop {
386//         interval.tick().await;
387//         if tx.send(RenderCommand::quit()).is_err() {
388//             break;
389//         }
390//     }
391// }