Skip to main content

linkmarks_cli/cmd/
list.rs

1//! `linkmarks list` — list bookmarks deterministically.
2//!
3//! Default source order:
4//! 1. `--source=store`: read from the local SQLite store.
5//! 2. `--source=chrome`: parse a Chromium JSON file.
6//!
7//! `--source` is optional. When omitted, the store is used if the DB
8//! exists; otherwise we fall back to the OS-typical Chrome path so the
9//! CLI stays useful before `init` is run.
10
11use crate::ui;
12use crate::Paths;
13use anyhow::{bail, Result};
14use clap::Args;
15use linkmarks_core::store;
16use linkmarks_core::traits::BookmarkSource;
17use std::path::PathBuf;
18
19#[derive(Args, Debug)]
20pub struct ListArgs {
21    /// Source to list from. `store` (default when DB exists) reads from
22    /// the SQLite store; `chrome` parses a Chromium JSON file.
23    #[arg(long)]
24    pub source: Option<String>,
25
26    /// Optional path to a source file (for `chrome`). Defaults to the
27    /// OS-typical location for the chosen source.
28    #[arg(long)]
29    pub path: Option<PathBuf>,
30
31    /// Page size for the store source. Defaults to 100.
32    #[arg(long, default_value = "100")]
33    pub limit: usize,
34
35    /// Offset for pagination. Defaults to 0.
36    #[arg(long, default_value = "0")]
37    pub offset: usize,
38}
39
40pub fn run(args: ListArgs, format: crate::Format, paths: Paths) -> Result<i32> {
41    let source_label = args
42        .source
43        .clone()
44        .unwrap_or_else(|| default_source_label(&paths.store).to_string());
45
46    match source_label.as_str() {
47        "store" => {
48            if !paths.store.exists() {
49                bail!(
50                    "store not found at {}; run `linkmarks init` first",
51                    paths.store.display()
52                );
53            }
54            let s = store::open(&paths.store)?;
55            let bookmarks = s.list(args.limit.max(1), args.offset)?;
56            let rendered = ui::render(&bookmarks, format)?;
57            print!("{rendered}");
58            Ok(crate::exit_codes::OK)
59        }
60        "chrome" => {
61            let kind = linkmarks_core::SourceKind::from_cli_str("chrome")
62                .ok_or_else(|| anyhow::anyhow!("unknown source 'chrome'"))?;
63            if !matches!(kind, linkmarks_core::SourceKind::Chromium) {
64                bail!("v1 only supports --source=chrome");
65            }
66            let path = args.path.clone().unwrap_or_else(default_chrome_path);
67            let src = linkmarks_bridge_chromium::ChromiumSource::open(&path)?;
68            let bookmarks = src.list()?;
69            let rendered = ui::render(&bookmarks, format)?;
70            print!("{rendered}");
71            Ok(crate::exit_codes::OK)
72        }
73        other => bail!("unsupported --source '{other}' (try `store` or `chrome`)"),
74    }
75}
76
77/// Decide the default source label: `store` if the DB exists,
78/// `chrome` otherwise. The store is preferred once `init` has run.
79fn default_source_label(store_path: &std::path::Path) -> &'static str {
80    if store_path.exists() {
81        "store"
82    } else {
83        "chrome"
84    }
85}
86
87fn default_chrome_path() -> PathBuf {
88    let home = std::env::var_os("HOME")
89        .map(PathBuf::from)
90        .unwrap_or_else(|| PathBuf::from("."));
91    home.join(".config/google-chrome/Default/Bookmarks")
92}