aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs509
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();
+}