From a38f613a79f9c332c80944ecd1e279c1e0c719c4 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 15 May 2025 16:37:59 -0400 Subject: [PATCH] updated CLI --- README.md | 51 ++++++++++++--------------- src/cli.rs | 81 +++++++++++++++++++++++++++++++++++++++---- src/cli/annotate.rs | 28 +++++++++++++++ src/cli/coll.rs | 26 ++++++++++++++ src/cli/commands.yaml | 81 +++++++++++++++++++++++++++++++++++++++++++ src/cli/event.rs | 24 +++++++++++++ src/cli/link.rs | 43 +++++++++++++++++++++++ src/cli/remind.rs | 22 ++++++++++++ src/cli/state.rs | 26 ++++++++++++++ src/cli/task.rs | 22 ++++++++++++ src/cli/version.rs | 18 ++++++++++ src/cli/view.rs | 24 +++++++++++++ src/main.rs | 54 ++++++++++++++++++----------- 13 files changed, 443 insertions(+), 57 deletions(-) create mode 100644 src/cli/annotate.rs create mode 100644 src/cli/coll.rs create mode 100644 src/cli/commands.yaml create mode 100644 src/cli/event.rs create mode 100644 src/cli/link.rs create mode 100644 src/cli/remind.rs create mode 100644 src/cli/state.rs create mode 100644 src/cli/task.rs create mode 100644 src/cli/version.rs create mode 100644 src/cli/view.rs diff --git a/README.md b/README.md index ffc35ff..4f2b44f 100644 --- a/README.md +++ b/README.md @@ -145,26 +145,26 @@ Paste & run each block in your terminal. --- -### 0 Prepare & build +### 0 Prepare, build & install ```bash -# Clone or cd into your Marlin repo cd ~/Documents/GitHub/Marlin - -# Build the release binary cargo build --release -``` + +sudo install -Dm755 target/release/marlin /usr/local/bin/marlin +```` + +> Now `marlin` is available everywhere. --- -### 1 Install on your PATH +### 1 Enable shell completion (optional but handy) ```bash -sudo install -Dm755 target/release/marlin /usr/local/bin/marlin +marlin completions bash > ~/.config/bash_completion.d/marlin +# or for zsh / fish, similarly... ``` -> Now `marlin` is available everywhere. - --- ### 2 Prepare a clean demo directory @@ -184,16 +184,14 @@ printf "fake jpg\n" > ~/marlin_demo/Media/Photos/vacation.jpg ### 3 Initialize & index files ```bash -# Use --verbose if you want full debug traces: marlin init marlin scan ~/marlin_demo -# or, to see every path tested: -marlin --verbose init +# show every path tested: marlin --verbose scan ~/marlin_demo ``` -> **Tip:** Rerun `marlin scan` after you add/remove/modify files; only changed files get re-indexed. +> Only changed files get re-indexed on subsequent runs. --- @@ -204,11 +202,10 @@ marlin --verbose scan ~/marlin_demo marlin tag "~/marlin_demo/Projects/Alpha/**/*" project/alpha # Mark all PDFs as reviewed -marlin attr set "~/marlin_demo/**/*.pdf" reviewed yes +marlin attr set "~/marlin_demo/**/*.pdf" reviewed=yes -# (or with debug) -marlin --verbose tag "~/marlin_demo/Projects/Alpha/**/*" project/alpha -marlin --verbose attr set "~/marlin_demo/**/*.pdf" reviewed yes +# Output as JSON instead: +marlin --format=json attr set "~/marlin_demo/**/*.pdf" reviewed=yes ``` --- @@ -219,38 +216,34 @@ marlin --verbose attr set "~/marlin_demo/**/*.pdf" reviewed yes # By tag or filename marlin search alpha -# Combined terms (AND across path+attrs) +# Combined terms: marlin search "reviewed AND pdf" -# Run a command on each hit -marlin search reviewed --exec "echo HIT → {}" - -# If things aren’t matching, add --verbose to see the underlying FTS query: -marlin --verbose search "reviewed AND pdf" +# Run a command on each hit: +marlin search reviewed --exec 'echo HIT → {}' ``` -> `{}` in `--exec` is replaced with each file’s path. - --- ### 6 Backup & restore ```bash -# Snapshot and store its name +# Snapshot snap=$(marlin backup | awk '{print $NF}') -# Simulate data loss +# Simulate loss rm ~/.local/share/marlin/index.db -# Restore instantly +# Restore marlin restore "$snap" -# Verify your files still show up +# Verify marlin search reviewed ``` > Backups live under `~/.local/share/marlin/backups` by default. + ##### What you just exercised | Command | Purpose | diff --git a/src/cli.rs b/src/cli.rs index b444222..986c837 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,15 +1,36 @@ // src/cli.rs -use std::path::PathBuf; -use clap::{Parser, Subcommand}; +pub mod link; +pub mod coll; +pub mod view; +pub mod state; +pub mod task; +pub mod remind; +pub mod annotate; +pub mod version; +pub mod event; + +use clap::{Parser, Subcommand, ArgEnum, Args, CommandFactory}; +use clap_complete::Shell; + +/// Output format for commands. +#[derive(ArgEnum, Clone, Copy, Debug)] +pub enum Format { + Text, + Json, +} /// Marlin – metadata-driven file explorer (CLI utilities) #[derive(Parser, Debug)] -#[command(author, version, about)] +#[command(author, version, about, propagate_version = true)] pub struct Cli { /// Enable debug logging and extra output #[arg(long)] pub verbose: bool, + /// Output format (text or JSON) + #[arg(long, default_value = "text", value_enum, global = true)] + pub format: Format, + #[command(subcommand)] pub command: Commands, } @@ -21,12 +42,15 @@ pub enum Commands { /// Scan one or more directories and populate the file index Scan { - paths: Vec, + /// Directories to scan (defaults to cwd) + paths: Vec, }, /// Tag files matching a glob pattern (hierarchical tags use `/`) Tag { + /// Glob or path pattern pattern: String, + /// Hierarchical tag name (`foo/bar`) tag_path: String, }, @@ -46,14 +70,57 @@ pub enum Commands { /// Create a timestamped backup of the database Backup, - /// Restore from a backup file (over-writes current DB) + /// Restore from a backup file (overwrites current DB) Restore { - backup_path: PathBuf, + backup_path: std::path::PathBuf, }, + + /// Generate shell completions (hidden) + #[command(hide = true)] + Completions { + #[arg(value_enum)] + shell: Shell, + }, + + /// File-to-file links + #[command(subcommand)] + Link { cmd: link::LinkCmd }, + + /// Collections (groups) of files + #[command(subcommand)] + Coll { cmd: coll::CollCmd }, + + /// Smart views (saved queries) + #[command(subcommand)] + View { cmd: view::ViewCmd }, + + /// Workflow states on files + #[command(subcommand)] + State { cmd: state::StateCmd }, + + /// TODO/tasks management + #[command(subcommand)] + Task { cmd: task::TaskCmd }, + + /// Reminders on files + #[command(subcommand)] + Remind { cmd: remind::RemindCmd }, + + /// File annotations and highlights + #[command(subcommand)] + Annotate { cmd: annotate::AnnotateCmd }, + + /// Version diffs + #[command(subcommand)] + Version { cmd: version::VersionCmd }, + + /// Calendar events & timelines + #[command(subcommand)] + Event { cmd: event::EventCmd }, } #[derive(Subcommand, Debug)] pub enum AttrCmd { Set { pattern: String, key: String, value: String }, - Ls { path: PathBuf }, + Ls { path: std::path::PathBuf }, } diff --git a/src/cli/annotate.rs b/src/cli/annotate.rs new file mode 100644 index 0000000..3acc8b7 --- /dev/null +++ b/src/cli/annotate.rs @@ -0,0 +1,28 @@ +// src/cli/annotate.rs +use clap::{Subcommand, Args}; +use rusqlite::Connection; +use crate::cli::Format; + +#[derive(Subcommand, Debug)] +pub enum AnnotateCmd { + Add (ArgsAdd), + List(ArgsList), +} + +#[derive(Args, Debug)] +pub struct ArgsAdd { + pub file: String, + pub note: String, + #[arg(long)] pub range: Option, + #[arg(long)] pub highlight: bool, +} + +#[derive(Args, Debug)] +pub struct ArgsList { pub file_pattern: String } + +pub fn run(cmd: &AnnotateCmd, conn: &mut Connection, format: Format) -> anyhow::Result<()> { + match cmd { + AnnotateCmd::Add(a) => todo!("annotate add {:?}", a), + AnnotateCmd::List(a) => todo!("annotate list {:?}", a), + } +} diff --git a/src/cli/coll.rs b/src/cli/coll.rs new file mode 100644 index 0000000..1ade309 --- /dev/null +++ b/src/cli/coll.rs @@ -0,0 +1,26 @@ +// src/cli/coll.rs +use clap::{Subcommand, Args}; +use rusqlite::Connection; +use crate::cli::Format; + +#[derive(Subcommand, Debug)] +pub enum CollCmd { + Create(CreateArgs), + Add (AddArgs), + List (ListArgs), +} + +#[derive(Args, Debug)] +pub struct CreateArgs { pub name: String } +#[derive(Args, Debug)] +pub struct AddArgs { pub name: String, pub file_pattern: String } +#[derive(Args, Debug)] +pub struct ListArgs { pub name: String } + +pub fn run(cmd: &CollCmd, conn: &mut Connection, format: Format) -> anyhow::Result<()> { + match cmd { + CollCmd::Create(a) => todo!("coll create {:?}", a), + CollCmd::Add(a) => todo!("coll add {:?}", a), + CollCmd::List(a) => todo!("coll list {:?}", a), + } +} diff --git a/src/cli/commands.yaml b/src/cli/commands.yaml new file mode 100644 index 0000000..19ea663 --- /dev/null +++ b/src/cli/commands.yaml @@ -0,0 +1,81 @@ +# cli/commands.yaml +# Philosophy: one canonical spec stops drift between docs & code. +link: + description: "Manage typed relationships between files" + actions: + add: + args: [from, to] + flags: ["--type"] + rm: + args: [from, to] + flags: ["--type"] + list: + args: [pattern] + flags: ["--direction", "--type"] + backlinks: + args: [pattern] + +coll: + description: "Manage named collections of files" + actions: + create: + args: [name] + add: + args: [name, file_pattern] + list: + args: [name] + +view: + description: "Save and use smart views (saved queries)" + actions: + save: + args: [view_name, query] + list: {} + exec: + args: [view_name] + +state: + description: "Track workflow states on files" + actions: + set: + args: [file_pattern, new_state] + transitions-add: + args: [from_state, to_state] + log: + args: [file_pattern] + +task: + description: "Extract TODOs and manage tasks" + actions: + scan: + args: [directory] + list: + flags: ["--due-today"] + +remind: + description: "Attach reminders to files" + actions: + set: + args: [file_pattern, timestamp, message] + +annotate: + description: "Add notes or highlights to files" + actions: + add: + args: [file, note] + flags: ["--range", "--highlight"] + list: + args: [file_pattern] + +version: + description: "Versioning and diffs" + actions: + diff: + args: [file] + +event: + description: "Link files to dates/events" + actions: + add: + args: [file, date, description] + timeline: {} diff --git a/src/cli/event.rs b/src/cli/event.rs new file mode 100644 index 0000000..c4120b7 --- /dev/null +++ b/src/cli/event.rs @@ -0,0 +1,24 @@ +// src/cli/event.rs +use clap::{Subcommand, Args}; +use rusqlite::Connection; +use crate::cli::Format; + +#[derive(Subcommand, Debug)] +pub enum EventCmd { + Add (ArgsAdd), + Timeline, +} + +#[derive(Args, Debug)] +pub struct ArgsAdd { + pub file: String, + pub date: String, + pub description: String, +} + +pub fn run(cmd: &EventCmd, conn: &mut Connection, format: Format) -> anyhow::Result<()> { + match cmd { + EventCmd::Add(a) => todo!("event add {:?}", a), + EventCmd::Timeline => todo!("event timeline"), + } +} diff --git a/src/cli/link.rs b/src/cli/link.rs new file mode 100644 index 0000000..f696360 --- /dev/null +++ b/src/cli/link.rs @@ -0,0 +1,43 @@ +// src/cli/link.rs +use clap::{Subcommand, Args}; +use rusqlite::Connection; +use crate::cli::Format; + +#[derive(Subcommand, Debug)] +pub enum LinkCmd { + Add(LinkArgs), + Rm (LinkArgs), + List(ListArgs), + Backlinks(BacklinksArgs), +} + +#[derive(Args, Debug)] +pub struct LinkArgs { + pub from: String, + pub to: String, + #[arg(long)] + pub r#type: Option, +} + +#[derive(Args, Debug)] +pub struct ListArgs { + pub pattern: String, + #[arg(long)] + pub direction: Option, + #[arg(long)] + pub r#type: Option, +} + +#[derive(Args, Debug)] +pub struct BacklinksArgs { + pub pattern: String, +} + +pub fn run(cmd: &LinkCmd, conn: &mut Connection, format: Format) -> anyhow::Result<()> { + match cmd { + LinkCmd::Add(args) => todo!("link add {:?}", args), + LinkCmd::Rm(args) => todo!("link rm {:?}", args), + LinkCmd::List(args) => todo!("link list {:?}", args), + LinkCmd::Backlinks(args) => todo!("link backlinks {:?}", args), + } +} diff --git a/src/cli/remind.rs b/src/cli/remind.rs new file mode 100644 index 0000000..eeda818 --- /dev/null +++ b/src/cli/remind.rs @@ -0,0 +1,22 @@ +// src/cli/remind.rs +use clap::{Subcommand, Args}; +use rusqlite::Connection; +use crate::cli::Format; + +#[derive(Subcommand, Debug)] +pub enum RemindCmd { + Set(ArgsSet), +} + +#[derive(Args, Debug)] +pub struct ArgsSet { + pub file_pattern: String, + pub timestamp: String, + pub message: String, +} + +pub fn run(cmd: &RemindCmd, conn: &mut Connection, format: Format) -> anyhow::Result<()> { + match cmd { + RemindCmd::Set(a) => todo!("remind set {:?}", a), + } +} diff --git a/src/cli/state.rs b/src/cli/state.rs new file mode 100644 index 0000000..33381c8 --- /dev/null +++ b/src/cli/state.rs @@ -0,0 +1,26 @@ +// src/cli/state.rs +use clap::{Subcommand, Args}; +use rusqlite::Connection; +use crate::cli::Format; + +#[derive(Subcommand, Debug)] +pub enum StateCmd { + Set(ArgsSet), + TransitionsAdd(ArgsTrans), + Log(ArgsLog), +} + +#[derive(Args, Debug)] +pub struct ArgsSet { pub file_pattern: String, pub new_state: String } +#[derive(Args, Debug)] +pub struct ArgsTrans { pub from_state: String, pub to_state: String } +#[derive(Args, Debug)] +pub struct ArgsLog { pub file_pattern: String } + +pub fn run(cmd: &StateCmd, conn: &mut Connection, format: Format) -> anyhow::Result<()> { + match cmd { + StateCmd::Set(a) => todo!("state set {:?}", a), + StateCmd::TransitionsAdd(a)=> todo!("state transitions-add {:?}", a), + StateCmd::Log(a) => todo!("state log {:?}", a), + } +} diff --git a/src/cli/task.rs b/src/cli/task.rs new file mode 100644 index 0000000..e25a53d --- /dev/null +++ b/src/cli/task.rs @@ -0,0 +1,22 @@ +// src/cli/task.rs +use clap::{Subcommand, Args}; +use rusqlite::Connection; +use crate::cli::Format; + +#[derive(Subcommand, Debug)] +pub enum TaskCmd { + Scan(ArgsScan), + List(ArgsList), +} + +#[derive(Args, Debug)] +pub struct ArgsScan { pub directory: String } +#[derive(Args, Debug)] +pub struct ArgsList { #[arg(long)] pub due_today: bool } + +pub fn run(cmd: &TaskCmd, conn: &mut Connection, format: Format) -> anyhow::Result<()> { + match cmd { + TaskCmd::Scan(a) => todo!("task scan {:?}", a), + TaskCmd::List(a) => todo!("task list {:?}", a), + } +} diff --git a/src/cli/version.rs b/src/cli/version.rs new file mode 100644 index 0000000..13476b7 --- /dev/null +++ b/src/cli/version.rs @@ -0,0 +1,18 @@ +// src/cli/version.rs +use clap::{Subcommand, Args}; +use rusqlite::Connection; +use crate::cli::Format; + +#[derive(Subcommand, Debug)] +pub enum VersionCmd { + Diff(ArgsDiff), +} + +#[derive(Args, Debug)] +pub struct ArgsDiff { pub file: String } + +pub fn run(cmd: &VersionCmd, conn: &mut Connection, format: Format) -> anyhow::Result<()> { + match cmd { + VersionCmd::Diff(a) => todo!("version diff {:?}", a), + } +} diff --git a/src/cli/view.rs b/src/cli/view.rs new file mode 100644 index 0000000..c07c8bb --- /dev/null +++ b/src/cli/view.rs @@ -0,0 +1,24 @@ +// src/cli/view.rs +use clap::{Subcommand, Args}; +use rusqlite::Connection; +use crate::cli::Format; + +#[derive(Subcommand, Debug)] +pub enum ViewCmd { + Save(ArgsSave), + List, + Exec(ArgsExec), +} + +#[derive(Args, Debug)] +pub struct ArgsSave { pub view_name: String, pub query: String } +#[derive(Args, Debug)] +pub struct ArgsExec { pub view_name: String } + +pub fn run(cmd: &ViewCmd, conn: &mut Connection, format: Format) -> anyhow::Result<()> { + match cmd { + ViewCmd::Save(a) => todo!("view save {:?}", a), + ViewCmd::List => todo!("view list"), + ViewCmd::Exec(a)=> todo!("view exec {:?}", a), + } +} diff --git a/src/main.rs b/src/main.rs index 46fe75b..6b98fbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,48 +6,55 @@ mod logging; mod scan; use anyhow::{Context, Result}; -use clap::Parser; +use clap::{Parser, Subcommand}; +use clap_complete::{generate, Shell}; use glob::Pattern; use rusqlite::params; use shellexpand; use shlex; -use std::{env, path::PathBuf, process::Command}; +use std::{env, io, path::PathBuf, process::Command}; use tracing::{debug, error, info}; use walkdir::WalkDir; -use cli::{AttrCmd, Cli, Commands}; +use cli::{Cli, Commands, Format}; fn main() -> Result<()> { // Parse CLI and bootstrap logging - let args = Cli::parse(); + let mut args = Cli::parse(); if args.verbose { - // switch on debug‐level logs env::set_var("RUST_LOG", "debug"); } logging::init(); + // Handle shell completions as a hidden command + if let Commands::Completions { shell } = args.command { + let mut cmd = Cli::command(); + generate(shell, &mut cmd, "marlin", &mut io::stdout()); + return Ok(()); + } + let cfg = config::Config::load()?; // Backup before any non-init, non-backup/restore command - if !matches!(args.command, Commands::Init | Commands::Backup | Commands::Restore { .. }) { - match db::backup(&cfg.db_path) { + match &args.command { + Commands::Init | Commands::Backup | Commands::Restore { .. } => {} + _ => match db::backup(&cfg.db_path) { Ok(path) => info!("Pre-command auto-backup created at {}", path.display()), - Err(e) => error!("Failed to create pre-command auto-backup: {}", e), - } + Err(e) => error!("Failed to create pre-command auto-backup: {}", e), + }, } // Open (and migrate) the DB let mut conn = db::open(&cfg.db_path)?; + // Dispatch match args.command { Commands::Init => { info!("Database initialised at {}", cfg.db_path.display()); } - Commands::Scan { paths } => { - // if none given, default to current dir let scan_paths = if paths.is_empty() { - vec![env::current_dir()?] + vec![std::env::current_dir()?] } else { paths }; @@ -55,38 +62,43 @@ fn main() -> Result<()> { scan::scan_directory(&mut conn, &p)?; } } - Commands::Tag { pattern, tag_path } => { apply_tag(&conn, &pattern, &tag_path)?; } - Commands::Attr { action } => match action { - AttrCmd::Set { pattern, key, value } => { + cli::AttrCmd::Set { pattern, key, value } => { attr_set(&conn, &pattern, &key, &value)?; } - AttrCmd::Ls { path } => { + cli::AttrCmd::Ls { path } => { attr_ls(&conn, &path)?; } }, - Commands::Search { query, exec } => { run_search(&conn, &query, exec)?; } - Commands::Backup => { let path = db::backup(&cfg.db_path)?; println!("Backup created: {}", path.display()); } - Commands::Restore { backup_path } => { drop(conn); db::restore(&backup_path, &cfg.db_path) .with_context(|| format!("Failed to restore DB from {}", backup_path.display()))?; - println!("Restored DB file from {}", backup_path.display()); + println!("Restored DB from {}", backup_path.display()); db::open(&cfg.db_path) .with_context(|| format!("Could not open restored DB at {}", cfg.db_path.display()))?; - info!("Successfully opened and processed restored database."); + info!("Successfully opened restored database."); } + // new domains delegate to their run() functions + Commands::Link { cmd } => cli::link::run(&cmd, &mut conn, args.format)?, + Commands::Coll { cmd } => cli::coll::run(&cmd, &mut conn, args.format)?, + Commands::View { cmd } => cli::view::run(&cmd, &mut conn, args.format)?, + Commands::State { cmd } => cli::state::run(&cmd, &mut conn, args.format)?, + Commands::Task { cmd } => cli::task::run(&cmd, &mut conn, args.format)?, + Commands::Remind { cmd } => cli::remind::run(&cmd, &mut conn, args.format)?, + Commands::Annotate { cmd } => cli::annotate::run(&cmd, &mut conn, args.format)?, + Commands::Version { cmd } => cli::version::run(&cmd, &mut conn, args.format)?, + Commands::Event { cmd } => cli::event::run(&cmd, &mut conn, args.format)?, } Ok(())