This commit is contained in:
thePR0M3TH3AN
2025-05-15 17:00:17 -04:00
parent a38f613a79
commit edeefd0033
7 changed files with 92 additions and 77 deletions

10
Cargo.lock generated
View File

@@ -163,6 +163,15 @@ dependencies = [
"strsim", "strsim",
] ]
[[package]]
name = "clap_complete"
version = "4.5.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c91d3baa3bcd889d60e6ef28874126a0b384fd225ab83aa6d8a801c519194ce1"
dependencies = [
"clap",
]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.32" version = "4.5.32"
@@ -374,6 +383,7 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"clap", "clap",
"clap_complete",
"directories", "directories",
"glob", "glob",
"rusqlite", "rusqlite",

View File

@@ -15,4 +15,4 @@ walkdir = "2.5"
shlex = "1.3" shlex = "1.3"
chrono = "0.4" chrono = "0.4"
shellexpand = "3.1" shellexpand = "3.1"
clap_complete = "4.1"

View File

@@ -3,7 +3,7 @@
# Marlin # 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. **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.* _No cloud, no telemetry your data never leaves the machine._
--- ---
@@ -30,7 +30,7 @@
▲ search / exec └──────┬──────┘ ▲ search / exec └──────┬──────┘
└────────── backup / restore ▼ └────────── backup / restore ▼
timestamped snapshots 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 git clone https://github.com/yourname/marlin.git
cd marlin cd marlin
cargo build --release 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 ```bash
marlin init # create DB (idempotent) marlin init # create DB (idempotent)
marlin scan ~/Pictures ~/Documents # index files marlin scan ~/Pictures ~/Documents # index files
marlin tag "~/Pictures/**/*.jpg" photos/trip-2024 # add tag marlin tag ~/Pictures/**/*.jpg photos/trip-2024 # add hierarchical tag
marlin attr set "~/Documents/**/*.pdf" reviewed yes marlin attr set ~/Documents/**/*.pdf reviewed yes # set custom attribute
marlin search reviewed --exec "xdg-open {}" # open matches marlin search reviewed --exec "xdg-open {}" # open matches
marlin backup # snapshot DB 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 shells completions folder.
---
### Database location ### Database location
* **Linux** `~/.local/share/marlin/index.db` * **Linux** `~/.local/share/marlin/index.db`
@@ -89,37 +107,39 @@ marlin <COMMAND> [ARGS]
init create / migrate database init create / migrate database
scan <PATHS>... walk directories & index files scan <PATHS>... walk directories & index files
tag "<glob>" <tag_path> add hierarchical tag tag "<glob>" <tag_path> add hierarchical tag
attr set|ls … manage custom attributes attr set <pattern> <key> <value> manage custom attributes
search <query> [--exec CMD] FTS query, optionally run CMD on each hit attr ls <path>
search <query> [--exec CMD] FTS5 query, optionally run CMD on each hit
backup create timestamped snapshot in backups/ backup create timestamped snapshot in backups/
restore <snapshot.db> replace DB with snapshot restore <snapshot.db> replace DB with snapshot
completions <shell> generate shell completions
``` ```
### Attribute subcommands ### Attribute subcommands
| Command | Example | | Command | Example |
| ---------- | ------------------------------------------------ | | ---------- | ---------------------------------------------- |
| `attr set` | `marlin attr set "~/Docs/**/*.pdf" reviewed yes` | | `attr set` | `marlin attr set ~/Docs/**/*.pdf reviewed yes` |
| `attr ls` | `marlin attr ls ~/Docs/report.pdf` | | `attr ls` | `marlin attr ls ~/Docs/report.pdf` |
--- ---
## Backups & restore ## Backups & restore
*Create snapshot* **Create snapshot**
```bash ```bash
marlin backup marlin backup
# → ~/.local/share/marlin/backups/backup_2025-05-14_22-15-30.db # → ~/.local/share/marlin/backups/backup_2025-05-14_22-15-30.db
``` ```
*Restore snapshot* **Restore snapshot**
```bash ```bash
marlin restore ~/.local/share/marlin/backups/backup_2025-05-14_22-15-30.db 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 ## Five-Minute Quickstart
Paste & run each block in your terminal. Just paste & run each block in your terminal.
---
### 0Prepare, build & install ### 0Prepare, build & install
```bash ```bash
cd ~/Documents/GitHub/Marlin cd ~/Documents/GitHub/Marlin
cargo build --release cargo build --release
sudo install -Dm755 target/release/marlin /usr/local/bin/marlin sudo install -Dm755 target/release/marlin /usr/local/bin/marlin
```` ```
> Now `marlin` is available everywhere. > Now `marlin` is available everywhere.
--- ### 1Enable shell completion
### 1Enable shell completion (optional but handy)
```bash ```bash
marlin completions bash > ~/.config/bash_completion.d/marlin mkdir -p ~/.config/bash_completion.d
# or for zsh / fish, similarly... marlin completions bash > ~/.config/bash_completion.d/marlin
``` ```
---
### 2Prepare a clean demo directory ### 2Prepare a clean demo directory
```bash ```bash
@@ -179,8 +186,6 @@ printf "Receipt PDF\n" > ~/marlin_demo/Docs/receipt.pdf
printf "fake jpg\n" > ~/marlin_demo/Media/Photos/vacation.jpg printf "fake jpg\n" > ~/marlin_demo/Media/Photos/vacation.jpg
``` ```
---
### 3Initialize & index files ### 3Initialize & index files
```bash ```bash
@@ -193,23 +198,19 @@ marlin --verbose scan ~/marlin_demo
> Only changed files get re-indexed on subsequent runs. > Only changed files get re-indexed on subsequent runs.
---
### 4Attach tags & attributes ### 4Attach tags & attributes
```bash ```bash
# Tag everything under “Alpha” # 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 # 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: # Output as JSON instead:
marlin --format=json attr set "~/marlin_demo/**/*.pdf" reviewed=yes marlin --format=json attr set ~/marlin_demo/**/*.pdf reviewed yes
``` ```
---
### 5Search your index ### 5Search your index
```bash ```bash
@@ -223,8 +224,6 @@ marlin search "reviewed AND pdf"
marlin search reviewed --exec 'echo HIT → {}' marlin search reviewed --exec 'echo HIT → {}'
``` ```
---
### 6Backup & restore ### 6Backup & restore
```bash ```bash
@@ -241,8 +240,7 @@ marlin restore "$snap"
marlin search reviewed marlin search reviewed
``` ```
> Backups live under `~/.local/share/marlin/backups` by default. ---
##### What you just exercised ##### What you just exercised
@@ -257,9 +255,7 @@ marlin search reviewed
| `marlin backup` | Timestamped snapshot of the DB | | `marlin backup` | Timestamped snapshot of the DB |
| `marlin restore` | Replace live DB with a chosen snapshot | | `marlin restore` | Replace live DB with a chosen snapshot |
Thats the complete surface area of Marlin today—feel free to play around or Thats the complete surface area of Marlin today—feel free to play around or point the scanner at real folders.
point the scanner at real folders.
--- ---

