#![feature(unix_socket_ancillary_data, peer_credentials_unix_socket)] #![allow(clippy::needless_range_loop)] use std::collections::HashMap; use std::ffi::OsStr; use std::fs::{self, File}; use std::io::{self, IsTerminal, Read, Write, stdout}; use std::os::unix::ffi::OsStrExt; use std::os::unix::io::AsRawFd; use std::path::Path; use std::process::Command; use std::sync::{Arc, Mutex}; use std::thread::sleep; use std::time::Duration; pub mod ansi; pub mod basedir; pub mod completion; pub mod ctrlc; pub mod cursor; pub mod date; pub mod defer; pub mod export_fun; pub mod history; pub mod linebuf; pub mod panic; pub mod parse; pub mod raw; pub mod reload; pub mod run; pub mod rw; pub mod serialization; pub mod wait; use linebuf::LineBuf; use raw::*; use crate::completion::{PathCache, completion}; use crate::ctrlc::CtrlC; use crate::cursor::{Direction, move_cursor}; use crate::history::HistoryEntry; use crate::parse::{Block, ExpString, Parse}; macro_rules! print { ($($x:tt)*) => {{ let res = write!(io::stdout(), $($x)*); res.unwrap(); io::stdout().flush().unwrap(); }} } macro_rules! println { () => {{ println!("") }}; ($($x:tt)*) => { { let res = write!(io::stdout(), $($x)*); res.unwrap(); let res = write!(io::stdout(), "\r\n"); res.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, prev_path: BString, builtins: HashMap, vars: HashMap, funs: HashMap, aliases: run::Aliases, socket_running: Option, path_cache: PathCache, ctrlc: CtrlC, debug_keystrokes: bool, loud: bool, /// n before end of history.len() /// 0 == not checking history history_visit: usize, } /// 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.len() > 1 { 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 clear_prompt(&mut self) { cursor::move_cursor(Direction::Right, self.line.distance_from_right_end()); for _ in 0..self.line.len() { print!("\x08 \x08"); } io::stdout().lock().flush().unwrap(); self.line.clear(); } fn reprint_prompt(&self) { print!("{}", self.prompt()); self.line.display_pre(); self.line.display_post(b""); } fn display_historic_entry(&mut self) { self.clear_prompt(); let new = if self.history_visit == 0 { Vec::new() } else { self.history[self.history.len() - self.history_visit] .cmd .clone() }; io::stdout().write_all(&new).unwrap(); io::stdout().flush().unwrap(); self.line.set_content(new); } fn history_up(&mut self) { if self.history_visit < self.history.len() { self.history_visit += 1; self.display_historic_entry(); } } fn history_down(&mut self) { if self.history_visit > 0 { self.history_visit -= 1; self.display_historic_entry(); } } // move to next fn move_left_word(&mut self) { let mut i = 0; // find word while let Some(b' ') = self.line.get_left() { self.line.left(); i += 1; } // skip it while let Some(x) = self.line.get_left() && !x.is_ascii_whitespace() { self.line.left(); i += 1; } cursor::move_cursor(Direction::Left, i); io::stdout().flush().unwrap(); } fn move_right_word(&mut self) { let mut i = 0; // find word while let Some(b' ') = self.line.get_right() { self.line.right(); i += 1; } // skip it while let Some(x) = self.line.get_right() && !x.is_ascii_whitespace() { self.line.right(); i += 1; } cursor::move_cursor(Direction::Right, i); io::stdout().flush().unwrap(); } fn type_byte(&mut self, b: u8) { self.line.add(b); io::stdout().lock().write_all(&[b]).unwrap(); self.line.display_post(b""); } fn type_bytes(&mut self, bs: &[u8]) { for b in bs.iter() { self.type_byte(*b); } } fn del_left(&mut self) { if self.line.del_left().is_some() { cursor::move_cursor(Direction::Left, 1); self.line.display_post(b" "); } } fn del_right(&mut self) { self.line.del_right(); self.line.display_post(b" "); } fn move_to_begin(&mut self) { cursor::move_cursor(Direction::Left, self.line.all_left()); io::stdout().flush().unwrap(); } fn move_to_end(&mut self) { cursor::move_cursor(Direction::Right, self.line.all_right()); io::stdout().flush().unwrap(); } fn del_right_word(&mut self) { let mut del = 0; while let Some(x) = self.line.get_right() && x == b' ' { self.line.del_right(); del += 1; } while let Some(x) = self.line.get_right() && x != b' ' { self.line.del_right(); del += 1; } self.line.display_post(&vec![b' '; del]); } } fn exec_rc_file(se: Arc>) { let rcfile = basedir::config_dir().join(".pishrc"); let mut rc = Vec::new(); if !rcfile.exists() { return; } let mut f = match File::open(&rcfile) { Ok(f) => f, Err(e) => { println!("failed to open {rcfile:?}: {e:?}"); return; } }; if let Err(e) = f.read_to_end(&mut rc) { println!("failed to read {rcfile:?}: {e:?}"); return; } let script = match parse::Script::parse_from_bytes(&rc) { Ok(s) => s, Err(e) => { println!("failed to parse rc file: {e:?}"); return; } }; run::run_script(se, script); } fn event_loop() { history::setup(); let stdin = io::stdin(); let fd = stdin.as_raw_fd(); let raw = ScopedRawMode::on_fd(fd); raw.enable(); fs::create_dir_all(basedir::config_dir()).unwrap(); fs::create_dir_all(basedir::data_dir()).unwrap(); let se = Session { raw, line: LineBuf::new(), history: Vec::new(), builtins: run::builtin_map(), prev_path: vec![b'.'], history_visit: 0, socket_running: None, vars: HashMap::new(), funs: HashMap::new(), aliases: run::Aliases::new(), path_cache: Default::default(), ctrlc: Default::default(), debug_keystrokes: false, loud: false, }; let session = Arc::new(Mutex::new(se)); exec_rc_file(session.clone()); session.lock().unwrap().loud = true; print!("{}", session.lock().unwrap().prompt()); completion::populate_path_cache(session.clone()); let _sock_dropper = export_fun::listen(session.clone()); let _ctrlc = ctrlc::setup(session.clone()); loop { let mut se = session.lock().unwrap(); use ansi::KeyboardInput as Kb; match ansi::read(se.debug_keystrokes) { Kb::CtrlA => se.move_to_begin(), Kb::CtrlB => { println!(" Ctrl+B is not yet implemented"); se.reprint_prompt(); } Kb::CtrlC => { se.clear_prompt(); se.history_visit = 0; } Kb::CtrlE => se.move_to_end(), Kb::Eof | Kb::CtrlD => break, Kb::CtrlL => { clear_screen(); print!("{}", se.prompt()); 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(); } Kb::CtrlR => { println!(" search is not yet implemented"); se.reprint_prompt(); } Kb::Key(b'\r' | b'\n') => { let line = se.line.into_bytes(); if !line.is_empty() { let parsed = match parse::do_parse(&line) { Ok(p) => p, Err((crate::parse::ParseError::Eof, _)) => { se.line.add(b'\n'); print!("\r\n> "); continue; } Err(e) => { println!("{e:?}\n{}", se.prompt()); continue; } }; print!("\r\n"); let entry = HistoryEntry::new(line.clone()); history::persist(&entry); se.history.push(entry); se.history_visit = 0; se.line.dump(); drop(se); run::run(session.clone(), parsed); } } Kb::Key(b'\t') => { let cmd = se.line.pre().to_vec(); drop(se); let comp = completion(session.clone(), &cmd); let mut se = session.lock().unwrap(); se.type_bytes(&comp.shared_prefix); if comp.suggestions.len() > 1 { print!("\r\n"); for s in comp.suggestions { io::stdout().lock().write_all(&s.display).unwrap(); println!(); } se.reprint_prompt(); } } Kb::Arrow(dir) => match dir { Direction::Up => se.history_up(), Direction::Down => se.history_down(), Direction::Left => { if se.line.left() { move_cursor(Direction::Left, 1); io::stdout().lock().flush().unwrap(); } } Direction::Right => { if se.line.right() { move_cursor(Direction::Right, 1); io::stdout().lock().flush().unwrap(); } } }, Kb::CtrlArrow(dir) => match dir { Direction::Left => se.move_left_word(), Direction::Right => se.move_right_word(), _ => { println!(" Ctrl+{dir:?} not implemented"); se.reprint_prompt(); } }, Kb::DeleteLeft => { 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].cmd.clone(); se.type_bytes(&cmd); } else { se.del_left(); } } Kb::DeleteRight => se.del_right(), Kb::CtrlDeleteRight => se.del_right_word(), Kb::Home => se.move_to_begin(), Kb::End => se.move_to_end(), Kb::Key(x) => se.type_byte(x), } } session.lock().unwrap().raw.disable(); } fn main() { export_fun::maybe_run_defined_function(); 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)] unsafe { reload::continue_reload() } } } // prevent incredibly fast panic loops sleep(Duration::from_secs(1)); } println!("bye"); }