mod config;
mod executor;
mod loader;
mod types;
mod ui;
use anyhow::{Context, Result, bail};
#[cfg(not(test))]
use arboard::Clipboard;
#[cfg(test)]
pub struct Clipboard;
#[cfg(test)]
impl Clipboard {
pub fn new() -> Result<Self> {
Ok(Clipboard)
}
pub fn set_text(&mut self, _text: String) -> Result<()> {
Ok(())
}
}
use clap::{Parser, Subcommand};
use config::{determine_config_directory, load_app_config};
use loader::load_commands;
use std::path::{Path, PathBuf};
use types::CommandDef;
use ui::{choose_command, select_and_execute_command};
fn get_scan_dirs(
cli_dir: &Option<PathBuf>,
primary: &Path,
extra_dirs: &[PathBuf],
) -> Vec<PathBuf> {
let mut dirs = Vec::new();
dirs.push(primary.to_path_buf());
if cli_dir.is_none() {
dirs.extend_from_slice(extra_dirs);
}
dirs
}
#[cfg(test)]
mod scan_dirs_tests {
use super::get_scan_dirs;
use std::path::PathBuf;
#[test]
fn with_dir_flag_only_primary() {
let primary = PathBuf::from("/only");
let cli_dir = Some(primary.clone());
let extras = vec![PathBuf::from("/a"), PathBuf::from("/b")];
let dirs = get_scan_dirs(&cli_dir, &primary, &extras);
assert_eq!(dirs, vec![primary]);
}
#[test]
fn without_dir_flag_includes_extras() {
let primary = PathBuf::from("/base");
let cli_dir: Option<PathBuf> = None;
let extras = vec![PathBuf::from("/a"), PathBuf::from("/b")];
let dirs = get_scan_dirs(&cli_dir, &primary, &extras);
let expected = vec![
PathBuf::from("/base"),
PathBuf::from("/a"),
PathBuf::from("/b"),
];
assert_eq!(dirs, expected);
}
}
#[derive(Parser, Debug)]
#[command(
name = "cmdy",
author,
version,
about = "Lists and runs predefined command snippets.",
long_about = None,
subcommand_required = false,
)]
struct CliArgs {
#[arg(long, value_name = "DIRECTORY")]
dir: Option<PathBuf>,
#[arg(short = 't', long = "tag", value_name = "TAG")]
tags: Vec<String>,
#[arg(short = 'q', long = "query", value_name = "QUERY")]
query: Option<String>,
#[command(subcommand)]
action: Option<Action>,
}
#[derive(Subcommand, Debug)]
enum Action {
Edit,
Clip,
}
fn main() -> Result<()> {
let cli_args = CliArgs::parse();
let app_config = load_app_config().context("Failed to load application configuration")?;
let config_dir = determine_config_directory(&cli_args.dir)?;
#[cfg(debug_assertions)]
println!("Using configuration directory: {:?}", config_dir);
let scan_dirs = get_scan_dirs(&cli_args.dir, &config_dir, &app_config.directories);
let mut commands_map = load_commands(&scan_dirs[0])
.with_context(|| format!("Failed to load command definitions from {:?}", scan_dirs[0]))?;
for extra_dir in scan_dirs.iter().skip(1) {
if extra_dir.is_dir() {
let extra_map = load_commands(extra_dir).with_context(|| {
format!("Failed to load command definitions from {:?}", extra_dir)
})?;
for (name, cmd_def) in extra_map {
if commands_map.contains_key(&name) {
let existing = &commands_map[&name];
bail!(
"Duplicate command snippet name '{}' found.\n Defined in: {}\n Also defined in: {}",
name,
cmd_def.source_file.display(),
existing.source_file.display()
);
}
commands_map.insert(name, cmd_def);
}
}
}
let mut commands_vec: Vec<CommandDef> = commands_map.into_values().collect();
commands_vec.sort_by(|a, b| a.description.cmp(&b.description));
if !cli_args.tags.is_empty() {
let filter_tags = &cli_args.tags;
commands_vec.retain(|cmd| cmd.tags.iter().any(|tag| filter_tags.contains(tag)));
if commands_vec.is_empty() {
eprintln!(
"No command snippets found matching tag(s): {:?}",
filter_tags
);
return Ok(());
}
}
match cli_args.action {
Some(Action::Edit) => {
let cmd_def = choose_command(
&commands_vec,
&config_dir,
&app_config.filter_command,
cli_args.query.as_deref(),
)?;
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
std::process::Command::new(editor)
.arg(&cmd_def.source_file)
.status()
.context("Failed to launch editor")?;
return Ok(());
}
Some(Action::Clip) => {
let cmd_def = choose_command(
&commands_vec,
&config_dir,
&app_config.filter_command,
cli_args.query.as_deref(),
)?;
let mut clipboard = Clipboard::new().context("Failed to access clipboard")?;
clipboard
.set_text(cmd_def.command.clone())
.context("Failed to copy to clipboard")?;
println!("Copied command to clipboard");
return Ok(());
}
None => {}
}
select_and_execute_command(
&commands_vec,
&config_dir,
&app_config.filter_command,
cli_args.query.as_deref(),
)
.context("Failed during command selection or execution")?;
Ok(())
}