Skip to main content

matchmaker/render/
mod.rs

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