1use chrono::{DateTime, Utc};
2use serde_json::{Value, json};
3
4use crate::model::{Attachment, Note, PinRecord, Tag};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum OutputFormat {
10 #[default]
11 Text,
12 Json,
13}
14
15impl std::str::FromStr for OutputFormat {
16 type Err = anyhow::Error;
17 fn from_str(s: &str) -> Result<Self, Self::Err> {
18 match s {
19 "json" => Ok(OutputFormat::Json),
20 "text" | "" => Ok(OutputFormat::Text),
21 other => anyhow::bail!("unknown format: {other}"),
22 }
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum NoteField {
30 Id,
31 Title,
32 Tags,
33 Hash,
34 Length,
35 Created,
36 Modified,
37 Pins,
38 Location,
39 Todos,
40 Done,
41 Attachments,
42 Content,
43 Locked,
44}
45
46impl NoteField {
47 fn name(&self) -> &'static str {
48 match self {
49 NoteField::Id => "id",
50 NoteField::Title => "title",
51 NoteField::Tags => "tags",
52 NoteField::Hash => "hash",
53 NoteField::Length => "length",
54 NoteField::Created => "created",
55 NoteField::Modified => "modified",
56 NoteField::Pins => "pins",
57 NoteField::Location => "location",
58 NoteField::Todos => "todos",
59 NoteField::Done => "done",
60 NoteField::Attachments => "attachments",
61 NoteField::Content => "content",
62 NoteField::Locked => "locked",
63 }
64 }
65}
66
67pub fn parse_note_fields(spec: &str) -> anyhow::Result<Vec<NoteField>> {
73 const ALL_FIELDS: &[NoteField] = &[
74 NoteField::Id,
75 NoteField::Title,
76 NoteField::Tags,
77 NoteField::Hash,
78 NoteField::Length,
79 NoteField::Created,
80 NoteField::Modified,
81 NoteField::Pins,
82 NoteField::Location,
83 NoteField::Todos,
84 NoteField::Done,
85 NoteField::Attachments,
86 NoteField::Locked,
87 ];
88
89 let parts: Vec<&str> = spec.split(',').map(str::trim).collect();
90
91 let mut fields = Vec::new();
93 for part in &parts {
94 match *part {
95 "all" => fields.extend_from_slice(ALL_FIELDS),
96 "content" => fields.push(NoteField::Content),
97 other => {
98 let f = field_from_str(other)?;
99 fields.push(f);
100 }
101 }
102 }
103 let mut seen = std::collections::HashSet::new();
105 fields.retain(|f| seen.insert(f.name()));
106 Ok(fields)
107}
108
109fn field_from_str(s: &str) -> anyhow::Result<NoteField> {
110 match s {
111 "id" => Ok(NoteField::Id),
112 "title" => Ok(NoteField::Title),
113 "tags" => Ok(NoteField::Tags),
114 "hash" => Ok(NoteField::Hash),
115 "length" => Ok(NoteField::Length),
116 "created" => Ok(NoteField::Created),
117 "modified" => Ok(NoteField::Modified),
118 "pins" => Ok(NoteField::Pins),
119 "location" => Ok(NoteField::Location),
120 "todos" => Ok(NoteField::Todos),
121 "done" => Ok(NoteField::Done),
122 "attachments" => Ok(NoteField::Attachments),
123 "content" => Ok(NoteField::Content),
124 "locked" => Ok(NoteField::Locked),
125 other => anyhow::bail!("invalid field: {other}"),
126 }
127}
128
129pub fn default_list_fields() -> Vec<NoteField> {
131 vec![NoteField::Id, NoteField::Title, NoteField::Tags]
132}
133
134pub fn default_show_fields() -> Vec<NoteField> {
136 vec![
137 NoteField::Id,
138 NoteField::Title,
139 NoteField::Tags,
140 NoteField::Hash,
141 NoteField::Length,
142 NoteField::Created,
143 NoteField::Modified,
144 NoteField::Pins,
145 NoteField::Location,
146 NoteField::Todos,
147 NoteField::Done,
148 NoteField::Attachments,
149 NoteField::Locked,
150 ]
151}
152
153fn fmt_ts(unix: i64) -> String {
156 DateTime::<Utc>::from_timestamp(unix, 0)
157 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())
158 .unwrap_or_else(|| unix.to_string())
159}
160
161fn note_field_value(note: &Note, field: NoteField) -> String {
164 match field {
165 NoteField::Id => note.id.clone(),
166 NoteField::Title => note.title.clone(),
167 NoteField::Tags => note.tags.join(","),
168 NoteField::Hash => note.hash(),
169 NoteField::Length => note.length().to_string(),
170 NoteField::Created => fmt_ts(note.created),
171 NoteField::Modified => fmt_ts(note.modified),
172 NoteField::Pins => note.pinned_in_tags.join(","),
173 NoteField::Location => String::new(), NoteField::Todos => note.todo_incompleted.to_string(),
175 NoteField::Done => note.todo_completed.to_string(),
176 NoteField::Attachments => note
177 .attachments
178 .iter()
179 .map(|a| a.filename.as_str())
180 .collect::<Vec<_>>()
181 .join(","),
182 NoteField::Content => note.text.clone(),
183 NoteField::Locked => note.locked.to_string(),
184 }
185}
186
187fn note_field_json(note: &Note, field: NoteField) -> (&'static str, Value) {
188 match field {
189 NoteField::Id => ("id", json!(note.id)),
190 NoteField::Title => ("title", json!(note.title)),
191 NoteField::Tags => ("tags", json!(note.tags)),
192 NoteField::Hash => ("hash", json!(note.hash())),
193 NoteField::Length => ("length", json!(note.length())),
194 NoteField::Created => ("created", json!(fmt_ts(note.created))),
195 NoteField::Modified => ("modified", json!(fmt_ts(note.modified))),
196 NoteField::Pins => ("pins", json!(note.pinned_in_tags)),
197 NoteField::Location => ("location", json!(null)),
198 NoteField::Todos => ("todos", json!(note.todo_incompleted)),
199 NoteField::Done => ("done", json!(note.todo_completed)),
200 NoteField::Attachments => (
201 "attachments",
202 json!(
203 note.attachments
204 .iter()
205 .map(|a| json!({"filename": a.filename, "size": a.size}))
206 .collect::<Vec<_>>()
207 ),
208 ),
209 NoteField::Content => ("content", json!(note.text)),
210 NoteField::Locked => ("locked", json!(note.locked)),
211 }
212}
213
214pub fn print_notes(notes: &[Note], fields: &[NoteField], format: OutputFormat) {
217 if notes.is_empty() {
218 eprintln!("No notes found.");
219 return;
220 }
221
222 match format {
223 OutputFormat::Text => {
224 for note in notes {
225 let row: Vec<String> = fields.iter().map(|&f| note_field_value(note, f)).collect();
226 println!("{}", row.join("\t"));
227 }
228 }
229 OutputFormat::Json => {
230 let arr: Vec<Value> = notes
231 .iter()
232 .map(|note| {
233 let mut map = serde_json::Map::new();
234 for &f in fields {
235 let (key, val) = note_field_json(note, f);
236 map.insert(key.to_string(), val);
237 }
238 Value::Object(map)
239 })
240 .collect();
241 println!("{}", serde_json::to_string_pretty(&arr).unwrap());
242 }
243 }
244}
245
246pub fn print_note_count(count: usize) {
247 println!("{count}");
248}
249
250pub fn print_tags(tags: &[Tag], format: OutputFormat) {
253 if tags.is_empty() {
254 eprintln!("No notes found.");
255 return;
256 }
257 match format {
258 OutputFormat::Text => {
259 for tag in tags {
260 println!("{}", tag.name);
261 }
262 }
263 OutputFormat::Json => {
264 let arr: Vec<Value> = tags.iter().map(|t| json!({"name": t.name})).collect();
265 println!("{}", serde_json::to_string_pretty(&arr).unwrap());
266 }
267 }
268}
269
270pub fn print_pins(pins: &[PinRecord], format: OutputFormat) {
273 if pins.is_empty() {
274 eprintln!("No notes found.");
275 return;
276 }
277 match format {
278 OutputFormat::Text => {
279 for pin in pins {
280 println!("{}", pin.pin);
281 }
282 }
283 OutputFormat::Json => {
284 let arr: Vec<Value> = pins
285 .iter()
286 .map(|p| json!({"noteId": p.note_id, "pin": p.pin}))
287 .collect();
288 println!("{}", serde_json::to_string_pretty(&arr).unwrap());
289 }
290 }
291}
292
293pub fn print_attachments(attachments: &[Attachment], format: OutputFormat) {
296 if attachments.is_empty() {
297 eprintln!("No notes found.");
298 return;
299 }
300 match format {
301 OutputFormat::Text => {
302 for att in attachments {
303 println!("{}\t{}", att.filename, att.size);
304 }
305 }
306 OutputFormat::Json => {
307 let arr: Vec<Value> = attachments
308 .iter()
309 .map(|a| json!({"filename": a.filename, "size": a.size}))
310 .collect();
311 println!("{}", serde_json::to_string_pretty(&arr).unwrap());
312 }
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn parse_default_fields() {
322 let fields = parse_note_fields("id,title,tags").unwrap();
323 assert_eq!(
324 fields,
325 vec![NoteField::Id, NoteField::Title, NoteField::Tags]
326 );
327 }
328
329 #[test]
330 fn parse_all_fields() {
331 let fields = parse_note_fields("all").unwrap();
332 assert!(!fields.contains(&NoteField::Content));
333 assert!(fields.contains(&NoteField::Hash));
334 }
335
336 #[test]
337 fn parse_all_with_content() {
338 let fields = parse_note_fields("all,content").unwrap();
339 assert!(fields.contains(&NoteField::Content));
340 }
341
342 #[test]
343 fn parse_unknown_field_errors() {
344 assert!(parse_note_fields("id,bogus").is_err());
345 }
346}