use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use clap_mangen::Man;
mod commands;
mod config;
mod path;
#[derive(Parser)]
#[command(name = "hk")]
#[command(version)]
#[command(about = "Hitchmark CLI: stable links to documents and paragraphs")]
#[command(
long_about = "hk creates and manages hook:// URIs — stable, addressable links \
to files, web pages, and intra-document locations.\n\nExamples:\n \
hk file ~/docs/note.md\n \
hk link ~/docs/note.md ~/docs/reference.md --note \"See this section\"\n \
hk list ~/docs/note.md\n \
hk list ~/docs/note.md --json\n \
hk delete ~/docs/note.md ~/docs/reference.md\n \
hk open \"hook://file/L3Zhci9sb2cvZG9jcy9ub3RlLm1k\""
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Link {
uri_a: String,
uri_b: String,
#[arg(long)]
note: Option<String>,
},
List {
uri: String,
#[arg(long)]
json: bool,
},
Delete {
uri_a: String,
uri_b: String,
#[arg(long, short = 'y')]
yes: bool,
},
Open {
uri: String,
},
File {
path: String,
#[arg(long)]
bookmark: bool,
},
Bookmark(commands::bookmark::BookmarkArgs),
Gc(commands::gc::GcArgs),
Export(commands::export::ExportArgs),
Import(commands::import::ImportArgs),
Purple {
path: String,
#[arg(long, default_value = "markdown")]
format: String,
},
Serve {
#[arg(long, default_value_t = 2701)]
port: u16,
#[arg(long, default_value = "127.0.0.1")]
host: String,
#[arg(long)]
pid_file: Option<String>,
},
Watch(commands::watch::WatchArgs),
Completions {
shell: Shell,
},
Manpage {
#[arg(long, default_value_t = default_man_dir())]
out: String,
},
Version {
#[arg(long)]
verbose: bool,
},
}
fn default_man_dir() -> String {
if cfg!(windows) {
std::env::var("USERPROFILE")
.map(|h| format!(r"{h}\man\man1"))
.unwrap_or_else(|_| r"C:\man\man1".to_string())
} else {
"/usr/local/share/man/man1".to_string()
}
}
fn main() -> anyhow::Result<()> {
env_logger::init();
let cli = Cli::parse();
let config = config::Config::load()?;
config.ensure_dir()?;
match cli.command {
Commands::Link { uri_a, uri_b, note } => {
let args = commands::link::LinkArgs { uri_a, uri_b, note };
commands::link::execute(args, &config.store_path)?;
}
Commands::List { uri, json } => {
let args = commands::list::ListArgs { uri, json };
commands::list::execute(args, &config.store_path)?;
}
Commands::Delete { uri_a, uri_b, yes } => {
let args = commands::delete::DeleteArgs { uri_a, uri_b, yes };
commands::delete::execute(args, &config.store_path)?;
}
Commands::Open { uri } => {
let args = commands::open::OpenArgs { uri };
commands::open::execute(args, &config.store_path)?;
}
Commands::File { path, bookmark } => {
let args = commands::file::FileArgs { path, bookmark };
commands::file::execute(args, &config.store_path)?;
}
Commands::Bookmark(args) => {
commands::bookmark::execute(args, &config.store_path)?;
}
Commands::Gc(args) => {
commands::gc::execute(args, &config.store_path)?;
}
Commands::Export(args) => {
commands::export::execute(args, &config.store_path)?;
}
Commands::Import(args) => {
commands::import::execute(args, &config.store_path)?;
}
Commands::Purple { path, format } => {
let args = commands::purple::PurpleArgs { path, format };
commands::purple::execute(args)?;
}
Commands::Serve {
port,
host,
pid_file,
} => {
let args = commands::serve::ServeArgs {
port,
host,
pid_file,
};
commands::serve::execute(args, &config.store_path)?;
}
Commands::Watch(args) => {
commands::watch::execute(args, &config.store_path)?;
}
Commands::Completions { shell } => {
generate(shell, &mut Cli::command(), "hk", &mut std::io::stdout());
}
Commands::Manpage { out } => {
let out_dir = std::path::PathBuf::from(&out);
std::fs::create_dir_all(&out_dir)
.map_err(|e| anyhow::anyhow!("Cannot create directory {out}: {e}"))?;
let man = Man::new(Cli::command());
let dest = out_dir.join("hk.1");
let mut f = std::fs::File::create(&dest)
.map_err(|e| anyhow::anyhow!("Cannot write {}: {e}", dest.display()))?;
man.render(&mut f)
.map_err(|e| anyhow::anyhow!("Man page render failed: {e}"))?;
println!("Wrote {}", dest.display());
}
Commands::Version { verbose } => {
println!("hk {}", env!("CARGO_PKG_VERSION"));
if verbose {
println!("store: {}", config.store_path.display());
println!("server: http://127.0.0.1:2701");
}
}
}
Ok(())
}