View File

@@ -9,11 +9,11 @@ pub mod annotate;
pub mod version; pub mod version;
pub mod event; pub mod event;
use clap::{Parser, Subcommand, ArgEnum, Args, CommandFactory}; use clap::{Parser, Subcommand, CommandFactory, ValueEnum};
use clap_complete::Shell; use clap_complete::Shell;
/// Output format for commands. /// Output format for commands.
#[derive(ArgEnum, Clone, Copy, Debug)] #[derive(ValueEnum, Clone, Copy, Debug)]
pub enum Format { pub enum Format {
Text, Text,
Json, Json,
@@ -78,45 +78,46 @@ pub enum Commands {
/// Generate shell completions (hidden) /// Generate shell completions (hidden)
#[command(hide = true)] #[command(hide = true)]
Completions { Completions {
/// Which shell to generate for
#[arg(value_enum)] #[arg(value_enum)]
shell: Shell, shell: Shell,
}, },
/// File-to-file links /// File-to-file links
#[command(subcommand)] #[command(subcommand)]
Link { cmd: link::LinkCmd }, Link(link::LinkCmd),
/// Collections (groups) of files /// Collections (groups) of files
#[command(subcommand)] #[command(subcommand)]
Coll { cmd: coll::CollCmd }, Coll(coll::CollCmd),
/// Smart views (saved queries) /// Smart views (saved queries)
#[command(subcommand)] #[command(subcommand)]
View { cmd: view::ViewCmd }, View(view::ViewCmd),
/// Workflow states on files /// Workflow states on files
#[command(subcommand)] #[command(subcommand)]
State { cmd: state::StateCmd }, State(state::StateCmd),
/// TODO/tasks management /// TODO/tasks management
#[command(subcommand)] #[command(subcommand)]
Task { cmd: task::TaskCmd }, Task(task::TaskCmd),
/// Reminders on files /// Reminders on files
#[command(subcommand)] #[command(subcommand)]
Remind { cmd: remind::RemindCmd }, Remind(remind::RemindCmd),
/// File annotations and highlights /// File annotations and highlights
#[command(subcommand)] #[command(subcommand)]
Annotate { cmd: annotate::AnnotateCmd }, Annotate(annotate::AnnotateCmd),
/// Version diffs /// Version diffs
#[command(subcommand)] #[command(subcommand)]
Version { cmd: version::VersionCmd }, Version(version::VersionCmd),
/// Calendar events & timelines /// Calendar events & timelines
#[command(subcommand)] #[command(subcommand)]
Event { cmd: event::EventCmd }, Event(event::EventCmd),
} }
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]

