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::{Child, Command, Stdio}; pub mod cursor; pub mod linebuf; pub mod panic; pub mod parse; pub mod raw; use linebuf::LineBuf; use raw::*; use crate::cursor::{Direction, move_cursor}; use crate::parse::Ast; 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 run_command(raw: &ScopedRawMode, line: Vec) { let parsed = parse::do_parse(&line); let parsed = match parsed { Ok(p) => p, Err(err) => { print!("{err:?}\r\n{PROMPT}"); return; } }; let Ast::Pipes(pipes) = parsed else { todo!("can only handle pipes"); }; // simple command that can probably be builtin // TODO: handle builtins uniformly instead of big if case if pipes.cmds.len() == 1 { let c = &pipes.cmds[0]; match &c.cmd[..] { b"cd" => { let target: &Path = match c.args.get(0).map(|v| &v[..]) { Some(b"-") => todo!("prev"), Some(path) => OsStr::from_bytes(path).as_ref(), None => todo!("homedir"), }; if let Err(_) = std::env::set_current_dir(target) { print!("ERR {PROMPT}"); } else { print!("{PROMPT}"); } return; } b"from" => todo!("read from file"), b"to" | b"into" => todo!("write into file"), b"append" => todo!("append to file"), _ => (), } } let mut children: Vec = Vec::new(); let mut prev_stdout = None; let mut spawn_error = false; raw.disable(); for (i, cmd) in pipes.cmds.iter().enumerate() { let mut command = Command::new(OsStr::from_bytes(&cmd.cmd)); for arg in cmd.args.iter() { command.arg(OsStr::from_bytes(arg)); } if let Some(stdout) = prev_stdout.take() { command.stdin(Stdio::from(stdout)); } if i < pipes.cmds.len() - 1 { command.stdout(Stdio::piped()); } let mut child = match command.spawn() { Ok(c) => c, Err(e) => { println!("failed to spawn {:?} - {e:?}", OsStr::from_bytes(&cmd.cmd)); spawn_error = true; break; } }; prev_stdout = child.stdout.take(); children.push(child); } let status_string; if spawn_error { for child in children.iter_mut() { if let Err(e) = child.kill() { println!("failed to kill child - {e:?}"); } } status_string = "ERR".into(); } else { let mut code = 0; for mut child in children { match child.wait() { Ok(ec) => { if let Some(c) = ec.code() { code = c; } } Err(e) => { println!("failed to wait for child - {e:?}") } } } if code == 0 { status_string = String::new(); } else { status_string = format!("{code}"); } } raw.enable(); print!("\r{status_string}{PROMPT}"); } 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 stdin = stdin.lock(); let mut stdout = stdout.lock(); let mut buffer = [0u8; 1]; let mut line = LineBuf::new(); let mut history = Vec::new(); print!("{PROMPT}"); loop { let Ok(_) = stdin.read_exact(&mut buffer) else { break; }; match buffer[0] { // EOF 4 => { break; } // Enter b'\r' => { let line = line.dump(); if !line.is_empty() { print!("\r\n"); history.push(line.clone()); run_command(&raw, line); } } // Backspace (127 on most systems) 127 => { if line.is_empty() && !line.is_dirty() && !history.is_empty() { // take previous command for editing let cmd = history[history.len() - 1].clone(); io::stdout().write_all(&cmd).unwrap(); io::stdout().flush().unwrap(); line.set_content(cmd); } else if line.del_left().is_some() { print!("\x08 \x08"); } } b'\t' => { todo!() } // 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(); } x => todo!("escape character {x}"), } } } b'|' if line.is_empty() && !history.is_empty() => { let mut cmd = history[history.len() - 1].clone(); cmd.extend_from_slice(b" | "); io::stdout().write_all(&cmd).unwrap(); io::stdout().flush().unwrap(); line.set_content(cmd); } // Normal character x => { line.add(x); stdout.write_all(&[x]).unwrap(); line.display_post(); } } } raw.disable(); } fn main() { if !io::stdin().is_terminal() { println!("need to run in a tty"); return; } crate::panic::hook(); loop { let res = std::panic::catch_unwind(event_loop); match res { Ok(_) => break, Err(_) => println!("sorry about that, let's continue with the shell"), } } println!("bye"); }