diff options
Diffstat (limited to 'src/terminal.rs')
| -rw-r--r-- | src/terminal.rs | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/src/terminal.rs b/src/terminal.rs new file mode 100644 index 0000000..78727c8 --- /dev/null +++ b/src/terminal.rs @@ -0,0 +1,210 @@ +#![forbid(unsafe_code)] +use crate::types::*; +use smol::prelude::*; + +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<std::io::Stdin>, + pub stdout: Unblock<std::io::Stdout>, + pub initial_termios: OnceCell<Termios>, + pub width: RwLock<usize>, + pub height: RwLock<usize>, +} + +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 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<u64>)> + { + 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 + + <u32 as Into<u64>>::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: u64, y: u64) -> 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[?", + } + } +} + |