From 2dc0e6470d65762df6c57e28cc32ee8b69844867 Mon Sep 17 00:00:00 2001 From: Irene Knapp Date: Thu, 26 Mar 2026 23:45:52 -0700 Subject: initial commit; draws the traditional tildes Force-Push: yes Change-Id: I18ada127a4c78e5ac74f5002ed3716c53a1e141b --- .envrc | 1 + .gitignore | 5 + Cargo.lock | 369 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 9 ++ flake.lock | 43 +++++++ flake.nix | 36 ++++++ src/main.rs | 253 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 716 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/main.rs diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a6e63d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/result +/result-* +/target +/.direnv +*.swp diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4c93618 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,369 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "ivy" +version = "0.1.0" +dependencies = [ + "smol", + "termios", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..782319c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ivy" +version = "0.1.0" +authors = ["Irene Knapp "] +edition = "2024" + +[dependencies] +smol = "2.0.2" +termios = "0.3.3" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..db6234f --- /dev/null +++ b/flake.lock @@ -0,0 +1,43 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1774313767, + "narHash": "sha256-hy0XTQND6avzGEUFrJtYBBpFa/POiiaGBr2vpU6Y9tY=", + "owner": "ipetkov", + "repo": "crane", + "rev": "3d9df76e29656c679c744968b17fbaf28f0e923d", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1774388614, + "narHash": "sha256-tFwzTI0DdDzovdE9+Ras6CUss0yn8P9XV4Ja6RjA+nU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1073dad219cb244572b74da2b20c7fe39cb3fa9e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..3109945 --- /dev/null +++ b/flake.nix @@ -0,0 +1,36 @@ +{ + inputs = { + nixpkgs = { + type = "github"; + owner = "NixOS"; + repo = "nixpkgs"; + ref = "nixos-25.11"; + }; + + crane = { + url = "github:ipetkov/crane"; + }; + }; + + outputs = { self, nixpkgs, crane }: + let supportedSystems = [ "aarch64-linux" "x86_64-linux" ]; + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); + in { + packages = forAllSystems (system: let pkgs = nixpkgsFor.${system}; in { + default = (crane.mkLib pkgs).buildPackage { + src = ./.; + }; + }); + + devShells = forAllSystems (system: let pkgs = nixpkgsFor.${system}; in { + default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + cargo + rustc + ]; + }; + }); + }; +} + 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 = 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.")) + } + } +} -- cgit 1.4.1