#![forbid(unsafe_code)] use crate::types::*; use smol::prelude::*; use crate::encoding; use smol::{ unblock, Unblock }; use smol::lock::{ OnceCell, RwLock }; use std::fmt::Display; use std::os::fd::AsRawFd; use termios::Termios; pub struct Terminal { pub stdin: Unblock, pub stdout: Unblock, 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()), stdout: Unblock::new(std::io::stdout()), 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(); Ok(()) } pub async fn zap_full_screen(&mut self) -> Result<()> { self.do_end_alternate_screen().await?; self.stdout.flush().await?; Ok(()) } pub async fn read_char(&mut self) -> Result { let mut buf = vec![0; 4]; loop { self.stdin.read_exact(&mut buf[0 .. 1]).await?; match encoding::get_utf8_byte_type(buf[0]) { UTF8ByteType::Single => { }, UTF8ByteType::Introducer(2) => { self.stdin.read_exact(&mut buf[1 .. 2]).await?; }, UTF8ByteType::Introducer(3) => { self.stdin.read_exact(&mut buf[1 .. 3]).await?; }, UTF8ByteType::Introducer(4) => { self.stdin.read_exact(&mut buf[1 .. 4]).await?; }, /* If it's not the start of a valid character, ignore it. */ _ => continue, } if let Ok(string) = std::str::from_utf8(&buf) && let Some(c) = string.chars().next() { return Ok(c); } } } 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: u64 = 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 + >::into(c.to_digit(10).unwrap()); } else { number = c.to_digit(10).unwrap().into(); 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<(u64, u64)> { 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.")) } } } impl EscapeType { fn intro(&self) -> &'static str { match self { EscapeType::CSI => "\x1b[", EscapeType::CSIPrivate => "\x1b[?", } } }