use arboard::Clipboard;
use clap::Parser;
use regex::Regex;
use std::io::{self, Read};
use std::process;
#[derive(Parser, Debug)]
#[command(
author,
version,
about = "A simple and fast CLI tool to slugify and unslugify text (branch names, URLs, etc)\n\n\
Examples:\n slug \"Hello World\"\n slug -u \"my-feature-branch\"\n slug \"Text Here\" --lowercase\n slug \"Text Here\" --uppercase\n slug \"Text Here\" --title\n echo \"from stdin\" | slug --no-clipboard",
after_help = "If no TEXT is given, reads from stdin.\n\
By default, slugified/unslugified result is copied to clipboard unless --no-clipboard is set.\n\
\n\
Output case is preserved unless you specify --lowercase, --uppercase, or --title (all are mutually exclusive). Use these flags to control the result form commonly needed for git branches, URLs, or for readability.",
next_line_help = true
)]
struct Args {
#[arg(short, long, group = "mode")]
slug: bool,
#[arg(short, long, group = "mode")]
unslug: bool,
#[arg(long, default_value_t = false)]
no_clipboard: bool,
#[arg(value_name = "TEXT")]
text: Option<String>,
#[arg(long, conflicts_with_all = ["uppercase", "title"])]
lowercase: bool,
#[arg(long, conflicts_with_all = ["lowercase", "title"])]
uppercase: bool,
#[arg(long, conflicts_with_all = ["lowercase", "uppercase"])]
title: bool,
}
fn main() {
let args = Args::parse();
let input = match &args.text {
Some(s) => s.clone(),
None => {
let mut buffer = String::new();
match io::stdin().read_to_string(&mut buffer) {
Ok(n) if n > 0 => buffer.trim_end_matches(&['\n', '\r'][..]).to_string(),
Ok(_) => {
eprintln!("Error: No input provided (neither TEXT argument nor stdin)");
process::exit(1);
}
Err(e) => {
eprintln!("Error: Failed to read from stdin: {}", e);
process::exit(1);
}
}
}
};
if input.trim().is_empty() {
eprintln!("Error: No input provided (empty string)");
process::exit(1);
}
let mut result = if args.unslug {
unslugify(&input)
} else if args.title {
slugify(&title_case(&input))
} else {
slugify(&input)
};
if args.lowercase {
result = result.to_lowercase();
} else if args.uppercase {
result = result.to_uppercase();
}
fn title_case(input: &str) -> String {
input
.split_whitespace()
.map(|word| {
let mut c = word.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + &c.as_str().to_lowercase(),
}
})
.collect::<Vec<String>>()
.join(" ")
}
println!("{result}");
if !args.no_clipboard {
match Clipboard::new() {
Ok(mut clipboard) => match clipboard.set_text(result.clone()) {
Ok(_) => eprintln!("✓ Copied to clipboard!"),
Err(e) => eprintln!("Warning: Failed to copy to clipboard: {e}"),
},
Err(e) => {
eprintln!("Warning: Could not access clipboard: {e}");
}
}
}
}
fn slugify(input: &str) -> String {
let re = Regex::new(r"[^\w\s-]").unwrap();
re.replace_all(input, "")
.split_whitespace()
.collect::<Vec<&str>>()
.join("-")
}
fn unslugify(input: &str) -> String {
input.replace('-', " ")
}