summary refs log tree commit diff
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs271
1 files changed, 271 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..3bcec63
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,271 @@
+#![forbid(unsafe_code)]
+use crate::prelude::*;
+
+use crate::decoding::CharBufReader;
+
+use nix::sys::termios::{self, Termios};
+use std::os::unix::io::{AsRawFd, RawFd};
+use std::sync::Arc;
+use tokio::io::{self, AsyncRead, AsyncWriteExt};
+use tokio::signal::unix::{signal, SignalKind};
+use tokio::sync::{mpsc, Mutex};
+use tokio::task;
+
+pub mod decoding;
+pub mod error;
+pub mod prelude;
+
+
+#[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)]
+struct Point {
+  x: usize,
+  y: usize,
+}
+
+impl Point {
+  pub fn new(x: usize, y: usize) -> Point {
+    Point {
+      x: x,
+      y: y,
+    }
+  }
+}
+
+
+#[derive(Debug,Eq,PartialEq)]
+struct LineBuffer {
+  lines: Vec<String>,
+  cursor: Point,
+}
+
+impl LineBuffer {
+  pub fn new() -> LineBuffer {
+    LineBuffer {
+      lines: vec![String::new()],
+      cursor: Point::new(0, 0),
+    }
+  }
+
+  pub fn insert(&mut self, input: &str) {
+    let line = &mut self.lines[self.cursor.y];
+    line.replace_range(self.cursor.x .. self.cursor.x, input);
+    self.cursor.x += input.len();
+  }
+
+  pub fn backspace(&mut self) {
+    if self.cursor.x > 0 {
+      let line = &mut self.lines[self.cursor.y];
+      line.replace_range(self.cursor.x - 1 .. self.cursor.x, "");
+      self.cursor.x -= 1;
+    } else if self.cursor.y > 0 {
+      let second_line = self.lines.remove(self.cursor.y);
+      let first_line = &mut self.lines[self.cursor.y - 1];
+
+      self.cursor.x = first_line.len();
+      self.cursor.y -= 1;
+
+      first_line.push_str(&second_line);
+    }
+  }
+
+  pub fn as_string(&self) -> String {
+    self.lines.join("\n")
+  }
+
+  pub fn cursor_to_end_of_line(&self) -> &str {
+    &self.lines[self.cursor.y][self.cursor.x ..]
+  }
+}
+
+
+pub enum Input {
+  String(String),
+  End,
+}
+
+
+#[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)]
+enum InputAction {
+  Backspace,
+  Execute,
+}
+
+
+pub struct Terminal<InputStream: AsyncRead + Unpin> {
+  reader: Arc<Mutex<CharBufReader<InputStream>>>,
+  line_buffer: LineBuffer,
+  file_descriptor: RawFd,
+  initial_termios: Termios,
+  interrupt_receiver: mpsc::Receiver<()>,
+}
+
+
+async fn handle_signals(interrupt_sender: mpsc::Sender<()>) -> Result<()>
+{
+  let mut stream = signal(SignalKind::interrupt()).map_err(error::internal)?;
+  /* TODO make it work on other signals:
+    SignalKind::hangup();
+    SignalKind::interrupt();
+    SignalKind::terminate();
+    SignalKind::quit();
+    */
+
+  loop {
+    stream.recv().await;
+    interrupt_sender.send(()).await.map_err(error::internal)?;
+  }
+}
+
+
+impl<InputStream: AsyncRead + AsRawFd + Unpin> Terminal<InputStream> {
+  pub fn init(input_stream: InputStream) -> Result<Terminal<InputStream>> {
+    let (interrupt_sender, interrupt_receiver) = mpsc::channel(1);
+
+    let _ = task::spawn(handle_signals(interrupt_sender));
+
+    let fd = input_stream.as_raw_fd();
+    let termios = termios::tcgetattr(fd).map_err(error::mode_setting)?;
+    let reader = Arc::new(Mutex::new(CharBufReader::new(input_stream)));
+    let line_buffer = LineBuffer::new();
+
+    let terminal = Terminal {
+      reader: reader,
+      line_buffer: line_buffer,
+      file_descriptor: fd,
+      initial_termios: termios,
+      interrupt_receiver: interrupt_receiver,
+    };
+
+    terminal.init_modes()?;
+
+    Ok(terminal)
+  }
+
+
+  pub fn cleanup(self) -> Result<()> {
+    self.cleanup_modes()?;
+
+    Ok(())
+  }
+
+
+  fn init_modes(&self) -> Result<()> {
+    let mut termios = self.initial_termios.clone();
+
+    termios.local_flags.remove(termios::LocalFlags::ECHO);
+    termios.local_flags.remove(termios::LocalFlags::ECHONL);
+    termios.local_flags.remove(termios::LocalFlags::ECHOCTL);
+    termios.local_flags.remove(termios::LocalFlags::ICANON);
+
+    termios.local_flags.insert(termios::LocalFlags::ISIG);
+
+    termios.control_chars[termios::SpecialCharacterIndices::VMIN as usize] = 1;
+    termios.control_chars[termios::SpecialCharacterIndices::VTIME as usize] = 0;
+
+    termios::tcsetattr(self.file_descriptor,
+                       termios::SetArg::TCSANOW,
+                       &termios)
+        .map_err(error::mode_setting)?;
+
+    Ok(())
+  }
+
+
+  pub fn cleanup_modes(&self) -> Result<()> {
+    let termios = self.initial_termios.clone();
+
+    termios::tcsetattr(self.file_descriptor,
+                       termios::SetArg::TCSANOW,
+                       &termios)
+        .map_err(error::mode_setting)?;
+
+    Ok(())
+  }
+
+
+  pub async fn handle_input(&mut self) -> Result<Input>
+  {
+    let mut stdout = io::stdout();
+
+    loop {
+      let mut action: Option<InputAction> = None;
+
+      let string = tokio::select! {
+        result = CharBufReader::fill_buf(Arc::clone(&self.reader)) => {
+          let string: String = result.map_err(error::input)?;
+          string
+        }
+        _ = self.interrupt_receiver.recv() => {
+          return Ok(Input::End);
+        }
+      };
+
+      let mut chars = string.char_indices();
+
+      for (_, c) in &mut chars {
+        match c {
+          '\n' => {
+            action = Some(InputAction::Execute);
+          }
+          '\u{7f}' => {
+            action = Some(InputAction::Backspace);
+          }
+          _ => {
+            self.line_buffer.insert(&c.to_string());
+
+            stdout.write_all(format!("{}", c).as_bytes()).await
+                .map_err(error::internal)?;
+            stdout.flush().await.map_err(error::internal)?;
+          }
+        }
+
+        if action.is_some() {
+          break;
+        }
+      }
+
+      let n_to_consume = match chars.next() {
+        Some((offset, _)) => offset,
+        None => string.len(),
+      };
+
+      Arc::get_mut(&mut self.reader).unwrap().lock().await.consume(n_to_consume);
+
+      match action {
+        Some(InputAction::Execute) => {
+          break;
+        }
+        Some(InputAction::Backspace) => {
+          self.line_buffer.backspace();
+
+          stdout.write_all(format!("\u{08}\u{1b}[1X{}",
+                                   self.line_buffer.cursor_to_end_of_line())
+                           .as_bytes()).await.map_err(error::internal)?;
+          stdout.flush().await.map_err(error::internal)?;
+        }
+        None => { }
+      }
+    }
+
+    println!("line buffer {:?}", self.line_buffer);
+    let input = Input::String(self.line_buffer.as_string());
+    Ok(input)
+  }
+}
+
+
+/*
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use std::io::Cursor;
+
+  #[test]
+  fn test_empty_input() {
+    let buffer = Cursor::new(vec![]);
+    let terminal = Terminal::init(buffer).unwrap();
+    let result = terminal.handle_input();
+    assert!(result.is_ok());
+  }
+}
+*/