diff options
author | Irene Knapp <ireneista@irenes.space> | 2024-03-12 22:06:24 -0700 |
---|---|---|
committer | Irene Knapp <ireneista@irenes.space> | 2024-03-12 22:06:24 -0700 |
commit | f25a79763b9ae493598230a109eb0788def17199 (patch) | |
tree | dfcee3742e7086985c7392944b0b98308c89b2c7 /src/lib.rs | |
parent | 7be9acd0bb08901c9fdfa45b694b7d3d5a594e70 (diff) |
Change-Id: I3478f222ee4b24d9d1796f0f58986186119bc2f7
Diffstat (limited to 'src/lib.rs')
-rw-r--r-- | src/lib.rs | 271 |
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()); + } +} +*/ |