lean_ctx_patterns/
ollama.rs1use crate::ansi::strip_ansi;
11
12pub fn compress(cmd: &str, output: &str) -> Option<String> {
13 let trimmed = output.trim();
14 if trimmed.is_empty() {
15 return Some("ollama: ok".to_string());
16 }
17
18 if cmd.contains(" list") || cmd.contains(" ls") {
19 return Some(compress_table(trimmed, "model(s)"));
20 }
21 if cmd.contains(" ps") {
22 return Some(compress_table(trimmed, "running"));
23 }
24 if cmd.contains(" pull") || cmd.contains(" push") {
25 return Some(compress_transfer(trimmed));
26 }
27
28 None
30}
31
32fn compress_table(output: &str, noun: &str) -> String {
35 let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
36 if lines.len() < 2 {
37 return output.to_string();
38 }
39 let header = split_cols(lines[0]);
40 let drop = header.iter().position(|c| c.eq_ignore_ascii_case("ID"));
41
42 let mut rows: Vec<String> = Vec::new();
43 for line in &lines[1..] {
44 let mut cols = split_cols(line);
45 if let Some(i) = drop
46 && i < cols.len()
47 {
48 cols.remove(i);
49 }
50 rows.push(cols.join(" "));
51 }
52 format!("ollama: {} {}\n{}", rows.len(), noun, rows.join("\n"))
53}
54
55fn compress_transfer(output: &str) -> String {
57 let mut layers = 0usize;
58 let mut success = false;
59 let mut errors: Vec<String> = Vec::new();
60
61 for raw in output.lines() {
62 let line = strip_ansi(raw);
63 let line = line.trim();
64 if line.is_empty() {
65 continue;
66 }
67 if (line.starts_with("pulling") || line.starts_with("pushing")) && line.contains('%') {
68 layers += 1;
69 } else if line == "success" {
70 success = true;
71 } else if line.contains("Error") || line.contains("error") {
72 errors.push(line.to_string());
73 }
74 }
75
76 if !errors.is_empty() {
77 return format!("ollama: FAILED\n {}", errors.join("\n "));
78 }
79 if success {
80 return format!("ollama: success ({layers} layers)");
81 }
82 format!("ollama: {layers} layers")
83}
84
85fn split_cols(line: &str) -> Vec<String> {
88 let mut cols = Vec::new();
89 let mut cur = String::new();
90 let mut spaces = 0;
91 for ch in line.trim().chars() {
92 if ch == ' ' {
93 spaces += 1;
94 continue;
95 }
96 if spaces >= 2 && !cur.is_empty() {
97 cols.push(std::mem::take(&mut cur));
98 } else if spaces == 1 && !cur.is_empty() {
99 cur.push(' ');
100 }
101 spaces = 0;
102 cur.push(ch);
103 }
104 if !cur.is_empty() {
105 cols.push(cur);
106 }
107 cols
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 const LIST: &str = "NAME ID SIZE MODIFIED\nllama3.2:latest a80c4f17acd5 2.0 GB 3 days ago\nqwen2.5-coder:7b 2b0496514337 4.7 GB 2 weeks ago\n";
115
116 #[test]
117 fn list_drops_id_keeps_name_size() {
118 let r = compress("ollama list", LIST).unwrap();
119 assert!(r.contains("2 model(s)"), "{r}");
120 assert!(r.contains("llama3.2:latest"), "{r}");
121 assert!(r.contains("2.0 GB"), "{r}");
122 assert!(r.contains("3 days ago"), "keeps multi-word column: {r}");
123 assert!(!r.contains("a80c4f17acd5"), "drops ID hash: {r}");
124 }
125
126 #[test]
127 fn pull_collapses_progress() {
128 let out = "pulling manifest\npulling aabbccdd... 100% ▕████████▏ 2.0 GB\npulling 1234efgh... 100% ▕██▏ 1.2 KB\nverifying sha256 digest\nwriting manifest\nsuccess\n";
129 let r = compress("ollama pull llama3.2", out).unwrap();
130 assert_eq!(r, "ollama: success (2 layers)");
131 }
132
133 #[test]
134 fn run_is_not_compressed() {
135 assert!(
136 compress(
137 "ollama run llama3.2 'hi'",
138 "The capital of France is Paris."
139 )
140 .is_none()
141 );
142 }
143
144 #[test]
145 fn empty_is_ok() {
146 assert_eq!(compress("ollama list", "").unwrap(), "ollama: ok");
147 }
148}