use std::path::Path;
use humansize::{BINARY, format_size};
use std::io::Write;
use crate::classify::Classification;
pub use crate::display::savings_suffix;
use crate::indexing::index_or_warn;
pub use crate::indexing::try_index;
pub use crate::init::InitFormat;
pub(crate) use crate::init_cmd::parse_init_mode;
pub use crate::init_cmd::{InitMode, cmd_init};
use crate::{
classify, commands_patterns, exec, failure_display, help, learn, pattern, pattern_load,
rewrite, session, store, usage,
};
pub enum Action {
Usage(&'static str),
Run(Vec<String>),
Recall {
query: String,
full: bool,
},
Forget,
Learn(Vec<String>, Option<String>),
Version,
Help(Option<String>),
Init(InitMode),
Patterns,
Rewrite(String),
Hook(Option<String>),
}
fn parse_recall_action(args: &[String]) -> Action {
let query: String = args
.iter()
.filter(|a| a.as_str() != "--full")
.cloned()
.collect::<Vec<_>>()
.join(" ");
let full = args.iter().any(|a| a.as_str() == "--full");
Action::Recall { query, full }
}
fn parse_learn_action(args: &[String]) -> Action {
let mut result: Vec<String> = Vec::new();
let mut hint: Option<String> = None;
let mut iter = args.iter().peekable();
while let Some(arg) = iter.next() {
if arg == "--hint" {
if let Some(hint_text) = iter.next() {
if !hint_text.starts_with('-') {
hint = Some(hint_text.clone());
} else {
result.push(hint_text.clone());
}
}
} else {
result.push(arg.clone());
}
}
Action::Learn(result, hint)
}
pub fn parse_action(args: &[String]) -> Action {
if args.get(1).is_some_and(|a| a == "--help" || a == "-h") {
if let Some(text) = args.first().and_then(|s| usage::for_subcommand(s)) {
return Action::Usage(text);
}
}
match args.first().map(|s| s.as_str()) {
None => Action::Help(None),
Some("recall") => parse_recall_action(&args[1..]),
Some("forget") => Action::Forget,
Some("learn") => parse_learn_action(&args[1..]),
Some("version") => Action::Version,
Some("help") => Action::Help(args.get(1).cloned()),
Some("init") => match parse_init_mode(&args[1..]) {
Ok(mode) => Action::Init(mode),
Err(e) => {
if e.starts_with("oo init: ") {
eprintln!("{e}");
} else {
eprintln!("oo: {e}");
}
std::process::exit(1);
}
},
Some("patterns") => Action::Patterns,
Some("rewrite") => {
let command: String = args
.get(1..)
.map(|rest| rest.join(" "))
.filter(|s| !s.is_empty())
.unwrap_or_default();
Action::Rewrite(command)
}
Some("hook") => Action::Hook(args.get(1).cloned()),
_ => Action::Run(args.to_vec()),
}
}
pub fn run_command_args(args: &[String]) -> (i32, Option<Classification>) {
if args.is_empty() {
eprintln!("oo: no command specified");
return (1, None);
}
let all_patterns = pattern_load::all_patterns();
let output = match exec::run(args) {
Ok(o) => o,
Err(e) => {
eprintln!("oo: {e}");
return (1, None);
}
};
let exit_code = output.exit_code;
let command = args.join(" ");
let merged = output.merged_lossy();
let classification = classify::classify(&output, &command, &all_patterns);
render_classification(&classification, &command, merged.len());
(exit_code, Some(classification))
}
pub fn cmd_run(args: &[String]) -> i32 {
run_command_args(args).0
}
pub fn render_classification(classification: &Classification, command: &str, original_size: usize) {
match classification {
Classification::Failure {
label,
output,
full_output,
} => failure_display::render(label, command, original_size, output, full_output),
Classification::Passthrough { output } => {
print!("{output}");
}
Classification::Success { label, summary } => {
let line = if summary.is_empty() {
format!("\u{2713} {label}")
} else {
format!("\u{2713} {label} ({summary})")
};
println!(
"{line}{}",
savings_suffix(original_size, line.len()).unwrap_or_default()
);
}
Classification::Bounded {
label,
output,
display,
size,
..
} => {
let human_size = format_size(*size, BINARY);
if index_or_warn(command, output) {
println!(
"\u{25CF} {label} (output truncated: {human_size} total \u{2192} use `oo recall` to query)"
);
} else {
println!(
"\u{25CF} {label} (output truncated: {human_size} total — NOT indexed, recall unavailable)"
);
}
print!("{display}");
}
Classification::Large {
label,
output,
size,
..
} => {
let indexed = try_index(command, output);
let human_size = format_size(*size, BINARY);
if indexed {
println!(
"\u{25CF} {label} (indexed {human_size} \u{2192} use `oo recall` to query)"
);
} else {
let truncated = classify::smart_truncate(output);
print!("{truncated}");
}
}
}
}
pub fn cmd_recall(query: &str, full: bool) -> i32 {
crate::recall_display::cmd_recall(query, full)
}
pub fn cmd_forget() -> i32 {
let mut store = match store::open() {
Ok(s) => s,
Err(e) => {
eprintln!("oo: {e}");
return 1;
}
};
let project_id = session::project_id();
match store.delete_project(&project_id) {
Ok(count) => {
println!("Cleared project data ({count} entries)");
0
}
Err(e) => {
eprintln!("oo: {e}");
1
}
}
}
pub fn cmd_learn(args: &[String], hint: Option<&str>) -> i32 {
if args.is_empty() {
eprintln!("oo: learn requires a command");
return 1;
}
let output = match exec::run(args) {
Ok(o) => o,
Err(e) => {
eprintln!("oo: {e}");
return 1;
}
};
let exit_code = output.exit_code;
let command = args.join(" ");
let merged = output.merged_lossy();
let original_size = merged.len();
let classification = classify::classify(&output, &command, pattern::builtins());
render_classification(&classification, &command, original_size);
let config = learn::load_learn_config().unwrap_or_else(|e| {
eprintln!("oo: config error: {e}");
learn::LearnConfig::default()
});
eprintln!(
" [learning pattern for \"{}\" ({})]",
classify::label(&command),
config.provider
);
if let Err(e) = learn::spawn_background(&command, &merged, exit_code, hint) {
eprintln!("oo: learn failed: {e}");
}
exit_code
}
pub fn write_learn_status(
status_path: &Path,
cmd_name: &str,
pattern_path: &Path,
) -> Result<(), std::io::Error> {
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(status_path)?;
writeln!(
file,
"learned pattern for {} → {}",
cmd_name,
pattern_path.display()
)
}
pub fn write_learn_status_failure(
status_path: &Path,
cmd_name: &str,
error_msg: &str,
) -> Result<(), std::io::Error> {
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(status_path)?;
let first_line = error_msg.lines().next().unwrap_or(error_msg);
writeln!(file, "FAILED {cmd_name}: {first_line}")
}
pub fn check_and_clear_learn_status(status_path: &Path) {
if let Ok(content) = std::fs::read_to_string(status_path) {
for line in content.lines() {
if let Some(rest) = line.strip_prefix("FAILED ") {
if let Some((cmd, msg)) = rest.split_once(": ") {
eprintln!("oo: learn failed for {cmd} — {msg}");
} else {
eprintln!("oo: learn failed — {rest}");
}
} else {
eprintln!("oo: {line}");
}
}
let _ = std::fs::remove_file(status_path);
}
}
pub fn cmd_rewrite(command: &str) -> i32 {
match rewrite::rewrite(command, &rewrite::PATTERNS) {
Some(rewritten) => {
println!("{rewritten}");
0
}
None => 1,
}
}
pub fn cmd_patterns() -> i32 {
self::commands_patterns::cmd_patterns()
}
pub fn cmd_patterns_in(dir: &Path) -> i32 {
self::commands_patterns::cmd_patterns_in(dir)
}
pub fn list_patterns_in(dir: &Path) -> bool {
self::commands_patterns::list_patterns_in(dir)
}
pub fn cmd_help(cmd: &str) -> i32 {
if let Some(text) = usage::for_subcommand(cmd) {
print!("{text}");
return 0;
}
match help::lookup(cmd) {
Ok(text) => {
print!("{text}");
0
}
Err(e) => {
eprintln!("oo: {e}");
1
}
}
}
pub fn cmd_hook(agent: &str) -> i32 {
match agent {
"claude" => crate::hook::cmd_hook_claude(),
other => {
eprintln!("oo: unknown hook agent '{other}'; supported hook agents: claude");
1
}
}
}
#[cfg(test)]
#[path = "commands_tests.rs"]
mod tests;