#![forbid(unsafe_code)] use crate::types::*; use smol::prelude::*; use crate::encoding; use smol::{ unblock, Unblock }; use smol::io::{ BoxedReader, BoxedWriter }; use smol::lock::{ OnceCell, RwLock }; use std::fmt::Display; use std::os::fd::AsRawFd; use termios::Termios; pub struct Terminal { pub stdin: BoxedReader, pub stdout: BoxedWriter, pub initial_termios: OnceCell, pub width: RwLock, pub height: RwLock, } pub enum EscapeType { CSI, CSIPrivate, } impl Terminal { pub async fn new() -> Self { Terminal { stdin: Unblock::new(std::io::stdin()).boxed_reader(), stdout: Unblock::new(std::io::stdout()).boxed_writer(), initial_termios: OnceCell::new(), width: RwLock::new(80), height: RwLock::new(24), } } pub async fn init_termios(&mut self) -> Result<()> { let _ = self.initial_termios.set(unblock(|| { let fd = std::io::stdin().as_raw_fd(); Termios::from_fd(fd) }).await?).await; let mut termios = self.initial_termios.get().unwrap().clone(); termios.c_lflag &= !(termios::ECHO | termios::ECHONL | termios::os::linux::ECHOCTL | termios::ICANON); termios.c_lflag |= termios::ISIG; termios.c_cc[termios::VMIN] = 1; termios.c_cc[termios::VTIME] = 0; unblock(move || { let fd = std::io::stdin().as_raw_fd(); termios::tcsetattr(fd, termios::TCSANOW, &mut termios) }).await?; Ok(()) } pub async fn zap_termios(&mut self) -> Result<()> { let mut termios = self.initial_termios.get().unwrap().clone(); unblock(move || { let fd = std::io::stdin().as_raw_fd(); termios::tcsetattr(fd, termios::TCSANOW, &mut termios) }).await?; Ok(()) } pub async fn init_full_screen(&mut self) -> Result<()> { self.do_start_alternate_screen().await?; self.do_cursor_position(0, 0).await?; let (width, height) = self.do_report_size().await?; *self.width.write().await = width.try_into().unwrap(); *self.height.write().await = height.try_into().unwrap(); self.set_scroll_region(0, height - 1).await?; Ok(()) } pub async fn zap_full_screen(&mut self) -> Result<()> { let (width, height) = self.do_report_size().await?; *self.width.write().await = width.try_into().unwrap(); *self.height.write().await = height.try_into().unwrap(); self.set_scroll_region(0, height).await?; self.do_end_alternate_screen().await?; self.stdout.flush().await?; Ok(()) } pub async fn read_char(&mut self) -> Result { encoding::read_utf8_char(&mut self.stdin).await } pub async fn do_escape(&mut self, escape_type: EscapeType, code: &str, parameters: &[impl Display]) -> Result<()> { self.stdout.write_all(escape_type.intro().as_bytes()).await?; for (index, parameter) in parameters.iter().enumerate() { self.stdout.write_all(if index < parameters.len() - 1 { format!("{};", parameter) } else { format!("{}{}", parameter, code) }.as_bytes()).await?; } Ok(()) } pub async fn read_report(&mut self, escape_type: EscapeType) -> Result<(char, Vec)> { let mut expected = escape_type.intro(); while expected.len() > 0 { let mut buf = vec![0; 1]; self.stdin.read_exact(&mut buf).await?; expected = &expected[1..]; } let mut values = Vec::new(); let mut number: usize = 0; let mut in_number = false; let mut after_semicolon = false; let mut buf = vec![0; 1]; self.stdin.read_exact(&mut buf).await?; loop { if let Ok(string) = std::str::from_utf8(&buf) && let Some(c) = string.chars().next() { if c.is_ascii_digit() { if in_number { number = number * 10 + >::try_into( c.to_digit(10).unwrap()).unwrap(); } else { number = >::try_into( c.to_digit(10).unwrap()).unwrap(); in_number = true; after_semicolon = false; } } else if c == ';' { if in_number { values.push(number); in_number = false; after_semicolon = true; } else { break; } } else if c.is_ascii_alphabetic() { if in_number { values.push(number); } else if after_semicolon { break; } return Ok((c, values)) } else { break; } } else { break; } self.stdin.read_exact(&mut buf).await?; } Err(std::io::Error::other("Invalid report from terminal.")) } // xterm pub async fn do_start_alternate_screen(&mut self) -> Result<()> { self.do_escape(EscapeType::CSIPrivate, "h", &[1049]).await } // xterm pub async fn do_end_alternate_screen(&mut self) -> Result<()> { self.do_escape(EscapeType::CSIPrivate, "l", &[1049]).await } // vt220? vt100? pub async fn do_cursor_position(&mut self, x: usize, y: usize) -> Result<()> { self.do_escape(EscapeType::CSI, "H", &[y + 1, x + 1]).await } // dtterm? xterm pub async fn do_report_size(&mut self) -> Result<(usize, usize)> { self.do_escape(EscapeType::CSI, "t", &[18]).await?; self.stdout.flush().await?; let (code, values) = self.read_report(EscapeType::CSI).await?; if code == 't' && values.len() == 3 && values[0] == 8 { Ok((values[2], values[1])) } else { Err(std::io::Error::other("Couldn't read terminal size.")) } } // vt100 pub async fn clear(&mut self) -> Result<()> { self.do_escape(EscapeType::CSI, "J", &[2]).await } // vt420, ECMA-48 pub async fn scroll_up(&mut self, distance: usize) -> Result<()> { self.do_escape(EscapeType::CSI, "S", &[distance]).await } // vt420, ECMA-48 pub async fn scroll_down(&mut self, distance: usize) -> Result<()> { self.do_escape(EscapeType::CSI, "T", &[distance]).await } pub async fn scroll(&mut self, distance_up: isize) -> Result<()> { if distance_up >= 0 { self.scroll_up(distance_up as usize).await } else { self.scroll_down(-distance_up as usize).await } } // vt100 pub async fn set_scroll_region(&mut self, top: usize, bottom: usize) -> Result<()> { self.do_escape(EscapeType::CSI, "r", &[top, bottom]).await } } impl EscapeType { fn intro(&self) -> &'static str { match self { EscapeType::CSI => "\x1b[", EscapeType::CSIPrivate => "\x1b[?", } } }