diff --git a/.config/ya.yml b/.config/ya.yml index f23ed71..218b0b4 100644 --- a/.config/ya.yml +++ b/.config/ya.yml @@ -1,6 +1,6 @@ install: prog: cargo - args: ["install", "--path", "."] + args: ["install", "--all-features", "--path", "."] chdir: $GIT_ROOT lint: @@ -19,7 +19,7 @@ format: build: prog: cargo - args: ["build", "--release"] + args: ["build", "--release", "--all-features"] release_patch: from: $GIT_ROOT/.config/ya/tag.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index faa51db..ac3bee5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,7 +61,7 @@ jobs: - name: Cargo Build if: ${{ ! matrix.use-cross }} - run: cargo build --locked --release --target ${{ matrix.target }} + run: cargo build --locked --release --all-features --target ${{ matrix.target }} - name: Setup Cross if: matrix.use-cross @@ -71,15 +71,34 @@ jobs: - name: Cross Build if: matrix.use-cross - run: cross build --locked --release --target ${{ matrix.target }} + run: cross build --locked --release --all-features --target ${{ matrix.target }} - - name: Compress Binary for Upload + - name: Compress `ya` Binary for Upload run: | mv target/${{ matrix.target }}/release/ya ya tar -czf ya-${{ matrix.target }}.tar.gz ya - - name: Upload tarball + - name: Compress `yadayada` Binary for Upload + run: | + mv target/${{ matrix.target }}/release/yadayada yyadayada + tar -czf yadayada-${{ matrix.target }}.tar.gz ya + + - name: Upload `ya` tarball uses: actions/upload-artifact@v3 with: - name: ${{ matrix.target }} + name: ya-${{ matrix.target }} path: ya-${{ matrix.target }}.tar.gz + + - name: Upload `yadayada` tarball + uses: actions/upload-artifact@v3 + with: + name: yadayada-${{ matrix.target }} + path: yadayada-${{ matrix.target }}.tar.gz + + - name: Upload Completions + uses: actions/upload-artifact@v3 + with: + name: completions + path: | + completions/release/* + !completions/release/.gitignore diff --git a/Cargo.lock b/Cargo.lock index 302be81..01e0546 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,6 +78,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "1.4.0" @@ -96,6 +105,12 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.3.19" @@ -116,9 +131,19 @@ dependencies = [ "anstream", "anstyle", "clap_lex", + "once_cell", "strsim", ] +[[package]] +name = "clap_complete" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.3.12" @@ -154,12 +179,41 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -199,6 +253,30 @@ dependencies = [ "libc", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "handlebars" +version = "4.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c3372087601b532857d332f5957cbae686da52bb7810bf038c3e3c3cc2fa0d" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "hashbrown" version = "0.14.0" @@ -292,6 +370,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + [[package]] name = "memchr" version = "2.5.0" @@ -304,6 +388,50 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "pest" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666d00490d4ac815001da55838c500eafb0320019bbaa44444137c48b443a853" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ca01446f50dbda87c1786af8770d535423fa8a53aec03b8f4e3d7eb10e0929" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56af0a30af74d0445c0bf6d9d051c979b516a1a5af790d251daee76005420a48" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "predicates" version = "3.0.3" @@ -378,9 +506,9 @@ checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "6d3e73c93c3240c0bda063c239298e633114c69a888c3e37ca8bb33f343e9890" [[package]] name = "serde_derive" @@ -393,6 +521,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.25" @@ -406,6 +545,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "strsim" version = "0.10.0" @@ -429,6 +579,38 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "unicode-ident" version = "1.0.8" @@ -447,6 +629,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wait-timeout" version = "0.2.0" @@ -529,8 +717,11 @@ dependencies = [ "anyhow", "assert_cmd", "clap", + "clap_complete", "colored", + "handlebars", "home", "serde_derive", + "serde_json", "serde_yaml", ] diff --git a/Cargo.toml b/Cargo.toml index 17c2e26..63ad826 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,16 +2,28 @@ name = "ya" version = "0.5.0" edition = "2021" +default-run = "ya" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +build = "build.rs" + +[build-dependencies] +clap = { version = "4.3.19", features = ["derive", "cargo"], optional = true } +clap_complete = { version = "4.3.2", optional = true } +handlebars = { version = "4.3.7", optional = true } +serde_json = { version = "1.0.104", optional = true } + [dependencies] anyhow = "1.0.72" -clap = { version = "4.3.19", features = ["derive"] } -colored = { version = "2.0.4" } +clap = { version = "4.3.19", features = ["derive", "cargo"] } +colored = "2.0.4" home = "0.5.5" serde_derive = "1.0.179" serde_yaml = "0.9.25" +clap_complete = { version = "4.3.2", optional = true } +handlebars = {version = "4.3.7", optional = true } +serde_json = { version = "1.0.104", optional = true } [dev-dependencies] assert_cmd = "2.0.12" @@ -21,3 +33,18 @@ strip = true lto = true codegen-units = 1 panic = "abort" + +[[bin]] +name = "ya" +path = "src/main.rs" + +[[bin]] +name = "yadayada" +path = "src/bin/yadayada.rs" +required-features = ["yadayada"] + +[features] +completion = ["clap_complete"] +templating = ["handlebars", "serde_json"] +gh-release = ["completion", "templating"] +yadayada = ["completion", "templating"] diff --git a/README.md b/README.md index 99efbbc..89e516d 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,7 @@ Read the following for information on the config file: [docs/config.md](docs/con ## CLI Read the following for information on the CLI: [docs/cli.md](docs/cli.md). + +## Completions + +Read the following for information on the completions: [docs/completions.md](docs/completions.md). diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..8bcd825 --- /dev/null +++ b/build.rs @@ -0,0 +1,28 @@ +#[cfg(not(feature = "gh-release"))] +use std::io::Error; + +#[cfg(feature = "gh-release")] +use clap::CommandFactory; + +#[cfg(feature = "gh-release")] +include!("src/cli.rs"); +#[cfg(feature = "gh-release")] +include!("src/completion.rs"); + +#[cfg(feature = "gh-release")] +fn build_completions() -> Result<(), Error> { + let release_dir = "completions/release"; + + let mut cmd = Args::command(); + + build_templated_completions(&mut cmd, release_dir)?; + + Ok(()) +} + +fn main() -> Result<(), Error> { + #[cfg(feature = "gh-release")] + build_completions()?; + + Ok(()) +} diff --git a/completions/release/.gitignore b/completions/release/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/completions/release/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/completions/templates/generated.hbs b/completions/templates/generated.hbs new file mode 100644 index 0000000..99ec542 --- /dev/null +++ b/completions/templates/generated.hbs @@ -0,0 +1,5 @@ +# Completions + +## Version {{ version }} + +{{ generated_completes }} diff --git a/completions/templates/ya.fish.hbs b/completions/templates/ya.fish.hbs new file mode 100644 index 0000000..5a8e027 --- /dev/null +++ b/completions/templates/ya.fish.hbs @@ -0,0 +1,41 @@ +# Completions for ya + +## Version {{ version }} + +# Cross Platform Utilities +function __ya_complete_key_filter + if command -v rg &> /dev/null + command rg '^([^:\s]+):.*$' $argv + return + end + command grep -E '^([^: ]+):.*$' $argv + +end + +function __ya_complete_key_clean + if command -v sd &> /dev/null + command sd '^([^:\s]+):.*$' '$1' $argv + return + end + command sed -E 's/^([^: ]+):.*$/\1/g' $argv +end + +function __ya_complete_commands + if command -v yadayada &> /dev/null + yadayada k + return + end + ya -p | __ya_complete_key_filter | __ya_complete_key_clean +end + +function __ya_complete_main + # Disabling default file completion + complete -c ya -f + + # Static Switches +{{ generated_completes }} + # Dynamic Subcommands + complete -c ya -n "not __fish_seen_subcommand_from (__ya_complete_commands)" -a "(__ya_complete_commands)" +end + +__ya_complete_main $argv diff --git a/docs/completions.md b/docs/completions.md new file mode 100644 index 0000000..85df87c --- /dev/null +++ b/docs/completions.md @@ -0,0 +1,63 @@ +# Completions + +`ya` supports completion for multiple shells, and can be installed in a couple of different ways. + +The best supported shell is `fish`, with best effort support a handful of other shells. + +Command completion is best done when used with the sister binary in this project, `yadayada`, which provides utilities for local development. + +## Installation + +Assuming you've installed `yadayada`, you can use the `install` command to install shell completion for any shell it supports (only `fish` as of now). + +### Fish Install + +The following is what you need to run to install shell completions for `fish`: + +```fish +yadayada i +``` + +This will install the completions for `ya` and `yadayada` into the appropriate directory expected by `fish`. + +### Manual Installation + +All shell completions are created during the build process and are available as part of the release artifacts. + +You will need to use the appropriate completion file for your shell, and place it in the appropriate directory. + +Unfortunately, this can be quite variable, so I recommend reading the documentation for your shell to find out where to place the completion file. + +## Shell Completion + +The default shell completion that can be expected is full completion for `yadayada` and switch completion for `ya`. + +### Default Completion + +```fish +❯ yadayada +help (Print this message or the help of the given subcommand(s)) install (Install command completion for `ya` and `yadayada`) keys (Print keys of a config) +``` + +```fish +❯ yadayada - +-h --help (Print help) -V --version (Print version) --no-color (No color) +``` + +```fish +❯ ya - +-c --config (The config file) -p --print (Print the config file before running) -V --version (Print version) --no-color (No color) +-h --help (Print help) -q --quiet (Suppress excess output) -x --execution (Print the executed command before executing it) --sd (Search and displacements) +``` + +The reason `ya` does not always have full completion for commands is because it is not possible to know at compilation time what the available commands are going to be at runtime, due to the nature of the `ya` config system. + +### Fish Completion + +Because I use `fish` as my shell, I have made an effort to provide the best completion for it. As a consequence, `ya` has full completion for commands by inspecting the configuration file at runtime. + +```fish +❯ ya +build install release_major release_patch +format lint release_minor test +``` diff --git a/docs/install.md b/docs/install.md index ba31c70..75ab823 100644 --- a/docs/install.md +++ b/docs/install.md @@ -26,9 +26,24 @@ brew install ya ```bash git clone https://github.com/yhakbar/ya.git cd ya +``` + +### With `yadayada` + +```bash +cargo install --features yadayada --path . +``` + +### Without `yadayada` + +```bash cargo install --path . ``` ## Manual -Go [here](https://github.com/yhakbar/ya/releases/latest) and download the binary for your platform. +Go [here](https://github.com/yhakbar/ya/releases/latest) and download the appropriate executable(s) for your platform. + +## Shell Completion + +I recommend reading [this](/docs/completions.md) for more information on how to install shell completions. diff --git a/src/bin/yadayada.rs b/src/bin/yadayada.rs new file mode 100644 index 0000000..2a5365b --- /dev/null +++ b/src/bin/yadayada.rs @@ -0,0 +1,113 @@ +use anyhow::Ok; +use clap::{CommandFactory, Parser, Subcommand}; +use home::home_dir; +use serde_yaml::Value; +use std::path::PathBuf; +use ya::{cli::Args, completion::build_fish_completion, config::get_config_path}; + +#[derive(Subcommand)] +pub enum YadaYadaSubcommand { + /// Install command completion for `ya` and `yadayada`. + #[command(about, long_about = None, alias = "i")] + Install { + /// The shell to install command completion for. + #[arg(short, long)] + shell: Option, + + /// The directory to install command completion to. + /// Defaults to best guess for the shell. + #[arg(short, long)] + directory: Option, + }, + + /// Print keys of a config. + #[command(about, long_about = None, alias = "k")] + Keys { + /// The config to print the keys of. + #[arg(short, long)] + config: Option, + }, +} + +/// Command completion to manage command completion for `ya`. +#[derive(Parser)] +#[command(author, version, about, long_about = None, arg_required_else_help(true))] +pub struct YadaYadaArgs { + /// No color. + #[arg(long, default_value_t = false)] + pub no_color: bool, + + /// Subcommand of `yadayada`. + #[command(subcommand)] + pub subcommand: Option, +} + +fn main() -> anyhow::Result<()> { + let args = YadaYadaArgs::parse(); + + if args.no_color { + colored::control::set_override(false); + } + + if let Some(subcommand) = args.subcommand { + match subcommand { + YadaYadaSubcommand::Install { shell, directory } => { + let shell = match shell { + Some(shell) => shell, + None => { + let shell_path_str = + std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()); + let shell_path = PathBuf::from(shell_path_str); + let shell = shell_path.file_name().unwrap().to_str().unwrap(); + shell.to_string() + } + }; + match shell.as_str() { + "fish" => { + let directory = match directory { + Some(directory) => Ok(directory), + None => { + if let Some(home_dir) = home_dir() { + let fish_dir = home_dir.join(".config/fish/completions"); + Ok(fish_dir) + } else { + return Err(anyhow::anyhow!("Could not find home directory")); + } + } + }?; + if let Some(directory) = directory.to_str() { + let mut cmd = Args::command(); + build_fish_completion(&mut cmd, directory, "ya")?; + let mut cmd = YadaYadaArgs::command(); + build_fish_completion(&mut cmd, directory, "yadayada")?; + } else { + return Err(anyhow::anyhow!("Could not convert directory to string")); + } + } + _ => { + return Err(anyhow::anyhow!("Shell `{}` not supported for automatic installation. Please install completion manually.", shell)); + } + } + } + YadaYadaSubcommand::Keys { config } => { + let config_path = get_config_path(&config)?; + let config = ya::config::parse_config_from_file(&config_path)?; + match config { + Value::Mapping(config) => { + let keys = config.keys(); + for key in keys { + let key = key.as_str(); + if let Some(key) = key { + println!("{}", key); + } + } + return Ok(()); + } + _ => return Ok(()), + } + } + } + } + + Ok(()) +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..7cc5c2c --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,40 @@ +use clap::Parser; + +use std::path::PathBuf; + +/// Automation tool for lazy people. +#[derive(Parser)] +#[command(author, version, about, long_about = None, arg_required_else_help(true))] +pub struct Args { + /// Suppress excess output. + #[arg(short, long, default_value_t = false)] + pub quiet: bool, + + /// The config file. + #[arg(short, long)] + pub config: Option, + + /// Print the config file before running. + #[arg(short, long, default_value_t = false)] + pub print: bool, + + /// Search and displacements. + #[arg(long)] + pub sd: Vec, + + /// Print the executed command before executing it. + #[arg(short = 'x', long, default_value_t = false)] + pub execution: bool, + + /// No color. + #[arg(long, default_value_t = false)] + pub no_color: bool, + + /// The command to run. + #[arg()] + pub command: Option, + + /// The extra arguments to pass to the command. + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + pub extra_args: Vec, +} diff --git a/src/cmd.rs b/src/cmd.rs index 0f19051..b7c57d3 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1,8 +1,10 @@ use colored::Colorize; use serde_yaml::Value; -use std::{process::Command, path::PathBuf}; +use std::{path::PathBuf, process::Command}; -use crate::config::{parse_cmd, resolve_chdir, FullCommand, ParsedConfig, CommandType, parse_config_from_file}; +use crate::config::{ + parse_cmd, parse_config_from_file, resolve_chdir, CommandType, FullCommand, ParsedConfig, +}; const FROM_RECURSION_LIMIT: u64 = 10; @@ -18,10 +20,16 @@ pub fn run_command_from_config( "Command {} not found in config", command_name ))?; - run_command(config, command_name, cmd, run_command_flags, extra_args, recursion_depth) + run_command( + config, + command_name, + cmd, + run_command_flags, + extra_args, + recursion_depth, + ) } - fn get_full_command_from_parsed_command(parsed_command: CommandType) -> Option { match parsed_command { CommandType::SimpleCommand(cmd) => Some(FullCommand { @@ -38,15 +46,14 @@ pub struct RunCommandFlags { pub sd: Vec, pub quiet: bool, pub execution: bool, - pub no_color: bool, } trait PrintExecution { - fn print_execution(&self, no_color: bool, extra_args: &[String]); + fn print_execution(&self, extra_args: &[String]); } impl PrintExecution for FullCommand { - fn print_execution(&self, no_color: bool, extra_args: &[String]) { + fn print_execution(&self, extra_args: &[String]) { let mut parsed_command = format!("$ {} {}", self.prog, self.args.join(" ")); if let Some(cmd) = &self.cmd { parsed_command.push_str(&format!(" '{}'", cmd)); @@ -54,9 +61,7 @@ impl PrintExecution for FullCommand { for arg in extra_args { parsed_command.push_str(&format!(" {}", arg)); } - if !no_color { - parsed_command = parsed_command.blue().bold().to_string(); - } + parsed_command = parsed_command.blue().bold().to_string(); println!("{}", parsed_command); } } @@ -73,7 +78,7 @@ fn run_command( return Err(anyhow::anyhow!( "Recursive command calls reached: {}", FROM_RECURSION_LIMIT - )) + )); } let command = parse_cmd(cmd)?; @@ -99,11 +104,16 @@ fn run_command( let from_path_buff = PathBuf::from(&from); let from_config = parse_config_from_file(from_path_buff.as_path())?; - run_command_from_config(&from_config, command_name, run_command_flags, extra_args, recursion_depth + 1)?; - return Ok(()) + run_command_from_config( + &from_config, + command_name, + run_command_flags, + extra_args, + recursion_depth + 1, + )?; + return Ok(()); } else { - return Err(anyhow::anyhow!("You must provide one of: a string representing a command, a fully qualified command, or a `from` field")) - + return Err(anyhow::anyhow!("You must provide one of: a string representing a command, a fully qualified command, or a `from` field")); } let FullCommand { @@ -127,7 +137,7 @@ fn run_command( } if run_command_flags.execution { - full_command.print_execution(run_command_flags.no_color, extra_args); + full_command.print_execution(extra_args); } let mut final_args = args.clone().to_vec(); diff --git a/src/completion.rs b/src/completion.rs new file mode 100644 index 0000000..41188ff --- /dev/null +++ b/src/completion.rs @@ -0,0 +1,144 @@ +#[cfg(feature = "completion")] +use clap_complete::{ + generate_to, + shells::{Bash, Elvish, Fish, PowerShell, Zsh}, +}; +#[cfg(feature = "templating")] +use handlebars::Handlebars; +#[cfg(feature = "templating")] +use serde_json::json; +#[cfg(feature = "templating")] +use std::env::temp_dir; +#[cfg(feature = "templating")] +use std::io::Error; + +#[cfg(feature = "templating")] +pub fn build_fish_completion( + cmd: &mut clap::Command, + release_dir: &str, + bin_name: &str, +) -> Result<(), Error> { + if let Some(outdir) = temp_dir().to_str() { + let path = generate_to(Fish, cmd, bin_name, outdir)?; + let template = match bin_name { + "ya" => Some("ya.fish.hbs"), + _ => None, + }; + template_completion(path, release_dir, template)? + } + Ok(()) +} + +#[cfg(feature = "templating")] +pub fn build_bash_completion( + cmd: &mut clap::Command, + release_dir: &str, + bin_name: &str, +) -> Result<(), Error> { + if let Some(outdir) = temp_dir().to_str() { + let path = generate_to(Bash, cmd, bin_name, outdir.clone())?; + template_completion(path, release_dir, None)? + } + Ok(()) +} + +#[cfg(feature = "templating")] +pub fn build_elvish_completion( + cmd: &mut clap::Command, + release_dir: &str, + bin_name: &str, +) -> Result<(), Error> { + if let Some(outdir) = temp_dir().to_str() { + let path = generate_to(Elvish, cmd, bin_name, outdir.clone())?; + template_completion(path, release_dir, None)? + } + Ok(()) +} + +#[cfg(feature = "templating")] +pub fn build_zsh_completion( + cmd: &mut clap::Command, + release_dir: &str, + bin_name: &str, +) -> Result<(), Error> { + if let Some(outdir) = temp_dir().to_str() { + let path = generate_to(Zsh, cmd, bin_name, outdir.clone())?; + template_completion(path, release_dir, None)? + } + Ok(()) +} + +#[cfg(feature = "templating")] +pub fn build_powershell_completion( + cmd: &mut clap::Command, + release_dir: &str, + bin_name: &str, +) -> Result<(), Error> { + if let Some(outdir) = temp_dir().to_str() { + let path = generate_to(PowerShell, cmd, bin_name, outdir.clone())?; + template_completion(path, release_dir, None)? + } + Ok(()) +} + +#[cfg(feature = "templating")] +pub fn template_completion( + generated_path: std::path::PathBuf, + outdir: &str, + template: Option<&str>, +) -> Result<(), Error> { + let tpl_str = match template { + Some("ya.fish.hbs") => include_str!("../completions/templates/ya.fish.hbs"), + Some(_) => include_str!("../completions/templates/generated.hbs"), + None => include_str!("../completions/templates/generated.hbs"), + }; + + let template = template.unwrap_or("generated.hbs"); + + let mut handlebars = Handlebars::new(); + match handlebars.register_template_string(template, tpl_str) { + Ok(_) => {} + Err(e) => { + println!("Error registering template: {}", e); + return Err(Error::new( + std::io::ErrorKind::Other, + "Error registering template", + )); + } + } + let generated_fish = std::fs::read_to_string(&generated_path).unwrap(); + + handlebars.register_escape_fn(handlebars::no_escape); + let version = clap::crate_version!(); + let data = json!({ "generated_completes": generated_fish, "version": version }); + let rendered = handlebars.render(template, &data).unwrap(); + if let Some(file_name) = generated_path.file_name() { + if let Some(file_name) = file_name.to_str() { + let out_path = format!("{}/{}", outdir, file_name); + match std::fs::write(&out_path, rendered) { + Ok(_) => {} + Err(e) => { + let error_msg = + format!("Error writing completion to file `{}`: {}", &out_path, e); + return Err(Error::new(std::io::ErrorKind::Other, error_msg)); + } + } + } + }; + Ok(()) +} + +#[cfg(feature = "gh-release")] +pub fn build_templated_completions( + cmd: &mut clap::Command, + release_dir: &str, +) -> Result<(), Error> { + for bin in &["ya", "yadayada"] { + build_fish_completion(cmd, release_dir, bin)?; + build_bash_completion(cmd, release_dir, bin)?; + build_elvish_completion(cmd, release_dir, bin)?; + build_zsh_completion(cmd, release_dir, bin)?; + build_powershell_completion(cmd, release_dir, bin)?; + } + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index e2d2111..c8137d6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -266,13 +266,11 @@ pub fn parse_cmd(cmd: &Value) -> anyhow::Result { } Ok(ParsedConfig { - parsed_command: CommandType::FullCommand( - FullCommand { - prog: prog.to_string(), - args, - cmd: cmd.map(|s| s.to_string()), - } - ), + parsed_command: CommandType::FullCommand(FullCommand { + prog: prog.to_string(), + args, + cmd: cmd.map(|s| s.to_string()), + }), pre_msg: pre_msg.map(|s| s.to_string()), post_msg: post_msg.map(|s| s.to_string()), pre_cmds, diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4b1e689 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod cli; +pub mod cmd; +pub mod completion; +pub mod config; +pub mod git; +pub mod validate; diff --git a/src/main.rs b/src/main.rs index 2b9e956..91bdd2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,57 +1,22 @@ -use clap::Parser; - -use std::path::PathBuf; - +mod cli; mod cmd; mod config; mod git; mod validate; +use clap::Parser; +use cli::Args; use cmd::{run_command_from_config, RunCommandFlags}; use config::{get_config_path, parse_config_from_file, print_config_from_file}; use validate::{validate_config_file, validate_sd}; -/// Automation tool for lazy people. -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None, arg_required_else_help(true))] -struct Args { - /// Suppress the output of `pre_msg` and `post_msg`. - #[arg(short, long, default_value_t = false)] - quiet: bool, - - /// The config file to use. - #[arg(short, long)] - config: Option, - - /// Print the config file before running the command. - #[arg(short, long, default_value_t = false)] - print: bool, - - /// Search and displacements to make in the command before running it. - /// Expects a key and value separated by an `=`. e.g. `--sd key=value` - #[arg(long)] - sd: Vec, - - /// Print the command that will be executed before executing it. - #[arg(short = 'x', long, default_value_t = false)] - execution: bool, - - /// Don't add color to anything printed to stdout by ya. - #[arg(long, default_value_t = false)] - no_color: bool, - - /// The command in the config to use. - #[arg()] - command: Option, - - /// The extra arguments to pass to the command - #[arg(allow_hyphen_values = true, trailing_var_arg = true)] - extra_args: Vec, -} - fn main() -> anyhow::Result<()> { let args = Args::parse(); + if args.no_color { + colored::control::set_override(false); + } + validate_sd(&args.sd)?; let config_path = get_config_path(&args.config)?; @@ -72,7 +37,6 @@ fn main() -> anyhow::Result<()> { sd: args.sd, quiet: args.quiet, execution: args.execution, - no_color: args.no_color, }, args.extra_args.as_slice(), 0,