summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs290
-rw-r--r--src/terminal.rs210
-rw-r--r--src/types.rs6
3 files changed, 344 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
     }
   }
 }
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[?",
+    }
+  }
+}
+
diff --git a/src/types.rs b/src/types.rs
new file mode 100644
index 0000000..ef45492
--- /dev/null
+++ b/src/types.rs
@@ -0,0 +1,6 @@
+#![forbid(unsafe_code)]
+pub use crate::terminal::Terminal;
+
+pub type Error = std::io::Error;
+pub type Result<T> = std::result::Result<T, Error>;
+