1use anyhow::Result;
2use crossterm::{
3 cursor,
4 event::{
5 self, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyEvent, KeyEventKind,
6 KeyModifiers,
7 },
8 execute,
9 terminal::{self, ClearType},
10};
11
12#[cfg(feature = "mouse-support")]
13use crossterm::event::{MouseEvent, MouseEventKind};
14
15use std::io::{self, Write};
16
17#[derive(Debug, Clone)]
19pub enum InputEvent {
20 Key(KeyEvent),
22 Paste(String),
24}
25
26pub struct Terminal {
27 size: (u16, u16),
28}
29
30impl Terminal {
31 pub fn new() -> Result<Self> {
32 let size = terminal::size()?;
33 Ok(Self { size })
34 }
35
36 pub fn enter_raw_mode() -> Result<()> {
37 terminal::enable_raw_mode()?;
38 execute!(
39 io::stdout(),
40 terminal::EnterAlternateScreen,
41 EnableBracketedPaste
42 )?;
43 Ok(())
44 }
45
46 pub fn exit_raw_mode() -> Result<()> {
47 execute!(
48 io::stdout(),
49 DisableBracketedPaste,
50 terminal::LeaveAlternateScreen
51 )?;
52 terminal::disable_raw_mode()?;
53 Ok(())
54 }
55
56 pub fn clear_screen() -> Result<()> {
57 execute!(io::stdout(), terminal::Clear(ClearType::All))?;
58 Ok(())
59 }
60
61 pub fn size(&self) -> (u16, u16) {
62 self.size
63 }
64
65 #[allow(dead_code)]
66 pub fn update_size(&mut self) -> Result<()> {
67 self.size = terminal::size()?;
68 Ok(())
69 }
70
71 #[allow(dead_code)]
72 pub fn flush() -> Result<()> {
73 io::stdout().flush()?;
74 Ok(())
75 }
76
77 pub fn read_key() -> Result<KeyEvent> {
78 loop {
79 let event = event::read()?;
80
81 match event {
82 Event::Key(key_event) => {
83 if key_event.kind == KeyEventKind::Press
85 || key_event.kind == KeyEventKind::Repeat
86 {
87 return Ok(key_event);
88 }
89 }
90 Event::Resize(_cols, _rows) => {
91 return Ok(KeyEvent::new(KeyCode::F(21), KeyModifiers::NONE));
93 }
94 Event::Paste(_text) => {
95 return Ok(KeyEvent::new(KeyCode::F(20), KeyModifiers::NONE));
98 }
99 #[cfg(feature = "mouse-support")]
100 Event::Mouse(mouse_event) => {
101 if let Some(key_event) = handle_mouse_event(mouse_event) {
103 return Ok(key_event);
104 }
105 }
106 _ => {
107 }
109 }
110 }
111 }
112
113 pub fn read_input() -> Result<InputEvent> {
118 loop {
119 let event = event::read()?;
120
121 match event {
122 Event::Key(key_event) => {
123 if key_event.kind == KeyEventKind::Press
125 || key_event.kind == KeyEventKind::Repeat
126 {
127 return Ok(InputEvent::Key(key_event));
128 }
129 }
130 Event::Resize(_cols, _rows) => {
131 return Ok(InputEvent::Key(KeyEvent::new(
133 KeyCode::F(21),
134 KeyModifiers::NONE,
135 )));
136 }
137 Event::Paste(text) => {
138 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
141 return Ok(InputEvent::Paste(normalized));
142 }
143 #[cfg(feature = "mouse-support")]
144 Event::Mouse(mouse_event) => {
145 if let Some(key_event) = handle_mouse_event(mouse_event) {
147 return Ok(InputEvent::Key(key_event));
148 }
149 }
150 _ => {
151 }
153 }
154 }
155 }
156
157 #[allow(dead_code)]
158 pub fn set_cursor_position(x: u16, y: u16) -> Result<()> {
159 execute!(io::stdout(), cursor::MoveTo(x, y))?;
160 Ok(())
161 }
162
163 #[allow(dead_code)]
164 pub fn hide_cursor() -> Result<()> {
165 execute!(io::stdout(), cursor::Hide)?;
166 Ok(())
167 }
168
169 pub fn show_cursor() -> Result<()> {
170 execute!(io::stdout(), cursor::Show)?;
171 Ok(())
172 }
173}
174
175#[cfg(feature = "mouse-support")]
176fn handle_mouse_event(mouse_event: MouseEvent) -> Option<KeyEvent> {
177 match mouse_event.kind {
178 MouseEventKind::ScrollUp => Some(KeyEvent::new(KeyCode::F(22), KeyModifiers::NONE)),
179 MouseEventKind::ScrollDown => Some(KeyEvent::new(KeyCode::F(23), KeyModifiers::NONE)),
180 _ => None,
181 }
182}
183
184impl Drop for Terminal {
185 fn drop(&mut self) {
186 let _ = Self::exit_raw_mode();
187 let _ = Self::show_cursor();
188 }
189}