From 1f569c461c8562bf55b2003ab5dcd7e396675dfb Mon Sep 17 00:00:00 2001 From: Javier Feliz Date: Wed, 3 Sep 2025 20:06:31 -0400 Subject: [PATCH] WIP --- .vscode/settings.json | 5 ++ src/lib.rs | 14 +++++ src/main.rs | 20 ++++++- src/plugins/drun.rs | 136 ++++++++++++++++++++++++++++++++++++++++++ src/plugins/mod.rs | 1 + 5 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/plugins/drun.rs create mode 100644 src/plugins/mod.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c156aeb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "search.exclude": { + "**/target": true + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 6cf3567..26ff83a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod drun; +pub mod plugins; pub mod util; pub enum LaunchError { CouldNotLaunch(String), @@ -10,3 +11,16 @@ pub trait LauncherListItem { fn execute(&self) -> Result<(), LaunchError>; fn icon(&self) -> String; } + +pub trait LauncherPlugin { + fn name() -> String; + fn priority() -> i32; + fn description() -> Option; + // Prefix to isolate results to only use this plugin + fn prefix() -> Option; + // Only search/use this plugin if the prefix was typed + fn by_prefix_only() -> bool; + // Actual item searching functions + fn default_list() -> Vec>; + fn filter(query: &str) -> Vec>; +} diff --git a/src/main.rs b/src/main.rs index 7fc1309..904c764 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,20 +3,21 @@ use gtk::gdk::Texture; use gtk::gdk_pixbuf::Pixbuf; use gtk::prelude::*; use gtk::{ - Application, ApplicationWindow, Box as GtkBox, Entry, IconTheme, Image, Label, ListBox, - Orientation, ScrolledWindow, + Application, ApplicationWindow, Box as GtkBox, Entry, EventControllerKey, IconTheme, Image, + Label, ListBox, Orientation, ScrolledWindow, }; use gtk4_layer_shell as layerShell; use layerShell::LayerShell; use std::cell::RefCell; use std::path::PathBuf; use std::rc::Rc; -use waycast::{LauncherListItem, drun}; +use waycast::{LauncherListItem, LauncherPlugin, drun}; struct AppModel { window: ApplicationWindow, list_box: ListBox, entries: Vec>, + plugins: Vec>, } struct ListItem { @@ -122,6 +123,19 @@ impl AppModel { model_clone.borrow().filter_list(&query); }); + // Add ESC key handler to close window + let key_controller = EventControllerKey::new(); + let window_clone = model.borrow().window.clone(); + key_controller.connect_key_pressed(move |_controller, keyval, _keycode, _state| { + if keyval == gtk::gdk::Key::Escape { + window_clone.close(); + gtk::glib::Propagation::Stop + } else { + gtk::glib::Propagation::Proceed + } + }); + model.borrow().window.add_controller(key_controller); + model } diff --git a/src/plugins/drun.rs b/src/plugins/drun.rs new file mode 100644 index 0000000..67c2584 --- /dev/null +++ b/src/plugins/drun.rs @@ -0,0 +1,136 @@ +use crate::LauncherPlugin; +use crate::{LaunchError, LauncherListItem}; +use gio::{AppInfo, DesktopAppInfo, Icon, prelude::*}; + +#[derive(Debug)] +pub struct DesktopEntry { + id: String, + name: String, + description: Option, + icon: Option, +} + +impl LauncherListItem for DesktopEntry { + fn title(&self) -> String { + return self.name.to_owned(); + } + + fn description(&self) -> Option { + 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::() { + // ThemedIcon may have multiple names, we take the first + if let Some(name) = ti.names().first() { + return name.to_string(); + } + } + + if let Ok(fi) = icon.clone().downcast::() { + if let Some(path) = fi.file().path() { + return path.to_string_lossy().to_string(); + } + } + } + + return "application-x-executable".into(); + } +} + +pub fn get_desktop_entries() -> Vec { + let mut entries = Vec::new(); + + for i in gio::AppInfo::all() { + let info: gio::DesktopAppInfo; + match i.downcast_ref::() { + Some(inf) => info = inf.to_owned(), + None => continue, + } + if !info.should_show() { + continue; + } + + let de = DesktopEntry { + id: info.id().unwrap_or_default().to_string(), + name: info.display_name().to_string(), + description: info.description(), + icon: info.icon(), + }; + + entries.push(de); + } + + entries +} + +struct DrunPlugin {} + +impl LauncherPlugin for DrunPlugin { + fn name() -> String { + return String::from("drun"); + } + + fn priority() -> i32 { + return 1000; + } + + fn description() -> Option { + return Some(String::from("List and launch an installed application")); + } + + // Prefix to isolate results to only use this plugin + fn prefix() -> Option { + return Some(String::from("app")); + } + // Only search/use this plugin if the prefix was typed + fn by_prefix_only() -> bool { + return false; + } + + // Actual item searching functions + fn default_list() -> Vec> { + let mut entries: Vec> = Vec::new(); + + for e in get_desktop_entries() { + entries.push(Box::new(e)); + } + + entries + } + + fn filter(query: &str) -> Vec> { + if query.is_empty() { + return DrunPlugin::default_list(); + } + + let query_lower = query.to_lowercase(); + let mut entries: Vec> = Vec::new(); + for entry in DrunPlugin::default_list() { + let title_lower = entry.title().to_lowercase(); + if title_lower.contains(&query_lower) { + entries.push(entry); + } + } + + entries + } +} diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs new file mode 100644 index 0000000..f7dc79b --- /dev/null +++ b/src/plugins/mod.rs @@ -0,0 +1 @@ +pub mod drun;