flesh/src/bin/flesh.rs

541 lines
17 KiB
Rust
Raw Normal View History

2024-02-06 22:39:08 +00:00
/* 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 <http://www.gnu.org/licenses/>.
*/
use {
2024-02-06 22:39:08 +00:00
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::{
2023-06-11 23:48:18 -07:00
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,
2023-03-15 21:55:10 -07:00
};
#[cfg(feature="posix")]
2024-02-06 22:39:08 +00:00
use flesh::aux::{ShellState, check_jobs};
#[cfg(feature="posix")]
use nix::unistd;
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>);
pub struct CustomValidator;
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 && !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<char> = Vec::new();
let mut within_string: Option<char> = 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() {
2024-02-06 22:39:08 +00:00
const HIST_FILE: &str = "/.flesh_hist";
const CONFIG_FILE_DEFAULT: &str = "/.fleshrc";
2023-06-11 23:48:18 -07:00
const VERSION: &str = env!("CARGO_PKG_VERSION");
if env::args().count() > 1 &&
env::args()
.collect::<Vec<_>>()
.contains(&"--version".to_string()) {
2024-02-06 22:39:08 +00:00
println!("Flesh {}", VERSION);
2023-06-11 23:48:18 -07:00
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<RefCell<ShellState>>;
#[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);
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(CustomValidator));
2023-03-15 21:55:10 -07:00
let mut xdimension: u16 = 0;
let mut ydimension: u16 = 0;
loop {
#[cfg(feature="posix")]
{
check_jobs(&mut prompt_ss.borrow_mut());
}
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();
// 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 {
2023-03-15 21:55:10 -07:00
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),
}
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(&L_PROMPT_VNAME.to_string(), &Seg::new(), true)
.unwrap_or_else(|err: Traceback| {
2023-03-23 11:52:36 -07:00
eprintln!("{}", err);
Box::new(Ctr::String("<prompt broken!>".to_string()))
});
let r_ctr = *syms
.call_symbol(&R_PROMPT_VNAME.to_string(), &Seg::new(), true)
.unwrap_or_else(|err: Traceback| {
2023-03-23 11:52:36 -07:00
eprintln!("{}", err);
Box::new(Ctr::String("<prompt broken!>".to_string()))
});
let d_ctr = *syms
.call_symbol(&PROMPT_DELIM_VNAME.to_string(), &Seg::new(), true)
.unwrap_or_else(|err: Traceback| {
2023-03-23 11:52:36 -07:00
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)
}
/* 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.
*
2024-02-06 22:39:08 +00:00
* 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)
}