use std::ffi::OsStr; use std::io::{self, IsTerminal, Read, Write}; use std::os::unix::ffi::OsStrExt; use std::os::unix::io::AsRawFd; use std::path::Path; use std::process::{Command, Stdio}; pub mod cursor; pub mod linebuf; pub mod panic; pub mod parse; pub mod raw; pub mod run; use linebuf::LineBuf; use raw::*; use crate::cursor::{Direction, move_cursor}; use crate::run::CommandDispatch; macro_rules! print { ($($x:tt)*) => {{ write!(io::stdout(), $($x)*).unwrap(); io::stdout().flush().unwrap(); }} } macro_rules! println { () => {{ println!("") }}; ($($x:tt)*) => {{ write!(io::stdout(), $($x)*).unwrap(); write!(io::stdout(), "\r\n").unwrap(); io::stdout().flush().unwrap(); }}; } const PROMPT: &str = "> "; fn completely_clear_screen() { print!("\x1B[2J\x1B[1;1H"); } fn clear_screen() { completely_clear_screen(); print!("{PROMPT}") } type BString = Vec; #[allow(non_camel_case_types)] type bstr = [u8]; pub struct Session { raw: ScopedRawMode, line: LineBuf, history: Vec, dispatch: CommandDispatch, } fn event_loop() { let stdin = io::stdin(); let stdout = io::stdout(); let fd = stdin.as_raw_fd(); let raw = ScopedRawMode::on_fd(fd); raw.enable(); let mut se = Session { raw, line: LineBuf::new(), history: Vec::new(), dispatch: CommandDispatch::new(), }; print!("{PROMPT}"); loop { let mut buf = [0u8; 1]; let Ok(_) = stdin.lock().read_exact(&mut buf) else { break; }; match buf[0] { // EOF 4 => { break; } // Ctrl+L 12 => clear_screen(), // Ctrl+R 18 => {} // Enter b'\r' => { let line = se.line.dump(); if !line.is_empty() { print!("\r\n"); se.history.push(line.clone()); run::run(&mut se, line); } } // Backspace (127 on most systems) 127 => { if se.line.is_empty() && !se.line.is_dirty() && !se.history.is_empty() { // take previous command for editing let cmd = se.history[se.history.len() - 1].clone(); io::stdout().write_all(&cmd).unwrap(); io::stdout().flush().unwrap(); se.line.set_content(cmd); } else if se.line.del_left().is_some() { print!("\x08 \x08"); } } b'\t' => { todo!() } // Escape sequence 27 => { let mut seq = [0u8; 2]; stdin.lock().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); se.line.right(); } b'D' => { move_cursor(Direction::Left, 1); se.line.left(); } x => todo!("escape character {x}"), } } } b'|' if se.line.is_empty() && !se.history.is_empty() => { let mut cmd = se.history[se.history.len() - 1].clone(); cmd.extend_from_slice(b" | "); io::stdout().write_all(&cmd).unwrap(); io::stdout().flush().unwrap(); se.line.set_content(cmd); } // Normal character x => { se.line.add(x); stdout.lock().write_all(&[x]).unwrap(); se.line.display_post(); } } } se.raw.disable(); } fn main() { if !io::stdin().is_terminal() { println!("need to run in a tty"); return; } crate::panic::hook(); // it is quite annoying when the terminal window closes due to a crash, so let's just catch all panics loop { let res = std::panic::catch_unwind(event_loop); match res { Ok(_) => break, Err(_) => continue, } } println!("bye"); }