#![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, tty_in: File, tty_out: File, history: Vec>, history_cursor: usize, column: usize, line: Vec, line_cursor: usize, prompt: Option>, hints_callback: Option &[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> { 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 { 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::() .unwrap() }