use std::{cell::RefCell, collections::VecDeque, default, process::ExitStatus, time::Instant};
use ::crossterm::style::Color;
use log::{info, warn};
use shrs_core::prelude::*;
use shrs_job::JobManager;
use shrs_lang::PosixLang;
use shrs_line::prelude::*;
use crate::{
history::{DefaultHistory, History},
prelude::*,
};
#[derive(Builder)]
#[builder(name = "ShellBuilder", pattern = "owned")]
#[builder(setter(prefix = "with"))]
pub struct ShellConfig {
#[builder(default = "Hooks::default()")]
pub hooks: Hooks,
#[builder(default = "Builtins::default()")]
pub builtins: Builtins,
#[builder(default = "Box::new(Line::default())")]
#[builder(setter(custom))]
pub readline: Box<dyn Readline>,
#[builder(default = "Alias::default()")]
pub alias: Alias,
#[builder(default = "Env::default()")]
pub env: Env,
#[builder(default = "Theme::default()")]
pub theme: Theme,
#[builder(default = "Box::new(PosixLang::default())")]
#[builder(setter(custom))]
pub lang: Box<dyn Lang>,
#[builder(default = "Vec::new()")]
#[builder(setter(custom))]
pub plugins: Vec<Box<dyn Plugin>>,
#[builder(default = "State::default()")]
#[builder(setter(custom))]
pub state: State,
#[builder(default = "Box::new(DefaultHistory::default())")]
#[builder(setter(custom))]
pub history: Box<dyn History<HistoryItem = String>>,
#[builder(default = "Box::new(DefaultKeybinding::default())")]
#[builder(setter(custom))]
pub keybinding: Box<dyn Keybinding>,
}
impl ShellBuilder {
pub fn with_plugin<P: std::any::Any + Plugin>(mut self, plugin: P) -> Self {
let mut cur_plugins = self.plugins.unwrap_or_default();
cur_plugins.push(Box::new(plugin));
self.plugins = Some(cur_plugins);
self
}
pub fn with_state<T: 'static>(mut self, state: T) -> Self {
let mut cur_state = self.state.unwrap_or_default();
cur_state.insert(state);
self.state = Some(cur_state);
self
}
pub fn with_lang(mut self, lang: impl Lang + 'static) -> Self {
self.lang = Some(Box::new(lang));
self
}
pub fn with_readline(mut self, line: impl Readline + 'static) -> Self {
self.readline = Some(Box::new(line));
self
}
pub fn with_history(mut self, history: impl History<HistoryItem = String> + 'static) -> Self {
self.history = Some(Box::new(history));
self
}
pub fn with_keybinding(mut self, keybinding: impl Keybinding + 'static) -> Self {
self.keybinding = Some(Box::new(keybinding));
self
}
}
impl ShellConfig {
pub fn run(mut self) -> anyhow::Result<()> {
let plugins = self.plugins.drain(..).collect::<Vec<_>>();
for plugin in plugins.iter() {
let plugin_meta = plugin.meta();
info!("Initializing plugin '{}'...", plugin_meta.name);
if let Err(e) = plugin.init(&mut self) {
match plugin.fail_mode() {
FailMode::Warn => warn!(
"Plugin '{}' failed to initialize with {}",
plugin_meta.name, e
),
FailMode::Abort => panic!(
"Plugin '{}' failed to initialize with {}",
plugin_meta.name, e
),
}
}
}
let mut ctx = Context {
alias: self.alias,
out: OutputWriter::new(self.theme.out_style, self.theme.err_style),
state: self.state,
jobs: Jobs::default(),
startup_time: Instant::now(),
history: self.history,
prompt_content_queue: PromptContentQueue::new(),
};
let mut rt = Runtime {
env: self.env,
working_dir: std::env::current_dir().unwrap(),
name: "shrs".into(),
args: vec![],
exit_status: 0,
};
let sh = Shell {
job_manager: RefCell::new(JobManager::default()),
builtins: self.builtins,
theme: self.theme,
lang: self.lang,
hooks: self.hooks,
signals: Signals::new().unwrap(),
keybinding: self.keybinding,
};
for plugin in plugins.iter() {
if let Err(e) = plugin.post_init(&sh, &mut ctx, &mut rt) {
let plugin_meta = plugin.meta();
info!("Post-initializing plugin '{}'...", plugin_meta.name);
match plugin.fail_mode() {
FailMode::Warn => warn!(
"Plugin '{}' failed to post-initialize with {}",
plugin_meta.name, e
),
FailMode::Abort => panic!(
"Plugin '{}' failed to post-initialize with {}",
plugin_meta.name, e
),
}
}
}
let mut readline = self.readline;
run_shell(&sh, &mut ctx, &mut rt, &mut readline)
}
}
fn run_shell(
sh: &Shell,
ctx: &mut Context,
rt: &mut Runtime,
readline: &mut Box<dyn Readline>,
) -> anyhow::Result<()> {
let res = sh.hooks.run::<StartupCtx>(
sh,
ctx,
rt,
StartupCtx {
startup_time: ctx.startup_time.elapsed(),
},
);
if let Err(_e) = res {
}
loop {
let line = readline.read_line(sh, ctx, rt);
let mut words = line
.split(' ')
.map(|s| s.trim_start_matches("\\\n").trim().to_string())
.filter(|s| !s.is_empty())
.collect::<Vec<_>>();
if let Some(first) = words.get_mut(0) {
let alias_ctx = AliasRuleCtx {
alias_name: first,
sh,
ctx,
rt,
};
if let Some(expanded) = ctx.alias.get(&alias_ctx).last() {
*first = expanded.to_string();
}
}
let line = words.join(" ");
let hook_ctx = BeforeCommandCtx {
raw_command: line.clone(),
command: line.clone(),
run_ctx: rt.clone(),
};
sh.hooks.run::<BeforeCommandCtx>(sh, ctx, rt, hook_ctx)?;
let cmd_name = match words.first() {
Some(cmd_name) => cmd_name,
None => continue,
};
let builtin_cmd = sh
.builtins
.iter()
.find(|(builtin_name, _)| *builtin_name == cmd_name)
.map(|(_, builtin_cmd)| builtin_cmd);
let mut cmd_output: CmdOutput = CmdOutput::error();
ctx.out.begin_collecting();
if let Some(builtin_cmd) = builtin_cmd {
let output = builtin_cmd.run(sh, ctx, rt, &words);
match output {
Ok(o) => cmd_output = o,
Err(e) => eprintln!("error: {e:?}"),
}
} else {
let output = sh.lang.eval(sh, ctx, rt, line.clone());
match output {
Ok(o) => cmd_output = o,
Err(e) => eprintln!("error: {e:?}"),
}
}
let (out, err) = ctx.out.end_collecting();
cmd_output.set_output(out, err);
let _ = sh.hooks.run(
sh,
ctx,
rt,
AfterCommandCtx {
command: line,
cmd_output,
},
);
let mut exit_statuses = vec![];
ctx.jobs.retain(|status: ExitStatus| {
exit_statuses.push(status);
});
for status in exit_statuses.into_iter() {
sh.hooks
.run::<JobExitCtx>(sh, ctx, rt, JobExitCtx { status })?;
}
}
}