commit 53bc25a59653b618f5ba774006522faaf00a2f68 Author: pjht Date: Mon Sep 23 14:44:16 2024 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..93f3335 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,30 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "line_discipline" +version = "0.1.0" +dependencies = [ + "itertools", + "termios", +] + +[[package]] +name = "termios" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..852c358 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "line_discipline" +version = "0.1.0" +edition = "2021" + +[dependencies] +itertools = "0.13.0" +termios = { version = "0.1.0", path = "../termios" } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f860500 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,185 @@ +use itertools::Itertools; +pub use termios::Termios; + +pub struct LineDiscipline { + input_buffer: Vec, + output_buffer: Vec, + termios: Termios, + output_paused: bool, + column: usize, +} + +impl LineDiscipline { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + LineDiscipline { + input_buffer: Vec::new(), + output_buffer: Vec::new(), + termios: Termios::default(), + output_paused: false, + column: 0, + } + } + + /// Processes input from the terminal and returns data to be read by the program plus any data + /// to be sent (if xon/xoff flow control is used) + pub fn process_terminal_input(&mut self, data: &[u8]) { + if !self.termios.cflags.cread { + return; + } + for &(mut ch) in data { + if self.termios.iflags.istrip { + ch &= 0x7F; + } + if ch == b'\r' && self.termios.iflags.igncr { + continue; + } else if ch == b'\r' && self.termios.iflags.icrnl { + ch = b'\n'; + } else if ch == b'\n' && self.termios.iflags.inlcr { + ch = b'\r'; + } + if ch as char == self.termios.verase && self.termios.lflags.icanon { + let buf_last = self.input_buffer.last().copied(); + if let Some(erased_ch) = buf_last { + let erased_ch = erased_ch as char; + if erased_ch != '\n' { + self.input_buffer.pop(); + self.erase_char(erased_ch); + } + } + } else if ch as char == self.termios.vkill && self.termios.lflags.icanon { + let buf_last = self.input_buffer.last().copied(); + while let Some(erased_ch) = buf_last { + let erased_ch = erased_ch as char; + if erased_ch != '\n' { + self.input_buffer.pop(); + self.erase_char(erased_ch); + } + } + } else if ch as char == self.termios.vstop && self.termios.iflags.ixon { + self.output_paused = true; + } else if (ch as char == self.termios.vstart && self.termios.iflags.ixon) + || (self.termios.iflags.ixany && self.output_paused) + { + self.output_paused = false; + } else { + self.input_buffer.push(ch); + if self.termios.lflags.echo || (ch == b'\n' && self.termios.lflags.echonl) { + if Self::is_nonprinting_control(ch as char) { + self.output_buffer.push(b'^'); + self.output_buffer.push(ch + b'@'); + self.column += 2; + } else if ch == b'\t' { + let curr_col = self.column; + let num_cols = 8 - (curr_col % 8); + for _ in 0..num_cols { + self.output_buffer.push(b' '); + } + self.column += num_cols; + } else { + self.output_buffer.push(ch); + self.column += 1; + } + } + } + } + } + + /// Processes output from the program and returns data to write to the terminal + pub fn process_program_output(&mut self, data: &[u8]) { + for &byte in data { + if self.termios.oflags.opost { + if byte == b'\n' && self.termios.oflags.onlcr { + if self.column != 0 { + self.output_buffer.push(b'\r'); + } + self.output_buffer.push(b'\n'); + self.column = 0; + } else if byte == b'\r' && self.termios.oflags.ocrnl { + self.output_buffer.push(b'\n'); + if self.termios.oflags.onlret { + self.column = 0; + } + } else { + self.output_buffer.push(byte); + } + } else if (byte == b'\n' && self.termios.oflags.onlret) || byte == b'\r' { + self.column = 0; + } + } + } + + pub fn get_terminal_output(&mut self) -> Vec { + if self.output_paused { + Vec::new() + } else { + std::mem::take(&mut self.output_buffer) + } + } + + pub fn get_program_input(&mut self) -> Vec { + if self.termios.lflags.icanon { + if let Some(last_full_line) = self.input_buffer.iter().positions(|&x| x == b'\n').last() { + let rem_buf = self.input_buffer.split_off(last_full_line + 1); + std::mem::replace(&mut self.input_buffer, rem_buf) + } else { + Vec::new() + } + } else { + std::mem::take(&mut self.input_buffer) + } + } + + fn is_nonprinting_control(ch: char) -> bool { + ch.is_ascii_control() && ch != '\r' && ch != '\n' && ch != '\t' + } + + pub fn termios(&self) -> &Termios { + &self.termios + } + + pub fn set_termios(&mut self, termios: Termios) { + self.termios = termios; + } + + fn erase_char(&mut self, ch: char) { + if !self.termios.lflags.echoe { + return; + } + if Self::is_nonprinting_control(ch) { + self.output_buffer.push(8); + self.output_buffer.push(8); + self.column -= 2; + } else if ch == '\t' { + let curr_col = self.column; + let mut before_tab_cols = 0; + let mut after_tab = false; + for &byte in self.input_buffer.iter().rev() { + if Self::is_nonprinting_control(byte as char) { + before_tab_cols += 2; + } else if byte == b'\t' { + after_tab = true; + break; + } else { + before_tab_cols += 1; + } + } + let num_cols = if after_tab { + 8 - before_tab_cols + } else { + curr_col - before_tab_cols + }; + for _ in 0..num_cols { + self.output_buffer.push(8); + } + self.column -= num_cols; + } else { + self.output_buffer.push(8); + self.column -= 1; + } + } + + pub fn output_paused(&self) -> bool { + self.output_paused + } +}