diff --git a/crates/derive/src/hooks.rs b/crates/derive/src/hooks.rs index 66c37b9e..274145f4 100644 --- a/crates/derive/src/hooks.rs +++ b/crates/derive/src/hooks.rs @@ -1,9 +1,36 @@ use proc_macro2::{Ident, Span, TokenStream}; use proc_macro_crate::{crate_name, FoundCrate}; use proc_macro_error::abort_call_site; -use quote::{quote, ToTokens}; +use quote::quote; use syn::DeriveInput; +pub struct Variant { + pub name: Ident, + pub command_name: String, + pub hook_name: String, +} + +impl Variant { + pub fn into_tuple(self) -> (Ident, String, String) { + (self.name, self.command_name, self.hook_name) + } + + pub fn unzip(variants: impl Iterator) -> (Vec, Vec, Vec) { + let mut names = Vec::new(); + let mut command_names = Vec::new(); + let mut hook_names = Vec::new(); + + for variant in variants { + let (name, command_name, hook_name) = variant.into_tuple(); + names.push(name); + command_names.push(command_name); + hook_names.push(hook_name); + } + + (names, command_names, hook_names) + } +} + pub fn hook_enum(input: DeriveInput) -> TokenStream { let struct_name = { let original_ident = &input.ident; @@ -18,23 +45,52 @@ pub fn hook_enum(input: DeriveInput) -> TokenStream { .variants .iter() .filter_map(|variant| { - if variant.attrs.iter().any(|attr| match attr.meta { + // TODO: Refactor this to use one attribute i.e #[hook(...)] + let attrs = &variant.attrs; + if attrs.iter().any(|attr| match attr.meta { syn::Meta::Path(ref p) => p.is_ident("no_hook"), _ => false, }) { None } else { - Some(variant.ident.to_token_stream()) + let mut variant = Variant { + name: variant.ident.clone(), + command_name: heck::AsKebabCase(variant.ident.to_string()).to_string(), + hook_name: heck::AsKebabCase(variant.ident.to_string()).to_string(), + }; + + for attr in attrs.iter() { + if let syn::Meta::NameValue(ref nv) = attr.meta { + if nv.path.is_ident("hook_name") { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(ref s), + .. + }) = nv.value + { + variant.hook_name = s.value(); + } + } + + if nv.path.is_ident("command_name") { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(ref s), + .. + }) = nv.value + { + variant.command_name = s.value(); + } + } + } + } + + Some(variant) } }) .collect::>(), _ => abort_call_site!("Can only be derived for enums"), }; - let command_names = variants - .iter() - .map(|variant| heck::AsKebabCase(variant.to_string()).to_string()) - .collect::>(); + let (variants, command_names, hook_names) = Variant::unzip(variants.into_iter()); let strum = match crate_name("strum").expect("strum is present in `Cargo.toml`") { FoundCrate::Itself => Ident::new("strum", Span::call_site()), @@ -43,16 +99,35 @@ pub fn hook_enum(input: DeriveInput) -> TokenStream { quote! { // TODO: Better way of doing this? or add support for meta in proc macro - #[derive(Debug, Clone, #strum::Display, #strum::IntoStaticStr, #strum::EnumIter, PartialEq, Eq)] - #[strum(serialize_all = "kebab-case")] + #[derive(Debug, Copy, Clone, #strum::EnumIter, PartialEq, Eq)] pub enum #struct_name { #(#variants),* } + impl #struct_name { + pub fn command<'a>(self) -> &'a str { + match self { + #(#struct_name::#variants => #command_names,)* + } + } + + pub fn hook<'a>(self) -> &'a str { + match self { + #(#struct_name::#variants => #hook_names,)* + } + } + } + + // impl std::fmt::Display for #struct_name { + // fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // write!(f, "{}", self.hook()) + // } + // } + impl From for #struct_name { fn from(string: String) -> Self { match string.as_str() { - #(#command_names => #struct_name::#variants,)* + #(stringify!(#hook_names) => #struct_name::#variants,)* _ => panic!("Invalid command name: {}", string), } } diff --git a/crates/derive/src/lib.rs b/crates/derive/src/lib.rs index 13234ef4..96bc91c0 100644 --- a/crates/derive/src/lib.rs +++ b/crates/derive/src/lib.rs @@ -12,7 +12,7 @@ pub fn derive_into_inner(input: TokenStream) -> TokenStream { inner::into_inner(parse_macro_input!(input as DeriveInput)).into() } -#[proc_macro_derive(Hooks, attributes(no_hook))] +#[proc_macro_derive(Hooks, attributes(no_hook, hook_name, command_name))] #[proc_macro_error] pub fn derive_hook_enum(input: TokenStream) -> TokenStream { hooks::hook_enum(parse_macro_input!(input as DeriveInput)).into() diff --git a/crates/derive/tests/helpers.rs b/crates/derive/tests/helpers.rs deleted file mode 100644 index d84a320d..00000000 --- a/crates/derive/tests/helpers.rs +++ /dev/null @@ -1,7 +0,0 @@ -use std::fmt::Display; - -use strum::IntoEnumIterator; - -pub fn enum_to_string() -> String { - T::iter().map(|v| v.to_string()).collect::() -} diff --git a/crates/derive/tests/raw_enum.rs b/crates/derive/tests/raw_enum.rs index 35bc63b8..1e25f1f6 100644 --- a/crates/derive/tests/raw_enum.rs +++ b/crates/derive/tests/raw_enum.rs @@ -1,10 +1,8 @@ #![allow(dead_code)] -mod helpers; - use sfsu_derive::Hooks; -use helpers::enum_to_string; +use strum::IntoEnumIterator; struct DummyStruct; @@ -16,7 +14,9 @@ enum EnumWithData { #[test] fn has_all_variants() { - let variants = enum_to_string::(); + let variants = EnumWithDataHooks::iter() + .map(|v| v.hook()) + .collect::(); assert_eq!(variants, "test1test2"); } @@ -31,7 +31,9 @@ enum EnumExclude { #[test] fn excludes_no_hook_variant() { - let variants = enum_to_string::(); + let variants = EnumExcludeHooks::iter() + .map(|v| v.hook()) + .collect::(); assert_eq!(variants, "test1test3"); } diff --git a/crates/sprinkles/src/lib.rs b/crates/sprinkles/src/lib.rs index a1acea59..0d1b3db5 100644 --- a/crates/sprinkles/src/lib.rs +++ b/crates/sprinkles/src/lib.rs @@ -17,6 +17,7 @@ pub mod git; pub mod output; pub mod packages; pub mod progress; +pub mod shell; mod opt; diff --git a/crates/sprinkles/src/shell.rs b/crates/sprinkles/src/shell.rs new file mode 100644 index 00000000..2f4ff6ea --- /dev/null +++ b/crates/sprinkles/src/shell.rs @@ -0,0 +1,45 @@ +use clap::ValueEnum; +use strum::Display; + +#[derive(Debug, Default, ValueEnum, Copy, Clone, Display, PartialEq, Eq)] +#[strum(serialize_all = "snake_case")] +pub enum Shell { + #[default] + Powershell, + Bash, + Zsh, + Nu, +} + +impl Shell { + #[must_use] + pub fn config(self) -> ShellConfig { + ShellConfig::new(self) + } +} + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +pub struct ShellConfig(Shell); + +impl ShellConfig { + #[must_use] + pub fn new(shell: Shell) -> Self { + Self(shell) + } + + #[must_use] + pub fn path(self) -> &'static str { + match self.0 { + Shell::Powershell => "$PROFILE", + Shell::Bash => "bashrc", + Shell::Zsh => "zshrc", + Shell::Nu => "$nu.config-path", + } + } +} + +impl std::fmt::Display for ShellConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.path()) + } +} diff --git a/src/commands/hook.rs b/src/commands/hook.rs index ea61d6a9..17298bb8 100644 --- a/src/commands/hook.rs +++ b/src/commands/hook.rs @@ -1,16 +1,6 @@ -use clap::{Parser, ValueEnum}; -use itertools::Itertools; -use strum::{Display, IntoEnumIterator}; - -#[derive(Debug, Default, ValueEnum, Copy, Clone, Display, PartialEq, Eq)] -#[strum(serialize_all = "snake_case")] -enum Shell { - #[default] - Powershell, - Bash, - Zsh, - Nu, -} +use clap::Parser; +use sprinkles::shell::Shell; +use strum::IntoEnumIterator; #[derive(Debug, Clone, Parser)] pub struct Args { @@ -24,6 +14,7 @@ pub struct Args { impl super::Command for Args { fn runner(self) -> Result<(), anyhow::Error> { let shell = self.shell; + let shell_config = shell.config(); let enabled_hooks: Vec = super::CommandsHooks::iter() .filter(|variant| !self.disable.contains(variant)) .collect(); @@ -34,7 +25,11 @@ impl super::Command for Args { // I would love to make this all one condition, but Powershell doesn't seem to support that elegantly for command in enabled_hooks { - print!(" '{command}' {{ return sfsu.exe $args }} "); + print!( + " '{hook}' {{ return sfsu.exe {command} }} ", + hook = command.hook(), + command = command.command() + ); } println!("default {{ scoop.ps1 @args }} }} }}"); @@ -44,35 +39,40 @@ impl super::Command for Args { // println!("# Invoke-Expression (&sfsu hook)"); } Shell::Bash | Shell::Zsh => { - let hook_list = enabled_hooks.iter().format(" | "); - println!( "SCOOP_EXEC=$(which scoop) \n\ scoop () {{ \n\ - case $1 in \n\ - ({hook_list}) sfsu.exe $@ ;; \n\ - (*) $SCOOP_EXEC $@ ;; \n\ + case $1 in" + ); + + for command in enabled_hooks { + println!( + "({hook}) sfsu.exe {command} ${{@:2}} ;;", + hook = command.hook(), + command = command.command() + ); + } + + println!( + "(*) $SCOOP_EXEC $@ ;; \n\ esac \n\ }} \n\n\ - # Add the following to the end of your ~/.{} \n\ - # source <(sfsu.exe hook --shell {shell})", - if shell == Shell::Bash { - "bashrc" - } else { - "zshrc" - } + # Add the following to the end of your ~/.{shell_config} \n\ + # source <(sfsu.exe hook --shell {shell})" ); } Shell::Nu => { for command in enabled_hooks { println!( - "def --wrapped \"scoop {command}\" [...rest] {{ sfsu {command} ...$rest }}" + "def --wrapped \"scoop {hook}\" [...rest] {{ sfsu {command} ...$rest }}", + hook = command.hook(), + command = command.command() ); } println!( "\n# To add this to your config, run `sfsu hook --shell {shell} | save ~/.cache/sfsu.nu`\n\ - # And then in your main config add the following line to the end:\n\ + # And then in your {shell_config} add the following line to the end:\n\ # source ~/.cache/sfsu.nu" ); }