use anyhow::Result;
use std::collections::HashMap;
use std::io::Write;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use crate::db;
use crate::executor::{self, ExecutionResult, Job};
use crate::prompt::CahierPrompt;
pub struct CommandContext<'a> {
pub db: &'a db::Database,
pub current_env: &'a Arc<Mutex<HashMap<String, String>>>,
pub jobs: &'a mut Vec<Job>,
pub pty_writer: &'a Arc<Mutex<Option<Box<dyn Write + Send>>>>,
pub max_output_size: usize,
pub prompt: &'a mut CahierPrompt,
pub aliases: &'a Arc<Mutex<HashMap<String, String>>>,
pub should_log: bool,
pub db_path: &'a str,
pub next_command: &'a mut Option<String>,
}
pub enum CommandResult {
Continue,
Exit,
}
pub trait Command: Send + Sync {
fn name(&self) -> &str;
fn execute(&self, args: &[&str], context: &mut CommandContext) -> Result<CommandResult>;
}
pub struct Registry {
commands: HashMap<String, Box<dyn Command>>,
}
impl Default for Registry {
fn default() -> Self {
Self::new()
}
}
impl Registry {
pub fn new() -> Self {
Self {
commands: HashMap::new(),
}
}
pub fn register(&mut self, cmd: Box<dyn Command>) {
self.commands.insert(cmd.name().to_string(), cmd);
}
pub fn get(&self, name: &str) -> Option<&dyn Command> {
self.commands.get(name).map(|b| b.as_ref())
}
pub fn command_names(&self) -> Vec<String> {
self.commands.keys().cloned().collect()
}
}
pub struct CdCommand;
impl Command for CdCommand {
fn name(&self) -> &str {
"cd"
}
fn execute(&self, args: &[&str], context: &mut CommandContext) -> Result<CommandResult> {
let current_pwd = std::env::current_dir().ok();
let path = if args.is_empty() {
dirs::home_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| ".".to_string())
} else if args[0] == "-" {
let env = context
.current_env
.lock()
.unwrap_or_else(|e| e.into_inner());
if let Some(old) = env.get("OLDPWD") {
println!("{}", old);
old.clone()
} else {
eprintln!("cd: OLDPWD not set");
context.prompt.set_last_success(false);
context
.prompt
.set_last_duration(Some(std::time::Duration::from_millis(0)));
return Ok(CommandResult::Continue);
}
} else {
args[0].to_string()
};
let start = Instant::now();
if let Err(e) = std::env::set_current_dir(&path) {
eprintln!("Error changing directory: {}", e);
context.prompt.set_last_success(false);
} else {
if let Ok(cwd) = std::env::current_dir() {
if let Some(cwd_str) = cwd.to_str() {
match context.current_env.lock() {
Ok(mut env) => {
env.insert("PWD".to_string(), cwd_str.to_string());
if let Some(prev) = current_pwd {
env.insert(
"OLDPWD".to_string(),
prev.to_string_lossy().to_string(),
);
}
}
Err(_) => eprintln!("Warning: Failed to lock env to update PWD"),
}
}
}
let cmd_line = format!("cd {}", path);
if context.should_log {
if let Err(e) = context.db.log_entry(&cmd_line, "", Some(0), 0, None) {
eprintln!("Error logging cd command: {}", e);
}
}
context.prompt.set_last_success(true);
}
context.prompt.set_last_duration(Some(start.elapsed()));
Ok(CommandResult::Continue)
}
}
pub struct JobsCommand;
impl Command for JobsCommand {
fn name(&self) -> &str {
"jobs"
}
fn execute(&self, _args: &[&str], context: &mut CommandContext) -> Result<CommandResult> {
let start = Instant::now();
for (i, job) in context.jobs.iter().enumerate() {
println!(
"[{}] {} {}",
i + 1,
job.command,
if i == context.jobs.len() - 1 { "+" } else { "" }
);
}
context.prompt.set_last_success(true);
context.prompt.set_last_duration(Some(start.elapsed()));
Ok(CommandResult::Continue)
}
}
pub struct ExitCommand;
impl Command for ExitCommand {
fn name(&self) -> &str {
"exit"
}
fn execute(&self, _args: &[&str], _context: &mut CommandContext) -> Result<CommandResult> {
Ok(CommandResult::Exit)
}
}
pub struct FgCommand;
impl Command for FgCommand {
fn name(&self) -> &str {
"fg"
}
fn execute(&self, args: &[&str], context: &mut CommandContext) -> Result<CommandResult> {
let start_total = Instant::now();
if context.jobs.is_empty() {
eprintln!("fg: current: no such job");
context.prompt.set_last_success(false);
context
.prompt
.set_last_duration(Some(start_total.elapsed()));
return Ok(CommandResult::Continue);
}
let job_index = if args.is_empty() {
context.jobs.len() - 1
} else {
let arg = args[0];
if let Ok(n) = arg.parse::<usize>() {
if n > 0 && n <= context.jobs.len() {
n - 1
} else {
eprintln!("fg: {}: no such job", arg);
context.prompt.set_last_success(false);
context
.prompt
.set_last_duration(Some(start_total.elapsed()));
return Ok(CommandResult::Continue);
}
} else {
eprintln!("fg: invalid job specifier");
context.prompt.set_last_success(false);
context
.prompt
.set_last_duration(Some(start_total.elapsed()));
return Ok(CommandResult::Continue);
}
};
let job = context.jobs.remove(job_index);
let job_command = job.command.clone();
println!("{}", job.command);
let start = Instant::now();
match executor::resume_job(
job,
context.max_output_size,
context.pty_writer,
context.current_env,
) {
Ok(res) => {
handle_execution_result(res, start, &job_command, context)?;
}
Err(e) => {
eprintln!("Error resuming job: {}", e);
context.prompt.set_last_success(false);
context.prompt.set_last_duration(Some(start.elapsed()));
}
}
Ok(CommandResult::Continue)
}
}
pub struct AliasCommand;
impl Command for AliasCommand {
fn name(&self) -> &str {
"alias"
}
fn execute(&self, args: &[&str], context: &mut CommandContext) -> Result<CommandResult> {
let start = Instant::now();
let mut aliases_guard = context
.aliases
.lock()
.map_err(|_| anyhow::anyhow!("Failed to lock aliases registry"))?;
let mut all_success = true;
if args.is_empty() {
for (name, value) in aliases_guard.iter() {
println!("alias {}='{}'", name, value);
}
} else {
for arg in args {
if let Some((name, value)) = arg.split_once('=') {
let name = name.trim().to_string();
let value = value.trim().to_string();
if !name.is_empty() {
aliases_guard.insert(name, value);
}
} else {
if let Some(value) = aliases_guard.get(*arg) {
println!("alias {}='{}'", arg, value);
} else {
eprintln!("alias: {}: not found", arg);
all_success = false;
}
}
}
}
context.prompt.set_last_success(all_success);
context.prompt.set_last_duration(Some(start.elapsed()));
Ok(CommandResult::Continue)
}
}
pub struct UnaliasCommand;
impl Command for UnaliasCommand {
fn name(&self) -> &str {
"unalias"
}
fn execute(&self, args: &[&str], context: &mut CommandContext) -> Result<CommandResult> {
let start = Instant::now();
let mut aliases_guard = context
.aliases
.lock()
.map_err(|_| anyhow::anyhow!("Failed to lock aliases registry"))?;
let mut all_success = true;
if args.is_empty() {
eprintln!("unalias: usage: unalias name [name ...]");
all_success = false;
} else {
for arg in args {
if aliases_guard.remove(*arg).is_none() {
eprintln!("unalias: {}: not found", arg);
all_success = false;
}
}
}
context.prompt.set_last_success(all_success);
context.prompt.set_last_duration(Some(start.elapsed()));
Ok(CommandResult::Continue)
}
}
pub struct EditCommand;
impl Command for EditCommand {
fn name(&self) -> &str {
"edit"
}
fn execute(&self, _args: &[&str], context: &mut CommandContext) -> Result<CommandResult> {
let start = Instant::now();
let db = db::Database::init(context.db_path)?;
if let Some(cmd) = crate::tui::run(db)? {
*context.next_command = Some(cmd);
}
context.prompt.set_last_success(true);
context.prompt.set_last_duration(Some(start.elapsed()));
Ok(CommandResult::Continue)
}
}
pub fn handle_execution_result(
res: ExecutionResult,
start_time: Instant,
input: &str,
context: &mut CommandContext,
) -> Result<()> {
match res {
ExecutionResult::Completed {
output,
exit_code,
output_file,
} => {
let duration = start_time.elapsed();
if context.should_log {
if let Err(e) = context.db.log_entry(
input,
&output,
exit_code,
duration.as_millis(),
output_file.as_deref(),
) {
eprintln!("Error logging command: {}", e);
}
}
if let Some(code) = exit_code {
context.prompt.set_last_success(code == 0);
} else {
context.prompt.set_last_success(false);
}
context.prompt.set_last_duration(Some(duration));
}
ExecutionResult::Suspended(mut job) => {
let id = context.jobs.len() + 1;
job.id = id;
println!("\n[{}] Stopped {}", id, job.command);
context.jobs.push(job);
context.prompt.set_last_success(true);
context.prompt.set_last_duration(Some(start_time.elapsed()));
}
}
Ok(())
}