bear-cli 0.2.4

A native Rust CLI for Bear.app on macOS using Bear's SQLite database for reads and x-callback-url actions for writes
Documentation
use std::path::PathBuf;

use clap::{Args, Parser, Subcommand};

#[derive(Parser, Debug)]
#[command(name = "bear")]
#[command(about = "Rust CLI for Bear.app on macOS", version)]
pub struct Cli {
    #[arg(
        long,
        global = true,
        env = "BEAR_DATABASE",
        help = "Path to Bear's macOS SQLite database. If omitted, bear-cli discovers it dynamically."
    )]
    pub database: Option<String>,

    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    Auth(AuthCommand),
    OpenNote(OpenNoteCommand),
    Tags,
    OpenTag(OpenTagCommand),
    Search(SearchCommand),
    Export(ExportCommand),
    Duplicates(DuplicatesCommand),
    Stats(StatsCommand),
    Health(HealthCommand),
    Untagged(FilterCommand),
    Todo(FilterCommand),
    Today(FilterCommand),
    Locked(FilterCommand),
    Create(CreateCommand),
    AddText(AddTextCommand),
    AddFile(AddFileCommand),
    GrabUrl(GrabUrlCommand),
    Trash(IdOrSearchCommand),
    Archive(IdOrSearchCommand),
    RenameTag(RenameTagCommand),
    DeleteTag(DeleteTagCommand),
    Raw(RawCommand),
}

#[derive(Args, Debug)]
pub struct AuthCommand {
    pub token: String,
}

#[derive(Args, Debug)]
pub struct OpenNoteCommand {
    #[arg(long)]
    pub id: Option<String>,
    #[arg(long)]
    pub title: Option<String>,
    #[arg(long, default_value_t = false)]
    pub exclude_trashed: bool,
}

#[derive(Args, Debug)]
pub struct OpenTagCommand {
    pub name: String,
}

#[derive(Args, Debug)]
pub struct SearchCommand {
    pub term: Option<String>,
    #[arg(long)]
    pub tag: Option<String>,
    #[arg(long)]
    pub since: Option<String>,
    #[arg(long)]
    pub before: Option<String>,
    #[arg(long, default_value_t = false)]
    pub json: bool,
}

#[derive(Args, Debug)]
pub struct DuplicatesCommand {
    #[arg(long, default_value_t = false)]
    pub json: bool,
}

#[derive(Args, Debug)]
pub struct ExportCommand {
    pub output: PathBuf,
    #[arg(long)]
    pub tag: Option<String>,
    #[arg(long, default_value_t = false)]
    pub frontmatter: bool,
    #[arg(long = "by-tag", default_value_t = false)]
    pub by_tag: bool,
}

#[derive(Args, Debug)]
pub struct StatsCommand {
    #[arg(long, default_value_t = false)]
    pub json: bool,
}

#[derive(Args, Debug)]
pub struct HealthCommand {
    #[arg(long, default_value_t = false)]
    pub json: bool,
}

#[derive(Args, Debug)]
pub struct FilterCommand {
    pub search: Option<String>,
}

#[derive(Args, Debug)]
pub struct CreateCommand {
    pub text: Option<String>,
    #[arg(long)]
    pub title: Option<String>,
    #[arg(long)]
    pub file: Option<PathBuf>,
    #[arg(long)]
    pub filename: Option<String>,
    #[arg(short = 't', long = "tag")]
    pub tag: Vec<String>,
    #[arg(long, default_value_t = false)]
    pub open_note: bool,
    #[arg(long, default_value_t = false)]
    pub new_window: bool,
    #[arg(long, default_value_t = false)]
    pub float: bool,
    #[arg(long, default_value_t = false)]
    pub show_window: bool,
    #[arg(long, default_value_t = false)]
    pub pin: bool,
    #[arg(long, default_value_t = false)]
    pub edit: bool,
    #[arg(long, default_value_t = false)]
    pub timestamp: bool,
    #[arg(long = "type")]
    pub kind: Option<String>,
    #[arg(long)]
    pub url: Option<String>,
}

#[derive(Args, Debug)]
pub struct AddTextCommand {
    pub text: Option<String>,
    #[arg(long)]
    pub file: Option<PathBuf>,
    #[arg(long)]
    pub id: Option<String>,
    #[arg(long)]
    pub title: Option<String>,
    #[arg(long)]
    pub header: Option<String>,
    #[arg(long, default_value = "append")]
    pub mode: String,
    #[arg(short = 't', long = "tag")]
    pub tag: Vec<String>,
    #[arg(long, default_value_t = false)]
    pub exclude_trashed: bool,
    #[arg(long, default_value_t = false)]
    pub new_line: bool,
    #[arg(long, default_value_t = false)]
    pub open_note: bool,
    #[arg(long, default_value_t = false)]
    pub new_window: bool,
    #[arg(long, default_value_t = false)]
    pub show_window: bool,
    #[arg(long, default_value_t = false)]
    pub edit: bool,
    #[arg(long, default_value_t = false)]
    pub timestamp: bool,
}

#[derive(Args, Debug)]
pub struct AddFileCommand {
    pub file: PathBuf,
    #[arg(long)]
    pub id: Option<String>,
    #[arg(long)]
    pub title: Option<String>,
    #[arg(long)]
    pub header: Option<String>,
    #[arg(long)]
    pub filename: Option<String>,
    #[arg(long, default_value = "append")]
    pub mode: String,
    #[arg(long, default_value_t = false)]
    pub open_note: bool,
    #[arg(long, default_value_t = false)]
    pub new_window: bool,
    #[arg(long, default_value_t = false)]
    pub show_window: bool,
    #[arg(long, default_value_t = false)]
    pub edit: bool,
}

#[derive(Args, Debug)]
pub struct GrabUrlCommand {
    pub url: String,
    #[arg(short = 't', long = "tag")]
    pub tag: Vec<String>,
    #[arg(long, default_value_t = false)]
    pub pin: bool,
    #[arg(long, default_value_t = false)]
    pub wait: bool,
}

#[derive(Args, Debug)]
pub struct IdOrSearchCommand {
    #[arg(long)]
    pub id: Option<String>,
    #[arg(long)]
    pub search: Option<String>,
    #[arg(long, default_value_t = false)]
    pub show_window: bool,
}

#[derive(Args, Debug)]
pub struct RenameTagCommand {
    pub name: String,
    pub new_name: String,
    #[arg(long, default_value_t = false)]
    pub show_window: bool,
}

#[derive(Args, Debug)]
pub struct DeleteTagCommand {
    pub name: String,
    #[arg(long, default_value_t = false)]
    pub show_window: bool,
}

#[derive(Args, Debug)]
pub struct RawCommand {
    pub action: String,
    #[arg(long)]
    pub token: Option<String>,
    #[arg(long, default_value_t = false)]
    pub use_saved_token: bool,
    #[arg(value_parser = parse_key_value)]
    pub params: Vec<(String, String)>,
}

fn parse_key_value(input: &str) -> Result<(String, String), String> {
    let (key, value) = input
        .split_once('=')
        .ok_or_else(|| "expected KEY=VALUE".to_string())?;
    Ok((key.to_string(), value.to_string()))
}