Skip to main content

linkmarks_cli/
ui.rs

1//! Output formatting helpers (table / json / yaml).
2
3use anyhow::Result;
4use linkmarks_core::Bookmark;
5use serde::Serialize;
6
7/// A row in the deterministic table view.
8///
9/// Column order is fixed: `id | canonical_url | title | tags | collection | updated_at`.
10#[derive(Debug, Serialize)]
11pub struct TableRow<'a> {
12    pub id: &'a str,
13    pub canonical_url: &'a str,
14    pub title: &'a str,
15    pub tags: String,
16    pub collection: String,
17    pub updated_at: String,
18}
19
20impl<'a> From<&'a Bookmark> for TableRow<'a> {
21    fn from(b: &'a Bookmark) -> Self {
22        Self {
23            id: &b.id.0,
24            canonical_url: &b.canonical_url,
25            title: &b.title,
26            tags: b.tags.join(","),
27            collection: b.collection.clone().unwrap_or_default(),
28            updated_at: b.updated_at.to_rfc3339(),
29        }
30    }
31}
32
33/// Sort + serialize bookmarks into the requested format.
34pub fn render(bookmarks: &[Bookmark], format: super::Format) -> Result<String> {
35    // Deterministic ordering: canonical URL asc, then id asc.
36    let mut sorted: Vec<&Bookmark> = bookmarks.iter().collect();
37    sorted.sort_by(|a, b| {
38        a.canonical_url
39            .cmp(&b.canonical_url)
40            .then_with(|| a.id.0.cmp(&b.id.0))
41    });
42
43    match format {
44        super::Format::Table => render_table(&sorted),
45        super::Format::Json => render_json(&sorted),
46        super::Format::Yaml => render_yaml(&sorted),
47    }
48}
49
50fn render_table(sorted: &[&Bookmark]) -> Result<String> {
51    let mut out = String::new();
52    out.push_str("id\tcanonical_url\ttitle\ttags\tcollection\tupdated_at\n");
53    for b in sorted {
54        let row = TableRow::from(*b);
55        out.push_str(&format!(
56            "{id}\t{canonical}\t{title}\t{tags}\t{collection}\t{updated}\n",
57            id = row.id,
58            canonical = row.canonical_url,
59            title = row.title,
60            tags = row.tags,
61            collection = row.collection,
62            updated = row.updated_at,
63        ));
64    }
65    Ok(out)
66}
67
68fn render_json(sorted: &[&Bookmark]) -> Result<String> {
69    // NDJSON: one bookmark per line, already sorted by the caller.
70    let mut out = String::new();
71    for b in sorted {
72        out.push_str(&serde_json::to_string(b)?);
73        out.push('\n');
74    }
75    Ok(out)
76}
77
78fn render_yaml(sorted: &[&Bookmark]) -> Result<String> {
79    let value = serde_yaml::to_string(sorted)?;
80    Ok(value)
81}