summary refs log tree commit diff
path: root/src/main.rs
diff options
context:
space:
mode:
authorIrene Knapp <ireneista@irenes.space>2026-03-26 23:45:52 -0700
committerIrene Knapp <ireneista@irenes.space>2026-03-26 23:46:35 -0700
commit2dc0e6470d65762df6c57e28cc32ee8b69844867 (patch)
treeefe4af98afbbefd6881632ef7614cbd8104a677e /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.rs253
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."))
+    }
+  }
+}