mod meta;
mod multiline;
mod print;
use crate::cli::Args;
use crate::script::{bindings, defaults::ScriptDefaults, engine::build_engine};
use rhai::{Dynamic, Scope, AST};
use rustyline::error::ReadlineError;
use rustyline::{Config, DefaultEditor};
use std::path::PathBuf;
pub(super) struct ReplState {
pub(super) engine: rhai::Engine,
pub(super) scope: Scope<'static>,
pub(super) user_asts: Vec<AST>,
pub(super) autoprint: bool,
pub(super) history: Vec<String>,
pub(super) defaults: ScriptDefaults,
}
pub fn run(args: &Args) -> i32 {
let defaults = ScriptDefaults::from_args(args);
let mut engine = build_engine(&defaults);
bindings::thread::register_repl_stub(&mut engine);
let mut scope = Scope::new();
scope.push_constant("args", bindings::cli::build_args_array(args));
scope.push_constant("flags", bindings::cli::build_flags_map(args));
scope.push_constant("script_path", "<repl>".to_string());
scope.push_constant(
"script_dir",
std::env::current_dir()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default(),
);
scope.push_constant("script_name", "repl".to_string());
let history_path = args
.repl_history
.clone()
.unwrap_or_else(default_history_path);
let mut state = ReplState {
engine,
scope,
user_asts: Vec::new(),
autoprint: true,
history: Vec::new(),
defaults,
};
let rl_config = Config::builder().auto_add_history(true).build();
let mut rl = match DefaultEditor::with_config(rl_config) {
Ok(e) => e,
Err(e) => {
eprintln!("error: could not initialize line editor: {e}");
return 1;
}
};
if history_path.exists() {
let _ = rl.load_history(&history_path);
}
eprintln!("recon REPL — :help for commands, :quit to exit");
let mut buffer = String::new();
loop {
let prompt = if buffer.is_empty() { ">>> " } else { "... " };
match rl.readline(prompt) {
Ok(line) => {
if buffer.is_empty() && line.trim_start().starts_with(':') {
match meta::dispatch(&line, &mut state) {
meta::Outcome::Continue => continue,
meta::Outcome::Quit => break,
meta::Outcome::Paste => {
paste_mode(&mut rl, &mut state);
continue;
}
}
}
if !buffer.is_empty() {
buffer.push('\n');
}
buffer.push_str(&line);
use multiline::Status;
match multiline::classify(&state.engine, &buffer) {
Status::NeedMore => continue,
Status::Syntax(msg) => {
eprintln!("error: {msg}");
buffer.clear();
continue;
}
Status::Complete => {
let source = std::mem::take(&mut buffer);
eval_and_print(&mut state, &source);
}
}
}
Err(ReadlineError::Interrupted) => {
buffer.clear();
eprintln!("^C");
}
Err(ReadlineError::Eof) => break,
Err(e) => {
eprintln!("error: {e}");
return 1;
}
}
}
if let Some(parent) = history_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = rl.save_history(&history_path);
0
}
fn eval_and_print(state: &mut ReplState, source: &str) {
let mut ast = match state
.engine
.compile_into_self_contained(&state.scope, source)
{
Ok(a) => a,
Err(e) => {
eprintln!("error: {e}");
return;
}
};
for prev in &state.user_asts {
ast.combine(prev.clone());
}
match state
.engine
.eval_ast_with_scope::<Dynamic>(&mut state.scope, &ast)
{
Ok(value) => {
if ast.iter_functions().count() > 0 {
state.user_asts.push(ast.clone_functions_only());
}
state.history.push(source.to_string());
if state.autoprint {
if let Some(s) = print::format(&value) {
println!("{s}");
}
}
}
Err(e) => {
eprintln!("error: {}", crate::script::error_hint::format(&state.engine, &e));
}
}
}
pub(super) fn eval_and_print_load(state: &mut ReplState, source: &str) {
eval_and_print(state, source);
}
pub(super) fn run_script_isolated(
path: &std::path::Path,
defaults: &ScriptDefaults,
) -> Result<Dynamic, String> {
let raw = std::fs::read_to_string(path)
.map_err(|e| format!("read {}: {e}", path.display()))?;
let source = if raw.starts_with("#!") {
format!("//{}", &raw[2..])
} else {
raw
};
let mut engine = build_engine(defaults);
bindings::thread::register_repl_stub(&mut engine);
let mut scope = Scope::new();
scope.push_constant("args", rhai::Array::new());
scope.push_constant("flags", rhai::Map::new());
scope.push_constant(
"script_path",
path.to_string_lossy().into_owned(),
);
scope.push_constant(
"script_dir",
path.parent()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default(),
);
scope.push_constant(
"script_name",
path.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default(),
);
let ast = engine
.compile_into_self_contained(&scope, &source)
.map_err(|e| e.to_string())?;
engine
.eval_ast_with_scope::<Dynamic>(&mut scope, &ast)
.map_err(|e| crate::script::error_hint::format(&engine, &e))
}
pub(super) fn build_flags_from_defaults(d: &ScriptDefaults) -> rhai::Map {
crate::script::bindings::cli::build_flags_from_defaults(d)
}
fn default_history_path() -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
.join(".recon")
.join("repl_history")
}
fn paste_mode(rl: &mut DefaultEditor, state: &mut ReplState) {
eprintln!("(paste mode: lines accumulate until ':end' on its own line)");
let mut buf = String::new();
loop {
match rl.readline("... ") {
Ok(line) if line.trim() == ":end" => break,
Ok(line) => {
if !buf.is_empty() {
buf.push('\n');
}
buf.push_str(&line);
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
eprintln!("(paste aborted)");
return;
}
Err(e) => {
eprintln!("error: {e}");
return;
}
}
}
if !buf.trim().is_empty() {
eval_and_print(state, &buf);
}
}