use std::ffi::OsString;
use std::path::Path;
use humansize::{BINARY, format_size};
use crate::argv;
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<OsString>),
Recall {
query: String,
full: bool,
},
Forget,
Learn(Vec<OsString>, Option<String>),
Version,
Help(Option<String>),
Init(InitMode),
Patterns,
Rewrite(String),
Hook(Option<String>),
}
fn parse_recall_action(args: &[OsString]) -> Action {
let full = args.iter().any(|a| a == "--full");
let query = args
.iter()
.filter(|a| **a != "--full")
.map(|a| a.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(" ");
Action::Recall { query, full }
}
fn parse_learn_action(args: &[OsString]) -> Action {
let mut result: Vec<OsString> = Vec::new();
let mut hint: Option<String> = None;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--hint" {
if let Some(hint_text) = iter.next() {
match argv::utf8_or(hint_text) {
Ok(text) if !text.starts_with('-') => hint = Some(text),
_ => result.push(hint_text.clone()),
}
}
} else {
result.push(arg.clone());
}
}
Action::Learn(result, hint)
}
pub fn parse_action(args: &[OsString]) -> Action {
if args.get(1).is_some_and(|a| a == "--help" || a == "-h") {
let first_str = args.first().and_then(|s| argv::utf8_or(s).ok());
if let Some(name) = first_str {
if let Some(text) = usage::for_subcommand(&name) {
return Action::Usage(text);
}
}
}
let first_lossy = args.first().map(|s| s.to_string_lossy().into_owned());
match first_lossy.as_deref() {
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).map(|a| a.to_string_lossy().into_owned())),
Some("init") => {
let init_args: Vec<String> = args[1..]
.iter()
.map(|a| a.to_string_lossy().into_owned())
.collect();
match parse_init_mode(&init_args) {
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(argv::lossy_join)
.filter(|s| !s.is_empty())
.unwrap_or_default();
Action::Rewrite(command)
}
Some("hook") => Action::Hook(args.get(1).map(|a| a.to_string_lossy().into_owned())),
_ => Action::Run(args.to_vec()),
}
}
pub fn run_command_args(args: &[OsString]) -> (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 = argv::lossy_join(args);
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: &[OsString]) -> 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)"
);
print!("{display}");
} else {
println!(
"\u{25CF} {label} (output truncated: {human_size} total — NOT indexed, recall unavailable)"
);
print!("{}", classify::bounded_truncate_indexed(output, false));
}
}
Classification::Large {
label,
output,
size,
..
} => {
let indexed = index_or_warn(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 {
println!("\u{25CF} {label} ({human_size} total — NOT indexed, recall unavailable)");
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: &[OsString], 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 = argv::lossy_join(args);
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: {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 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;