View File

@@ -6,7 +6,7 @@ mod logging;
mod scan; mod scan;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand, CommandFactory};
use clap_complete::{generate, Shell}; use clap_complete::{generate, Shell};
use glob::Pattern; use glob::Pattern;
use rusqlite::params; use rusqlite::params;
@@ -26,10 +26,11 @@ fn main() -> Result<()> {
} }
logging::init(); logging::init();
// Handle shell completions as a hidden command // If the user asked for completions, generate and exit immediately.
if let Commands::Completions { shell } = args.command { if let Commands::Completions { shell } = &args.command {
let mut cmd = Cli::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(()); return Ok(());
} }
@@ -47,8 +48,11 @@ fn main() -> Result<()> {
// Open (and migrate) the DB // Open (and migrate) the DB
let mut conn = db::open(&cfg.db_path)?; let mut conn = db::open(&cfg.db_path)?;
// Dispatch // Dispatch all commands
match args.command { match args.command {
Commands::Completions { .. } => {
// no-op, already handled above
}
Commands::Init => { Commands::Init => {
info!("Database initialised at {}", cfg.db_path.display()); info!("Database initialised at {}", cfg.db_path.display());
} }
@@ -90,15 +94,15 @@ fn main() -> Result<()> {
info!("Successfully opened restored database."); info!("Successfully opened restored database.");
} }
// new domains delegate to their run() functions // new domains delegate to their run() functions
Commands::Link { cmd } => cli::link::run(&cmd, &mut conn, args.format)?, Commands::Link(link_cmd) => cli::link::run(&link_cmd, &mut conn, args.format)?,
Commands::Coll { cmd } => cli::coll::run(&cmd, &mut conn, args.format)?, Commands::Coll(coll_cmd) => cli::coll::run(&coll_cmd, &mut conn, args.format)?,
Commands::View { cmd } => cli::view::run(&cmd, &mut conn, args.format)?, Commands::View(view_cmd) => cli::view::run(&view_cmd, &mut conn, args.format)?,
Commands::State { cmd } => cli::state::run(&cmd, &mut conn, args.format)?, Commands::State(state_cmd) => cli::state::run(&state_cmd, &mut conn, args.format)?,
Commands::Task { cmd } => cli::task::run(&cmd, &mut conn, args.format)?, Commands::Task(task_cmd) => cli::task::run(&task_cmd, &mut conn, args.format)?,
Commands::Remind { cmd } => cli::remind::run(&cmd, &mut conn, args.format)?, Commands::Remind(rm_cmd) => cli::remind::run(&rm_cmd, &mut conn, args.format)?,
Commands::Annotate { cmd } => cli::annotate::run(&cmd, &mut conn, args.format)?, Commands::Annotate(an_cmd) => cli::annotate::run(&an_cmd, &mut conn, args.format)?,
Commands::Version { cmd } => cli::version::run(&cmd, &mut conn, args.format)?, Commands::Version(v_cmd) => cli::version::run(&v_cmd, &mut conn, args.format)?,
Commands::Event { cmd } => cli::event::run(&cmd, &mut conn, args.format)?, Commands::Event(e_cmd) => cli::event::run(&e_cmd, &mut conn, args.format)?,
} }
Ok(()) 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)")?; conn.prepare("INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?1, ?2)")?;
let mut count = 0; 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(); let path_str = entry.path().to_string_lossy();
debug!("testing path: {}", path_str); debug!("testing path: {}", path_str);
if !pat.matches(&path_str) { if !pat.matches(&path_str) {

Binary file not shown.

View File

@@ -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