aboutsummaryrefslogtreecommitdiff
path: root/src/common/term.rs
diff options
context:
space:
mode:
authorevuez <julien@mulga.net>2022-11-26 15:38:06 -0500
committerevuez <julien@mulga.net>2024-04-03 22:44:12 +0200
commit86098797034cbc7eb6db0cee54e17f8dcaedbc5d (patch)
tree29b6225ead843eb9022296a54657bbadfa1c4da0 /src/common/term.rs
downloadblom-86098797034cbc7eb6db0cee54e17f8dcaedbc5d.tar.gz
Initial commitHEADmain
Diffstat (limited to 'src/common/term.rs')
-rw-r--r--src/common/term.rs517
1 files changed, 517 insertions, 0 deletions
diff --git a/src/common/term.rs b/src/common/term.rs
new file mode 100644
index 0000000..44fd819
--- /dev/null
+++ b/src/common/term.rs
@@ -0,0 +1,517 @@
+#![allow(clippy::manual_range_contains)]
+#![allow(clippy::type_complexity)]
+use libc::BRKINT;
+use libc::CS8;
+use libc::ECHO;
+use libc::ICANON;
+use libc::ICRNL;
+use libc::IEXTEN;
+use libc::INPCK;
+use libc::ISIG;
+use libc::ISTRIP;
+use libc::IXON;
+use libc::OPOST;
+use libc::TCSAFLUSH;
+use libc::TIOCGWINSZ;
+use libc::VMIN;
+use libc::VTIME;
+use log::trace;
+use std::cmp::max;
+use std::cmp::min;
+use std::fs::File;
+use std::io::Read;
+use std::io::Write;
+use std::mem;
+use std::os::unix::io::AsRawFd;
+
+enum Key {
+ Null = 0,
+ CtrlA = 1,
+ CtrlB = 2,
+ CtrlC = 3,
+ CtrlD = 4,
+ CtrlE = 5,
+ CtrlF = 6,
+ CtrlH = 8,
+ Tab = 9,
+ CtrlK = 11,
+ CtrlL = 12,
+ Enter = 13,
+ CtrlN = 14,
+ CtrlP = 16,
+ CtrlT = 20,
+ CtrlU = 21,
+ CtrlW = 23,
+ Esc = 27,
+ Backspace = 127,
+}
+
+pub struct Term {
+ initial_termios: Option<libc::termios>,
+ tty_in: File,
+ tty_out: File,
+ history: Vec<Vec<u8>>,
+ history_cursor: usize,
+ column: usize,
+ line: Vec<u8>,
+ line_cursor: usize,
+ prompt: Option<Vec<u8>>,
+ hints_callback: Option<fn(&[u8]) -> &[u8]>,
+ columns: usize,
+ max_rows: usize,
+}
+
+#[derive(Debug)]
+struct Position {
+ column: usize,
+ #[allow(dead_code)]
+ row: usize,
+}
+
+impl Term {
+ pub fn new() -> Self {
+ let tty_out = File::options()
+ .read(true)
+ .write(true)
+ .open("/dev/tty")
+ .unwrap();
+ let tty_in = tty_out.try_clone().unwrap();
+
+ Self {
+ initial_termios: None,
+ tty_out,
+ tty_in,
+ history: Vec::new(),
+ history_cursor: 0,
+ column: 0,
+ line: Vec::new(),
+ line_cursor: 0,
+ prompt: None,
+ hints_callback: None,
+ columns: 0,
+ max_rows: 0,
+ }
+ }
+
+ pub fn setup_hints(&mut self, hints_callback: fn(&[u8]) -> &[u8]) {
+ self.hints_callback = Some(hints_callback);
+ }
+
+ pub fn setup_prompt(&mut self, prompt: &[u8]) {
+ self.prompt = Some(prompt.to_vec());
+ }
+
+ pub fn edit(&mut self) -> Option<Vec<u8>> {
+ self.enable_raw_mode();
+
+ self.columns = self.get_columns();
+ self.column = self.get_cursor_position().unwrap().column;
+
+ self.refresh_line();
+
+ loop {
+ let mut buffer: [u8; 1] = [0];
+
+ if self.tty_in.read(&mut buffer).unwrap_or(0) != 1 {
+ break;
+ }
+
+ match buffer[0] {
+ x if (x == Key::Tab as u8) => {
+ todo!("PRESSED TAB");
+ }
+ x if (x == Key::CtrlC as u8) => {
+ self.tty_out.write_all(b"\r\n").unwrap();
+ self.disable_raw_mode();
+ std::process::exit(1);
+ }
+ x if (x == Key::Enter as u8) => {
+ let line = self.line.clone();
+
+ self.history_cursor = self.history.len();
+ self.history.push(self.line.clone());
+
+ self.line.clear();
+ self.line_cursor = 0;
+
+ self.tty_out.write_all(b"\r\n").unwrap();
+ self.disable_raw_mode();
+
+ return Some(line);
+ }
+ x if (x == Key::Backspace as u8) => {
+ if self.line_cursor < 1 {
+ continue;
+ }
+
+ self.line_cursor -= 1;
+ self.line.remove(self.line_cursor);
+
+ self.refresh_line();
+ }
+ x if (x == Key::Null as u8) => {}
+ x if (x == Key::CtrlD as u8) => {
+ if !self.line.is_empty() {
+ self.delete_right();
+ } else {
+ self.tty_out.write_all(b"\r\n").unwrap();
+ self.disable_raw_mode();
+ std::process::exit(1);
+ }
+ }
+ x if (x == Key::CtrlA as u8) => self.move_to_start(),
+ x if (x == Key::CtrlE as u8) => self.move_to_end(),
+ x if (x == Key::CtrlB as u8) => self.move_left(),
+ x if (x == Key::CtrlF as u8) => self.move_right(),
+ x if (x == Key::CtrlH as u8) => self.delete_left(),
+ x if (x == Key::CtrlK as u8) => self.delete_to_end(),
+ x if (x == Key::CtrlL as u8) => {
+ self.clear_screen();
+ self.refresh_line();
+ }
+ // Next line in history
+ x if (x == Key::CtrlN as u8) => self.history_next(),
+ // Previous line in history
+ x if (x == Key::CtrlP as u8) => self.history_prev(),
+ x if (x == Key::CtrlT as u8) => self.swap_one(),
+ x if (x == Key::CtrlU as u8) => self.delete_to_start(),
+ x if (x == Key::CtrlW as u8) => self.delete_word_left(),
+ x if (x == Key::Esc as u8) => {
+ let mut seq: [u8; 1] = [0];
+ if self.tty_in.read(&mut seq).unwrap_or(0) != 1 {
+ continue;
+ }
+ if seq[0] != b'[' {
+ // FIXME: Handle Esc+Key / "Esc 0" sequences
+ continue;
+ }
+ if self.tty_in.read(&mut seq).unwrap_or(0) != 1 {
+ break;
+ }
+ if seq[0] >= b'0' && seq[0] <= b'9' {
+ // FIXME: Handle extended escape sequence
+ continue;
+ }
+ match seq[0] {
+ // Up
+ b'A' => self.history_prev(),
+ // Down
+ b'B' => self.history_next(),
+ // Right
+ b'C' => self.move_right(),
+ // Left
+ b'D' => self.move_left(),
+ // Home
+ b'H' => self.move_to_start(),
+ // End
+ b'F' => self.move_to_end(),
+ _ => break,
+ }
+ }
+ x => {
+ self.line.insert(self.line_cursor, x);
+ self.line_cursor += 1;
+ self.refresh_line();
+ }
+ }
+
+ self.tty_out.flush().unwrap();
+ }
+
+ self.disable_raw_mode();
+
+ None
+ }
+
+ fn history_next(&mut self) {
+ if self.history.is_empty() {
+ return;
+ }
+
+ self.line = self.history[self.history_cursor].clone();
+ self.line_cursor = self.line.len();
+
+ self.refresh_line();
+
+ self.history_cursor = min(self.history_cursor + 1, self.history.len() - 1);
+ }
+
+ fn history_prev(&mut self) {
+ if self.history.is_empty() {
+ return;
+ }
+
+ // FIXME: Save current line before replacing it with line for the history.
+
+ self.line = self.history[self.history_cursor].clone();
+ self.line_cursor = self.line.len();
+
+ self.refresh_line();
+
+ self.history_cursor = self.history_cursor.saturating_sub(1);
+ }
+
+ fn move_right(&mut self) {
+ self.line_cursor = min(self.line_cursor + 1, self.line.len());
+ self.refresh_line();
+ }
+
+ fn move_left(&mut self) {
+ self.line_cursor = self.line_cursor.saturating_sub(1);
+ self.refresh_line();
+ }
+
+ fn move_to_start(&mut self) {
+ self.line_cursor = 0;
+ self.refresh_line();
+ }
+ fn move_to_end(&mut self) {
+ self.line_cursor = self.line.len();
+ self.refresh_line();
+ }
+
+ fn delete_word_left(&mut self) {
+ if self.line_cursor == 0 {
+ return;
+ }
+
+ let cursor = self.line_cursor;
+
+ while self.line_cursor > 0 && self.line[self.line_cursor - 1] == b' ' {
+ self.line_cursor -= 1;
+ }
+ while self.line_cursor > 0 && self.line[self.line_cursor - 1] != b' ' {
+ self.line_cursor -= 1;
+ }
+
+ self.line.drain(self.line_cursor..cursor);
+ self.refresh_line();
+ }
+
+ fn delete_left(&mut self) {
+ if self.line_cursor == 0 {
+ return;
+ }
+
+ self.line.remove(self.line_cursor - 1);
+ self.line_cursor = self.line_cursor.saturating_sub(1);
+ self.refresh_line();
+ }
+
+ fn delete_right(&mut self) {
+ if self.line_cursor >= self.line.len() {
+ return;
+ }
+
+ self.line.remove(self.line_cursor);
+ self.refresh_line();
+ }
+
+ fn delete_to_start(&mut self) {
+ if self.line_cursor == 0 {
+ return;
+ }
+
+ self.line.drain(0..self.line_cursor);
+ self.line_cursor = 0;
+ self.refresh_line();
+ }
+
+ fn delete_to_end(&mut self) {
+ if self.line_cursor >= self.line.len() {
+ return;
+ }
+
+ self.line.truncate(self.line_cursor);
+ self.refresh_line();
+ }
+
+ fn swap_one(&mut self) {
+ if self.line_cursor == 0 {
+ return;
+ }
+
+ self.line_cursor = min(self.line_cursor + 1, self.line.len());
+ self.line.swap(self.line_cursor - 2, self.line_cursor - 1);
+ self.refresh_line();
+ }
+
+ fn refresh_line(&mut self) {
+ let prompt_len = self.prompt.as_ref().map(|x| x.len()).unwrap_or(0);
+
+ let rows = (self.line.len() + prompt_len + self.columns - 1) / self.columns;
+ let prev_rows = self.max_rows;
+ let curr_row = (self.line_cursor + prompt_len + self.columns - 1) / self.columns;
+ self.column = (prompt_len + self.line_cursor) % self.columns;
+
+ self.max_rows = max(self.max_rows, rows);
+
+ //
+ // Clear rows
+ //
+
+ // Go down to the last row if the cursor is not already there.
+ if curr_row < prev_rows {
+ trace!("Go down {}", prev_rows - curr_row);
+ write!(self.tty_out, "\x1b[{}B", prev_rows - curr_row).unwrap();
+ }
+
+ // Clear each row and go up.
+ for _ in 1..prev_rows {
+ trace!("Clear row, go up");
+ self.tty_out.write_all(b"\r\x1b[0K\x1b[1A").unwrap();
+ }
+
+ // Clear the top row.
+ trace!("Clear top row");
+ self.tty_out.write_all(b"\r\x1b[0K").unwrap();
+
+ //
+ // Rewrite prompt and line
+ //
+
+ if let Some(prompt) = &self.prompt {
+ self.tty_out.write_all(prompt).unwrap();
+ }
+
+ self.tty_out.write_all(&self.line).unwrap();
+
+ //
+ // Display hints if any
+ //
+
+ if self.hints_callback.is_some() && !self.line.is_empty() {
+ let hints = self.hints_callback.unwrap()(&self.line);
+
+ if !hints.is_empty() {
+ // Column for the last character on the current row.
+ let max_column = (prompt_len + self.line.len()) % (self.columns + 1);
+ let max_len = min(self.columns - max_column, hints.len());
+
+ self.tty_out.write_all(b"\x1b[2m").unwrap();
+ self.tty_out.write_all(&hints[..max_len]).unwrap();
+ self.tty_out.write_all(b"\x1b[22m").unwrap();
+ }
+ }
+
+ //
+ // Move cursor to the right position.
+ //
+
+ if self.column == 0
+ && self.line.len() == self.line_cursor
+ && (prompt_len + self.line_cursor) > 0
+ {
+ // We reached the end of a row, insert a new line and go back to the first row.
+ trace!("Insert linefeed");
+ self.max_rows = max(self.max_rows, rows + 1);
+ self.tty_out.write_all(b"\r\n").unwrap();
+ } else if self.column == 0 {
+ self.tty_out.write_all(b"\r").unwrap();
+ } else {
+ // Move to the right column.
+ write!(self.tty_out, "\r\x1b[{}C", self.column).unwrap();
+
+ if rows > curr_row {
+ // Move up to the current row.
+ write!(self.tty_out, "\x1b[{}A", rows - curr_row).unwrap();
+ }
+ }
+ }
+
+ fn enable_raw_mode(&mut self) {
+ let mut termios = mem::MaybeUninit::uninit();
+
+ if unsafe { libc::tcgetattr(self.tty_out.as_raw_fd(), termios.as_mut_ptr()) } == -1 {
+ panic!("Failed to enable raw mode");
+ }
+
+ self.initial_termios = Some(unsafe { termios.assume_init() });
+ let mut raw = self.initial_termios.unwrap();
+
+ raw.c_iflag &= !(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+ raw.c_oflag &= !OPOST;
+ raw.c_cflag |= CS8;
+ raw.c_lflag &= !(ECHO | ICANON | IEXTEN | ISIG);
+ raw.c_cc[VMIN] = 1;
+ raw.c_cc[VTIME] = 0;
+
+ if unsafe { libc::tcsetattr(self.tty_out.as_raw_fd(), TCSAFLUSH, &raw) } < 0 {
+ panic!("Failed to enable raw mode");
+ }
+ }
+
+ fn disable_raw_mode(&mut self) {
+ if self.initial_termios.is_some()
+ && unsafe {
+ libc::tcsetattr(
+ self.tty_out.as_raw_fd(),
+ TCSAFLUSH,
+ &self.initial_termios.unwrap(),
+ )
+ } != -1
+ {
+ self.initial_termios = None;
+ }
+ }
+
+ fn get_columns(&mut self) -> usize {
+ let ws = libc::winsize {
+ ws_row: 0,
+ ws_col: 0,
+ ws_xpixel: 0,
+ ws_ypixel: 0,
+ };
+
+ if unsafe { libc::ioctl(self.tty_out.as_raw_fd(), TIOCGWINSZ, &ws) } < 0 || ws.ws_col == 0 {
+ todo!("Query the terminal for columns count.");
+ }
+
+ ws.ws_col as usize
+ }
+
+ fn get_cursor_position(&mut self) -> Option<Position> {
+ self.enable_raw_mode();
+
+ self.tty_out.write_all(b"\x1b[6n").unwrap();
+ self.tty_out.flush().unwrap();
+
+ let mut buffer: [u8; 32] = [0; 32];
+
+ let mut sep_index = None;
+ let mut index = 0;
+
+ let mut bytes = (&self.tty_in).bytes();
+ while let Some(Ok(byte)) = bytes.next() {
+ if byte == b'R' || index >= buffer.len() {
+ break;
+ }
+ if byte == b';' {
+ sep_index = Some(index);
+ }
+
+ buffer[index] = byte;
+ index += 1;
+ }
+
+ if buffer[0] != Key::Esc as u8 || buffer[1] != b'[' {
+ return None;
+ }
+
+ sep_index.map(|sep_index| Position {
+ row: as_usize(&buffer[2..sep_index]),
+ column: as_usize(&buffer[sep_index + 1..index]),
+ })
+ }
+
+ fn clear_screen(&mut self) {
+ self.tty_out.write_all(b"\x1b[H\x1b[2J").unwrap();
+ }
+}
+
+fn as_usize(array: &[u8]) -> usize {
+ std::str::from_utf8(array)
+ .unwrap()
+ .parse::<usize>()
+ .unwrap()
+}