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);
|
let config = parse_macro_input!(input as PluginConfig);
|
||||||
config.generate().into()
|
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 gio::{prelude::*, AppInfo, DesktopAppInfo, Icon};
|
||||||
use waycast_core::{LaunchError, LauncherListItem, LauncherPlugin};
|
use waycast_core::{LaunchError, LauncherListItem, LauncherPlugin};
|
||||||
use waycast_macros::plugin;
|
use waycast_macros::{plugin, launcher_entry};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DesktopEntry {
|
pub struct DesktopEntry {
|
||||||
@ -11,52 +11,45 @@ pub struct DesktopEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LauncherListItem for DesktopEntry {
|
impl LauncherListItem for DesktopEntry {
|
||||||
fn id(&self) -> String {
|
launcher_entry! {
|
||||||
self.id.clone()
|
id: self.id.clone(),
|
||||||
}
|
title: self.name.to_owned(),
|
||||||
|
description: {
|
||||||
|
if let Some(glib_string) = &self.description {
|
||||||
|
Some(glib_string.to_string().to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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
|
||||||
|
if let Some(name) = ti.names().first() {
|
||||||
|
return name.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn title(&self) -> String {
|
if let Ok(fi) = icon.clone().downcast::<gio::FileIcon>() {
|
||||||
return self.name.to_owned();
|
if let Some(path) = fi.file().path() {
|
||||||
}
|
return path.to_string_lossy().to_string();
|
||||||
|
}
|
||||||
fn description(&self) -> Option<String> {
|
|
||||||
if let Some(glib_string) = &self.description {
|
|
||||||
return Some(glib_string.to_string().to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
if let Some(icon) = &self.icon {
|
|
||||||
if let Ok(ti) = icon.clone().downcast::<gio::ThemedIcon>() {
|
|
||||||
// ThemedIcon may have multiple names, we take the first
|
|
||||||
if let Some(name) = ti.names().first() {
|
|
||||||
return name.to_string();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"application-x-executable".into()
|
||||||
if let Ok(fi) = icon.clone().downcast::<gio::FileIcon>() {
|
},
|
||||||
if let Some(path) = fi.file().path() {
|
execute: {
|
||||||
return path.to_string_lossy().to_string();
|
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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "application-x-executable".into();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use walkdir::{DirEntry, WalkDir};
|
use walkdir::{DirEntry, WalkDir};
|
||||||
use waycast_macros::plugin;
|
use waycast_macros::{plugin, launcher_entry};
|
||||||
|
|
||||||
use waycast_core::{LaunchError, LauncherListItem, LauncherPlugin};
|
use waycast_core::{LaunchError, LauncherListItem, LauncherPlugin};
|
||||||
|
|
||||||
@ -25,61 +25,51 @@ impl FileEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LauncherListItem for FileEntry {
|
impl LauncherListItem for FileEntry {
|
||||||
fn id(&self) -> String {
|
launcher_entry! {
|
||||||
self.path.to_string_lossy().to_string()
|
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()),
|
||||||
fn title(&self) -> String {
|
icon: {
|
||||||
return String::from(self.path.file_name().unwrap().to_string_lossy());
|
let (content_type, _) = gio::content_type_guess(Some(&self.path), None);
|
||||||
}
|
let icon = gio::content_type_get_icon(&content_type);
|
||||||
fn description(&self) -> Option<String> {
|
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
|
||||||
Some(self.path.to_string_lossy().to_string())
|
if let Some(icon_name) = themed_icon.names().first() {
|
||||||
}
|
return icon_name.to_string();
|
||||||
|
}
|
||||||
fn execute(&self) -> Result<(), LaunchError> {
|
|
||||||
println!("Executing: {}", self.path.display());
|
|
||||||
|
|
||||||
// Use xdg-open directly since it works properly with music files
|
|
||||||
match Command::new("xdg-open").arg(&self.path).spawn() {
|
|
||||||
Ok(_) => {
|
|
||||||
println!("Successfully launched with xdg-open");
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
String::from("text-x-generic")
|
||||||
println!("xdg-open failed: {}", e);
|
},
|
||||||
// Fallback to GIO method
|
execute: {
|
||||||
let file_gio = gio::File::for_path(&self.path);
|
println!("Executing: {}", self.path.display());
|
||||||
let ctx = gio::AppLaunchContext::new();
|
|
||||||
match gio::AppInfo::launch_default_for_uri(file_gio.uri().as_str(), Some(&ctx)) {
|
// Use xdg-open directly since it works properly with music files
|
||||||
Ok(()) => {
|
match Command::new("xdg-open").arg(&self.path).spawn() {
|
||||||
println!("Successfully launched with GIO fallback");
|
Ok(_) => {
|
||||||
Ok(())
|
println!("Successfully launched with xdg-open");
|
||||||
}
|
Ok(())
|
||||||
Err(e2) => {
|
}
|
||||||
println!("GIO fallback also failed: {}", e2);
|
Err(e) => {
|
||||||
Err(LaunchError::CouldNotLaunch(format!(
|
println!("xdg-open failed: {}", e);
|
||||||
"Both xdg-open and GIO failed: {} / {}",
|
// Fallback to GIO method
|
||||||
e, e2
|
let file_gio = gio::File::for_path(&self.path);
|
||||||
)))
|
let ctx = gio::AppLaunchContext::new();
|
||||||
|
match gio::AppInfo::launch_default_for_uri(file_gio.uri().as_str(), Some(&ctx)) {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("Successfully launched with GIO fallback");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e2) => {
|
||||||
|
println!("GIO fallback also failed: {}", e2);
|
||||||
|
Err(LaunchError::CouldNotLaunch(format!(
|
||||||
|
"Both xdg-open and GIO failed: {} / {}",
|
||||||
|
e, e2
|
||||||
|
)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_search_list() -> Vec<PathBuf> {
|
pub fn default_search_list() -> Vec<PathBuf> {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
pub mod drun;
|
pub mod drun;
|
||||||
pub mod file_search;
|
pub mod file_search;
|
||||||
|
|
||||||
// Re-export the macro for external use
|
// Re-export the macros for external use
|
||||||
pub use waycast_macros::plugin;
|
pub use waycast_macros::{plugin, launcher_entry};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user