crossterm-keybind-derive 0.3.4

A crossplatform terminal library for keybinds
Documentation
use proc_macro::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{Attribute, DeriveInput, Error, Fields, Ident, Meta, Result, Variant};

struct Event {
    name: Ident,
    attrs: Vec<Attribute>,
    default_keybindings: String,
}

impl Event {
    pub fn from_variant(
        Variant {
            attrs,
            ident,
            fields,
            ..
        }: Variant,
    ) -> Result<Event> {
        if fields != Fields::Unit {
            return Err(Error::new(
                ident.span(),
                "KeyBind derive only use for enum without fields",
            ));
        }
        let mut new_attrs = Vec::new();
        let mut default_keybindings = "[]".to_string();
        for attr in attrs.into_iter() {
            if attr.path().is_ident("keybindings") {
                let Meta::List(ref meta_list) = attr.meta else {
                    return Err(Error::new(
                        ident.span(),
                        "Keybindings is incorrect, for example correct format is #[keybings[\"Control+c\",\"Q\"]]",
                    ));
                };
                let default_keybindings_str = format!("{}", meta_list.to_token_stream())[11..]
                    .trim()
                    .to_string();

                #[cfg(feature = "check")]
                if let Err(e) = serde_json::from_str::<crossterm_keybind_core::KeyBindings>(
                    &default_keybindings_str,
                ) {
                    return Err(Error::new(
                        ident.span(),
                        format!("{} Keybinding check fail: {}", ident, e),
                    ));
                }

                default_keybindings = default_keybindings_str;
            } else {
                new_attrs.push(attr)
            }
        }
        Ok(Event {
            name: ident,
            attrs: new_attrs,
            default_keybindings,
        })
    }
}

pub(crate) struct Events {
    attrs: Vec<Attribute>,
    name: Ident,
    inner: Vec<Event>,
}

impl Events {
    /// Generate the token stream for the patch struct and it resulting implementations
    pub fn into_token_stream(self) -> Result<TokenStream> {
        use convert_case::{Case, Casing};

        let Events {
            name,
            inner,
            attrs: enum_attrs,
        } = self;
        let mut fields = Vec::new();
        let mut lowers = Vec::new();
        let mut uppers = Vec::new();
        let mut attrs = Vec::new();
        let mut defaults = Vec::new();

        for e in inner.into_iter() {
            let name = e.name.to_string();
            fields.push(syn::Ident::new(&name, Span::call_site().into()));
            lowers.push(syn::Ident::new(
                &name.from_case(Case::UpperCamel).to_case(Case::Snake),
                Span::call_site().into(),
            ));
            uppers.push(syn::Ident::new(
                &name.from_case(Case::UpperCamel).to_case(Case::Constant),
                Span::call_site().into(),
            ));
            attrs.push(e.attrs);
            let default_stream: proc_macro2::TokenStream = e.default_keybindings.parse().unwrap();
            defaults.push(default_stream);
        }

        #[cfg(feature = "safety")]
        let safety_check_para_impl = quote! {
            static BINDING_INIT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
        };
        #[cfg(not(feature = "safety"))]
        let safety_check_para_impl = quote! {};

        #[cfg(feature = "safety")]
        let safety_check_init_impl = quote! {
            if BINDING_INIT.load(std::sync::atomic::Ordering::Relaxed) {
                return Err(crossterm_keybind::Error::ConfigDoubleInitError);
            } else {
                BINDING_INIT.store(true, std::sync::atomic::Ordering::Release);
            }
        };
        #[cfg(not(feature = "safety"))]
        let safety_check_init_impl = quote! {};

        #[cfg(feature = "safety")]
        let safety_check_match_impl = quote! {
            if !BINDING_INIT.load(std::sync::atomic::Ordering::Acquire) {
                // NOTE 
                // You are using crossterm in an unexpected way, we prevent UB but not panic
                // in runtime, please run anyfunction after `init_and_load`
                // https://docs.rs/crossterm-keybind/latest/crossterm_keybind/trait.KeyBindTrait.html#tymethod.init_and_load
                crossterm_keybind::log::warn!("Keybindings are used without initialization, it will never match");
                return false;
            }
        };
        #[cfg(not(feature = "safety"))]
        let safety_check_match_impl = quote! {};

        #[cfg(feature = "safety")]
        let safety_check_dispatch_impl = quote! {
            if !BINDING_INIT.load(std::sync::atomic::Ordering::Acquire) {
                // NOTE 
                // You are using crossterm in an unexpected way, we prevent UB but not panic
                // in runtime, please run anyfunction after `init_and_load`
                // https://docs.rs/crossterm-keybind/latest/crossterm_keybind/trait.KeyBindTrait.html#tymethod.init_and_load
                crossterm_keybind::log::warn!("Keybindings are used without initialization, it will never match");
                return Vec::new();
            }
        };
        #[cfg(not(feature = "safety"))]
        let safety_check_dispatch_impl = quote! {};

        #[cfg(feature = "safety")]
        let safety_check_display_impl = quote! {
            if !BINDING_INIT.load(std::sync::atomic::Ordering::Acquire) {
                // NOTE 
                // You are using crossterm in an unexpected way, we prevent UB but not panic
                // in runtime, please run anyfunction after `init_and_load`
                // https://docs.rs/crossterm-keybind/latest/crossterm_keybind/trait.KeyBindTrait.html#tymethod.init_and_load
                crossterm_keybind::log::warn!("Keybindings are not initialized");
                return String::new();
            }
        };
        #[cfg(not(feature = "safety"))]
        let safety_check_display_impl = quote! {};

        Ok(quote! {
            use crossterm_keybind::toml_example;
            use crossterm_keybind::toml_example::TomlExample;
            use crossterm_keybind::struct_patch;
            use crossterm_keybind::struct_patch::Patch;
            use crossterm_keybind::toml;

            #(
                #enum_attrs
            )*
            #[derive(crossterm_keybind::struct_patch::Patch, crossterm_keybind::toml_example::TomlExample, serde::Deserialize)]
            #[patch(name = "KeyBinding")]
            #[patch(attribute(derive(serde::Deserialize)))]
            struct DefaultBinding {
                #(
                    #( #attrs )*
                    #[toml_example(default=#defaults)]
                    #lowers: crossterm_keybind::KeyBindings,
                )*

            }

            #safety_check_para_impl
            #(
                static mut #uppers: core::mem::MaybeUninit<crossterm_keybind::KeyBindings> = core::mem::MaybeUninit::uninit();
            )*

