use std::io::{self, Read, Write, IsTerminal}; use std::os::unix::io::AsRawFd; use std::process::Command; use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; use termios::*; struct ScopedRawMode { fd: i32, settings: Termios, } impl Drop for ScopedRawMode { fn drop(&mut self) { self.disable(); } } impl ScopedRawMode { fn on_fd(fd: i32) -> Self { let settings = Termios::from_fd(fd).unwrap(); Self { fd, settings } } fn enable(&self) { let mut settings = self.settings.clone(); cfmakeraw(&mut settings); tcsetattr(self.fd, TCSANOW, &settings).unwrap(); } fn disable(&self) { tcsetattr(self.fd, TCSANOW, &self.settings).unwrap(); } } macro_rules! print { ($($x:tt)*) => {{ write!(io::stdout(), $($x)*).unwrap(); io::stdout().flush().unwrap(); }} } struct LineBuffer { pre: Vec, post: Vec, } #[allow(unused)] impl LineBuffer { pub fn new() -> Self { Self { pre: Vec::new(), post: Vec::new(), } } pub fn del_left(&mut self) -> Option { self.pre.pop() } pub fn del_right(&mut self) -> Option { self.post.pop() } pub fn left(&mut self) -> bool { if let Some(byte) = self.del_left() { self.post.push(byte); true } else { false } } pub fn right(&mut self) -> bool { if let Some(byte) = self.del_right() { self.pre.push(byte); true } else { false } } pub fn add(&mut self, chr: u8) { self.pre.push(chr); } /// returns the whole contents of the buffer, and empties it in the process pub fn dump(&mut self) -> Vec { while self.right() {} let mut buf = Vec::new(); core::mem::swap(&mut self.pre, &mut buf); buf } pub fn display_post(&self) { for &x in self.post.iter().rev() { io::stdout().write_all(&[x]).unwrap(); } move_cursor(Direction::Left, self.post.len()); io::stdout().flush().unwrap(); } } #[derive(Debug, Clone, Copy)] pub enum Direction { Up, Down, Left, Right, } pub fn move_cursor(direction: Direction, n: usize) { if n == 0 { return; } let code = match direction { Direction::Up => 'A', // CUU Direction::Down => 'B', // CUD Direction::Right => 'C', // CUF Direction::Left => 'D', // CUB }; print!("\x1b[{n}{code}"); } /// Represents a cursor position #[derive(Debug, Clone, Copy)] pub struct CursorPos { pub row: usize, pub col: usize, } fn main() { let stdin = io::stdin(); let stdout = io::stdout(); if !stdin.is_terminal() { println!("need to run in a tty"); return; } let fd = stdin.as_raw_fd(); let raw = ScopedRawMode::on_fd(fd); raw.enable(); let mut stdin = stdin.lock(); let mut stdout = stdout.lock(); let mut buffer = [0u8; 1]; let mut line = LineBuffer::new(); let prompt = "> "; print!("{prompt}"); loop { let Ok(_) = stdin.read_exact(&mut buffer) else { break; }; match buffer[0] { // EOF 4 => { break; } // Enter b'\r' => { print!("\r\n"); let line = line.dump(); let words: Vec<&[u8]> = line.split(|x| *x == b' ').collect(); if words.is_empty() { print!("{prompt}"); continue; } let mut cmd = Command::new(OsStr::from_bytes(words[0])); for arg in words[1..].iter() { cmd.arg(OsStr::from_bytes(arg)); } raw.disable(); let status = cmd.status(); raw.enable(); let status_string = match status { Ok(ec) if ec.success() => String::new(), Ok(ec) => format!("{ec} "), Err(_) => String::from("IO ERROR "), }; //ensure_newline(); print!("{status_string}{prompt}"); } // Backspace (127 on most systems) 127 => { if line.del_left().is_some() { print!("\x08 \x08"); } } // Escape sequence 27 => { let mut seq = [0u8; 2]; stdin.read_exact(&mut seq).unwrap(); if seq[0] == b'[' { match seq[1] { b'A' => { // up }, b'B' => { // down }, b'C' => { move_cursor(Direction::Right, 1); line.right(); } b'D' => { move_cursor(Direction::Left, 1); line.left(); } _ => {} } } } // Normal character x => { line.add(x); stdout.write_all(&[x]).unwrap(); line.display_post(); } } } }