Skip to main content

snip_it/commands/
list_cmd.rs

1use crate::commands::{get_library_path, load_snippets};
2use crate::error::SnipResult;
3use crossterm::style::{Color, Stylize, style};
4use fuzzy_matcher::FuzzyMatcher;
5use fuzzy_matcher::skim::SkimMatcherV2;
6use std::path::PathBuf;
7
8/// Output format for the `list` command.
9#[derive(Debug, Clone, PartialEq)]
10pub enum ListFormat {
11    /// Human-readable table format (default).
12    Default,
13    /// JSON output for scripting.
14    Json,
15    /// CSV output for spreadsheet import.
16    Csv,
17}
18
19/// Lists snippets from the library, optionally filtered and in a given format.
20pub fn run(
21    filter: Option<String>,
22    config: Option<PathBuf>,
23    library: Option<String>,
24    format: ListFormat,
25) -> SnipResult<()> {
26    let snippets = if config.is_some() {
27        load_snippets(&config)?
28    } else {
29        let lib_path = match get_library_path(library)? {
30            Some(p) => p,
31            None => {
32                eprintln!("No library found. Create one with 'snp library create <name>'");
33                return Ok(());
34            }
35        };
36        crate::library::load_library(&lib_path)?
37    };
38
39    let matcher = SkimMatcherV2::default();
40
41    let filtered: Vec<_> = if let Some(ref filter_str) = filter {
42        snippets
43            .snippets
44            .iter()
45            .enumerate()
46            .filter(|(_, s)| !s.deleted)
47            .filter(|(_, s)| {
48                let display = format!("{} {}", s.description, s.command);
49                matcher.fuzzy_match(&display, filter_str).is_some()
50            })
51            .collect()
52    } else {
53        snippets
54            .snippets
55            .iter()
56            .enumerate()
57            .filter(|(_, s)| !s.deleted)
58            .collect()
59    };
60
61    match format {
62        ListFormat::Json => {
63            let items: Vec<_> = filtered
64                .iter()
65                .map(|(_, s)| {
66                    serde_json::json!({
67                        "description": s.description,
68                        "command": s.command,
69                        "output": s.output,
70                        "tags": s.tags,
71                        "folders": s.folders,
72                        "favorite": s.favorite,
73                    })
74                })
75                .collect();
76            println!(
77                "{}",
78                serde_json::to_string_pretty(&items).map_err(|e| {
79                    crate::error::SnipError::runtime_error(
80                        "JSON serialization failed",
81                        Some(&e.to_string()),
82                    )
83                })?
84            );
85        }
86        ListFormat::Csv => {
87            println!("description,command,output,tags,folders,favorite");
88            for (_, s) in filtered {
89                let tags = s.tags.join(";");
90                let folders = s.folders.join(";");
91                println!(
92                    "{},{},{},{},{},{}",
93                    csv_escape(&s.description),
94                    csv_escape(&s.command),
95                    csv_escape(&s.output),
96                    csv_escape(&tags),
97                    csv_escape(&folders),
98                    s.favorite
99                );
100            }
101        }
102        ListFormat::Default => {
103            for (_, s) in filtered {
104                println!("{}", style("-----").with(Color::Blue));
105                println!(
106                    "{}: {}",
107                    style(&s.description).with(Color::Green),
108                    style(&s.command).with(Color::White)
109                );
110                println!(
111                    "{}: {}",
112                    style("Output").with(Color::Yellow),
113                    style(&s.output).with(Color::White)
114                );
115                println!(
116                    "{}: {}",
117                    style("Tags").with(Color::Cyan),
118                    style(s.tags.join(", ")).with(Color::White)
119                );
120            }
121        }
122    }
123    Ok(())
124}
125
126fn csv_escape(s: &str) -> String {
127    let field = if s
128        .chars()
129        .next()
130        .is_some_and(|first| matches!(first, '=' | '+' | '-' | '@'))
131    {
132        format!("\t{s}")
133    } else {
134        s.to_string()
135    };
136
137    if field.contains(',')
138        || s.contains('"')
139        || field.contains('\n')
140        || field.contains('\r')
141        || field.contains('\t')
142    {
143        format!("\"{}\"", field.replace('"', "\"\""))
144    } else {
145        field
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn csv_escape_quotes_commas_and_quotes() {
155        assert_eq!(csv_escape("desc, with comma"), "\"desc, with comma\"");
156        assert_eq!(csv_escape("echo \"quoted\""), "\"echo \"\"quoted\"\"\"");
157    }
158
159    #[test]
160    fn csv_escape_prefixes_formula_values_before_quoting() {
161        assert_eq!(csv_escape("=cmd"), "\"\t=cmd\"");
162        assert_eq!(csv_escape("=cmd,with comma"), "\"\t=cmd,with comma\"");
163    }
164}