flesh/src/bin/relish.rs

395 lines
13 KiB
Rust
Raw Normal View History

/* 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 <http://www.gnu.org/licenses/>.
*/
use {
relish::{
ast::{
eval, lex, run,
Ctr, Seg, SymTable,
load_defaults, load_environment
},
stdlib::{
static_stdlib, dynamic_stdlib
},
aux::ShellState,
},
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,
2023-03-15 21:55:10 -07:00
};
2023-03-15 21:55:10 -07:00
#[derive(Clone)]
2023-03-23 11:52:36 -07:00
pub struct CustomPrompt(String, String, String);
#[derive(Clone)]
pub struct CustomCompleter(Vec<String>);
2023-03-23 11:52:36 -07:00
impl Prompt for CustomPrompt {
2023-03-15 21:55:10 -07:00
fn render_prompt_left(&self) -> Cow<str> {
{
2023-03-23 11:52:36 -07:00
Cow::Owned(self.0.to_owned())
2023-03-15 21:55:10 -07:00
}
}
2023-03-15 21:55:10 -07:00
fn render_prompt_right(&self) -> Cow<str> {
{
2023-03-23 11:52:36 -07:00
Cow::Owned(self.1.to_owned())
2023-03-15 21:55:10 -07:00
}
}
fn render_prompt_indicator(&self, _edit_mode: PromptEditMode) -> Cow<str> {
2023-03-23 11:52:36 -07:00
Cow::Owned(self.2.to_owned())
2023-03-15 21:55:10 -07:00
}
fn render_prompt_multiline_indicator(&self) -> Cow<str> {
Cow::Borrowed("++++")
}
fn render_prompt_history_search_indicator(
&self,
history_search: PromptHistorySearch,
) -> Cow<str> {
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<Suggestion> {
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,
append_whitespace: false,
span: Span {
start,
end: pos
}
});
}
}
}
} 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()
// TODO: dont
.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;
let shell_state_bindings = Rc::new(RefCell::from(ShellState {
parent_pid: unistd::Pid::from_raw(0),
parent_pgid: unistd::Pid::from_raw(0),
children: vec![],
last_exit_code: 0,
}));
// 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("RELISH_CFG_FILE").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);
2023-03-15 21:55:10 -07:00
let maybe_hist: Box<FileBackedHistory>;
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);
}
2023-03-15 21:55:10 -07:00
rl = rl.with_hinter(Box::new(
DefaultHinter::default()
.with_style(Style::new().italic().fg(Color::LightGray)),
)).with_validator(Box::new(DefaultValidator));
// repl :)
loop {
2023-03-23 11:52:36 -07:00
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));
2023-03-15 21:55:10 -07:00
let user_doc = rl.read_line(&readline_prompt).unwrap();
match user_doc {
2023-03-15 21:55:10 -07:00
Signal::Success(line) => {
println!(""); // add a new line before output gets printed
2023-03-05 22:21:18 -08:00
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),
}
2023-03-15 21:55:10 -07:00
},
Signal::CtrlD => {
println!("EOF!");
return
2023-03-15 21:55:10 -07:00
},
Signal::CtrlC => {
println!("Interrupted!");
},
}
}
}
2023-03-23 11:52:36 -07:00
fn make_prompt(syms: &mut SymTable) -> CustomPrompt {
let l_ctr = *syms
.call_symbol(&"CFG_RELISH_L_PROMPT".to_string(), &Seg::new(), true)
.unwrap_or_else(|err: String| {
eprintln!("{}", err);
Box::new(Ctr::String("<prompt broken!>".to_string()))
});
let r_ctr = *syms
.call_symbol(&"CFG_RELISH_R_PROMPT".to_string(), &Seg::new(), true)
.unwrap_or_else(|err: String| {
eprintln!("{}", err);
Box::new(Ctr::String("<prompt broken!>".to_string()))
});
let d_ctr = *syms
.call_symbol(&"CFG_RELISH_PROMPT_DELIMITER".to_string(), &Seg::new(), true)
.unwrap_or_else(|err: String| {
eprintln!("{}", err);
Box::new(Ctr::String("<prompt broken!>".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)
}