diff options
| author | Jonas Maier <jonas@x77.dev> | 2026-05-09 11:30:28 +0200 |
|---|---|---|
| committer | Jonas Maier <jonas@x77.dev> | 2026-05-09 11:30:28 +0200 |
| commit | dd24cc2aec9ece8214ec1a4eff4abd26d00ea083 (patch) | |
| tree | 354f4cb1c13ba20291fd57680aafaa4df67b99bf /src/lib.rs | |
| parent | edfc7e48c563a97399d18e3ef44fd595c0fd4e45 (diff) | |
| download | pish-dd24cc2aec9ece8214ec1a4eff4abd26d00ea083.tar.gz | |
simple script file test
Diffstat (limited to 'src/lib.rs')
| -rw-r--r-- | src/lib.rs | 509 |
1 files changed, 509 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..cf57cee --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,509 @@ +#![feature( + unix_socket_ancillary_data, + peer_credentials_unix_socket, + associated_type_defaults +)] +#![allow(clippy::needless_range_loop)] + +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fs::{self, File}; +use std::hash::Hash; +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 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, PostExpansion}; + +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<u8>; +#[allow(non_camel_case_types)] +type bstr = [u8]; + +pub struct Session { + raw: Option<ScopedRawMode>, + line: LineBuf, + history: Vec<HistoryEntry>, + prev_path: BString, + builtins: HashMap<BString, &'static dyn run::BuiltinClone>, + vars: HashMap<BString, BString>, + funs: HashMap<BString, Block>, + aliases: run::Aliases, + socket_running: Option<export_fun::SocketRunning>, + path_cache: PathCache, + ctrlc: CtrlC, + + /// terminfo identifier to command invocation + ti_keybinds: HashMap<BString, parse::Command<PostExpansion>>, + /// byte literals to command invocation + ascii_keybinds: HashMap<BString, parse::Command<PostExpansion>>, + + debug_keystrokes: bool, + loud: bool, + + /// n before end of history.len() + /// 0 == not checking history + history_visit: usize, +} + +impl Session { + pub fn new_noninteractive() -> Self { + Self { + raw: None, + line: LineBuf::new(), + history: Vec::new(), + prev_path: b".".into(), + builtins: HashMap::new(), + vars: 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(), + debug_keystrokes: false, + loud: false, + history_visit: 0, + } + } +} + +/// relative path -- in case it is a proper subpath the result starts with a slash `/` +fn relative_path(root: &Path, target: &Path) -> Option<String> { + 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<String> { + 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()) + } + + // TODO: prompt should be BString as well + fn prompt(&self) -> String { + #[cfg(debug_assertions)] + let dev = "dev "; + #[cfg(not(debug_assertions))] + let dev = ""; + format!("{dev}[{}]# ", 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 prompt_clear(&mut self) { + self.clear_prompt(); + self.history_visit = 0; + } + + 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(); + } + } + + 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 del_left_or_previous(&mut self) { + if self.line.is_empty() && !self.line.is_dirty() && !self.history.is_empty() { + // take previous command for editing + let cmd = self.history[self.history.len() - 1].cmd.clone(); + self.type_bytes(&cmd); + } else { + self.del_left(); + } + } + + 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_left_word(&mut self) { + let mut del = 0; + while let Some(x) = self.line.get_left() + && x == b' ' + { + self.line.del_left(); + del += 1; + } + while let Some(x) = self.line.get_left() + && x != b' ' + { + self.line.del_left(); + del += 1; + } + cursor::move_cursor(Direction::Left, del); + self.line.display_post(&vec![b' '; del]); + } + + 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 cursor_left(&mut self) { + if self.line.left() { + move_cursor(Direction::Left, 1); + io::stdout().lock().flush().unwrap(); + } + } + + fn cursor_right(&mut self) { + if self.line.right() { + move_cursor(Direction::Right, 1); + io::stdout().lock().flush().unwrap(); + } + } + + // move to next + fn cursor_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 cursor_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 complete(session: Arc<Mutex<Session>>) { + let cmd = session.lock().unwrap().line.pre().to_vec(); + + 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(); + } + } + + fn try_submit_command(session: Arc<Mutex<Session>>) { + 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.add(b'\n'); + print!("\r\n> "); + 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); + run::run(session.clone(), parsed); + } + } + + fn screen_clear(&mut self) { + clear_screen(); + self.reprint_prompt(); + } + + fn raw_enable(&self) { + if let Some(r) = &self.raw { + r.enable(); + } + } + + fn raw_disable(&self) { + if let Some(r) = &self.raw { + r.disable(); + } + } +} + +fn exec_rc_file(se: Arc<Mutex<Session>>) { + let _ = run::source( + se, + basedir::config_dir().join(".pishrc").as_os_str().as_bytes(), + ); +} + +pub fn event_loop() { + history::setup(); + ansi::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: Some(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, + ti_keybinds: HashMap::new(), + ascii_keybinds: HashMap::new(), + }; + + 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()); + + 'repl: loop { + let mut se = session.lock().unwrap(); + + let Some(key) = ansi::read(se.debug_keystrokes) else { + break; + }; + + if se.debug_keystrokes { + 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.type_byte(x), + 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(); +} |