            impl crossterm_keybind::KeyBindTrait for #name {
                fn init_and_load(patch_path: Option<std::path::PathBuf>) -> Result<(), crossterm_keybind::Error>{
                    #safety_check_init_impl
                    let mut key_config: DefaultBinding =
                        toml::from_str(&DefaultBinding::toml_example()).map_err(|e|crossterm_keybind::Error::DefaultConfigError(e.to_string()))?;
                    if let Some(p) = patch_path {
                        let contents = std::fs::read_to_string(p).map_err(crossterm_keybind::Error::ReadConfigError)?;
                        let patch: KeyBinding =
                            toml::from_str(&contents).map_err(|e|crossterm_keybind::Error::LoadConfigError(e.to_string()))?;
                        key_config.apply(patch);
                    }

                    unsafe {
                        #(
                            #uppers = core::mem::MaybeUninit::new(key_config.#lowers);
                        )*
                    }

                    Ok(())
                }

                fn match_any(&self, key_event: &crossterm_keybind::event::KeyEvent) -> bool {
                    #safety_check_match_impl
                    use #name as E;
                    match self {
                        #(
                            E::#fields => unsafe { #uppers.assume_init_ref() }.match_any(key_event),
                        )*
                    }
                }

                fn toml_example() -> String {
                    DefaultBinding::toml_example()
                }

                fn to_toml_example<P: AsRef<std::path::Path>>(file_name: P) -> std::io::Result<()> {
                    DefaultBinding::to_toml_example(file_name)
                }

                fn key_bindings_display(&self) -> String {
                    #safety_check_display_impl
                    match self {
                        #(
                            #name::#fields => format!("{}", unsafe { #uppers.assume_init_ref() }),
                        )*
                    }
                }

                fn key_bindings_display_with_format(&self, f: &crossterm_keybind::DisplayFormat) -> String {
                    #safety_check_display_impl
                    match self {
                        #(
                            #name::#fields => {
                                match f {
                                    crossterm_keybind::DisplayFormat::Symbols => format!("{}", unsafe { #uppers.assume_init_ref() }),
                                    crossterm_keybind::DisplayFormat::Debug => format!("{:?}", unsafe { #uppers.assume_init_ref() }),
                                    _f => unsafe { #uppers.assume_init_ref() }.display(_f),
                                }
                            },
                        )*
                    }
                }

                fn dispatch(key_event: &crossterm_keybind::event::KeyEvent) -> Vec<Self> {
                    let mut output = Vec::new();
                    #safety_check_dispatch_impl
                    #(
                        if unsafe { #uppers.assume_init_ref() }.match_any(key_event) {
                            output.push(#name::#fields);
                        }
                    )*
                    output
                }
            }

        }.into())
    }
    /// Parse enum to Events
    pub fn from_ast(
        DeriveInput {
            ident, data, attrs, ..
        }: DeriveInput,
    ) -> Result<Events> {
        let syn::Data::Enum(syn::DataEnum { variants, .. }) = data else {
            return Err(syn::Error::new(
                ident.span(),
                "KeyBind derive only use for enum",
            ));
        };
        let mut inner = Vec::new();

        for v in variants.into_iter() {
            inner.push(Event::from_variant(v)?);
        }

        Ok(Events {
            name: ident,
            inner,
            attrs,
        })
    }
}