diff --git a/Cargo.lock b/Cargo.lock index 1426346..9e46be2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,6 +163,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91d3baa3bcd889d60e6ef28874126a0b384fd225ab83aa6d8a801c519194ce1" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.32" @@ -374,6 +383,7 @@ dependencies = [ "anyhow", "chrono", "clap", + "clap_complete", "directories", "glob", "rusqlite", diff --git a/Cargo.toml b/Cargo.toml index 438a4a3..1bc20b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,4 @@ walkdir = "2.5" shlex = "1.3" chrono = "0.4" shellexpand = "3.1" - +clap_complete = "4.1" diff --git a/README.md b/README.md index 4f2b44f..e3de85f 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ # Marlin -**Marlin** is a lightweight, metadata-driven file indexer that runs 100 % on your computer. It scans folders, stores paths and file stats in SQLite, lets you attach hierarchical **tags** and **custom attributes**, takes automatic snapshots, and offers instant full-text search via FTS5. -*No cloud, no telemetry – your data never leaves the machine.* +**Marlin** is a lightweight, metadata-driven file indexer that runs 100 % on your computer. It scans folders, stores paths and file stats in SQLite, lets you attach hierarchical **tags** and **custom attributes**, takes automatic snapshots, and offers instant full-text search via FTS5. +_No cloud, no telemetry – your data never leaves the machine._ --- @@ -30,7 +30,7 @@ ▲ search / exec └──────┬──────┘ └────────── backup / restore ▼ timestamped snapshots -``` +```` --- @@ -51,7 +51,9 @@ macOS & Windows users: let the Rust installer pull the matching build tools. git clone https://github.com/yourname/marlin.git cd marlin cargo build --release -sudo install -Dm755 target/release/marlin /usr/local/bin/marlin # optional + +# (Optional) Install the binary into your PATH: +sudo install -Dm755 target/release/marlin /usr/local/bin/marlin ``` --- @@ -61,12 +63,28 @@ sudo install -Dm755 target/release/marlin /usr/local/bin/marlin # optional ```bash marlin init # create DB (idempotent) marlin scan ~/Pictures ~/Documents # index files -marlin tag "~/Pictures/**/*.jpg" photos/trip-2024 # add tag -marlin attr set "~/Documents/**/*.pdf" reviewed yes +marlin tag ~/Pictures/**/*.jpg photos/trip-2024 # add hierarchical tag +marlin attr set ~/Documents/**/*.pdf reviewed yes # set custom attribute marlin search reviewed --exec "xdg-open {}" # open matches marlin backup # snapshot DB ``` +--- + +### Enable shell completions (optional but handy) + +```bash +# create the directory if needed +mkdir -p ~/.config/bash_completion.d + +# dump Bash completion +marlin completions bash > ~/.config/bash_completion.d/marlin +``` + +For Zsh, Fish, etc., redirect into your shell’s completions folder. + +--- + ### Database location * **Linux** `~/.local/share/marlin/index.db` @@ -89,37 +107,39 @@ marlin [ARGS] init create / migrate database scan ... walk directories & index files tag "" add hierarchical tag -attr set|ls … manage custom attributes -search [--exec CMD] FTS query, optionally run CMD on each hit +attr set manage custom attributes +attr ls +search [--exec CMD] FTS5 query, optionally run CMD on each hit backup create timestamped snapshot in backups/ restore replace DB with snapshot +completions generate shell completions ``` ### Attribute subcommands -| Command | Example | -| ---------- | ------------------------------------------------ | -| `attr set` | `marlin attr set "~/Docs/**/*.pdf" reviewed yes` | -| `attr ls` | `marlin attr ls ~/Docs/report.pdf` | +| Command | Example | +| ---------- | ---------------------------------------------- | +| `attr set` | `marlin attr set ~/Docs/**/*.pdf reviewed yes` | +| `attr ls` | `marlin attr ls ~/Docs/report.pdf` | --- ## Backups & restore -*Create snapshot* +**Create snapshot** ```bash marlin backup # → ~/.local/share/marlin/backups/backup_2025-05-14_22-15-30.db ``` -*Restore snapshot* +**Restore snapshot** ```bash marlin restore ~/.local/share/marlin/backups/backup_2025-05-14_22-15-30.db ``` -Marlin also takes an **automatic safety backup before every schema migration**. +Marlin also takes an **automatic safety backup before every non-init command**. --- @@ -133,40 +153,27 @@ The versioned migration system preserves your data across upgrades. --- -## Roadmap - -See [`ROADMAP.md`](./ROADMAP.md) for the full development plan. - ---- - ## Five-Minute Quickstart -Paste & run each block in your terminal. - ---- +Just paste & run each block in your terminal. ### 0 Prepare, build & install ```bash cd ~/Documents/GitHub/Marlin cargo build --release - sudo install -Dm755 target/release/marlin /usr/local/bin/marlin -```` +``` > Now `marlin` is available everywhere. ---- - -### 1 Enable shell completion (optional but handy) +### 1 Enable shell completion ```bash -marlin completions bash > ~/.config/bash_completion.d/marlin -# or for zsh / fish, similarly... +mkdir -p ~/.config/bash_completion.d +marlin completions bash > ~/.config/bash_completion.d/marlin ``` ---- - ### 2 Prepare a clean demo directory ```bash @@ -179,8 +186,6 @@ printf "Receipt PDF\n" > ~/marlin_demo/Docs/receipt.pdf printf "fake jpg\n" > ~/marlin_demo/Media/Photos/vacation.jpg ``` ---- - ### 3 Initialize & index files ```bash @@ -193,23 +198,19 @@ marlin --verbose scan ~/marlin_demo > Only changed files get re-indexed on subsequent runs. ---- - ### 4 Attach tags & attributes ```bash # Tag everything under “Alpha” -marlin tag "~/marlin_demo/Projects/Alpha/**/*" project/alpha +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 # Output as JSON instead: -marlin --format=json attr set "~/marlin_demo/**/*.pdf" reviewed=yes +marlin --format=json attr set ~/marlin_demo/**/*.pdf reviewed yes ``` ---- - ### 5 Search your index ```bash @@ -223,8 +224,6 @@ marlin search "reviewed AND pdf" marlin search reviewed --exec 'echo HIT → {}' ``` ---- - ### 6 Backup & restore ```bash @@ -241,8 +240,7 @@ marlin restore "$snap" marlin search reviewed ``` -> Backups live under `~/.local/share/marlin/backups` by default. - +--- ##### What you just exercised @@ -257,9 +255,7 @@ marlin search reviewed | `marlin backup` | Timestamped snapshot of the DB | | `marlin restore` | Replace live DB with a chosen snapshot | -That’s the complete surface area of Marlin today—feel free to play around or -point the scanner at real folders. - +That’s the complete surface area of Marlin today—feel free to play around or point the scanner at real folders. --- diff --git a/src/cli.rs b/src/cli.rs index 986c837..702ea70 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,11 +9,11 @@ pub mod annotate; pub mod version; pub mod event; -use clap::{Parser, Subcommand, ArgEnum, Args, CommandFactory}; +use clap::{Parser, Subcommand, CommandFactory, ValueEnum}; use clap_complete::Shell; /// Output format for commands. -#[derive(ArgEnum, Clone, Copy, Debug)] +#[derive(ValueEnum, Clone, Copy, Debug)] pub enum Format { Text, Json, @@ -78,45 +78,46 @@ pub enum Commands { /// Generate shell completions (hidden) #[command(hide = true)] Completions { + /// Which shell to generate for #[arg(value_enum)] shell: Shell, }, /// File-to-file links #[command(subcommand)] - Link { cmd: link::LinkCmd }, + Link(link::LinkCmd), /// Collections (groups) of files #[command(subcommand)] - Coll { cmd: coll::CollCmd }, + Coll(coll::CollCmd), /// Smart views (saved queries) #[command(subcommand)] - View { cmd: view::ViewCmd }, + View(view::ViewCmd), /// Workflow states on files #[command(subcommand)] - State { cmd: state::StateCmd }, + State(state::StateCmd), /// TODO/tasks management #[command(subcommand)] - Task { cmd: task::TaskCmd }, + Task(task::TaskCmd), /// Reminders on files #[command(subcommand)] - Remind { cmd: remind::RemindCmd }, + Remind(remind::RemindCmd), /// File annotations and highlights #[command(subcommand)] - Annotate { cmd: annotate::AnnotateCmd }, + Annotate(annotate::AnnotateCmd), /// Version diffs #[command(subcommand)] - Version { cmd: version::VersionCmd }, + Version(version::VersionCmd), /// Calendar events & timelines #[command(subcommand)] - Event { cmd: event::EventCmd }, + Event(event::EventCmd), } #[derive(Subcommand, Debug)] diff --git a/src/main.rs b/src/main.rs index 6b98fbc..b648bf2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ mod logging; mod scan; use anyhow::{Context, Result}; -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, CommandFactory}; use clap_complete::{generate, Shell}; use glob::Pattern; use rusqlite::params; @@ -26,10 +26,11 @@ fn main() -> Result<()> { } logging::init(); - // Handle shell completions as a hidden command - if let Commands::Completions { shell } = args.command { + // If the user asked for completions, generate and exit immediately. + if let Commands::Completions { shell } = &args.command { let mut cmd = Cli::command(); - generate(shell, &mut cmd, "marlin", &mut io::stdout()); + // Shell is Copy so we can deref it safely + generate(*shell, &mut cmd, "marlin", &mut io::stdout()); return Ok(()); } @@ -47,8 +48,11 @@ fn main() -> Result<()> { // Open (and migrate) the DB let mut conn = db::open(&cfg.db_path)?; - // Dispatch + // Dispatch all commands match args.command { + Commands::Completions { .. } => { + // no-op, already handled above + } Commands::Init => { info!("Database initialised at {}", cfg.db_path.display()); } @@ -90,15 +94,15 @@ fn main() -> Result<()> { 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)?, + Commands::Link(link_cmd) => cli::link::run(&link_cmd, &mut conn, args.format)?, + Commands::Coll(coll_cmd) => cli::coll::run(&coll_cmd, &mut conn, args.format)?, + Commands::View(view_cmd) => cli::view::run(&view_cmd, &mut conn, args.format)?, + Commands::State(state_cmd) => cli::state::run(&state_cmd, &mut conn, args.format)?, + Commands::Task(task_cmd) => cli::task::run(&task_cmd, &mut conn, args.format)?, + Commands::Remind(rm_cmd) => cli::remind::run(&rm_cmd, &mut conn, args.format)?, + Commands::Annotate(an_cmd) => cli::annotate::run(&an_cmd, &mut conn, args.format)?, + Commands::Version(v_cmd) => cli::version::run(&v_cmd, &mut conn, args.format)?, + Commands::Event(e_cmd) => cli::event::run(&e_cmd, &mut conn, args.format)?, } Ok(()) @@ -117,7 +121,11 @@ fn apply_tag(conn: &rusqlite::Connection, pattern: &str, tag_path: &str) -> Resu conn.prepare("INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?1, ?2)")?; let mut count = 0; - for entry in WalkDir::new(&root).into_iter().filter_map(Result::ok).filter(|e| e.file_type().is_file()) { + for entry in WalkDir::new(&root) + .into_iter() + .filter_map(Result::ok) + .filter(|e| e.file_type().is_file()) + { let path_str = entry.path().to_string_lossy(); debug!("testing path: {}", path_str); if !pat.matches(&path_str) { diff --git a/target/release/marlin b/target/release/marlin index dffd7de..a0b1a83 100755 Binary files a/target/release/marlin and b/target/release/marlin differ diff --git a/target/release/marlin.d b/target/release/marlin.d index 8d05671..2ef8e0d 100644 --- a/target/release/marlin.d +++ b/target/release/marlin.d @@ -1 +1 @@ -/home/user/Documents/GitHub/Marlin/target/release/marlin: /home/user/Documents/GitHub/Marlin/src/cli.rs /home/user/Documents/GitHub/Marlin/src/config.rs /home/user/Documents/GitHub/Marlin/src/db/migrations/0001_initial_schema.sql /home/user/Documents/GitHub/Marlin/src/db/migrations/0002_update_fts_and_triggers.sql /home/user/Documents/GitHub/Marlin/src/db/mod.rs /home/user/Documents/GitHub/Marlin/src/logging.rs /home/user/Documents/GitHub/Marlin/src/main.rs /home/user/Documents/GitHub/Marlin/src/scan.rs +/home/user/Documents/GitHub/Marlin/target/release/marlin: /home/user/Documents/GitHub/Marlin/src/cli/annotate.rs /home/user/Documents/GitHub/Marlin/src/cli/coll.rs /home/user/Documents/GitHub/Marlin/src/cli/event.rs /home/user/Documents/GitHub/Marlin/src/cli/link.rs /home/user/Documents/GitHub/Marlin/src/cli/remind.rs /home/user/Documents/GitHub/Marlin/src/cli/state.rs /home/user/Documents/GitHub/Marlin/src/cli/task.rs /home/user/Documents/GitHub/Marlin/src/cli/version.rs /home/user/Documents/GitHub/Marlin/src/cli/view.rs /home/user/Documents/GitHub/Marlin/src/cli.rs /home/user/Documents/GitHub/Marlin/src/config.rs /home/user/Documents/GitHub/Marlin/src/db/migrations/0001_initial_schema.sql /home/user/Documents/GitHub/Marlin/src/db/migrations/0002_update_fts_and_triggers.sql /home/user/Documents/GitHub/Marlin/src/db/mod.rs /home/user/Documents/GitHub/Marlin/src/logging.rs /home/user/Documents/GitHub/Marlin/src/main.rs /home/user/Documents/GitHub/Marlin/src/scan.rs