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(); }}; } fn completely_clear_screen() { print!("\x1B[2J\x1B[1;1H"); } fn clear_screen() { completely_clear_screen(); } type BString = Vec; #[allow(non_camel_case_types)] type bstr = [u8]; pub struct Session { raw: ScopedRawMode, line: LineBuf, history: Vec, dispatch: CommandDispatch, } /// relative path -- in case it is a proper subpath the result starts with a slash `/` fn relative_path(root: &Path, target: &Path) -> Option { let root = root.to_string_lossy(); let mut target = target.to_string_lossy().to_string(); if !target.ends_with("/") { target += "/"; } if let Some(("", leaf)) = target.split_once(&*root) { Some(leaf.into()) } else { None } } impl Session { fn pretty_cwd_res(&self) -> io::Result { let dir = std::env::current_dir()?; let mut s = if let Some(home_dir) = std::env::home_dir() { if let Some(rela) = relative_path(&home_dir, &dir) { format!("~{rela}") } else { dir.to_string_lossy().to_string() } } else { dir.to_string_lossy().to_string() }; while s.ends_with("/") { s.remove(s.len() - 1); } Ok(s) } fn pretty_cwd(&self) -> String { self.pretty_cwd_res().unwrap_or_else(|_| String::new()) } fn prompt(&self) -> String { format!("{} $ ", self.pretty_cwd()) } } fn read1() -> u8 { let mut buf = [0]; io::stdin().lock().read_exact(&mut buf).unwrap(); buf[0] } 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!("{}", se.prompt()); loop { let mut buf = [0u8; 1]; let Ok(_) = stdin.lock().read_exact(&mut buf) else { break; }; match buf[0] { // Ctrl+C 3 => { // clear line cursor::move_cursor(Direction::Right, se.line.distance_from_right_end()); for _ in 0..se.line.len() { write!(io::stdout(), "\x08 \x08").unwrap(); } io::stdout().lock().flush().unwrap(); se.line.clear(); } // EOF 4 => { break; } // Ctrl+L 12 => { clear_screen(); write!(io::stdout(), "{}", se.prompt()).unwrap(); io::stdout().write_all(&se.line.into_bytes()).unwrap(); cursor::move_cursor(Direction::Left, se.line.distance_from_right_end()); io::stdout().lock().flush().unwrap(); } // 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 = vec![read1()]; if seq[0] == b'[' { // still more while { let last = seq[seq.len() - 1]; last < 0x40 || last > 0x7E || seq.len() == 1 } { seq.push(read1()); } match seq[1] { b'A' => { // up } b'B' => { // down } b'C' => { if se.line.right() { move_cursor(Direction::Right, 1); io::stdout().lock().flush().unwrap(); } } b'D' => { if se.line.left() { move_cursor(Direction::Left, 1); io::stdout().lock().flush().unwrap(); } } b'3' => { if seq.len() > 2 && seq[2] == b'~' { // delete se.line.del_right(); se.line.display_post(b" "); } else { todo!("unhandled: {seq:?}"); } } 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(b""); } } } se.raw.disable(); } #[cfg(debug_assertions)] fn reload_shell() { use std::{env, ffi::CString, ptr}; // path to this executable let exe = env::current_exe().unwrap(); let exe_c = CString::new(exe.as_os_str().as_bytes()).unwrap(); // argv let args: Vec = env::args().map(|a| CString::new(a).unwrap()).collect(); let mut argv: Vec<*const libc::c_char> = args.iter().map(|a| a.as_ptr()).collect(); argv.push(ptr::null()); // environment let env: Vec = env::vars() .map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap()) .collect(); let mut envp: Vec<*const libc::c_char> = env.iter().map(|e| e.as_ptr()).collect(); envp.push(ptr::null()); unsafe { libc::execve(exe_c.as_ptr(), argv.as_ptr(), envp.as_ptr()); } eprintln!("exec failed"); } 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(_) => { #[cfg(debug_assertions)] if run::RELOAD.load(std::sync::atomic::Ordering::SeqCst) { reload_shell(); println!("failed to reload shell"); } } } } println!("bye"); }