#![feature( unix_socket_ancillary_data, peer_credentials_unix_socket, associated_type_defaults, slice_split_once )] #![allow(clippy::needless_range_loop)] use std::collections::HashMap; use std::ffi::OsStr; use std::fs::{self, File}; use std::io::{self, Read, Write}; 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}; pub mod ansi; pub mod basedir; pub mod completion; pub mod consts; pub mod ctrlc; pub mod cursor; pub mod date; pub mod defer; pub mod export_fun; pub mod history; pub mod line; pub mod panic; pub mod parse; pub mod ps1; pub mod raw; pub mod reload; pub mod run; pub mod rw; pub mod serialization; pub mod syntax_highlighting; pub mod variants; pub mod wait; mod bitset; use raw::*; use crate::completion::{PathCache, completion}; use crate::ctrlc::CtrlC; use crate::history::HistoryEntry; use crate::line::Line; use crate::parse::{Block, ExpString, Parse, PostExpansion}; use crate::ps1::Prompt; 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]; trait PushAll { fn push_all(&mut self, other: &bstr); } impl PushAll for BString { fn push_all(&mut self, other: &bstr) { for &c in other { self.push(c); } } } pub struct Session { raw: Option, line: Line, history: Vec, prev_path: BString, builtins: HashMap, vars: run::Vars, funs: HashMap, aliases: run::Aliases, socket_running: Option, path_cache: PathCache, ctrlc: CtrlC, /// terminfo identifier to command invocation ti_keybinds: HashMap>, /// byte literals to command invocation ascii_keybinds: HashMap>, /// n before end of history.len() /// 0 == not checking history history_visit: usize, highlighter: syntax_highlighting::Highlighter, prompt: Prompt, terminal_input: Option, } impl Session { pub fn new_noninteractive() -> Self { let mut vars = run::Vars::default(); let prompt = Prompt::new(&mut vars); Self { raw: None, line: Line::new(), history: Vec::new(), prev_path: b".".into(), builtins: HashMap::new(), funs: HashMap::new(), aliases: run::Aliases::new(), socket_running: None, path_cache: Default::default(), ctrlc: Default::default(), ti_keybinds: HashMap::new(), ascii_keybinds: HashMap::new(), history_visit: 0, highlighter: syntax_highlighting::Highlighter::new(), prompt, vars, terminal_input: None, } } } /// 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 } } fn pretty_cwd_res() -> 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() -> BString { pretty_cwd_res() .unwrap_or_else(|_| String::new()) .into_bytes() } impl Session { fn prompt_clear(&mut self) { self.line.clear_prompt().unwrap(); self.history_visit = 0; } fn reprint_prompt(this: Arc>) { let prompt = this.lock().unwrap().prompt.prompt().to_vec(); io::stdout().write_all(&prompt).unwrap(); let mut this = this.lock().unwrap(); if let Some(ti) = this.terminal_input.as_mut() && let Some(pos) = ti.query_cursor_position() { this.line.set_begin_of_line(pos); } this.line.mark_dirty(); this.cohere().unwrap(); } fn display_historic_entry(&mut self) { self.line.clear_prompt().unwrap(); let new = if self.history_visit == 0 { Vec::new() } else { self.history[self.history.len() - self.history_visit] .cmd .clone() }; self.line.set_content(new).unwrap(); } 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(); } } fn del_left_or_previous(&mut self) { if self.line.is_empty() && !self.line.had_contents() && !self.history.is_empty() { // take previous command for editing let cmd = self.history[self.history.len() - 1].cmd.clone(); self.line.type_bytes(&cmd).unwrap(); } else { self.line.del_left().unwrap(); } } fn prompt_pipe_previous(&mut self) { if self.line.is_empty() && let Some(prev) = self.history.last() { let mut cmd = prev.cmd.clone(); cmd.push_all(b" | "); self.line.type_bytes(&cmd).unwrap(); } else { self.line.type_byte(b'|').unwrap(); } } fn complete(session: Arc>) { let cmd = session.lock().unwrap().line.pre().to_vec(); let comp = completion(session.clone(), &cmd); let mut se = session.lock().unwrap(); se.line.type_bytes(&comp.shared_prefix).unwrap(); if comp.suggestions.len() > 1 { print!("\r\n"); for s in comp.suggestions { io::stdout().lock().write_all(&s.display).unwrap(); println!(); } drop(se); Self::reprint_prompt(session); } } fn reevaluate_prompt_inner(&mut self) -> bool { if self.prompt.requires_update(&self.vars) { self.prompt.load_prompt(&mut self.vars); true } else { false } } fn reevaluate_prompt(se: Arc>) { if se.lock().unwrap().reevaluate_prompt_inner() { let mut prompt = se.lock().unwrap().prompt.clone(); prompt.eval_prompt(se.clone()); se.lock().unwrap().prompt = prompt; } } fn try_submit_command(session: Arc>) { let mut se = session.lock().unwrap(); let line = se.line.into_bytes(); if !line.is_empty() { let parsed = match parse::do_parse(&line) { Ok(p) => p, Err(_) => { se.line.type_byte(b'\n').unwrap(); return; } }; 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); let status_string = run::run(session.clone(), parsed); if !status_string.is_empty() { println!("{status_string}"); } Self::reevaluate_prompt(session.clone()); Self::reprint_prompt(session); let _ = std::io::stdout().lock().flush(); } } fn screen_clear(this: Arc>) { clear_screen(); Self::reprint_prompt(this); } fn raw_enable(&self) { if let Some(r) = &self.raw { r.enable(); } } fn raw_disable(&self) { if let Some(r) = &self.raw { r.disable(); } } /// do cleanup actions after a bunch of mutations. fn cohere(&mut self) -> io::Result<()> { self.line.highlight_syntax(&mut self.highlighter)?; Ok(()) } } const DEFAULT_PROFILE: &[u8] = include_bytes!("profile"); fn exec_rc_file(se: Arc>) { let rcfile = basedir::config_dir().join(".pishrc"); if let Ok(true) = fs::exists(&rcfile) { let _ = run::source(se, rcfile.as_os_str().as_bytes()); } else { println!("{rcfile:?} does not exist. running default profile."); let script = parse::Script::parse_from_bytes(DEFAULT_PROFILE).unwrap(); run::run_script(se, script); } } #[test] fn default_profile_should_parse() { parse::Script::parse_from_bytes(DEFAULT_PROFILE).expect("default profile does not parse."); } pub fn event_loop() { fs::create_dir_all(basedir::config_dir()).unwrap(); fs::create_dir_all(basedir::data_dir()).unwrap(); fs::create_dir_all(basedir::state_dir()).unwrap(); history::setup(); ansi::setup(); let stdin = io::stdin(); let fd = stdin.as_raw_fd(); let raw = ScopedRawMode::on_fd(fd); raw.enable(); let se = Session { raw: Some(raw), line: Line::new(), builtins: run::builtin_map(), prev_path: vec![b'.'], history_visit: 0, socket_running: None, path_cache: Default::default(), ctrlc: Default::default(), terminal_input: Some(ansi::TerminalInput::new()), ..Session::new_noninteractive() }; let session = Arc::new(Mutex::new(se)); exec_rc_file(session.clone()); Session::reevaluate_prompt(session.clone()); Session::reprint_prompt(session.clone()); completion::populate_path_cache(session.clone()); let _sock_dropper = export_fun::listen(session.clone()); let _ctrlc = ctrlc::setup(session.clone()); 'repl: loop { let mut se = session.lock().unwrap(); let _ = se.cohere(); let Some(key) = se.terminal_input.as_mut().unwrap().read() else { break; }; if se.terminal_input.as_mut().unwrap().debug { println!("{key:?}"); } if let Some(cmd) = se.ascii_keybinds.get(key.as_bytes()) { let cmd = cmd.clone(); drop(se); // not sure if/how to report this error - would be strange to print something to console every time a keybind command returns nonzero exit code. let _ = run::run_quiet(session.clone(), cmd); continue 'repl; } match key { ansi::KbInput::Key([x]) => se.line.type_byte(x).unwrap(), ansi::KbInput::Escape(escape) => { for terminfo_key in escape.keys.iter() { if let Some(cmd) = se.ti_keybinds.get(terminfo_key.as_bytes()) { let cmd = cmd.clone(); drop(se); // not sure if/how to report this error - would be strange to print something to console every time a keybind command returns nonzero exit code. let _ = run::run_quiet(session.clone(), cmd); continue 'repl; } } } ansi::KbInput::InvalidEscape(_) => continue, } } session.lock().unwrap().raw_disable(); } pub fn icon() -> BString { const DATA: &[u8] = include_bytes!("icon.txt"); const COLOR0: &[u8] = b"\x1b[100m"; const COLOR1: &[u8] = b"\x1b[47m"; const COLOR_RESET: &[u8] = b"\x1b[0m"; let mut color = COLOR0; let mut buf = BString::new(); for line in DATA.split(|x| *x == b'\n') { if line.starts_with(b"-") { color = COLOR1; continue; } let mut colored = false; for &b in line { if b == b'#' { if !colored { buf.push_all(color); colored = true; } } else if colored { buf.push_all(COLOR_RESET); colored = false; } buf.push_all(b" "); } if colored { buf.push_all(COLOR_RESET); } buf.push_all(b"\r\n"); } buf }