1use crate::app_event::{AppEvent, AppEventReceiver, AppEventSender};
11use crossterm::event::EventStream;
12use futures::StreamExt;
13use std::io::{self, stdout, Stdout};
14use std::time::Duration;
15use tokio::time::interval;
16
17use crossterm::{
18 event::{
19 DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
20 KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
21 },
22 execute,
23 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
24};
25use ratatui::{backend::CrosstermBackend, Terminal};
26
27pub type TuiTerminal = Terminal<CrosstermBackend<Stdout>>;
29
30const TICK_RATE_MS: u64 = 16;
32
33const MIN_RENDER_INTERVAL_MS: u64 = 16;
35
36pub struct TuiRunnerConfig {
38 pub mouse_capture: bool,
40 pub keyboard_enhancement: bool,
42 pub alternate_screen: bool,
44}
45
46impl Default for TuiRunnerConfig {
47 fn default() -> Self {
48 Self {
49 mouse_capture: true,
50 keyboard_enhancement: true,
51 alternate_screen: false, }
53 }
54}
55
56pub fn init_terminal(config: &TuiRunnerConfig) -> io::Result<TuiTerminal> {
58 enable_raw_mode()?;
59
60 if config.alternate_screen {
61 execute!(stdout(), EnterAlternateScreen)?;
62 }
63
64 if config.keyboard_enhancement {
66 let _ = execute!(
67 stdout(),
68 PushKeyboardEnhancementFlags(
69 KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
70 | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
71 )
72 );
73 }
74
75 execute!(stdout(), EnableBracketedPaste)?;
76
77 if config.mouse_capture {
78 execute!(stdout(), EnableMouseCapture)?;
79 }
80
81 let backend = CrosstermBackend::new(stdout());
82 Terminal::new(backend)
83}
84
85pub fn restore_terminal(config: &TuiRunnerConfig) -> io::Result<()> {
87 if config.keyboard_enhancement {
88 let _ = execute!(stdout(), PopKeyboardEnhancementFlags);
89 }
90
91 if config.mouse_capture {
92 let _ = execute!(stdout(), DisableMouseCapture);
93 }
94
95 execute!(stdout(), DisableBracketedPaste)?;
96
97 if config.alternate_screen {
98 execute!(stdout(), LeaveAlternateScreen)?;
99 }
100
101 disable_raw_mode()?;
102 Ok(())
103}
104
105pub async fn run_event_loop(
114 app_tx: AppEventSender,
115 mut app_rx: AppEventReceiver,
116 mut on_event: impl FnMut(AppEvent) -> bool, ) {
118 let mut event_stream = EventStream::new();
119 let mut tick_interval = interval(Duration::from_millis(TICK_RATE_MS));
120
121 loop {
122 tokio::select! {
123 maybe_event = event_stream.next() => {
125 match maybe_event {
126 Some(Ok(event)) => {
127 if !on_event(AppEvent::Terminal(event)) {
128 break;
129 }
130 }
131 Some(Err(e)) => {
132 let _ = app_tx.send(AppEvent::Error(e.to_string()));
133 }
134 None => break, }
136 }
137
138 Some(event) = app_rx.recv() => {
140 if !on_event(event) {
141 break;
142 }
143 }
144
145 _ = tick_interval.tick() => {
147 if !on_event(AppEvent::Tick) {
148 break;
149 }
150 }
151 }
152 }
153}
154
155pub struct FrameRateLimiter {
157 last_render: std::time::Instant,
158 min_interval: Duration,
159}
160
161impl Default for FrameRateLimiter {
162 fn default() -> Self {
163 Self {
164 last_render: std::time::Instant::now(),
165 min_interval: Duration::from_millis(MIN_RENDER_INTERVAL_MS),
166 }
167 }
168}
169
170impl FrameRateLimiter {
171 pub fn should_render(&mut self) -> bool {
173 let now = std::time::Instant::now();
174 if now.duration_since(self.last_render) >= self.min_interval {
175 self.last_render = now;
176 true
177 } else {
178 false
179 }
180 }
181
182 pub fn force_render(&mut self) {
184 self.last_render = std::time::Instant::now();
185 }
186}