Skip to main content

double_o/
commands.rs

1use humansize::{BINARY, format_size};
2
3use crate::classify::Classification;
4use crate::store::SessionMeta;
5use crate::util::{format_age, now_epoch};
6use crate::{classify, exec, help, init, learn, pattern, session, store};
7
8pub enum Action {
9    Run(Vec<String>),
10    Recall(String),
11    Forget,
12    Learn(Vec<String>),
13    Version,
14    Help(Option<String>),
15    Init,
16}
17
18pub fn parse_action(args: &[String]) -> Action {
19    match args.first().map(|s| s.as_str()) {
20        None => Action::Help(None),
21        Some("recall") => Action::Recall(args[1..].join(" ")),
22        Some("forget") => Action::Forget,
23        Some("learn") => Action::Learn(args[1..].to_vec()),
24        Some("version") => Action::Version,
25        // `oo help <cmd>` — look up cheat sheet; `oo help` alone shows usage
26        Some("help") => Action::Help(args.get(1).cloned()),
27        Some("init") => Action::Init,
28        _ => Action::Run(args.to_vec()),
29    }
30}
31
32pub fn cmd_run(args: &[String]) -> i32 {
33    if args.is_empty() {
34        eprintln!("oo: no command specified");
35        return 1;
36    }
37
38    // Load patterns: user patterns first (override), then builtins
39    let user_patterns = pattern::load_user_patterns(&learn::patterns_dir());
40    let builtin_patterns = pattern::builtins();
41    let mut all_patterns: Vec<&pattern::Pattern> = Vec::new();
42    for p in &user_patterns {
43        all_patterns.push(p);
44    }
45    for p in builtin_patterns {
46        all_patterns.push(p);
47    }
48
49    // Run command
50    let output = match exec::run(args) {
51        Ok(o) => o,
52        Err(e) => {
53            eprintln!("oo: {e}");
54            return 1;
55        }
56    };
57
58    let exit_code = output.exit_code;
59    let command = args.join(" ");
60
61    let combined: Vec<&pattern::Pattern> = all_patterns;
62    let classification = classify_with_refs(&output, &command, &combined);
63
64    // Print result
65    match &classification {
66        Classification::Failure { label, output } => {
67            println!("\u{2717} {label}\n");
68            println!("{output}");
69        }
70        Classification::Passthrough { output } => {
71            print!("{output}");
72        }
73        Classification::Success { label, summary } => {
74            if summary.is_empty() {
75                println!("\u{2713} {label}");
76            } else {
77                println!("\u{2713} {label} ({summary})");
78            }
79        }
80        Classification::Large {
81            label,
82            output,
83            size,
84            ..
85        } => {
86            // Index into store
87            let indexed = try_index(&command, output);
88            let human_size = format_size(*size, BINARY);
89            if indexed {
90                println!(
91                    "\u{25CF} {label} (indexed {human_size} \u{2192} use `oo recall` to query)"
92                );
93            } else {
94                // Couldn't index, show truncated output instead
95                let truncated = classify::smart_truncate(output);
96                print!("{truncated}");
97            }
98        }
99    }
100
101    exit_code
102}
103
104/// Classify using a slice of pattern references.
105pub fn classify_with_refs(
106    output: &exec::CommandOutput,
107    command: &str,
108    patterns: &[&pattern::Pattern],
109) -> Classification {
110    let merged = output.merged_lossy();
111    let lbl = classify::label(command);
112
113    if output.exit_code != 0 {
114        let filtered = match pattern::find_matching_ref(command, patterns) {
115            Some(pat) => {
116                if let Some(failure) = &pat.failure {
117                    pattern::extract_failure(failure, &merged)
118                } else {
119                    classify::smart_truncate(&merged)
120                }
121            }
122            _ => classify::smart_truncate(&merged),
123        };
124        return Classification::Failure {
125            label: lbl,
126            output: filtered,
127        };
128    }
129
130    if merged.len() <= 4096 {
131        return Classification::Passthrough { output: merged };
132    }
133
134    if let Some(pat) = pattern::find_matching_ref(command, patterns) {
135        if let Some(sp) = &pat.success {
136            if let Some(summary) = pattern::extract_summary(sp, &merged) {
137                return Classification::Success {
138                    label: lbl,
139                    summary,
140                };
141            }
142        }
143    }
144
145    let size = merged.len();
146    Classification::Large {
147        label: lbl,
148        output: merged,
149        size,
150    }
151}
152
153pub fn try_index(command: &str, content: &str) -> bool {
154    let mut store = match store::open() {
155        Ok(s) => s,
156        Err(_) => return false,
157    };
158
159    let project_id = session::project_id();
160    let meta = SessionMeta {
161        source: "oo".into(),
162        session: session::session_id(),
163        command: command.into(),
164        timestamp: now_epoch(),
165    };
166
167    // Lazy TTL cleanup (best-effort)
168    let _ = store.cleanup_stale(&project_id, 86400);
169
170    store.index(&project_id, content, &meta).is_ok()
171}
172
173pub fn cmd_recall(query: &str) -> i32 {
174    if query.is_empty() {
175        eprintln!("oo: recall requires a query");
176        return 1;
177    }
178
179    let mut store = match store::open() {
180        Ok(s) => s,
181        Err(e) => {
182            eprintln!("oo: {e}");
183            return 1;
184        }
185    };
186
187    let project_id = session::project_id();
188
189    match store.search(&project_id, query, 5) {
190        Ok(results) if results.is_empty() => {
191            println!("No results found.");
192            0
193        }
194        Ok(results) => {
195            for r in &results {
196                if let Some(meta) = &r.meta {
197                    let age = format_age(meta.timestamp);
198                    println!("[session] {} ({age}):", meta.command);
199                } else {
200                    println!("[memory] project memory:");
201                }
202                // Indent content
203                for line in r.content.lines() {
204                    println!("  {line}");
205                }
206                println!();
207            }
208            0
209        }
210        Err(e) => {
211            eprintln!("oo: {e}");
212            1
213        }
214    }
215}
216
217pub fn cmd_forget() -> i32 {
218    let mut store = match store::open() {
219        Ok(s) => s,
220        Err(e) => {
221            eprintln!("oo: {e}");
222            return 1;
223        }
224    };
225
226    let project_id = session::project_id();
227    let sid = session::session_id();
228
229    match store.delete_by_session(&project_id, &sid) {
230        Ok(count) => {
231            println!("Cleared session data ({count} entries)");
232            0
233        }
234        Err(e) => {
235            eprintln!("oo: {e}");
236            1
237        }
238    }
239}
240
241pub fn cmd_learn(args: &[String]) -> i32 {
242    if args.is_empty() {
243        eprintln!("oo: learn requires a command");
244        return 1;
245    }
246
247    // Run the command normally first
248    let output = match exec::run(args) {
249        Ok(o) => o,
250        Err(e) => {
251            eprintln!("oo: {e}");
252            return 1;
253        }
254    };
255
256    let exit_code = output.exit_code;
257    let command = args.join(" ");
258    let merged = output.merged_lossy();
259
260    // Show normal oo output first
261    let patterns = pattern::builtins();
262    let classification = classify::classify(&output, &command, patterns);
263    match &classification {
264        Classification::Failure { label, output } => {
265            println!("\u{2717} {label}\n");
266            println!("{output}");
267        }
268        Classification::Passthrough { output } => {
269            print!("{output}");
270        }
271        Classification::Success { label, summary } => {
272            if summary.is_empty() {
273                println!("\u{2713} {label}");
274            } else {
275                println!("\u{2713} {label} ({summary})");
276            }
277        }
278        Classification::Large { label, size, .. } => {
279            let human_size = format_size(*size, BINARY);
280            println!("\u{25CF} {label} (indexed {human_size} \u{2192} use `oo recall` to query)");
281        }
282    }
283
284    // Spawn background learn process
285    if let Err(e) = learn::spawn_background(&command, &merged, exit_code) {
286        eprintln!("oo: learn failed: {e}");
287    } else {
288        eprintln!("  [learning pattern for \"{}\"]", classify::label(&command));
289    }
290
291    exit_code
292}
293
294pub fn cmd_help(cmd: &str) -> i32 {
295    match help::lookup(cmd) {
296        Ok(text) => {
297            print!("{text}");
298            0
299        }
300        Err(e) => {
301            eprintln!("oo: {e}");
302            1
303        }
304    }
305}
306
307pub fn cmd_init() -> i32 {
308    match init::run() {
309        Ok(()) => 0,
310        Err(e) => {
311            eprintln!("oo: {e}");
312            1
313        }
314    }
315}
316
317#[cfg(test)]
318#[path = "commands_tests.rs"]
319mod tests;