snip_it/commands/
list_cmd.rs1use 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#[derive(Debug, Clone, PartialEq)]
10pub enum ListFormat {
11 Default,
13 Json,
15 Csv,
17}
18
19pub 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}