diff options
author | evuez <julien@mulga.net> | 2022-11-26 15:38:06 -0500 |
---|---|---|
committer | evuez <julien@mulga.net> | 2024-04-03 22:44:12 +0200 |
commit | 86098797034cbc7eb6db0cee54e17f8dcaedbc5d (patch) | |
tree | 29b6225ead843eb9022296a54657bbadfa1c4da0 /src/common/term.rs | |
download | blom-86098797034cbc7eb6db0cee54e17f8dcaedbc5d.tar.gz |
Diffstat (limited to 'src/common/term.rs')
-rw-r--r-- | src/common/term.rs | 517 |
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() +} |