1use async_channel::{Receiver, Sender, TryRecvError};
19use async_trait::async_trait;
20use crossterm::event::{self, KeyEventKind};
21use crossterm::tty::IsTty;
22use crossterm::{QueueableCommand, cursor, style, terminal};
23use endbasic_core::exec::Signal;
24use endbasic_std::console::graphics::InputOps;
25use endbasic_std::console::{
26 CharsXY, ClearType, Console, Key, get_env_var_as_u16, read_key_from_stdin, remove_control_chars,
27};
28use std::cmp::Ordering;
29use std::collections::VecDeque;
30use std::io::{self, StdoutLock, Write};
31
32pub struct TerminalConsole {
34 is_tty: bool,
37
38 fg_color: Option<u8>,
40
41 bg_color: Option<u8>,
43
44 cursor_visible: bool,
46
47 alt_active: bool,
49
50 sync_enabled: bool,
52
53 on_key_rx: Receiver<Key>,
55}
56
57impl Drop for TerminalConsole {
58 fn drop(&mut self) {
59 if self.is_tty {
60 terminal::disable_raw_mode().unwrap();
61 }
62 }
63}
64
65impl TerminalConsole {
66 pub fn from_stdio(signals_tx: Sender<Signal>) -> io::Result<Self> {
71 let (terminal, _on_key_tx) = Self::from_stdio_with_injector(signals_tx)?;
72 Ok(terminal)
73 }
74
75 pub fn from_stdio_with_injector(signals_tx: Sender<Signal>) -> io::Result<(Self, Sender<Key>)> {
83 let (on_key_tx, on_key_rx) = async_channel::unbounded();
84
85 let is_tty = io::stdin().is_tty() && io::stdout().is_tty();
86
87 if is_tty {
88 terminal::enable_raw_mode()?;
89 tokio::task::spawn(TerminalConsole::raw_key_handler(on_key_tx.clone(), signals_tx));
90 } else {
91 tokio::task::spawn(TerminalConsole::stdio_key_handler(on_key_tx.clone()));
92 }
93
94 Ok((
95 Self {
96 is_tty,
97 fg_color: None,
98 bg_color: None,
99 cursor_visible: true,
100 alt_active: false,
101 sync_enabled: true,
102 on_key_rx,
103 },
104 on_key_tx,
105 ))
106 }
107
108 async fn raw_key_handler(on_key_tx: Sender<Key>, signals_tx: Sender<Signal>) {
111 use event::{KeyCode, KeyModifiers};
112
113 let mut done = false;
114 while !done {
115 let key = match event::read() {
116 Ok(event::Event::Key(ev)) => {
117 if ev.kind != KeyEventKind::Press {
118 continue;
119 }
120
121 match ev.code {
122 KeyCode::Backspace => Key::Backspace,
123 KeyCode::End => Key::End,
124 KeyCode::Esc => Key::Escape,
125 KeyCode::Home => Key::Home,
126 KeyCode::Tab => Key::Tab,
127 KeyCode::Up => Key::ArrowUp,
128 KeyCode::Down => Key::ArrowDown,
129 KeyCode::Left => Key::ArrowLeft,
130 KeyCode::Right => Key::ArrowRight,
131 KeyCode::PageDown => Key::PageDown,
132 KeyCode::PageUp => Key::PageUp,
133 KeyCode::Char('a') if ev.modifiers == KeyModifiers::CONTROL => Key::Home,
134 KeyCode::Char('b') if ev.modifiers == KeyModifiers::CONTROL => {
135 Key::ArrowLeft
136 }
137 KeyCode::Char('c') if ev.modifiers == KeyModifiers::CONTROL => {
138 Key::Interrupt
139 }
140 KeyCode::Char('d') if ev.modifiers == KeyModifiers::CONTROL => Key::Eof,
141 KeyCode::Char('e') if ev.modifiers == KeyModifiers::CONTROL => Key::End,
142 KeyCode::Char('f') if ev.modifiers == KeyModifiers::CONTROL => {
143 Key::ArrowRight
144 }
145 KeyCode::Char('j') if ev.modifiers == KeyModifiers::CONTROL => Key::NewLine,
146 KeyCode::Char('m') if ev.modifiers == KeyModifiers::CONTROL => Key::NewLine,
147 KeyCode::Char('n') if ev.modifiers == KeyModifiers::CONTROL => {
148 Key::ArrowDown
149 }
150 KeyCode::Char('p') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowUp,
151 KeyCode::Char(ch) => Key::Char(ch),
152 KeyCode::Enter => Key::NewLine,
153 _ => Key::Unknown,
154 }
155 }
156 Ok(_) => {
157 continue;
159 }
160 Err(_) => {
161 Key::Unknown
163 }
164 };
165
166 done = key == Key::Eof;
167 if key == Key::Interrupt {
168 signals_tx
174 .send(Signal::Break)
175 .await
176 .expect("Send to unbounded channel should not have failed")
177 }
178
179 let _ = on_key_tx.send(key).await;
183 }
184
185 signals_tx.close();
186 on_key_tx.close();
187 }
188
189 async fn stdio_key_handler(on_key_tx: Sender<Key>) {
192 let mut buffer = VecDeque::default();
199
200 let mut done = false;
201 while !done {
202 let key = match read_key_from_stdin(&mut buffer) {
203 Ok(key) => key,
204 Err(_) => {
205 Key::Unknown
207 }
208 };
209
210 done = key == Key::Eof;
211
212 let _ = on_key_tx.send(key).await;
216 }
217
218 on_key_tx.close();
219 }
220
221 fn maybe_flush(&self, mut lock: StdoutLock<'_>) -> io::Result<()> {
223 if self.sync_enabled { lock.flush() } else { Ok(()) }
224 }
225}
226
227#[async_trait(?Send)]
228impl InputOps for TerminalConsole {
229 async fn poll_key(&mut self) -> io::Result<Option<Key>> {
230 match self.on_key_rx.try_recv() {
231 Ok(k) => Ok(Some(k)),
232 Err(TryRecvError::Empty) => Ok(None),
233 Err(TryRecvError::Closed) => Ok(Some(Key::Eof)),
234 }
235 }
236
237 async fn read_key(&mut self) -> io::Result<Key> {
238 match self.on_key_rx.recv().await {
239 Ok(k) => Ok(k),
240 Err(_) => Ok(Key::Eof),
241 }
242 }
243}
244
245#[async_trait(?Send)]
246impl Console for TerminalConsole {
247 fn clear(&mut self, how: ClearType) -> io::Result<()> {
248 let how = match how {
249 ClearType::All => terminal::ClearType::All,
250 ClearType::CurrentLine => terminal::ClearType::CurrentLine,
251 ClearType::PreviousChar => {
252 let stdout = io::stdout();
253 let mut stdout = stdout.lock();
254 stdout.write_all(b"\x08 \x08")?;
255 return self.maybe_flush(stdout);
256 }
257 ClearType::UntilNewLine => terminal::ClearType::UntilNewLine,
258 };
259 let stdout = io::stdout();
260 let mut stdout = stdout.lock();
261 stdout.queue(terminal::Clear(how))?;
262 if how == terminal::ClearType::All {
263 stdout.queue(cursor::MoveTo(0, 0))?;
264 }
265 self.maybe_flush(stdout)
266 }
267
268 fn color(&self) -> (Option<u8>, Option<u8>) {
269 (self.fg_color, self.bg_color)
270 }
271
272 fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
273 if fg == self.fg_color && bg == self.bg_color {
274 return Ok(());
275 }
276
277 let stdout = io::stdout();
278 let mut stdout = stdout.lock();
279 if fg != self.fg_color {
280 let ct_fg = match fg {
281 None => style::Color::Reset,
282 Some(color) => style::Color::AnsiValue(color),
283 };
284 stdout.queue(style::SetForegroundColor(ct_fg))?;
285 self.fg_color = fg;
286 }
287 if bg != self.bg_color {
288 let ct_bg = match bg {
289 None => style::Color::Reset,
290 Some(color) => style::Color::AnsiValue(color),
291 };
292 stdout.queue(style::SetBackgroundColor(ct_bg))?;
293 self.bg_color = bg;
294 }
295 self.maybe_flush(stdout)
296 }
297
298 fn enter_alt(&mut self) -> io::Result<()> {
299 if !self.alt_active {
300 let stdout = io::stdout();
301 let mut stdout = stdout.lock();
302 stdout.queue(terminal::EnterAlternateScreen)?;
303 self.alt_active = true;
304 self.maybe_flush(stdout)
305 } else {
306 Ok(())
307 }
308 }
309
310 fn hide_cursor(&mut self) -> io::Result<()> {
311 if self.cursor_visible {
312 let stdout = io::stdout();
313 let mut stdout = stdout.lock();
314 stdout.queue(cursor::Hide)?;
315 self.cursor_visible = false;
316 self.maybe_flush(stdout)
317 } else {
318 Ok(())
319 }
320 }
321
322 fn is_interactive(&self) -> bool {
323 self.is_tty
324 }
325
326 fn leave_alt(&mut self) -> io::Result<()> {
327 if self.alt_active {
328 let stdout = io::stdout();
329 let mut stdout = stdout.lock();
330 stdout.queue(terminal::LeaveAlternateScreen)?;
331 self.alt_active = false;
332 self.maybe_flush(stdout)
333 } else {
334 Ok(())
335 }
336 }
337
338 fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
339 #[cfg(debug_assertions)]
340 {
341 let size = self.size_chars()?;
342 assert!(pos.x < size.x);
343 assert!(pos.y < size.y);
344 }
345
346 let stdout = io::stdout();
347 let mut stdout = stdout.lock();
348 stdout.queue(cursor::MoveTo(pos.x, pos.y))?;
349 self.maybe_flush(stdout)
350 }
351
352 fn move_within_line(&mut self, off: i16) -> io::Result<()> {
353 let stdout = io::stdout();
354 let mut stdout = stdout.lock();
355 match off.cmp(&0) {
356 Ordering::Less => stdout.queue(cursor::MoveLeft(-off as u16)),
357 Ordering::Equal => return Ok(()),
358 Ordering::Greater => stdout.queue(cursor::MoveRight(off as u16)),
359 }?;
360 self.maybe_flush(stdout)
361 }
362
363 fn print(&mut self, text: &str) -> io::Result<()> {
364 let text = remove_control_chars(text.to_owned());
365
366 let stdout = io::stdout();
367 let mut stdout = stdout.lock();
368 stdout.write_all(text.as_bytes())?;
369 if self.is_tty {
370 stdout.write_all(b"\r\n")?;
371 } else {
372 stdout.write_all(b"\n")?;
373 }
374 Ok(())
375 }
376
377 async fn poll_key(&mut self) -> io::Result<Option<Key>> {
378 (self as &mut dyn InputOps).poll_key().await
379 }
380
381 async fn read_key(&mut self) -> io::Result<Key> {
382 (self as &mut dyn InputOps).read_key().await
383 }
384
385 fn show_cursor(&mut self) -> io::Result<()> {
386 if !self.cursor_visible {
387 let stdout = io::stdout();
388 let mut stdout = stdout.lock();
389 stdout.queue(cursor::Show)?;
390 self.cursor_visible = true;
391 self.maybe_flush(stdout)
392 } else {
393 Ok(())
394 }
395 }
396
397 fn size_chars(&self) -> io::Result<CharsXY> {
398 let lines = get_env_var_as_u16("LINES");
402 let columns = get_env_var_as_u16("COLUMNS");
403 let size = match (lines, columns) {
404 (Some(l), Some(c)) => CharsXY::new(c, l),
405 (l, c) => {
406 let (actual_columns, actual_lines) = terminal::size()?;
407 CharsXY::new(c.unwrap_or(actual_columns), l.unwrap_or(actual_lines))
408 }
409 };
410 Ok(size)
411 }
412
413 fn write(&mut self, text: &str) -> io::Result<()> {
414 let text = remove_control_chars(text.to_owned());
415
416 let stdout = io::stdout();
417 let mut stdout = stdout.lock();
418 stdout.write_all(text.as_bytes())?;
419 self.maybe_flush(stdout)
420 }
421
422 fn sync_now(&mut self) -> io::Result<()> {
423 if self.sync_enabled { Ok(()) } else { io::stdout().flush() }
424 }
425
426 fn set_sync(&mut self, enabled: bool) -> io::Result<bool> {
427 if !self.sync_enabled {
428 io::stdout().flush()?;
429 }
430 let previous = self.sync_enabled;
431 self.sync_enabled = enabled;
432 Ok(previous)
433 }
434}