Reducing hella boilerplate with these macros. Thanks Claude
This commit is contained in:
parent
10c59c5412
commit
e54cb62aa7
@ -254,3 +254,166 @@ pub fn plugin(input: TokenStream) -> TokenStream {
|
||||
let config = parse_macro_input!(input as PluginConfig);
|
||||
config.generate().into()
|
||||
}
|
||||
|
||||
/// LauncherListItem configuration parsed from the macro input
|
||||
struct LauncherEntryConfig {
|
||||
id: Expr,
|
||||
title: Expr,
|
||||
description: Option<Expr>,
|
||||
icon: Expr,
|
||||
execute: Expr,
|
||||
}
|
||||
|
||||
impl Parse for LauncherEntryConfig {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let mut id = None;
|
||||
let mut title = None;
|
||||
let mut description = None;
|
||||
let mut icon = None;
|
||||
let mut execute = None;
|
||||
|
||||
// Parse comma-separated key: value pairs
|
||||
while !input.is_empty() {
|
||||
if input.peek(Token![,]) {
|
||||
input.parse::<Token![,]>()?;
|
||||
}
|
||||
|
||||
if input.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let key: Ident = input.parse()?;
|
||||
input.parse::<Token![:]>()?;
|
||||
|
||||
match key.to_string().as_str() {
|
||||
"id" => {
|
||||
let expr: Expr = input.parse()?;
|
||||
id = Some(expr);
|
||||
}
|
||||
"title" => {
|
||||
let expr: Expr = input.parse()?;
|
||||
title = Some(expr);
|
||||
}
|
||||
"description" => {
|
||||
let expr: Expr = input.parse()?;
|
||||
description = Some(expr);
|
||||
}
|
||||
"icon" => {
|
||||
let expr: Expr = input.parse()?;
|
||||
icon = Some(expr);
|
||||
}
|
||||
"execute" => {
|
||||
let expr: Expr = input.parse()?;
|
||||
execute = Some(expr);
|
||||
}
|
||||
_ => {
|
||||
let key_str = key.to_string();
|
||||
return Err(syn::Error::new_spanned(
|
||||
key,
|
||||
format!("Unknown launcher entry configuration key: {}", key_str),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
let id = id.ok_or_else(|| {
|
||||
syn::Error::new(input.span(), "LauncherListItem must have an 'id' field")
|
||||
})?;
|
||||
let title = title.ok_or_else(|| {
|
||||
syn::Error::new(input.span(), "LauncherListItem must have a 'title' field")
|
||||
})?;
|
||||
let icon = icon.ok_or_else(|| {
|
||||
syn::Error::new(input.span(), "LauncherListItem must have an 'icon' field")
|
||||
})?;
|
||||
let execute = execute.ok_or_else(|| {
|
||||
syn::Error::new(input.span(), "LauncherListItem must have an 'execute' field")
|
||||
})?;
|
||||
|
||||
Ok(LauncherEntryConfig {
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
execute,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl LauncherEntryConfig {
|
||||
/// Generate the implementation of LauncherListItem trait methods
|
||||
fn generate(&self) -> proc_macro2::TokenStream {
|
||||
let id_expr = &self.id;
|
||||
let title_expr = &self.title;
|
||||
let icon_expr = &self.icon;
|
||||
let execute_expr = &self.execute;
|
||||
|
||||
// Generate description method
|
||||
let description_method = if let Some(ref desc_expr) = self.description {
|
||||
quote! {
|
||||
fn description(&self) -> Option<String> {
|
||||
#desc_expr
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
fn description(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
fn id(&self) -> String {
|
||||
#id_expr
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
#title_expr
|
||||
}
|
||||
|
||||
#description_method
|
||||
|
||||
fn icon(&self) -> String {
|
||||
#icon_expr
|
||||
}
|
||||
|
||||
fn execute(&self) -> Result<(), waycast_core::LaunchError> {
|
||||
#execute_expr
|
||||
}
|
||||
|
||||
// Stub implementations to satisfy rust-analyzer
|
||||
// These are only compiled when rust-analyzer is checking the code
|
||||
#[cfg(rust_analyzer)]
|
||||
fn id(&self) -> String { unimplemented!("Generated by launcher_entry! macro") }
|
||||
#[cfg(rust_analyzer)]
|
||||
fn title(&self) -> String { unimplemented!("Generated by launcher_entry! macro") }
|
||||
#[cfg(rust_analyzer)]
|
||||
fn description(&self) -> Option<String> { unimplemented!("Generated by launcher_entry! macro") }
|
||||
#[cfg(rust_analyzer)]
|
||||
fn icon(&self) -> String { unimplemented!("Generated by launcher_entry! macro") }
|
||||
#[cfg(rust_analyzer)]
|
||||
fn execute(&self) -> Result<(), waycast_core::LaunchError> { unimplemented!("Generated by launcher_entry! macro") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The launcher_entry! proc macro
|
||||
///
|
||||
/// Usage inside impl LauncherListItem block:
|
||||
/// ```rust
|
||||
/// impl LauncherListItem for FileEntry {
|
||||
/// launcher_entry! {
|
||||
/// id: self.path.to_string_lossy().to_string(),
|
||||
/// title: self.path.file_name().unwrap().to_string_lossy().to_string(),
|
||||
/// description: Some(self.path.to_string_lossy().to_string()),
|
||||
/// icon: "text-x-generic".to_string(),
|
||||
/// execute: { println!("Opening file"); Ok(()) }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro]
|
||||
pub fn launcher_entry(input: TokenStream) -> TokenStream {
|
||||
let config = parse_macro_input!(input as LauncherEntryConfig);
|
||||
config.generate().into()
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
use gio::{prelude::*, AppInfo, DesktopAppInfo, Icon};
|
||||
use waycast_core::{LaunchError, LauncherListItem, LauncherPlugin};
|
||||
use waycast_macros::plugin;
|
||||
use waycast_macros::{plugin, launcher_entry};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DesktopEntry {
|
||||
@ -11,36 +11,17 @@ pub struct DesktopEntry {
|
||||
}
|
||||
|
||||
impl LauncherListItem for DesktopEntry {
|
||||
fn id(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
return self.name.to_owned();
|
||||
}
|
||||
|
||||
fn description(&self) -> Option<String> {
|
||||
launcher_entry! {
|
||||
id: self.id.clone(),
|
||||
title: self.name.to_owned(),
|
||||
description: {
|
||||
if let Some(glib_string) = &self.description {
|
||||
return Some(glib_string.to_string().to_owned());
|
||||
Some(glib_string.to_string().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
fn execute(&self) -> Result<(), LaunchError> {
|
||||
if let Some(di) = DesktopAppInfo::new(&self.id) {
|
||||
let app: AppInfo = di.upcast();
|
||||
let ctx = gio::AppLaunchContext::new();
|
||||
if app.launch(&[], Some(&ctx)).ok().is_none() {
|
||||
return Err(LaunchError::CouldNotLaunch("App failed to launch".into()));
|
||||
};
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(LaunchError::CouldNotLaunch("Invalid .desktop entry".into()));
|
||||
}
|
||||
|
||||
fn icon(&self) -> String {
|
||||
},
|
||||
icon: {
|
||||
if let Some(icon) = &self.icon {
|
||||
if let Ok(ti) = icon.clone().downcast::<gio::ThemedIcon>() {
|
||||
// ThemedIcon may have multiple names, we take the first
|
||||
@ -55,8 +36,20 @@ impl LauncherListItem for DesktopEntry {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "application-x-executable".into();
|
||||
"application-x-executable".into()
|
||||
},
|
||||
execute: {
|
||||
if let Some(di) = DesktopAppInfo::new(&self.id) {
|
||||
let app: AppInfo = di.upcast();
|
||||
let ctx = gio::AppLaunchContext::new();
|
||||
if app.launch(&[], Some(&ctx)).ok().is_none() {
|
||||
return Err(LaunchError::CouldNotLaunch("App failed to launch".into()));
|
||||
};
|
||||
Ok(())
|
||||
} else {
|
||||
Err(LaunchError::CouldNotLaunch("Invalid .desktop entry".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
use waycast_macros::plugin;
|
||||
use waycast_macros::{plugin, launcher_entry};
|
||||
|
||||
use waycast_core::{LaunchError, LauncherListItem, LauncherPlugin};
|
||||
|
||||
@ -25,18 +25,21 @@ impl FileEntry {
|
||||
}
|
||||
|
||||
impl LauncherListItem for FileEntry {
|
||||
fn id(&self) -> String {
|
||||
self.path.to_string_lossy().to_string()
|
||||
launcher_entry! {
|
||||
id: self.path.to_string_lossy().to_string(),
|
||||
title: String::from(self.path.file_name().unwrap().to_string_lossy()),
|
||||
description: Some(self.path.to_string_lossy().to_string()),
|
||||
icon: {
|
||||
let (content_type, _) = gio::content_type_guess(Some(&self.path), None);
|
||||
let icon = gio::content_type_get_icon(&content_type);
|
||||
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
|
||||
if let Some(icon_name) = themed_icon.names().first() {
|
||||
return icon_name.to_string();
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
return String::from(self.path.file_name().unwrap().to_string_lossy());
|
||||
}
|
||||
fn description(&self) -> Option<String> {
|
||||
Some(self.path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
fn execute(&self) -> Result<(), LaunchError> {
|
||||
String::from("text-x-generic")
|
||||
},
|
||||
execute: {
|
||||
println!("Executing: {}", self.path.display());
|
||||
|
||||
// Use xdg-open directly since it works properly with music files
|
||||
@ -66,19 +69,6 @@ impl LauncherListItem for FileEntry {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn icon(&self) -> String {
|
||||
let (content_type, _) = gio::content_type_guess(Some(&self.path), None);
|
||||
|
||||
let icon = gio::content_type_get_icon(&content_type);
|
||||
|
||||
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
|
||||
if let Some(icon_name) = themed_icon.names().first() {
|
||||
return icon_name.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
String::from("text-x-generic")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
pub mod drun;
|
||||
pub mod file_search;
|
||||
|
||||
// Re-export the macro for external use
|
||||
pub use waycast_macros::plugin;
|
||||
// Re-export the macros for external use
|
||||
pub use waycast_macros::{plugin, launcher_entry};
|
||||
|
Loading…
x
Reference in New Issue
Block a user