1use crate::store::memory::{Memory, MemoryStats, ProjectSummary, SearchResult};
2use unicode_width::UnicodeWidthStr;
3
4pub const GREEN: &str = "\x1b[32m";
6pub const YELLOW: &str = "\x1b[33m";
8pub const CYAN: &str = "\x1b[36m";
10pub const RED: &str = "\x1b[31m";
12pub const BOLD: &str = "\x1b[1m";
14pub const DIM: &str = "\x1b[2m";
16pub const RESET: &str = "\x1b[0m";
18
19pub 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
72pub 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
102pub 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
145pub 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
194pub 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
221pub fn print_success(msg: &str) {
223 println!("{GREEN}✓ {}{}", msg, RESET);
224}
225
226pub fn print_error(msg: &str) {
228 eprintln!("{RED}✗ {}{}", msg, RESET);
229}
230
231pub fn print_warning(msg: &str) {
233 println!("{YELLOW}⚠ {}{}", msg, RESET);
234}
235
236pub 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
282pub 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
298pub 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
315pub 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
341pub 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
364pub 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
419pub 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
444pub 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
454pub 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
487pub 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}