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 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 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 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 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 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 let truncated = classify::smart_truncate(output);
96 print!("{truncated}");
97 }
98 }
99 }
100
101 exit_code
102}
103
104pub 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 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 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 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 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 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;