use std::path::PathBuf;
use std::sync::LazyLock;
use clap::{Parser, Subcommand};
use clap_complete::Shell;
use snip_it::commands;
use snip_it::config;
use snip_it::error::SnipResult;
use snip_it::logging::{
init_default_logging, log_shutdown_info, log_startup_info, setup_panic_handler,
};
mod update;
static RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
tokio::runtime::Runtime::new().unwrap_or_else(|e| {
eprintln!("Failed to create async runtime: {e}. Ensure no other process is consuming excessive system resources.");
std::process::exit(1);
})
});
#[cfg(unix)]
fn setup_signal_handler() {
use signal_hook::flag;
use snip_it::ui;
let terminate = ui::get_terminate();
if let Err(e) = flag::register(signal_hook::consts::signal::SIGINT, terminate.clone()) {
eprintln!("Failed to set Ctrl+C handler: {e}");
std::process::exit(1);
}
if let Err(e) = flag::register(signal_hook::consts::signal::SIGTERM, terminate) {
eprintln!("Failed to set SIGTERM handler: {e}");
std::process::exit(1);
}
}
#[cfg(windows)]
fn setup_signal_handler() {
}
#[derive(Debug, Parser)]
#[command(
name = "snp",
about = "A fast, terminal-based snippet manager with fuzzy search, clipboard support, and optional self-hosted sync",
version = env!("CARGO_PKG_VERSION"),
after_help = "Config: ~/.config/snp/snippets.toml | Docs: https://github.com/eggstack/snip-it"
)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Debug, Subcommand)]
enum Commands {
#[command(alias = "v")]
Version,
Update {
#[arg(long, help = "Check for an update without installing it")]
dry_run: bool,
#[arg(long, help = "Use Cargo's locked dependency versions")]
locked: bool,
},
#[command(alias = "n")]
New {
#[arg(default_value = "")]
command: String,
#[arg(short, long, action = clap::ArgAction::SetTrue)]
tags: bool,
#[arg(short, long, action = clap::ArgAction::SetTrue)]
multiline: bool,
#[arg(short = 'd', long)]
description: Option<String>,
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(short, long)]
library: Option<String>,
},
#[command(alias = "l")]
List {
#[arg(short, long)]
filter: Option<String>,
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(short, long)]
library: Option<String>,
#[arg(long, action = clap::ArgAction::SetTrue)]
#[arg(conflicts_with = "csv")]
json: bool,
#[arg(long, action = clap::ArgAction::SetTrue)]
#[arg(conflicts_with = "json")]
csv: bool,
},
#[command(alias = "r")]
Run {
#[arg(short, long)]
filter: Option<String>,
#[arg(long, action = clap::ArgAction::SetTrue)]
sync: bool,
#[arg(short, long)]
library: Option<String>,
},
#[command(alias = "c")]
Clip {
#[arg(short, long)]
filter: Option<String>,
#[arg(long, action = clap::ArgAction::SetTrue)]
sync: bool,
#[arg(short, long)]
library: Option<String>,
},
#[command(alias = "s")]
Search {
#[arg(short, long)]
filter: Option<String>,
#[arg(long, action = clap::ArgAction::SetTrue)]
sync: bool,
#[arg(short, long)]
library: Option<String>,
},
#[command(alias = "e")]
Edit {
#[arg(short, long)]
library: Option<String>,
},
#[command(alias = "k")]
Keybindings,
#[command(alias = "y")]
Sync {
#[arg(short, long, help = "Sync a specific library")]
library: Option<String>,
#[arg(long, action = clap::ArgAction::SetTrue, help = "List connected servers")]
servers: bool,
#[arg(long, action = clap::ArgAction::SetTrue, help = "Upload local changes only")]
#[arg(conflicts_with = "pull_only")]
push_only: bool,
#[arg(long, action = clap::ArgAction::SetTrue, help = "Download remote changes only")]
#[arg(conflicts_with = "push_only")]
pull_only: bool,
#[arg(long, action = clap::ArgAction::SetTrue, help = "Show what would be synced")]
dry_run: bool,
},
#[command(alias = "cr")]
Cron {
#[arg(short, long, default_value = "15")]
interval: u32,
},
#[command(alias = "reg")]
Register {
#[arg(long, default_value = crate::config::DEFAULT_SERVER_URL)]
server: String,
#[arg(long, action = clap::ArgAction::SetTrue)]
force: bool,
},
#[command(alias = "lib")]
Library {
#[command(subcommand)]
command: LibraryCommands,
},
#[command(alias = "p")]
Premade {
#[command(subcommand)]
command: PremadeCommands,
},
#[command(alias = "g")]
Completions {
#[arg(value_enum)]
shell: Shell,
},
}
#[derive(Debug, Subcommand)]
enum LibraryCommands {
#[command(alias = "l")]
List,
#[command(alias = "c")]
Create { name: String },
#[command(alias = "d")]
Delete {
name: String,
#[arg(short, long, action = clap::ArgAction::SetTrue)]
force: bool,
},
#[command(alias = "p")]
SetPrimary { name: String },
#[command(alias = "s")]
Show { name: Option<String> },
}
#[derive(Debug, Subcommand)]
enum PremadeCommands {
#[command(alias = "l")]
List,
Get { name: Option<String> },
#[command(alias = "s")]
Sync,
#[command(alias = "se")]
Search { query: String },
#[command(alias = "u")]
Update { name: String },
}
fn dispatch_command(cli: Option<Commands>) -> SnipResult<()> {
match cli {
None => {
commands::run_cmd::run(None, false, None, &RUNTIME)?;
}
Some(Commands::Version) => {
println!("snp {}", env!("CARGO_PKG_VERSION"));
}
Some(Commands::Update { dry_run, locked }) => {
update::run(dry_run, locked).map_err(|error| {
snip_it::error::SnipError::runtime_error("update failed", Some(&error))
})?;
}
Some(Commands::New {
command,
tags,
multiline,
description,
config,
library,
}) => {
commands::new_cmd::run(command, description, tags, multiline, config, library)?;
}
Some(Commands::List {
filter,
config,
library,
json,
csv,
}) => {
let format = if json {
commands::list_cmd::ListFormat::Json
} else if csv {
commands::list_cmd::ListFormat::Csv
} else {
commands::list_cmd::ListFormat::Default
};
commands::list_cmd::run(filter, config, library, format)?;
}
Some(Commands::Run {
filter,
sync,
library,
}) => {
commands::run_cmd::run(filter, sync, library, &RUNTIME)?;
}
Some(Commands::Clip {
filter,
sync,
library,
}) => {
commands::clip_cmd::run(filter, sync, library, None, &RUNTIME)?;
}
Some(Commands::Search {
filter,
sync,
library,
}) => {
commands::search_cmd::run(filter, sync, library, None, &RUNTIME)?;
}
Some(Commands::Edit { library }) => {
commands::edit_cmd::run(library, None)?;
}
Some(Commands::Keybindings) => {
commands::keybindings_cmd::run()?;
}
Some(Commands::Sync {
library,
servers,
push_only,
pull_only,
dry_run,
}) => {
let options = commands::sync_cmd::SyncOptions {
library,
servers,
push_only,
pull_only,
dry_run,
};
commands::sync_cmd::run(options, &RUNTIME)?;
}
Some(Commands::Cron { interval }) => {
commands::cron_cmd::run(interval)?;
}
Some(Commands::Register { server, force }) => {
commands::register_cmd::run(server, force, &RUNTIME)?;
}
Some(Commands::Library { command }) => match command {
LibraryCommands::List => commands::library_cmd::run_list()?,
LibraryCommands::Create { name } => commands::library_cmd::run_create(name)?,
LibraryCommands::Delete { name, force } => {
commands::library_cmd::run_delete(name, force)?
}
LibraryCommands::SetPrimary { name } => commands::library_cmd::run_set_primary(name)?,
LibraryCommands::Show { name } => commands::library_cmd::run_show(name)?,
},
Some(Commands::Premade { command }) => match command {
PremadeCommands::List => commands::premade_cmd::run_list(&RUNTIME)?,
PremadeCommands::Get { name } => {
let all = name.as_ref().is_some_and(|n| n == "all");
commands::premade_cmd::run_get(name, all, &RUNTIME)?;
}
PremadeCommands::Sync => commands::premade_cmd::run_sync(&RUNTIME)?,
PremadeCommands::Search { query } => {
commands::premade_cmd::run_search(query, &RUNTIME)?;
}
PremadeCommands::Update { name } => {
commands::premade_cmd::run_update(name, &RUNTIME)?;
}
},
Some(Commands::Completions { shell }) => {
let mut cmd = <Cli as clap::CommandFactory>::command();
clap_complete::generate(shell, &mut cmd, "snp", &mut std::io::stdout());
}
}
Ok(())
}
fn main() {
setup_panic_handler();
setup_signal_handler();
init_default_logging();
log_startup_info();
let cli = Cli::parse();
if let Err(e) = dispatch_command(cli.command) {
eprintln!("error: {e}");
log_shutdown_info();
std::process::exit(1);
}
log_shutdown_info();
}