Skip to main content

mneme/cli/
output.rs

1use crate::store::memory::{Memory, MemoryStats, ProjectSummary, SearchResult};
2use unicode_width::UnicodeWidthStr;
3
4/// ANSI green color code.
5pub const GREEN: &str = "\x1b[32m";
6/// ANSI yellow color code.
7pub const YELLOW: &str = "\x1b[33m";
8/// ANSI cyan color code.
9pub const CYAN: &str = "\x1b[36m";
10/// ANSI red color code.
11pub const RED: &str = "\x1b[31m";
12/// ANSI bold code.
13pub const BOLD: &str = "\x1b[1m";
14/// ANSI dim code.
15pub const DIM: &str = "\x1b[2m";
16/// ANSI reset code.
17pub const RESET: &str = "\x1b[0m";
18
19/// Prints a single memory with full details.
20pub fn print_memory(memory: &Memory) {
21    println!(
22        "{BOLD}┌{}
23",
24        "─".repeat(78)
25    );
26    println!("{BOLD}│ {CYAN}{}{RESET}", memory.title);
27    println!(
28        "{BOLD}├{}
29",
30        "─".repeat(78)
31    );
32    println!("{DIM}│ ID:{RESET}        {}", memory.id);
33    println!("{DIM}│ Project:{RESET}   {}", memory.project);
34    println!("{DIM}│ Type:{RESET}      {}", memory.memory_type);
35    println!("{DIM}│ Importance:{RESET} {}", memory.importance);
36    println!("{DIM}│ Scope:{RESET}     {}", memory.scope);
37    println!(
38        "{DIM}│ Created:{RESET}   {}",
39        memory.created_at.format("%Y-%m-%d %H:%M:%S")
40    );
41    println!(
42        "{DIM}│ Updated:{RESET}   {}",
43        memory.updated_at.format("%Y-%m-%d %H:%M:%S")
44    );
45    if !memory.tags.is_empty() {
46        println!("{DIM}│ Tags:{RESET}      {}", memory.tags.join(", "));
47    }
48    if let Some(topic_key) = &memory.topic_key {
49        println!("{DIM}│ Topic:{RESET}     {}", topic_key);
50    }
51    println!("{BOLD}├{}", "─".repeat(78));
52    println!("{RESET}{}", memory.content);
53    if let Some(what) = &memory.what {
54        println!("\n{BOLD}What:{RESET}\n{}", what);
55    }
56    if let Some(why) = &memory.why {
57        println!("\n{BOLD}Why:{RESET}\n{}", why);
58    }
59    if let Some(ctx) = &memory.context {
60        println!("\n{BOLD}Context:{RESET}\n{}", ctx);
61    }
62    if let Some(learned) = &memory.learned {
63        println!("\n{BOLD}Learned:{RESET}\n{}", learned);
64    }
65    println!(
66        "{BOLD}└{}
67",
68        "─".repeat(78)
69    );
70}
71
72/// Prints a table of memories.
73pub fn print_memory_list(memories: &[Memory]) {
74    if memories.is_empty() {
75        println!("{YELLOW}No memories found.{RESET}");
76        return;
77    }
78
79    let headers = ["ID", "TYPE", "IMP", "TITLE", "PROJECT", "DATE"];
80    let col_widths = [10, 14, 6, 28, 14, 10];
81
82    print_table_header(&headers, &col_widths);
83
84    for mem in memories {
85        let id_short = &mem.id.to_string()[..8];
86        let type_str = mem.memory_type.to_string();
87        let imp_str = mem.importance.to_string();
88        let title = truncate(&mem.title, col_widths[3]);
89        let project = truncate(&mem.project, col_widths[4]);
90        let date = mem.updated_at.format("%Y-%m-%d").to_string();
91
92        println!(
93            "{} {:<10} {:<14} {:<6} {:<28} {:<14} {:<10} {}",
94            DIM, id_short, type_str, imp_str, title, project, date, RESET
95        );
96    }
97
98    println!();
99    println!("{DIM}Total: {}{} memories{}", memories.len(), RESET, RESET);
100}
101
102/// Prints search results.
103pub fn print_search_results(results: &[SearchResult]) {
104    if results.is_empty() {
105        println!("{YELLOW}No results found.{RESET}");
106        return;
107    }
108
109    println!("{BOLD}{} result(s):{RESET}\n", results.len());
110
111    for (i, result) in results.iter().enumerate() {
112        let mem = &result.memory;
113        let score_color = if result.score > 1.5 {
114            GREEN
115        } else if result.score > 0.8 {
116            CYAN
117        } else {
118            YELLOW
119        };
120
121        println!(
122            "{BOLD}[{}]{RESET} {CYAN}{}{RESET} {}{:.2}{}",
123            i + 1,
124            mem.title,
125            score_color,
126            result.score,
127            RESET
128        );
129        println!(
130            "    {DIM}ID:{} {} | {} | {} | {}{}",
131            RESET,
132            &mem.id.to_string()[..8],
133            mem.memory_type,
134            mem.importance,
135            mem.project,
136            RESET
137        );
138        if let Some(snippet) = &result.snippet {
139            println!("    {DIM}{}{}", snippet, RESET);
140        }
141        println!();
142    }
143}
144
145/// Prints memory stats.
146pub fn print_stats(stats: &MemoryStats) {
147    println!(
148        "{BOLD}Stats for project '{}{}'{RESET}\n",
149        CYAN, stats.project
150    );
151    println!("  {DIM}Total memories:{RESET}  {}", stats.total_memories);
152    println!("  {DIM}Total relations:{RESET} {}", stats.total_relations);
153    println!("  {DIM}Total sessions:{RESET}  {}", stats.total_sessions);
154    println!("  {DIM}Total prompts:{RESET}   {}", stats.total_prompts);
155
156    if !stats.by_type.is_empty() {
157        println!("\n  {BOLD}By type:{RESET}");
158        for (t, count) in &stats.by_type {
159            println!("    {:<16} {}", t, count);
160        }
161    }
162
163    if !stats.by_importance.is_empty() {
164        println!("\n  {BOLD}By importance:{RESET}");
165        for (imp, count) in &stats.by_importance {
166            println!("    {:<16} {}", imp, count);
167        }
168    }
169
170    if !stats.by_scope.is_empty() {
171        println!("\n  {BOLD}By scope:{RESET}");
172        for (scope, count) in &stats.by_scope {
173            println!("    {:<16} {}", scope, count);
174        }
175    }
176
177    if let Some(oldest) = stats.oldest_memory {
178        println!(
179            "\n  {DIM}Oldest memory:{RESET}  {}",
180            oldest.format("%Y-%m-%d")
181        );
182    }
183    if let Some(newest) = stats.newest_memory {
184        println!(
185            "  {DIM}Newest memory:{RESET}  {}",
186            newest.format("%Y-%m-%d")
187        );
188    }
189    if let Some(most_accessed) = &stats.most_accessed {
190        println!("  {DIM}Most accessed:{RESET}  {}", most_accessed);
191    }
192}
193
194/// Prints a list of projects.
195pub fn print_projects(projects: &[ProjectSummary]) {
196    if projects.is_empty() {
197        println!("{YELLOW}No projects found.{RESET}");
198        return;
199    }
200
201    let headers = ["PROJECT", "MEMORIES", "SESSIONS", "LAST ACTIVITY"];
202    let col_widths = [24, 12, 12, 20];
203
204    print_table_header(&headers, &col_widths);
205
206    for proj in projects {
207        let last = proj
208            .last_activity
209            .map(|d| d.format("%Y-%m-%d %H:%M").to_string())
210            .unwrap_or_else(|| "-".to_string());
211        println!(
212            "{DIM} {:<24} {:<12} {:<12} {:<20} {}",
213            proj.name, proj.memory_count, proj.session_count, last, RESET
214        );
215    }
216
217    println!();
218    println!("{DIM}Total: {}{} projects{}", projects.len(), RESET, RESET);
219}
220
221/// Prints a success message.
222pub fn print_success(msg: &str) {
223    println!("{GREEN}✓ {}{}", msg, RESET);
224}
225
226/// Prints an error message.
227pub fn print_error(msg: &str) {
228    eprintln!("{RED}✗ {}{}", msg, RESET);
229}
230
231/// Prints a warning message.
232pub fn print_warning(msg: &str) {
233    println!("{YELLOW}⚠ {}{}", msg, RESET);
234}
235
236/// Prints an audit report.
237pub fn print_audit(report: &crate::store::memory::AuditReport) {
238    println!("{BOLD}Audit Report{RESET}\n");
239    println!(
240        "  {DIM}Average revisions:{RESET}  {:.2}",
241        report.average_revisions
242    );
243    println!(
244        "  {DIM}Duplicate groups:{RESET}   {}",
245        report.duplicate_groups
246    );
247    println!();
248
249    println!("{BOLD}Type Distribution:{RESET}");
250    for (t, count) in &report.type_distribution {
251        println!("  {:<16} {}", t, count);
252    }
253    println!();
254
255    println!(
256        "{BOLD}Stale memories ({}){RESET}",
257        report.stale_memories.len()
258    );
259    for mem in &report.stale_memories {
260        println!("  - {} ({})", mem.title, mem.id);
261    }
262    println!();
263
264    println!(
265        "{BOLD}Untagged memories ({}){RESET}",
266        report.untagged_memories.len()
267    );
268    for mem in &report.untagged_memories {
269        println!("  - {} ({})", mem.title, mem.id);
270    }
271    println!();
272
273    println!(
274        "{BOLD}Short memories ({}){RESET}",
275        report.short_memories.len()
276    );
277    for mem in &report.short_memories {
278        println!("  - {} ({})", mem.title, mem.id);
279    }
280}
281
282/// Prints duplicate groups.
283pub fn print_duplicate_groups(groups: &[crate::store::memory::DuplicateGroup]) {
284    if groups.is_empty() {
285        println!("{YELLOW}No duplicate groups found.{RESET}");
286        return;
287    }
288    println!("{BOLD}{} duplicate group(s):{RESET}\n", groups.len());
289    for (i, group) in groups.iter().enumerate() {
290        println!("{BOLD}[{}]{RESET} Score: {:.3}", i + 1, group.cosine_score);
291        for (id, title) in group.memory_ids.iter().zip(group.titles.iter()) {
292            println!("  - {} ({})", title, id);
293        }
294        println!();
295    }
296}
297
298/// Prints a graph.
299pub fn print_graph(graph: &crate::store::memory::GraphData) {
300    println!("{BOLD}Knowledge Graph{RESET}\n");
301    println!("{BOLD}Nodes ({}){RESET}", graph.nodes.len());
302    for node in &graph.nodes {
303        println!("  {} [{}] {}", node.id, node.memory_type, node.title);
304    }
305    println!();
306    println!("{BOLD}Edges ({}){RESET}", graph.edges.len());
307    for edge in &graph.edges {
308        println!(
309            "  {} --[{} ({:.2})]--> {}",
310            edge.source, edge.relation_type, edge.confidence, edge.target
311        );
312    }
313}
314
315/// Prints a health report.
316pub fn print_health(report: &crate::store::memory::HealthReport) {
317    println!("{BOLD}System Health{RESET}\n");
318    println!("  {DIM}Version:{RESET}            {}", report.version);
319    println!(
320        "  {DIM}DB size:{RESET}            {:.2} MB",
321        report.db_size_mb
322    );
323    println!(
324        "  {DIM}Total memories:{RESET}     {}",
325        report.total_memories
326    );
327    println!(
328        "  {DIM}Orphaned:{RESET}           {}",
329        report.orphaned_memories
330    );
331    println!(
332        "  {DIM}Unindexed embeddings:{RESET} {}",
333        report.unindexed_embeddings
334    );
335    println!(
336        "  {DIM}Embedding model:{RESET}    {}",
337        report.embedding_model
338    );
339}
340
341/// Prints reminders.
342pub fn print_remind(memories: &[crate::store::memory::Memory]) {
343    if memories.is_empty() {
344        println!("{YELLOW}No reminders found.{RESET}");
345        return;
346    }
347    println!("{BOLD}{} reminder(s):{RESET}\n", memories.len());
348    for (i, mem) in memories.iter().enumerate() {
349        println!(
350            "{BOLD}[{}]{RESET} {} ({})",
351            i + 1,
352            mem.title,
353            mem.importance
354        );
355        println!(
356            "    {DIM}{}{}",
357            &mem.content[..mem.content.len().min(100)],
358            RESET
359        );
360        println!();
361    }
362}
363
364/// Prints knowledge gaps.
365pub fn print_knowledge_gaps(report: &crate::store::memory::KnowledgeGapsReport) {
366    println!("{BOLD}Knowledge Gaps{RESET}\n");
367    println!(
368        "  {DIM}Coverage score:{RESET} {:.1}%",
369        report.coverage_score * 100.0
370    );
371    println!();
372    if report.gaps.is_empty() {
373        println!("{GREEN}No significant gaps detected.{RESET}");
374        return;
375    }
376    for gap in &report.gaps {
377        println!("{YELLOW}⚠ {}{RESET}", gap.area);
378        println!("  Count: {}", gap.count);
379        println!("  Suggestion: {}", gap.suggestion);
380        println!();
381    }
382}
383
384fn print_table_header(headers: &[&str], widths: &[usize]) {
385    print!("{BOLD}");
386    for (i, header) in headers.iter().enumerate() {
387        let w = widths.get(i).copied().unwrap_or(12);
388        print!(" {:<1$}", header, w);
389    }
390    println!("{RESET}");
391    print!("{DIM}");
392    for (i, _header) in headers.iter().enumerate() {
393        let w = widths.get(i).copied().unwrap_or(12);
394        print!(" {}", "─".repeat(w));
395    }
396    println!("{RESET}");
397}
398
399fn truncate(s: &str, max_width: usize) -> String {
400    let width = s.width();
401    if width <= max_width {
402        return s.to_string();
403    }
404
405    let mut result = String::new();
406    let mut current_width = 0;
407    for ch in s.chars() {
408        let ch_width = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1);
409        if current_width + ch_width + 1 > max_width {
410            result.push('…');
411            break;
412        }
413        result.push(ch);
414        current_width += ch_width;
415    }
416    result
417}
418
419/// Prints a list of sync peers.
420pub fn print_peer_list(peers: &[crate::sync::peer::Peer]) {
421    if peers.is_empty() {
422        println!("{YELLOW}No peers found.{RESET}");
423        return;
424    }
425
426    let headers = ["ID", "NAME", "TRANSPORT", "ADDRESS", "PROJECT", "AUTO"];
427    let col_widths = [10, 20, 12, 24, 16, 6];
428
429    print_table_header(&headers, &col_widths);
430
431    for peer in peers {
432        let id_short = &peer.id.to_string()[..8];
433        let auto = if peer.auto_sync { "yes" } else { "no" };
434        println!(
435            "{DIM} {:<10} {:<20} {:<12} {:<24} {:<16} {:<6} {RESET}",
436            id_short, peer.name, peer.transport, peer.address, peer.project, auto
437        );
438    }
439
440    println!();
441    println!("{DIM}Total: {}{} peers{}", peers.len(), RESET, RESET);
442}
443
444/// Prints sync status.
445pub fn print_sync_status(stats: &crate::sync::protocol::ExportStats) {
446    println!("{BOLD}Sync Export{RESET}\n");
447    println!(
448        "  {DIM}Memories exported:{RESET} {}",
449        stats.memories_exported
450    );
451    println!("  {DIM}Bytes written:{RESET} {}", stats.bytes_written);
452}
453
454/// Prints sync results.
455pub fn print_sync_result(results: &[crate::sync::protocol::SyncResult]) {
456    if results.is_empty() {
457        println!("{YELLOW}No sync results.{RESET}");
458        return;
459    }
460
461    println!("{BOLD}Sync Results{RESET}\n");
462    for result in results {
463        let status_color = match result.status {
464            crate::sync::protocol::SyncStatus::Ok => GREEN,
465            crate::sync::protocol::SyncStatus::Partial => YELLOW,
466            crate::sync::protocol::SyncStatus::Error => RED,
467        };
468        println!(
469            "  {DIM}Peer:{RESET} {} ({:?})",
470            result.peer_name, result.direction
471        );
472        println!(
473            "  {DIM}Sent:{RESET} {} {DIM}Received:{RESET} {} {DIM}Conflicts:{RESET} {}",
474            result.memories_sent, result.memories_received, result.conflicts_resolved
475        );
476        println!(
477            "  {DIM}Duration:{RESET} {}ms {DIM}Status:{RESET} {}{:?}{}",
478            result.duration_ms, status_color, result.status, RESET
479        );
480        if let Some(ref error) = result.error {
481            println!("  {RED}Error: {}{}", error, RESET);
482        }
483        println!();
484    }
485}
486
487/// Prints sync log.
488pub fn print_sync_log(entries: &[crate::sync::protocol::SyncResult]) {
489    if entries.is_empty() {
490        println!("{YELLOW}No sync log entries.{RESET}");
491        return;
492    }
493
494    println!("{BOLD}Sync Log{RESET}\n");
495    for entry in entries {
496        println!(
497            "  {DIM}{} | {} | sent:{} recv:{} conflicts:{}{}",
498            entry.peer_name,
499            entry.duration_ms,
500            entry.memories_sent,
501            entry.memories_received,
502            entry.conflicts_resolved,
503            RESET
504        );
505    }
506}