/* Flesh: Flexible Shell * Copyright (C) 2021 Ava Affine * * 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 { flesh::{ ast::{ eval, lex, run, Ctr, Seg, SymTable, Symbol, Traceback, }, 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, }, }, std::{ cell::RefCell, rc::Rc, borrow::Cow, env, env::current_dir, path::{PathBuf, Path}, }, reedline::{ FileBackedHistory, DefaultHinter, Reedline, Signal, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, Completer, Suggestion, Span, KeyModifiers, KeyCode, ReedlineEvent, Keybindings, ColumnarMenu, Emacs, ReedlineMenu, Validator, ValidationResult, default_emacs_keybindings, }, nu_ansi_term::{Color, Style}, dirs::home_dir, termion::terminal_size, }; #[cfg(feature="posix")] use flesh::aux::{ShellState, check_jobs}; #[cfg(feature="posix")] use nix::unistd; #[derive(Clone)] pub struct CustomPrompt(String, String, String); #[derive(Clone)] pub struct CustomCompleter(Vec); pub struct CustomValidator; 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 && !tok.contains('/') { 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 } }); continue; } 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(); let rel = path.is_relative(); if rel { path = current_dir_path.join(path); } if path.exists() && path.is_dir() { if let Ok(entries) = path.read_dir() { for entry in entries { if let Ok(e) = entry { let mut path = e.path(); if rel { path = path.strip_prefix(current_dir_path.clone()) .unwrap() .to_path_buf(); } let d = path.is_dir(); let mut str_path = path.as_os_str() .to_str() .expect("inexplicable stringification error") .to_owned(); if d { str_path = str_path + "/"; } sugg.push(Suggestion { value: str_path, 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() ) { let mut path = e.path(); if rel { path = path.strip_prefix(current_dir_path.clone()) .unwrap() .to_path_buf(); } sugg.push(Suggestion { value: 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 } } impl Validator for CustomValidator { fn validate(&self, line: &str) -> ValidationResult { if incomplete_brackets(line) { ValidationResult::Incomplete } else { ValidationResult::Complete } } } fn incomplete_brackets(line: &str) -> bool { let mut balance: Vec = Vec::new(); let mut within_string: Option = None; for c in line.chars() { match c { c if ['"', '`', '\''].contains(&c) => { match within_string { Some(w) if c == w => { balance.pop(); within_string = None } Some(_) => {}, None => { balance.push(c); within_string = Some(c) }, } }, '(' if within_string.is_none() => balance.push(')'), ')' => if let Some(last) = balance.last() { if last == &c { balance.pop(); } }, _ => {} } } !balance.is_empty() } 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 = "/.flesh_hist"; const CONFIG_FILE_DEFAULT: &str = "/.fleshrc"; const VERSION: &str = env!("CARGO_PKG_VERSION"); if env::args().count() > 1 && env::args() .collect::>() .contains(&"--version".to_string()) { println!("Flesh {}", VERSION); return; } // 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; // setup symtable let mut syms = SymTable::new(); load_defaults(&mut syms); load_environment(&mut syms); static_stdlib(&mut syms); #[cfg(feature="posix")] let prompt_ss: Rc>; #[cfg(feature="posix")] { 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, })); prompt_ss = shell_state_bindings.clone(); dynamic_stdlib(&mut syms, Some(shell_state_bindings)); } // reload this later with the state bindings #[cfg(not(feature="posix"))] dynamic_stdlib(&mut syms); // 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: Traceback| eprintln!("failed to load script {}\n{}", cfg_file, err)); } #[cfg(feature="posix")] { dynamic_stdlib(&mut syms, Some(prompt_ss.clone())); } // 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(CustomValidator)); let mut xdimension: u16 = 0; let mut ydimension: u16 = 0; loop { #[cfg(feature="posix")] { 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 mut l = line.as_str().to_owned(); if !l.starts_with('(') { l = "(".to_owned() + &l + ")"; } 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: Traceback| { 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: Traceback| { 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: Traceback| { 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 Flesh, 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) }