Skip to main content

mnemo/
list.rs

1//! Affichage des dernières commandes (`mnemo list`).
2//!
3//! Pratique pour repérer l'ID d'une commande à supprimer avec `mnemo delete`.
4
5use anyhow::Result;
6use serde::Serialize;
7use std::io::Write;
8use std::path::Path;
9
10use crate::config;
11use crate::db::{self, CommandRecord, SearchFilter};
12
13/// Nombre de commandes affichées par défaut.
14const DEFAULT_LIMIT: usize = 20;
15
16/// Ligne JSON pour `mnemo list --json`.
17#[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
27/// Point d'entrée de `mnemo list`.
28pub 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    // Écriture via un stdout verrouillé : un `BrokenPipe` (sortie pipée vers
40    // `head`, `less`…) remonte alors comme une erreur propre plutôt que de
41    // faire paniquer `println!`.
42    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
72/// Nom de projet (dernier segment de `git_root`) ou `cwd`, pour l'affichage.
73fn 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
84/// Ligne courte et alignée : `   123  2026-06-14  projet  [0]  commande`.
85/// Réutilisée par `mnemo delete` / `mnemo prune` pour prévisualiser.
86pub 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
106/// Tronque une chaîne (affichage) à `max` caractères, suffixe `…`.
107fn 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}