1use anyhow::Result;
6use serde::Serialize;
7use std::io::Write;
8use std::path::Path;
9
10use crate::config;
11use crate::db::{self, CommandRecord, SearchFilter};
12
13const DEFAULT_LIMIT: usize = 20;
15
16#[derive(Serialize)]
18struct JsonRow<'a> {
19 id: i64,
20 created_at: &'a str,
21 cwd: Option<&'a str>,
22 git_root: Option<&'a str>,
23 exit_code: Option<i64>,
24 command: &'a str,
25}
26
27pub fn run(
29 limit: Option<usize>,
30 project: Option<String>,
31 branch: Option<String>,
32 json: bool,
33) -> Result<()> {
34 let limit = limit.unwrap_or(DEFAULT_LIMIT);
35 let conn = db::open(&config::db_path()?)?;
36 let filter = SearchFilter { project, branch };
37 let records = db::fetch_filtered(&conn, &filter, limit)?;
38
39 let stdout = std::io::stdout();
43 let mut out = stdout.lock();
44
45 if json {
46 let rows: Vec<JsonRow> = records
47 .iter()
48 .map(|r| JsonRow {
49 id: r.id,
50 created_at: &r.created_at,
51 cwd: r.cwd.as_deref(),
52 git_root: r.git_root.as_deref(),
53 exit_code: r.exit_code,
54 command: &r.command,
55 })
56 .collect();
57 writeln!(out, "{}", serde_json::to_string_pretty(&rows)?)?;
58 return Ok(());
59 }
60
61 if records.is_empty() {
62 writeln!(out, "Aucune commande à afficher.")?;
63 return Ok(());
64 }
65
66 for r in &records {
67 writeln!(out, "{}", short_line(r))?;
68 }
69 Ok(())
70}
71
72fn location(r: &CommandRecord) -> String {
74 if let Some(root) = r.git_root.as_deref().filter(|s| !s.is_empty()) {
75 let name = Path::new(root)
76 .file_name()
77 .and_then(|n| n.to_str())
78 .unwrap_or(root);
79 return name.to_string();
80 }
81 r.cwd.clone().unwrap_or_else(|| "-".to_string())
82}
83
84pub fn short_line(r: &CommandRecord) -> String {
87 let date = r
88 .created_at
89 .split_whitespace()
90 .next()
91 .unwrap_or(&r.created_at);
92 let exit = r
93 .exit_code
94 .map(|c| c.to_string())
95 .unwrap_or_else(|| "-".to_string());
96 format!(
97 "{:>6} {:<10} {:<20} [{:>3}] {}",
98 r.id,
99 date,
100 truncate(&location(r), 20),
101 exit,
102 r.command
103 )
104}
105
106fn truncate(s: &str, max: usize) -> String {
108 if s.chars().count() <= max {
109 return s.to_string();
110 }
111 let mut out: String = s.chars().take(max.saturating_sub(1)).collect();
112 out.push('…');
113 out
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 fn rec(id: i64, command: &str) -> CommandRecord {
121 CommandRecord {
122 id,
123 command: command.to_string(),
124 cwd: Some("/home/u/proj/mnemo".to_string()),
125 shell: None,
126 hostname: None,
127 exit_code: Some(0),
128 created_at: "2026-06-14 12:00:00".to_string(),
129 git_root: Some("/home/u/proj/mnemo".to_string()),
130 git_branch: Some("main".to_string()),
131 git_remote: None,
132 session_id: None,
133 }
134 }
135
136 #[test]
137 fn short_line_contient_id_et_commande() {
138 let line = short_line(&rec(123, "cargo build"));
139 assert!(line.contains("123"));
140 assert!(line.contains("mnemo"));
141 assert!(line.contains("cargo build"));
142 }
143
144 #[test]
145 fn troncature() {
146 assert_eq!(truncate("abc", 5), "abc");
147 assert_eq!(truncate("abcdef", 4), "abc…");
148 }
149}