diff options
| author | Irene Knapp <ireneista@irenes.space> | 2026-03-26 23:45:52 -0700 |
|---|---|---|
| committer | Irene Knapp <ireneista@irenes.space> | 2026-03-26 23:46:35 -0700 |
| commit | 2dc0e6470d65762df6c57e28cc32ee8b69844867 (patch) | |
| tree | efe4af98afbbefd6881632ef7614cbd8104a677e /src/main.rs | |
initial commit; draws the traditional tildes
Force-Push: yes Change-Id: I18ada127a4c78e5ac74f5002ed3716c53a1e141b
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 253 |
1 files changed, 253 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f24bcbc --- /dev/null +++ b/src/main.rs @@ -0,0 +1,253 @@ +#![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<T> = std::result::Result<T, Error>; + +struct Ivy { + stdin: Unblock<std::io::Stdin>, + stdout: Unblock<std::io::Stdout>, + initial_termios: OnceCell<Termios>, + width: RwLock<u64>, + height: RwLock<u64>, +} + +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<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 + 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.")) + } + } +} |