/* relish: versatile lisp shell * Copyright (C) 2021 Aidan Hahn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ use { relish::{ ast::{ eval, lex, run, Ctr, Seg, SymTable, Symbol, }, stdlib::{ static_stdlib, dynamic_stdlib, load_defaults, load_environment, CONSOLE_XDIM_VNAME, CONSOLE_YDIM_VNAME, CFG_FILE_VNAME, L_PROMPT_VNAME, R_PROMPT_VNAME, PROMPT_DELIM_VNAME, }, aux::{ShellState, check_jobs}, }, std::{ cell::RefCell, rc::Rc, borrow::Cow, env, env::current_dir, path::{PathBuf, Path}, }, reedline::{ FileBackedHistory, DefaultHinter, DefaultValidator, Reedline, Signal, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, Completer, Suggestion, Span, KeyModifiers, KeyCode, ReedlineEvent, Keybindings, ColumnarMenu, Emacs, ReedlineMenu, default_emacs_keybindings, }, nix::unistd, nu_ansi_term::{Color, Style}, dirs::home_dir, termion::terminal_size, }; #[derive(Clone)] pub struct CustomPrompt(String, String, String); #[derive(Clone)] pub struct CustomCompleter(Vec); impl Prompt for CustomPrompt { fn render_prompt_left(&self) -> Cow { { Cow::Owned(self.0.to_owned()) } } fn render_prompt_right(&self) -> Cow { { Cow::Owned(self.1.to_owned()) } } fn render_prompt_indicator(&self, _edit_mode: PromptEditMode) -> Cow { Cow::Owned(self.2.to_owned()) } fn render_prompt_multiline_indicator(&self) -> Cow { Cow::Borrowed("++++") } fn render_prompt_history_search_indicator( &self, history_search: PromptHistorySearch, ) -> Cow { let prefix = match history_search.status { PromptHistorySearchStatus::Passing => "", PromptHistorySearchStatus::Failing => "failing ", }; Cow::Owned(format!( "({}reverse-search: {}) ", prefix, history_search.term )) } } impl Completer for CustomCompleter { fn complete(&mut self, line: &str, pos: usize) -> Vec { let current_dir_path = current_dir() .expect("current dir bad?"); let (tok, is_str, start) = get_token_to_complete(line, pos); let mut sugg = vec![]; if !is_str { let mut offcenter_match = vec![]; for sym in &self.0 { if sym.starts_with(tok.as_str()) { sugg.push(Suggestion { value: sym.clone(), description: None, extra: None, append_whitespace: false, span: Span { start, end: pos } }); } if sym.contains(tok.as_str()) { offcenter_match.push(sym.clone()); } } for i in offcenter_match { sugg.push(Suggestion { value: i.clone(), description: None, extra: None, append_whitespace: false, span: Span { start, end: pos } }); } } else { let mut path: PathBuf = Path::new(&tok).to_path_buf(); if path.is_relative() { path = current_dir_path.join(path); } if let Ok(npath) = path.canonicalize() { path = npath; } if path.exists() && path.is_dir() { if let Ok(entries) = path.read_dir() { for entry in entries { if let Ok(e) = entry { sugg.push(Suggestion { value: e.path() .as_os_str() .to_str() .expect("bad dir somehow?") .to_owned() + "/", description: None, extra: None, span: Span { start, end: pos }, append_whitespace: false }); } } } } else { // check parent to autocomplete path if let Some(parent) = path.parent() { if let Ok(entries) = parent.read_dir() { for entry in entries { if let Ok(e) = entry { if e.file_name() .to_string_lossy() .starts_with(path.file_name() .expect("bad file somehow?") .to_string_lossy() .to_mut() .as_str() ) { sugg.push(Suggestion { value: e.path() .as_os_str() .to_str() .expect("bad dir somehow?") .to_owned(), description: None, extra: None, append_whitespace: false, span: Span { start, end: pos } }); } } } } } } } sugg } } fn add_menu_keybindings(keybindings: &mut Keybindings) { keybindings.add_binding( KeyModifiers::NONE, KeyCode::Tab, ReedlineEvent::UntilFound(vec![ ReedlineEvent::Menu("completion_menu".to_string()), ReedlineEvent::MenuNext, ]), ); } fn main() { const HIST_FILE: &str = "/.relish_hist"; const CONFIG_FILE_DEFAULT: &str = "/.relishrc"; // default config file dirs let home_dir = home_dir().unwrap().to_str().unwrap().to_owned(); let hist_file_name = home_dir.clone() + HIST_FILE; let cfg_file_name = home_dir + CONFIG_FILE_DEFAULT; // TODO: the next two decls should probably be conditional on CFG_RELISH_POSIX // but in both cases the data must live for the whole program let shell_state_bindings = Rc::new(RefCell::from(ShellState { parent_pid: unistd::Pid::from_raw(0), parent_sid: unistd::Pid::from_raw(0), children: vec![], last_exit_code: 0, attr: None, })); let prompt_ss = shell_state_bindings.clone(); // setup symtable let mut syms = SymTable::new(); load_defaults(&mut syms); load_environment(&mut syms); static_stdlib(&mut syms).unwrap_or_else(|err: String| eprintln!("{}", err)); // reload this later with the state bindings dynamic_stdlib(&mut syms, None).unwrap_or_else(|err: String| eprintln!("{}", err)); // if there are args those are scripts, run them and exit if env::args().count() > 1 { let mut iter = env::args(); iter.next(); for i in iter { run(i, &mut syms).unwrap(); } return } // this is a user shell. attempt to load configuration { // scope the below borrow of syms let cfg_file = env::var(CFG_FILE_VNAME).unwrap_or(cfg_file_name); run(cfg_file.clone(), &mut syms) .unwrap_or_else(|err: String| eprintln!("failed to load script {}\n{}", cfg_file, err)); } dynamic_stdlib(&mut syms, Some(shell_state_bindings)).unwrap_or_else(|err: String| eprintln!("{}", err)); // setup readline let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu")); let mut keybindings = default_emacs_keybindings(); add_menu_keybindings(&mut keybindings); let edit_mode = Box::new(Emacs::new(keybindings)); let mut rl = Reedline::create() .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) .with_edit_mode(edit_mode) .with_quick_completions(true) .with_partial_completions(true) .with_ansi_colors(true); let maybe_hist: Box; if !hist_file_name.is_empty() { maybe_hist = Box::new( FileBackedHistory::with_file(5000, hist_file_name.into()) .expect("error reading history!") ); rl = rl.with_history(maybe_hist); } rl = rl.with_hinter(Box::new( DefaultHinter::default() .with_style(Style::new().italic().fg(Color::LightGray)), )).with_validator(Box::new(DefaultValidator)); let mut xdimension: u16 = 0; let mut ydimension: u16 = 0; loop { { // update state check_jobs(&mut prompt_ss.borrow_mut()); } let readline_prompt = make_prompt(&mut syms); let completer = make_completer(&mut syms); // realloc with each loop because syms can change rl = rl.with_completer(Box::new(completer)); let user_doc = rl.read_line(&readline_prompt).unwrap(); // doing this update here prevents needing to update twice before dimensions take effect (xdimension, ydimension) = check_and_update_console_dimensions(&mut syms, xdimension, ydimension); match user_doc { Signal::Success(line) => { println!(""); // add a new line before output gets printed let l = line.as_str().to_owned(); match lex(&l) { Ok(a) => match eval(&a, &mut syms) { Ok(a) => println!("{}", a), Err(s) => println!("{}", s), }, Err(s) => println!("{}", s), } }, Signal::CtrlD => { println!("EOF!"); return }, Signal::CtrlC => { println!("Interrupted!"); }, } } } fn make_prompt(syms: &mut SymTable) -> CustomPrompt { let l_ctr = *syms .call_symbol(&L_PROMPT_VNAME.to_string(), &Seg::new(), true) .unwrap_or_else(|err: String| { eprintln!("{}", err); Box::new(Ctr::String("".to_string())) }); let r_ctr = *syms .call_symbol(&R_PROMPT_VNAME.to_string(), &Seg::new(), true) .unwrap_or_else(|err: String| { eprintln!("{}", err); Box::new(Ctr::String("".to_string())) }); let d_ctr = *syms .call_symbol(&PROMPT_DELIM_VNAME.to_string(), &Seg::new(), true) .unwrap_or_else(|err: String| { eprintln!("{}", err); Box::new(Ctr::String("".to_string())) }); let l_str: String; let r_str: String; let d_str: String; if let Ctr::String(s) = l_ctr { l_str = s; } else { l_str = l_ctr.to_string(); } if let Ctr::String(s) = r_ctr { r_str = s; } else { r_str = r_ctr.to_string(); } if let Ctr::String(s) = d_ctr { d_str = s; } else { d_str = d_ctr.to_string(); } CustomPrompt(l_str, r_str, d_str) } fn make_completer(syms: &mut SymTable) -> CustomCompleter { let mut toks = vec![]; for i in syms.keys(){ toks.push(i.clone()); } CustomCompleter(toks) } fn get_token_to_complete(line: &str, pos: usize) -> (String, bool, usize) { let mut res = String::with_capacity(1); let mut doc = line.chars().rev(); for _ in pos..line.len() { doc.next(); } let mut is_str = false; let mut start_idx = pos; for idx in (0..pos).rev() { let iter = doc.next() .expect(format!("AC: no idx {}", idx) .as_str()); match iter { ' ' | '(' | '\n' => { break }, '\'' | '"' => { is_str = true; break }, _ => { start_idx = idx; res.insert(0, iter); } } } return (res, is_str, start_idx) } /* It was considered that a SIGWINCH handler would be a more * elegant solution to this. Such a solution would need to * modify the current libc based approach to use a state-enclosing * closure as a signal handler. As of May 2023 there is no clear * way of doing such a thing. * * Luckily this data is only used within Relish, so we can simply * rely on it only being read during the evaluation phase of the REPL. * This method will at least work for that. (ava) */ fn check_and_update_console_dimensions( syms: &mut SymTable, last_xdim: u16, last_ydim: u16 ) -> (u16, u16) { let (xdim, ydim) = terminal_size().expect("Couldnt get console dimensions"); if xdim != last_xdim { syms.insert( String::from(CONSOLE_XDIM_VNAME), Symbol::from_ast( &String::from(CONSOLE_XDIM_VNAME), &String::from("Length of current console"), &Seg::from_mono(Box::new(Ctr::Integer(xdim.into()))), None, ) ); } if ydim != last_ydim { syms.insert( String::from(CONSOLE_YDIM_VNAME), Symbol::from_ast( &String::from(CONSOLE_YDIM_VNAME), &String::from("Height of current console"), &Seg::from_mono(Box::new(Ctr::Integer(ydim.into()))), None, ) ); } (xdim, ydim) }