summary refs log tree commit diff
path: root/src/terminal.rs
diff options
context:
space:
mode:
authorIrene Knapp <ireneista@irenes.space>2026-03-27 03:04:59 -0700
committerIrene Knapp <ireneista@irenes.space>2026-03-27 03:04:59 -0700
commit4997c2a4e4856b49668a3d1257e967597c7dfb92 (patch)
treeca3e489cac81a176eed5cf3e9ceaa9949f4a9d88 /src/terminal.rs
parent2dc0e6470d65762df6c57e28cc32ee8b69844867 (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/terminal.rs')
-rw-r--r--src/terminal.rs210
1 files changed, 210 insertions, 0 deletions
diff --git a/src/terminal.rs b/src/terminal.rs
new file mode 100644
index 0000000..78727c8
--- /dev/null
+++ b/src/terminal.rs
@@ -0,0 +1,210 @@
+#![forbid(unsafe_code)]
+use crate::types::*;
+use smol::prelude::*;
+
+use smol::{ unblock, Unblock };
+use smol::lock::{ OnceCell, RwLock };
+use std::fmt::Display;
+use std::os::fd::AsRawFd;
+use termios::Termios;
+
+
+pub struct Terminal {
+  pub stdin: Unblock<std::io::Stdin>,
+  pub stdout: Unblock<std::io::Stdout>,
+  pub initial_termios: OnceCell<Termios>,
+  pub width: RwLock<usize>,
+  pub height: RwLock<usize>,
+}
+
+pub enum EscapeType {
+  CSI,
+  CSIPrivate,
+}
+
+
+impl Terminal {
+  pub async fn new() -> Self {
+    Terminal {
+      stdin: Unblock::new(std::io::stdin()),
+      stdout: Unblock::new(std::io::stdout()),
+      initial_termios: OnceCell::new(),
+      width: RwLock::new(80),
+      height: RwLock::new(24),
+    }
+  }
+
+  pub 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(())
+  }
+
+  pub 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(())
+  }
+
+  pub 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.try_into().unwrap();
+    *self.height.write().await = height.try_into().unwrap();
+
+    Ok(())
+  }
+
+  pub async fn zap_full_screen(&mut self) -> Result<()> {
+    self.do_end_alternate_screen().await?;
+    self.stdout.flush().await?;
+
+    Ok(())
+  }
+
+  pub 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(())
+  }
+
+  pub 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
+  pub async fn do_start_alternate_screen(&mut self) -> Result<()> {
+    self.do_escape(EscapeType::CSIPrivate, "h", &[1049]).await
+  }
+
+  // xterm
+  pub async fn do_end_alternate_screen(&mut self) -> Result<()> {
+    self.do_escape(EscapeType::CSIPrivate, "l", &[1049]).await
+  }
+
+  // vt220? vt100?
+  pub 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
+  pub 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."))
+    }
+  }
+}
+
+
+impl EscapeType {
+  fn intro(&self) -> &'static str {
+    match self {
+      EscapeType::CSI => "\x1b[",
+      EscapeType::CSIPrivate => "\x1b[?",
+    }
+  }
+}
+