use std::sync::atomic::Ordering;
use tracing::{debug, info, warn};
use reedline::HistoryItem;
use std::path::PathBuf;
use crate::engine::builtins::{alias, source, unalias};
use crate::engine::classifier::{is_ai_goodbye_response, InputType};
use crate::engine::expand;
use crate::engine::{execute, try_builtin, CommandResult, LoopAction};
use super::Shell;
impl Shell {
pub(super) async fn handle_input(&mut self, line: &str) -> bool {
info!("\n\n==== USER INPUT RECEIVED, START PROCESS ====");
let line = line.trim().to_string();
if line.is_empty() {
return true;
}
let line = if let Some(expanded) = expand::expand_alias(&line, &self.aliases) {
debug!(original = %line, expanded = %expanded, "Alias expanded");
expanded
} else {
line
};
debug!(input = %line, "User input received");
if let Some(result) = self.try_shell_builtins(&line) {
let path_before = std::env::var("PATH").ok();
return self.handle_builtin(&line, result, path_before);
}
let path_before = std::env::var("PATH").ok();
if let Some(result) = try_builtin(&line) {
return self.handle_builtin(&line, result, path_before);
}
let input_type = self.classifier.classify(&line);
debug!(input = %line, classification = ?input_type, "Input classified");
let (result, from_tool_call, should_update_exit_code, executed_command) = match input_type {
InputType::Goodbye => {
info!("Goodbye input detected, exiting shell");
return false;
}
InputType::Command => {
debug!(input = %line, "Executing as command (no AI)");
(execute(&line), false, true, None)
}
InputType::NaturalLanguage => {
let ai_result = self.route_to_ai(&line).await;
(
ai_result.result,
ai_result.from_tool_call,
ai_result.should_update_exit_code,
ai_result.executed_command,
)
}
};
if should_update_exit_code {
self.last_exit_code
.store(result.exit_code, Ordering::Relaxed);
}
println!();
self.record_history(&line, &result);
if let Some(ref cmd) = executed_command {
if let Err(e) = self
.editor
.history_mut()
.save(HistoryItem::from_command_line(cmd))
{
warn!("Failed to save AI-executed command to reedline history: {e}");
}
}
if result.exit_code != 0 {
self.investigate_error(&line, &result, from_tool_call).await;
}
if !from_tool_call && is_ai_goodbye_response(&result.stdout) {
info!("AI goodbye response detected, exiting shell");
self.farewell_shown = true;
return false;
}
info!("\n==== FINISHED PROCESS ====\n\n");
true
}
fn handle_builtin(
&mut self,
line: &str,
result: CommandResult,
path_before: Option<String>,
) -> bool {
debug!(
command = %line,
exit_code = result.exit_code,
action = ?result.action,
"Builtin command executed"
);
let path_after = std::env::var("PATH").ok();
if path_before != path_after {
info!("PATH changed, reloading classifier cache");
self.classifier.reload_path_cache();
}
self.last_exit_code
.store(result.exit_code, Ordering::Relaxed);
println!();
match result.action {
LoopAction::Continue => {
self.record_history(line, &result);
true
}
LoopAction::Exit => {
info!("Exit command received");
false
}
}
}
fn try_shell_builtins(&mut self, input: &str) -> Option<CommandResult> {
let first_word = input.split_whitespace().next().unwrap_or("");
if !matches!(first_word, "alias" | "unalias" | "source") {
return None;
}
let tokens = match shell_words::split(input) {
Ok(t) => t,
Err(e) => {
let msg = format!("jarvish: parse error: {e}\n");
eprint!("{msg}");
return Some(CommandResult::error(msg, 1));
}
};
if tokens.is_empty() {
return Some(CommandResult::success(String::new()));
}
if tokens
.iter()
.any(|t| matches!(t.as_str(), "|" | ">" | ">>" | "<" | "&&" | "||" | ";"))
{
return None;
}
let expanded: Vec<String> = tokens
.into_iter()
.map(|t| expand::expand_token(&t))
.collect();
let args: Vec<&str> = expanded[1..].iter().map(|s| s.as_str()).collect();
let result = match first_word {
"alias" => alias::execute_with_aliases(&args, &mut self.aliases),
"unalias" => unalias::execute_with_aliases(&args, &mut self.aliases),
"source" => {
let path_str = match source::parse(&args) {
Ok(p) => p,
Err(cmd_result) => return Some(cmd_result),
};
let path = PathBuf::from(&path_str);
self.reload_config(&path)
}
_ => unreachable!(),
};
debug!(
command = %first_word,
exit_code = result.exit_code,
"shell builtin executed"
);
Some(result)
}
fn record_history(&self, line: &str, result: &CommandResult) {
if result.action == LoopAction::Continue {
if let Some(ref bb) = self.black_box {
if let Err(e) = bb.record(line, result) {
warn!("Failed to record history: {e}");
eprintln!("jarvish: warning: failed to record history: {e}");
}
}
}
}
}