#![warn(missing_docs)]
#[cfg(feature = "listen")]
use std::fmt::Display;
use std::{collections::BTreeSet, sync::Mutex};
use convert_case::{Case, Casing};
use proc_macro::{Span, TokenStream};
use quote::{format_ident, quote, ToTokens};
use syn::{
parse_macro_input, punctuated::Punctuated, token::Comma, FnArg, Ident, ItemFn, ItemUse,
Lifetime, LifetimeParam, Pat, PathSegment, ReturnType, Signature, Type,
};
#[cfg(feature = "listen")]
use syn::ItemStruct;
#[cfg(feature = "listen")]
#[proc_macro_attribute]
pub fn emit_or_listen(_: TokenStream, stream: TokenStream) -> TokenStream {
let stream_struct = parse_macro_input!(stream as ItemStruct);
let stream = quote! {
#[cfg_attr(target_family = "wasm", tauri_interop::listen_to)]
#[cfg_attr(not(target_family = "wasm"), tauri_interop::emit)]
#stream_struct
};
TokenStream::from(stream.to_token_stream())
}
#[cfg(feature = "listen")]
fn get_event_name<S, F>(struct_name: &S, field_name: &F) -> String
where
S: Display,
F: Display,
{
format!("{struct_name}::{field_name}")
}
#[cfg(feature = "listen")]
#[proc_macro_attribute]
pub fn emit(_: TokenStream, stream: TokenStream) -> TokenStream {
let stream_struct = parse_macro_input!(stream as ItemStruct);
if stream_struct.fields.is_empty() {
panic!("No fields provided")
}
if stream_struct
.fields
.iter()
.any(|field| field.ident.is_none())
{
panic!("Tuple Structs aren't supported")
}
let name = format_ident!("{}Emit", stream_struct.ident);
let variants = stream_struct
.fields
.iter()
.map(|field| {
let field_ident = field.ident.as_ref().expect("handled before");
let variation = field_ident.to_string().to_case(Case::Pascal);
(field_ident, format_ident!("{variation}"), &field.ty)
})
.collect::<Vec<_>>();
let struct_ident = &stream_struct.ident;
let mut updaters = Vec::new();
let mapped_variants = variants
.iter()
.map(|(field_ident, variant_ident, ty)| {
let update = format_ident!("update_{}", field_ident);
updaters.push(quote!{
pub fn #update(&mut self, handle: &tauri::AppHandle, #field_ident: #ty) -> Result<(), tauri::Error> {
self.#field_ident = #field_ident;
self.emit(handle, #name::#variant_ident)
}
});
let event_name = get_event_name(struct_ident, field_ident);
quote! {
#name::#variant_ident => {
log::trace!(
"Emitted event [{}::{}]",
stringify!(#struct_ident),
stringify!(#variant_ident),
);
handle.emit_all(#event_name, self.#field_ident.clone())
}
}
})
.collect::<Vec<_>>();
let variants = variants
.into_iter()
.map(|(_, variation, _)| variation)
.collect::<Vec<_>>();
let stream = quote! {
#[derive(Debug, Clone)]
pub enum #name {
#( #variants ),*
}
#stream_struct
impl #struct_ident {
#( #updaters )*
#[must_use]
pub fn emit(&self, handle: &::tauri::AppHandle, field: #name) -> Result<(), tauri::Error> {
use tauri::Manager;
match field {
#( #mapped_variants ),*
}
}
}
};
TokenStream::from(stream.to_token_stream())
}
#[cfg(feature = "listen")]
#[proc_macro_attribute]
pub fn listen_to(_: TokenStream, stream: TokenStream) -> TokenStream {
let stream_struct = parse_macro_input!(stream as ItemStruct);
if stream_struct.fields.is_empty() {
panic!("No fields provided")
}
if stream_struct
.fields
.iter()
.any(|field| field.ident.is_none())
{
panic!("Tuple Structs aren't supported")
}
let struct_ident = &stream_struct.ident;
let mapped_variants = stream_struct
.fields
.iter()
.map(|field| {
let ty = &field.ty;
let field_ident = field
.ident
.as_ref()
.expect("handled before")
.clone();
let fn_ident = field_ident.to_string().to_case(Case::Snake).to_lowercase();
let event_name = get_event_name(struct_ident, &field_ident);
let leptos = cfg!(feature = "leptos").then(|| {
let use_fn_name = format_ident!("use_{fn_ident}");
quote! {
#[must_use = "If the returned handle is dropped, the contained closure goes out of scope and can't be called"]
pub fn #use_fn_name(initial_value: #ty) -> (::leptos::ReadSignal<#ty>, ::leptos::WriteSignal<#ty>) {
::tauri_interop::listen::ListenHandle::use_register(#event_name, initial_value)
}
}
});
let listen_to_fn_name = format_ident!("listen_to_{fn_ident}");
quote! {
#leptos
#[must_use = "If the returned handle is dropped, the contained closure goes out of scope and can't be called"]
pub async fn #listen_to_fn_name<'s>(callback: impl Fn(#ty) + 'static) -> ::tauri_interop::listen::ListenResult<'s> {
::tauri_interop::listen::ListenHandle::register(#event_name, callback).await
}
}
}).collect::<Vec<_>>();
let stream = quote! {
#stream_struct
impl #struct_ident {
#( #mapped_variants )*
}
};
TokenStream::from(stream.to_token_stream())
}
lazy_static::lazy_static! {
static ref HANDLER_LIST: Mutex<BTreeSet<String>> = Mutex::new(Default::default());
}
const ARGUMENT_LIFETIME: &str = "'arg_lifetime";
const TAURI_TYPES: [&str; 3] = ["State", "AppHandle", "Window"];
fn is_tauri_type(segment: &PathSegment) -> bool {
TAURI_TYPES.contains(&segment.ident.to_string().as_str())
}
fn is_result(segment: &PathSegment) -> bool {
segment.ident.to_string().as_str() == "Result"
}
#[derive(PartialEq)]
enum Invoke {
Empty,
AsyncEmpty,
Async,
AsyncResult,
}
#[proc_macro_attribute]
pub fn binding(_: TokenStream, stream: TokenStream) -> TokenStream {
let ItemFn { attrs, sig, .. } = parse_macro_input!(stream as ItemFn);
let Signature {
ident,
mut generics,
inputs,
variadic,
output,
asyncness,
..
} = sig;
let invoke_type = match &output {
ReturnType::Default => {
if asyncness.is_some() {
Invoke::AsyncEmpty
} else {
Invoke::Empty
}
}
ReturnType::Type(_, ty) => match ty.as_ref() {
Type::Path(path) if path.path.segments.iter().any(is_result) => Invoke::AsyncResult,
Type::Path(_) => Invoke::Async,
others => panic!("no support for '{}'", others.to_token_stream()),
},
};
let mut requires_lifetime_constrain = false;
let mut args_inputs: Punctuated<Ident, Comma> = Punctuated::new();
let wasm_inputs = inputs
.into_iter()
.filter_map(|mut fn_inputs| {
if let FnArg::Typed(ref mut typed) = fn_inputs {
match typed.ty.as_mut() {
Type::Path(path) if path.path.segments.iter().any(is_tauri_type) => {
return None
}
Type::Reference(reference) => {
reference.lifetime =
Some(Lifetime::new(ARGUMENT_LIFETIME, Span::call_site().into()));
requires_lifetime_constrain = true;
}
_ => {}
}
if let Pat::Ident(ident) = typed.pat.as_ref() {
args_inputs.push(ident.ident.clone());
return Some(fn_inputs);
}
}
None
})
.collect::<Punctuated<FnArg, Comma>>();
if requires_lifetime_constrain {
let lt = Lifetime::new(ARGUMENT_LIFETIME, Span::call_site().into());
generics
.params
.push(syn::GenericParam::Lifetime(LifetimeParam::new(lt)))
}
let async_ident = (invoke_type.ne(&Invoke::Empty)).then_some(format_ident!("async"));
let invoke = match invoke_type {
Invoke::Empty => quote!(::tauri_interop::bindings::invoke(stringify!(#ident), args);),
Invoke::Async | Invoke::AsyncEmpty => {
quote!(::tauri_interop::command::async_invoke(stringify!(#ident), args).await)
}
Invoke::AsyncResult => {
quote!(::tauri_interop::command::invoke_catch(stringify!(#ident), args).await)
}
};
let args_ident = format_ident!("{}Args", ident.to_string().to_case(Case::Pascal));
let stream = quote! {
#[derive(::serde::Serialize, ::serde::Deserialize)]
struct #args_ident #generics {
#wasm_inputs
}
#( #attrs )*
pub #async_ident fn #ident #generics (#wasm_inputs) #variadic #output
{
let args = #args_ident { #args_inputs };
let args = ::serde_wasm_bindgen::to_value(&args)
.expect("serialized arguments");
#invoke
}
};
TokenStream::from(stream.to_token_stream())
}
#[proc_macro_attribute]
pub fn command(_: TokenStream, stream: TokenStream) -> TokenStream {
let fn_item = syn::parse::<ItemFn>(stream).unwrap();
HANDLER_LIST
.lock()
.unwrap()
.insert(fn_item.sig.ident.to_string());
let command_macro = quote! {
#[cfg_attr(target_family = "wasm", tauri_interop::binding)]
#[cfg_attr(not(target_family = "wasm"), tauri::command(rename_all = "snake_case"))]
#fn_item
};
TokenStream::from(command_macro.to_token_stream())
}
#[proc_macro]
pub fn collect_commands(_: TokenStream) -> TokenStream {
let handler = HANDLER_LIST.lock().unwrap();
let handler = handler
.iter()
.map(|s| format_ident!("{s}"))
.collect::<Punctuated<Ident, Comma>>();
let stream = quote! {
#[cfg(not(target_family = "wasm"))]
pub fn get_handlers() -> impl Fn(tauri::Invoke) {
::tauri::generate_handler![ #handler ]
}
};
TokenStream::from(stream.to_token_stream())
}
#[proc_macro_attribute]
pub fn host_usage(_: TokenStream, stream: TokenStream) -> TokenStream {
let item_use = parse_macro_input!(stream as ItemUse);
let command_macro = quote! {
#[cfg(not(target_family = "wasm"))]
#item_use
};
TokenStream::from(command_macro.to_token_stream())
}
#[proc_macro_attribute]
pub fn wasm_usage(_: TokenStream, stream: TokenStream) -> TokenStream {
let item_use = parse_macro_input!(stream as ItemUse);
let command_macro = quote! {
#[cfg(target_family = "wasm")]
#item_use
};
TokenStream::from(command_macro.to_token_stream())
}