#![forbid(unsafe_code)] use smol::prelude::*; use smol::{ unblock, Unblock, Timer }; use smol::lock::{ OnceCell, RwLock }; use std::fmt::Display; use std::os::fd::AsRawFd; use std::process::ExitCode; use std::time::Duration; use termios::Termios; type Error = std::io::Error; type Result = std::result::Result; struct Ivy { stdin: Unblock, stdout: Unblock, initial_termios: OnceCell, width: RwLock, height: RwLock, } enum EscapeType { CSI, CSIPrivate, } impl EscapeType { fn intro(&self) -> &'static str { match self { EscapeType::CSI => "\x1b[", EscapeType::CSIPrivate => "\x1b[?", } } } fn main() -> ExitCode { smol::block_on(async { if let Err(e) = Ivy::new().await.run().await { println!("{:?}", e); ExitCode::FAILURE } else { ExitCode::SUCCESS } }) } impl Ivy { async fn new() -> Self { Ivy { stdin: Unblock::new(std::io::stdin()), stdout: Unblock::new(std::io::stdout()), initial_termios: OnceCell::new(), width: RwLock::new(80), height: RwLock::new(24), } } async fn run(mut self) -> Result<()> { let mut error = None; error = error.or(self.init_termios().await.err()); error = error.or(self.init_full_screen().await.err()); error = error.or({ for _ in 1 .. *self.height.read().await { self.stdout.write_all("\n~".as_bytes()).await?; } self.do_cursor_position(0, 0).await?; self.stdout.flush().await?; Ok(()) }.err()); Timer::after(Duration::from_millis(1000)).await; error = error.or(self.zap_full_screen().await.err()); error = error.or(self.zap_termios().await.err()); if let Some(error) = error { Err(error) } else { Ok(()) } } 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(()) } 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(()) } 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; *self.height.write().await = height; Ok(()) } async fn zap_full_screen(&mut self) -> Result<()> { self.do_end_alternate_screen().await?; self.stdout.flush().await?; Ok(()) } 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(()) } 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 async fn do_start_alternate_screen(&mut self) -> Result<()> { self.do_escape(EscapeType::CSIPrivate, "h", &[1049]).await } // xterm async fn do_end_alternate_screen(&mut self) -> Result<()> { self.do_escape(EscapeType::CSIPrivate, "l", &[1049]).await } // vt220? vt100? 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 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.")) } } }