1use std::io::{self, Write};
2use std::sync::LazyLock;
3use std::sync::Mutex;
4use std::time::Duration;
5
6use crossterm::event::{self, Event, KeyEvent, KeyboardEnhancementFlags};
7
8#[derive(Debug, Clone)]
15pub enum TerminalEvent {
16 Key(KeyEvent),
17 Paste(String),
18 Resize(u16, u16),
19}
20
21pub trait TerminalTrait {
28 fn start(&mut self, writer: &mut dyn Write) -> io::Result<()>;
29 fn stop(&mut self, writer: &mut dyn Write) -> io::Result<()>;
30 fn drain_input(&mut self, max_ms: u64) -> io::Result<()>;
31 fn write(&self, writer: &mut dyn Write, data: &str) -> io::Result<()>;
32 fn size(&self) -> io::Result<(u16, u16)>;
33 fn kitty_protocol_active(&self) -> bool;
34 fn move_by(&self, writer: &mut dyn Write, lines: i32) -> io::Result<()>;
35 fn hide_cursor(&self, writer: &mut dyn Write) -> io::Result<()>;
36 fn show_cursor(&self, writer: &mut dyn Write) -> io::Result<()>;
37 fn clear_line(&self, writer: &mut dyn Write) -> io::Result<()>;
38 fn clear_from_cursor(&self, writer: &mut dyn Write) -> io::Result<()>;
39 fn clear_screen(&self, writer: &mut dyn Write) -> io::Result<()>;
40 fn set_title(&self, writer: &mut dyn Write, title: &str) -> io::Result<()>;
41 fn set_progress(&self, writer: &mut dyn Write, active: bool) -> io::Result<()>;
42 fn set_color_scheme_notifications(
46 &self,
47 writer: &mut dyn Write,
48 enabled: bool,
49 ) -> io::Result<()>;
50}
51
52use std::sync::atomic::{AtomicBool, Ordering};
59use std::sync::mpsc;
60
61static EVENT_TX: LazyLock<Mutex<Option<mpsc::Sender<TerminalEvent>>>> =
62 LazyLock::new(|| Mutex::new(None));
63
64static EVENT_RX: LazyLock<Mutex<Option<mpsc::Receiver<TerminalEvent>>>> =
65 LazyLock::new(|| Mutex::new(None));
66
67static STDIN_THREAD_HANDLE: LazyLock<Mutex<Option<std::thread::JoinHandle<()>>>> =
68 LazyLock::new(|| Mutex::new(None));
69
70static STDIN_RUNNING: AtomicBool = AtomicBool::new(false);
71
72pub fn start_stdin_reader() {
75 let (tx, rx) = mpsc::channel();
76 *EVENT_TX.lock().unwrap() = Some(tx.clone());
77 *EVENT_RX.lock().unwrap() = Some(rx);
78 STDIN_RUNNING.store(true, Ordering::SeqCst);
79
80 let handle = std::thread::spawn(move || {
81 while STDIN_RUNNING.load(Ordering::SeqCst) {
85 match event::poll(std::time::Duration::from_millis(100)) {
86 Ok(true) => {
87 match event::read() {
89 Ok(Event::Key(key)) => {
90 let _ = tx.send(TerminalEvent::Key(key));
91 }
92 Ok(Event::Paste(content)) => {
93 let _ = tx.send(TerminalEvent::Paste(content));
94 }
95 Ok(Event::Resize(w, h)) => {
96 let _ = tx.send(TerminalEvent::Resize(w, h));
97 }
98 Ok(_) => {}
99 Err(_) => {
100 break;
102 }
103 }
104 }
105 Ok(false) => {
106 }
108 Err(_) => {
109 break;
111 }
112 }
113 }
114 });
115
116 *STDIN_THREAD_HANDLE.lock().unwrap() = Some(handle);
117}
118
119pub fn stop_stdin_reader() {
124 STDIN_RUNNING.store(false, Ordering::SeqCst);
125 let mut guard = EVENT_TX.lock().unwrap();
126 *guard = None;
127 drop(guard);
128 let mut rx_guard = EVENT_RX.lock().unwrap();
129 *rx_guard = None;
130}
131
132pub fn join_stdin_reader() {
134 let mut guard = STDIN_THREAD_HANDLE.lock().unwrap();
135 if let Some(handle) = guard.take() {
136 let _ = handle.join();
137 }
138}
139
140pub fn try_recv_terminal_event() -> Option<TerminalEvent> {
147 use std::sync::mpsc::TryRecvError;
148 let rx_opt = EVENT_RX.lock().unwrap().take();
149 let rx = rx_opt.as_ref()?;
150 let (event, keep) = match rx.try_recv() {
151 Ok(event) => (Some(event), true),
152 Err(TryRecvError::Empty) => (None, true),
153 Err(TryRecvError::Disconnected) => (None, false),
154 };
155 let _ = rx;
156 if keep {
157 *EVENT_RX.lock().unwrap() = rx_opt;
158 }
159 event
160}
161
162pub struct ProcessTerminal {
167 was_raw: bool,
168 kitty_active: bool,
169}
170
171impl ProcessTerminal {
172 pub fn new() -> Self {
173 Self {
174 was_raw: false,
175 kitty_active: false,
176 }
177 }
178
179 fn enable_bracketed_paste(&self, writer: &mut dyn Write) -> io::Result<()> {
180 write!(writer, "\x1b[?2004h")?;
181 writer.flush()
182 }
183
184 fn disable_bracketed_paste(&self, writer: &mut dyn Write) -> io::Result<()> {
185 write!(writer, "\x1b[?2004l")?;
186 writer.flush()
187 }
188
189 fn enable_kitty_protocol(&mut self, writer: &mut dyn Write) -> io::Result<()> {
190 if self.kitty_active {
191 return Ok(());
192 }
193 let flags = KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
199 | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
200 | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS;
201 write!(writer, "\x1b[>{}u", flags.bits())?;
202 writer.flush()?;
203 self.kitty_active = true;
204 Ok(())
205 }
206
207 fn disable_kitty_protocol(&mut self, writer: &mut dyn Write) -> io::Result<()> {
208 if self.kitty_active {
209 write!(writer, "\x1b[<u")?;
211 writer.flush()?;
212 self.kitty_active = false;
213 }
214 Ok(())
215 }
216}
217
218impl Default for ProcessTerminal {
219 fn default() -> Self {
220 Self::new()
221 }
222}
223
224impl Drop for ProcessTerminal {
225 fn drop(&mut self) {
226 if self.was_raw {
227 let _ = crossterm::terminal::disable_raw_mode();
228 }
229 }
230}
231
232impl TerminalTrait for ProcessTerminal {
233 fn start(&mut self, writer: &mut dyn Write) -> io::Result<()> {
234 crossterm::terminal::enable_raw_mode()?;
235 self.was_raw = true;
236 self.enable_bracketed_paste(writer)?;
237 self.enable_kitty_protocol(writer)?;
245 let _ = crossterm::terminal::size();
247 Ok(())
248 }
249
250 fn stop(&mut self, writer: &mut dyn Write) -> io::Result<()> {
251 self.disable_kitty_protocol(writer)?;
252 self.disable_bracketed_paste(writer)?;
253 if self.was_raw {
254 crossterm::terminal::disable_raw_mode()?;
255 self.was_raw = false;
256 }
257 Ok(())
258 }
259
260 fn drain_input(&mut self, max_ms: u64) -> io::Result<()> {
261 let mut buf = Vec::new();
263 self.disable_kitty_protocol(&mut buf)?;
264 if !buf.is_empty() {
265 let stdout = std::io::stdout();
266 let mut handle = stdout.lock();
267 handle.write_all(&buf)?;
268 handle.flush()?;
269 }
270
271 let start = std::time::Instant::now();
272 let mut last_data = start;
273 loop {
274 if start.elapsed().as_millis() as u64 >= max_ms {
275 break;
276 }
277 if event::poll(Duration::from_millis(10))? {
278 let _ = event::read()?;
279 last_data = std::time::Instant::now();
280 } else if last_data.elapsed().as_millis() > 50 {
281 break;
282 }
283 }
284 Ok(())
285 }
286
287 fn write(&self, writer: &mut dyn Write, data: &str) -> io::Result<()> {
288 write!(writer, "{}", data)?;
289 writer.flush()
290 }
291
292 fn size(&self) -> io::Result<(u16, u16)> {
293 crossterm::terminal::size()
294 }
295
296 fn kitty_protocol_active(&self) -> bool {
297 self.kitty_active
298 }
299
300 fn move_by(&self, writer: &mut dyn Write, lines: i32) -> io::Result<()> {
301 if lines > 0 {
302 write!(writer, "\x1b[{}B", lines)?;
303 } else if lines < 0 {
304 write!(writer, "\x1b[{}A", -lines)?;
305 }
306 writer.flush()
307 }
308
309 fn hide_cursor(&self, writer: &mut dyn Write) -> io::Result<()> {
310 write!(writer, "\x1b[?25l")?;
311 writer.flush()
312 }
313
314 fn show_cursor(&self, writer: &mut dyn Write) -> io::Result<()> {
315 write!(writer, "\x1b[?25h")?;
316 writer.flush()
317 }
318
319 fn clear_line(&self, writer: &mut dyn Write) -> io::Result<()> {
320 write!(writer, "\x1b[2K")?;
321 writer.flush()
322 }
323
324 fn clear_from_cursor(&self, writer: &mut dyn Write) -> io::Result<()> {
325 write!(writer, "\x1b[J")?;
326 writer.flush()
327 }
328
329 fn clear_screen(&self, writer: &mut dyn Write) -> io::Result<()> {
330 write!(writer, "\x1b[2J\x1b[H")?;
331 writer.flush()
332 }
333
334 fn set_title(&self, writer: &mut dyn Write, title: &str) -> io::Result<()> {
335 write!(writer, "\x1b]0;{}\x07", title)?;
336 writer.flush()
337 }
338
339 fn set_progress(&self, writer: &mut dyn Write, active: bool) -> io::Result<()> {
340 if active {
341 write!(writer, "\x1b]9;4;3\x07")?;
342 } else {
343 write!(writer, "\x1b]9;4;0;\x07")?;
344 }
345 writer.flush()
346 }
347
348 fn set_color_scheme_notifications(
349 &self,
350 writer: &mut dyn Write,
351 enabled: bool,
352 ) -> io::Result<()> {
353 if enabled {
354 write!(writer, "\x1b[?2031h")?;
355 } else {
356 write!(writer, "\x1b[?2031l")?;
357 }
358 writer.flush()
359 }
360}