1use std::io::{self, Write};
2use std::time::Duration;
3
4use crossterm::event::{self, Event, KeyEvent, KeyboardEnhancementFlags};
5
6#[derive(Debug, Clone)]
13pub enum TerminalEvent {
14 Key(KeyEvent),
15 Paste(String),
16 Resize(u16, u16),
17}
18
19pub trait TerminalTrait {
26 fn start(&mut self, writer: &mut dyn Write) -> io::Result<()>;
27 fn stop(&mut self, writer: &mut dyn Write) -> io::Result<()>;
28 fn drain_input(&mut self, max_ms: u64) -> io::Result<()>;
29 fn write(&self, writer: &mut dyn Write, data: &str) -> io::Result<()>;
30 fn size(&self) -> io::Result<(u16, u16)>;
31 fn kitty_protocol_active(&self) -> bool;
32 fn move_by(&self, writer: &mut dyn Write, lines: i32) -> io::Result<()>;
33 fn hide_cursor(&self, writer: &mut dyn Write) -> io::Result<()>;
34 fn show_cursor(&self, writer: &mut dyn Write) -> io::Result<()>;
35 fn clear_line(&self, writer: &mut dyn Write) -> io::Result<()>;
36 fn clear_from_cursor(&self, writer: &mut dyn Write) -> io::Result<()>;
37 fn clear_screen(&self, writer: &mut dyn Write) -> io::Result<()>;
38 fn set_title(&self, writer: &mut dyn Write, title: &str) -> io::Result<()>;
39 fn set_progress(&self, writer: &mut dyn Write, active: bool) -> io::Result<()>;
40 fn set_color_scheme_notifications(
44 &self,
45 writer: &mut dyn Write,
46 enabled: bool,
47 ) -> io::Result<()>;
48}
49
50pub fn poll_terminal_event(timeout: Option<Duration>) -> io::Result<Option<TerminalEvent>> {
57 if event::poll(timeout.unwrap_or(Duration::ZERO))? {
58 match event::read()? {
59 Event::Key(key) => Ok(Some(TerminalEvent::Key(key))),
60 Event::Paste(content) => Ok(Some(TerminalEvent::Paste(content))),
61 Event::Resize(w, h) => Ok(Some(TerminalEvent::Resize(w, h))),
62 _ => Ok(None),
63 }
64 } else {
65 Ok(None)
66 }
67}
68
69pub fn poll_key_event(timeout: Option<Duration>) -> io::Result<Option<KeyEvent>> {
70 match poll_terminal_event(timeout)? {
71 Some(TerminalEvent::Key(key)) => Ok(Some(key)),
72 _ => Ok(None),
73 }
74}
75
76pub fn read_key_event() -> io::Result<KeyEvent> {
77 loop {
78 match event::read()? {
79 Event::Key(key) => return Ok(key),
80 Event::Paste(_) => continue,
81 Event::Resize(_, _) => continue,
82 _ => continue,
83 }
84 }
85}
86
87pub struct ProcessTerminal {
92 was_raw: bool,
93 kitty_active: bool,
94}
95
96impl ProcessTerminal {
97 pub fn new() -> Self {
98 Self {
99 was_raw: false,
100 kitty_active: false,
101 }
102 }
103
104 fn enable_bracketed_paste(&self, writer: &mut dyn Write) -> io::Result<()> {
105 write!(writer, "\x1b[?2004h")?;
106 writer.flush()
107 }
108
109 fn disable_bracketed_paste(&self, writer: &mut dyn Write) -> io::Result<()> {
110 write!(writer, "\x1b[?2004l")?;
111 writer.flush()
112 }
113
114 fn enable_kitty_protocol(&mut self, writer: &mut dyn Write) -> io::Result<()> {
115 let flags = KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
116 | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
117 | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS;
118 write!(writer, "\x1b[>{}u", flags.bits())?;
119 writer.flush()?;
120 self.kitty_active = true;
121 Ok(())
122 }
123
124 fn disable_kitty_protocol(&mut self, writer: &mut dyn Write) -> io::Result<()> {
125 if self.kitty_active {
126 write!(writer, "\x1b[<u")?;
127 writer.flush()?;
128 self.kitty_active = false;
129 }
130 Ok(())
131 }
132}
133
134impl Default for ProcessTerminal {
135 fn default() -> Self {
136 Self::new()
137 }
138}
139
140impl Drop for ProcessTerminal {
141 fn drop(&mut self) {
142 if self.was_raw {
143 let _ = crossterm::terminal::disable_raw_mode();
144 }
145 }
146}
147
148impl TerminalTrait for ProcessTerminal {
149 fn start(&mut self, writer: &mut dyn Write) -> io::Result<()> {
150 crossterm::terminal::enable_raw_mode()?;
151 self.was_raw = true;
152 self.enable_bracketed_paste(writer)?;
153 self.enable_kitty_protocol(writer)?;
154 let _ = crossterm::terminal::size();
156 Ok(())
157 }
158
159 fn stop(&mut self, writer: &mut dyn Write) -> io::Result<()> {
160 self.disable_kitty_protocol(writer)?;
161 self.disable_bracketed_paste(writer)?;
162 if self.was_raw {
163 crossterm::terminal::disable_raw_mode()?;
164 self.was_raw = false;
165 }
166 Ok(())
167 }
168
169 fn drain_input(&mut self, max_ms: u64) -> io::Result<()> {
170 let mut buf = Vec::new();
172 self.disable_kitty_protocol(&mut buf)?;
173 if !buf.is_empty() {
174 let stdout = std::io::stdout();
175 let mut handle = stdout.lock();
176 handle.write_all(&buf)?;
177 handle.flush()?;
178 }
179
180 let start = std::time::Instant::now();
181 let mut last_data = start;
182 loop {
183 if start.elapsed().as_millis() as u64 >= max_ms {
184 break;
185 }
186 if event::poll(Duration::from_millis(10))? {
187 let _ = event::read()?;
188 last_data = std::time::Instant::now();
189 } else if last_data.elapsed().as_millis() > 50 {
190 break;
191 }
192 }
193 Ok(())
194 }
195
196 fn write(&self, writer: &mut dyn Write, data: &str) -> io::Result<()> {
197 write!(writer, "{}", data)?;
198 writer.flush()
199 }
200
201 fn size(&self) -> io::Result<(u16, u16)> {
202 crossterm::terminal::size()
203 }
204
205 fn kitty_protocol_active(&self) -> bool {
206 self.kitty_active
207 }
208
209 fn move_by(&self, writer: &mut dyn Write, lines: i32) -> io::Result<()> {
210 if lines > 0 {
211 write!(writer, "\x1b[{}B", lines)?;
212 } else if lines < 0 {
213 write!(writer, "\x1b[{}A", -lines)?;
214 }
215 writer.flush()
216 }
217
218 fn hide_cursor(&self, writer: &mut dyn Write) -> io::Result<()> {
219 write!(writer, "\x1b[?25l")?;
220 writer.flush()
221 }
222
223 fn show_cursor(&self, writer: &mut dyn Write) -> io::Result<()> {
224 write!(writer, "\x1b[?25h")?;
225 writer.flush()
226 }
227
228 fn clear_line(&self, writer: &mut dyn Write) -> io::Result<()> {
229 write!(writer, "\x1b[2K")?;
230 writer.flush()
231 }
232
233 fn clear_from_cursor(&self, writer: &mut dyn Write) -> io::Result<()> {
234 write!(writer, "\x1b[J")?;
235 writer.flush()
236 }
237
238 fn clear_screen(&self, writer: &mut dyn Write) -> io::Result<()> {
239 write!(writer, "\x1b[2J\x1b[H")?;
240 writer.flush()
241 }
242
243 fn set_title(&self, writer: &mut dyn Write, title: &str) -> io::Result<()> {
244 write!(writer, "\x1b]0;{}\x07", title)?;
245 writer.flush()
246 }
247
248 fn set_progress(&self, writer: &mut dyn Write, active: bool) -> io::Result<()> {
249 if active {
250 write!(writer, "\x1b]9;4;3\x07")?;
251 } else {
252 write!(writer, "\x1b]9;4;0;\x07")?;
253 }
254 writer.flush()
255 }
256
257 fn set_color_scheme_notifications(
258 &self,
259 writer: &mut dyn Write,
260 enabled: bool,
261 ) -> io::Result<()> {
262 if enabled {
263 write!(writer, "\x1b[?2031h")?;
264 } else {
265 write!(writer, "\x1b[?2031l")?;
266 }
267 writer.flush()
268 }
269}
270
271use crossterm::{cursor, execute, terminal::ClearType};
276
277pub struct Terminal {
278 inner: ProcessTerminal,
279}
280
281impl Terminal {
282 pub fn new() -> Self {
283 Self {
284 inner: ProcessTerminal::new(),
285 }
286 }
287
288 pub fn enter_raw_mode(&mut self) -> io::Result<()> {
289 let mut buf = Vec::new();
290 self.inner.start(&mut buf)?;
291 if !buf.is_empty() {
292 let stdout = std::io::stdout();
293 let mut handle = stdout.lock();
294 handle.write_all(&buf)?;
295 handle.flush()?;
296 }
297 Ok(())
298 }
299
300 pub fn leave_raw_mode(&mut self) -> io::Result<()> {
301 let mut buf = Vec::new();
302 self.inner.stop(&mut buf)?;
303 if !buf.is_empty() {
304 let stdout = std::io::stdout();
305 let mut handle = stdout.lock();
306 handle.write_all(&buf)?;
307 handle.flush()?;
308 }
309 Ok(())
310 }
311
312 pub fn show_cursor(writer: &mut impl Write) -> io::Result<()> {
313 execute!(writer, cursor::Show)
314 }
315
316 pub fn hide_cursor(writer: &mut impl Write) -> io::Result<()> {
317 execute!(writer, cursor::Hide)
318 }
319
320 pub fn move_cursor_to(writer: &mut impl Write, row: u16, col: u16) -> io::Result<()> {
321 execute!(writer, cursor::MoveTo(col, row))
322 }
323
324 pub fn clear_line(writer: &mut impl Write) -> io::Result<()> {
325 execute!(writer, crossterm::terminal::Clear(ClearType::CurrentLine))
326 }
327
328 pub fn clear_screen(writer: &mut impl Write) -> io::Result<()> {
329 execute!(writer, crossterm::terminal::Clear(ClearType::All))
330 }
331
332 pub fn size() -> io::Result<(u16, u16)> {
333 crossterm::terminal::size()
334 }
335
336 pub fn write(writer: &mut impl Write, data: &str) -> io::Result<()> {
337 write!(writer, "{}", data)?;
338 writer.flush()
339 }
340
341 pub fn begin_sync(writer: &mut impl Write) -> io::Result<()> {
342 write!(writer, "\x1b[?2026h")?;
343 writer.flush()
344 }
345
346 pub fn end_sync(writer: &mut impl Write) -> io::Result<()> {
347 write!(writer, "\x1b[?2026l")?;
348 writer.flush()
349 }
350
351 pub fn set_color_scheme_notifications(
352 writer: &mut impl Write,
353 enabled: bool,
354 ) -> io::Result<()> {
355 if enabled {
356 write!(writer, "\x1b[?2031h")?;
357 } else {
358 write!(writer, "\x1b[?2031l")?;
359 }
360 writer.flush()
361 }
362}
363
364impl Default for Terminal {
365 fn default() -> Self {
366 Self::new()
367 }
368}
369
370impl Drop for Terminal {
371 fn drop(&mut self) {
372 let _ = self.inner.stop(&mut std::io::sink());
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn test_new_terminal() {
382 let term = ProcessTerminal::new();
383 assert!(!term.kitty_protocol_active());
384 }
385
386 #[test]
387 fn test_drain_input_timeout() {
388 let mut term = ProcessTerminal::new();
389 let _ = term.drain_input(10);
391 }
392}