diff options
| author | Irene Knapp <ireneista@irenes.space> | 2026-03-27 03:04:59 -0700 |
|---|---|---|
| committer | Irene Knapp <ireneista@irenes.space> | 2026-03-27 03:04:59 -0700 |
| commit | 4997c2a4e4856b49668a3d1257e967597c7dfb92 (patch) | |
| tree | ca3e489cac81a176eed5cf3e9ceaa9949f4a9d88 /src/main.rs | |
| parent | 2dc0e6470d65762df6c57e28cc32ee8b69844867 (diff) | |
implement abstractions Terminal and Buffer
Terminal encapsulates most of the already-existing logic for drawing things Buffer corresponds directly to the user-facing concept of a buffer. it has facilities for creating a new empty one; for loading itself from a file; and for scanning its contents to compute the offsets to the start of each line. at the moment, the assumption is that buffers are always entirely in-memory, but this will not always be true, so significant attention has been paid to aysnc behavior. there's locks in a few places which may or may not turn out to be how it ultimately works, but they seem like a credible first attempt. the draw() routine is the heart of what exists so-far, doing all the really interesting stuff. Force-Push: yes Change-Id: Ifddc5debb12628233113c0bd6db3ea8cf10e6a5a
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 290 |
1 files changed, 128 insertions, 162 deletions
diff --git a/src/main.rs b/src/main.rs index f24bcbc..d0c07e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,37 +1,30 @@ #![forbid(unsafe_code)] +use crate::types::*; use smol::prelude::*; -use smol::{ unblock, Unblock, Timer }; -use smol::lock::{ OnceCell, RwLock }; -use std::fmt::Display; -use std::os::fd::AsRawFd; +use smol::Timer; +use smol::fs::File; +use smol::lock::RwLock; +use std::path::PathBuf; use std::process::ExitCode; +use std::ops::Range; use std::time::Duration; -use termios::Termios; -type Error = std::io::Error; -type Result<T> = std::result::Result<T, Error>; +mod terminal; +mod types; + struct Ivy { - stdin: Unblock<std::io::Stdin>, - stdout: Unblock<std::io::Stdout>, - initial_termios: OnceCell<Termios>, - width: RwLock<u64>, - height: RwLock<u64>, + terminal: RwLock<Terminal>, + buffer: RwLock<Buffer>, + argument_files: Vec<PathBuf>, } -enum EscapeType { - CSI, - CSIPrivate, -} - -impl EscapeType { - fn intro(&self) -> &'static str { - match self { - EscapeType::CSI => "\x1b[", - EscapeType::CSIPrivate => "\x1b[?", - } - } +struct Buffer { + path: Option<PathBuf>, + contents: RwLock<Vec<u8>>, + lines: RwLock<Vec<usize>>, + has_end_newline: RwLock<bool>, } @@ -50,36 +43,31 @@ fn main() -> ExitCode { 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), + terminal: RwLock::new(Terminal::new().await), + buffer: RwLock::new(Buffer::new().await), + argument_files: Vec::new(), } } 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?; - } + error = error.or(self.init_arguments().await.err()); + error = error.or(self.init_editor().await.err()); - self.do_cursor_position(0, 0).await?; + error = error.or(self.terminal.write().await + .init_termios().await.err()); + error = error.or(self.terminal.write().await + .init_full_screen().await.err()); - self.stdout.flush().await?; - - Ok(()) - }.err()); + error = error.or(self.draw().await.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()); + error = error.or(self.terminal.write().await + .zap_full_screen().await.err()); + error = error.or(self.terminal.write().await + .zap_termios().await.err()); if let Some(error) = error { Err(error) @@ -88,166 +76,144 @@ impl Ivy { } } - 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(); + async fn init_arguments(&mut self) -> Result<()> { + self.argument_files.clear(); - termios.c_lflag &= !(termios::ECHO - | termios::ECHONL - | termios::os::linux::ECHOCTL - | termios::ICANON); - termios.c_lflag |= termios::ISIG; + let mut args = std::env::args(); - termios.c_cc[termios::VMIN] = 1; - termios.c_cc[termios::VTIME] = 0; + let _ = args.next(); - unblock(move || { - let fd = std::io::stdin().as_raw_fd(); - termios::tcsetattr(fd, termios::TCSANOW, &mut termios) - }).await?; + for argument in args { + self.argument_files.push(argument.into()); + } 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?; + async fn init_editor(&mut self) -> Result<()> { + if !self.argument_files.is_empty() { + let path = self.argument_files.remove(0); + let new_buffer = Buffer::from_file(path).await?; + *self.buffer.write().await = new_buffer; + } Ok(()) } - async fn init_full_screen(&mut self) -> Result<()> { - self.do_start_alternate_screen().await?; - self.do_cursor_position(0, 0).await?; + async fn draw(&mut self) -> Result<()> { + let mut terminal = self.terminal.write().await; + let buffer = self.buffer.read().await; - let (width, height) = self.do_report_size().await?; - *self.width.write().await = width; - *self.height.write().await = height; + let height = *terminal.height.read().await; - Ok(()) - } + let mut screen_y = 0; + for logical_y in 0 .. height - 1 { + if let Some(span) = buffer.line_span(logical_y).await { + if logical_y > 0 { + terminal.stdout.write_all("\n".as_bytes()).await?; + } - async fn zap_full_screen(&mut self) -> Result<()> { - self.do_end_alternate_screen().await?; - self.stdout.flush().await?; + terminal.stdout.write_all(&buffer.contents.read().await[span]).await?; + + screen_y = logical_y + 1; + } else { + break; + } + } + + for _ in screen_y .. height - 1 { + terminal.stdout.write_all("\n~".as_bytes()).await?; + } + + terminal.stdout.write_all("\n status goes here".as_bytes()).await?; + + terminal.do_cursor_position(0, 0).await?; + terminal.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?; +impl Buffer { + pub async fn new() -> Self { + Buffer { + path: None, + contents: RwLock::new("\n".to_string().into_bytes()), + lines: RwLock::new(vec![0]), + has_end_newline: RwLock::new(true), } - - Ok(()) } - async fn read_report(&mut self, escape_type: EscapeType) - -> Result<(char, Vec<u64>)> - { - let mut expected = escape_type.intro(); + pub async fn from_file(path: PathBuf) -> Result<Self> { + let mut file = File::open(&path).await?; - while expected.len() > 0 { - let mut buf = vec![0; 1]; - self.stdin.read_exact(&mut buf).await?; + let mut contents = Vec::new(); + let _ = file.read_to_end(&mut contents).await?; - expected = &expected[1..]; - } + let mut result = Buffer { + path: Some(path), + contents: RwLock::new(contents), + lines: RwLock::new(vec![0]), + has_end_newline: RwLock::new(true), + }; - let mut values = Vec::new(); + result.count_lines().await; - let mut number: u64 = 0; - let mut in_number = false; - let mut after_semicolon = false; + Ok(result) + } - let mut buf = vec![0; 1]; - self.stdin.read_exact(&mut buf).await?; + pub async fn count_lines(&mut self) { + let contents = self.contents.read().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(); + *self.lines.write().await = vec![0]; - in_number = true; - after_semicolon = false; - } - } else if c == ';' { - if in_number { - values.push(number); + if contents.len() == 0 { + *self.has_end_newline.write().await = false; + } else { + let mut offset = 0; + + while offset < contents.len() { + let c = contents[offset]; + + if c == b'\n' { + offset += 1; - in_number = false; - after_semicolon = true; + if offset < contents.len() { + self.lines.write().await.push(offset); } else { - break; + *self.has_end_newline.write().await = true; } - } else if c.is_ascii_alphabetic() { - if in_number { - values.push(number); - } else if after_semicolon { - break; - } - - return Ok((c, values)) - } else { - break; + } else if offset + 1 == contents.len() { + *self.has_end_newline.write().await = false; } - } else { - break; - } - self.stdin.read_exact(&mut buf).await?; + offset += 1; + } } - - 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 - } + pub async fn line_span(&self, index: usize) -> Option<Range<usize>> { + let lines = self.lines.read().await; - // xterm - async fn do_end_alternate_screen(&mut self) -> Result<()> { - self.do_escape(EscapeType::CSIPrivate, "l", &[1049]).await - } + if index < lines.len() { + let start = lines[index]; - // 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 - } + if index + 1 < lines.len() { + let end = lines[index + 1] - 1; + + Some(start .. end) + } else { + let end = if *self.has_end_newline.read().await { + self.contents.read().await.len() - 1 + } else { + self.contents.read().await.len() + }; - // 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])) + Some(start .. end) + } } else { - Err(std::io::Error::other("Couldn't read terminal size.")) + None } } } |