From 1a2de21bf6288549a66e499ab2952825211e32d2 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 15 Mar 2026 17:45:24 +0100 Subject: [PATCH] feat: implement OS bridge and enhance app launcher functionality --- Cargo.lock | 7 +- crates/k-launcher-kernel/src/lib.rs | 36 +++- crates/k-launcher-os-bridge/Cargo.toml | 2 + crates/k-launcher-os-bridge/src/lib.rs | 5 +- .../k-launcher-os-bridge/src/unix_launcher.rs | 118 ++++++++++++ crates/k-launcher-ui/src/app.rs | 23 ++- crates/k-launcher-ui/src/lib.rs | 7 +- crates/k-launcher/Cargo.toml | 1 + crates/k-launcher/src/main.rs | 10 +- crates/plugins/plugin-apps/Cargo.toml | 7 +- crates/plugins/plugin-apps/src/lib.rs | 174 ++---------------- crates/plugins/plugin-apps/src/linux.rs | 133 +++++++++++++ crates/plugins/plugin-apps/src/main.rs | 1 - crates/plugins/plugin-calc/src/lib.rs | 21 +-- crates/plugins/plugin-cmd/Cargo.toml | 1 - crates/plugins/plugin-cmd/src/lib.rs | 90 +-------- crates/plugins/plugin-files/src/lib.rs | 12 +- crates/plugins/plugin-files/src/platform.rs | 9 + 18 files changed, 363 insertions(+), 294 deletions(-) create mode 100644 crates/k-launcher-os-bridge/src/unix_launcher.rs create mode 100644 crates/plugins/plugin-apps/src/linux.rs delete mode 100644 crates/plugins/plugin-apps/src/main.rs create mode 100644 crates/plugins/plugin-files/src/platform.rs diff --git a/Cargo.lock b/Cargo.lock index 94b81e5..ecc5e2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1764,6 +1764,7 @@ version = "0.1.0" dependencies = [ "iced", "k-launcher-kernel", + "k-launcher-os-bridge", "k-launcher-ui", "plugin-apps", "plugin-calc", @@ -1785,6 +1786,10 @@ dependencies = [ [[package]] name = "k-launcher-os-bridge" version = "0.1.0" +dependencies = [ + "k-launcher-kernel", + "libc", +] [[package]] name = "k-launcher-ui" @@ -2738,7 +2743,6 @@ version = "0.1.0" dependencies = [ "async-trait", "k-launcher-kernel", - "libc", "serde", "serde_json", "tokio", @@ -2761,7 +2765,6 @@ version = "0.1.0" dependencies = [ "async-trait", "k-launcher-kernel", - "libc", "tokio", ] diff --git a/crates/k-launcher-kernel/src/lib.rs b/crates/k-launcher-kernel/src/lib.rs index 5cbb568..0b4432c 100644 --- a/crates/k-launcher-kernel/src/lib.rs +++ b/crates/k-launcher-kernel/src/lib.rs @@ -43,6 +43,22 @@ impl Score { } } +// --- LaunchAction (port) --- + +pub enum LaunchAction { + SpawnProcess(String), + SpawnInTerminal(String), + OpenPath(String), + CopyToClipboard(String), + Custom(Arc), +} + +// --- AppLauncher port trait --- + +pub trait AppLauncher: Send + Sync { + fn execute(&self, action: &LaunchAction); +} + // --- SearchResult --- pub struct SearchResult { @@ -51,7 +67,8 @@ pub struct SearchResult { pub description: Option, pub icon: Option, pub score: Score, - pub on_execute: Arc, + pub action: LaunchAction, + pub on_select: Option>, } impl std::fmt::Debug for SearchResult { @@ -73,6 +90,13 @@ pub trait Plugin: Send + Sync { async fn search(&self, query: &str) -> Vec; } +// --- SearchEngine port trait --- + +#[async_trait] +pub trait SearchEngine: Send + Sync { + async fn search(&self, query: &str) -> Vec; +} + // --- Kernel (Application use case) --- pub struct Kernel { @@ -94,6 +118,13 @@ impl Kernel { } } +#[async_trait] +impl SearchEngine for Kernel { + async fn search(&self, query: &str) -> Vec { + self.search(query).await + } +} + // --- Tests --- #[cfg(test)] @@ -126,7 +157,8 @@ mod tests { description: None, icon: None, score: Score::new(*score), - on_execute: Arc::new(|| {}), + action: LaunchAction::Custom(Arc::new(|| {})), + on_select: None, }) .collect() } diff --git a/crates/k-launcher-os-bridge/Cargo.toml b/crates/k-launcher-os-bridge/Cargo.toml index 397509c..6c82834 100644 --- a/crates/k-launcher-os-bridge/Cargo.toml +++ b/crates/k-launcher-os-bridge/Cargo.toml @@ -4,3 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] +k-launcher-kernel = { path = "../k-launcher-kernel" } +libc = "0.2" diff --git a/crates/k-launcher-os-bridge/src/lib.rs b/crates/k-launcher-os-bridge/src/lib.rs index ed4f09d..89bb1e1 100644 --- a/crates/k-launcher-os-bridge/src/lib.rs +++ b/crates/k-launcher-os-bridge/src/lib.rs @@ -1,4 +1,7 @@ -/// Configuration for the launcher window. +mod unix_launcher; + +pub use unix_launcher::UnixAppLauncher; + pub struct WindowConfig { pub width: f32, pub height: f32, diff --git a/crates/k-launcher-os-bridge/src/unix_launcher.rs b/crates/k-launcher-os-bridge/src/unix_launcher.rs new file mode 100644 index 0000000..98dc32f --- /dev/null +++ b/crates/k-launcher-os-bridge/src/unix_launcher.rs @@ -0,0 +1,118 @@ +use std::process::{Command, Stdio}; +use std::os::unix::process::CommandExt; + +use k_launcher_kernel::{AppLauncher, LaunchAction}; + +fn parse_term_cmd(s: &str) -> (String, Vec) { + let mut parts = s.split_whitespace(); + let bin = parts.next().unwrap_or("").to_string(); + let args = parts.map(str::to_string).collect(); + (bin, args) +} + +fn which(bin: &str) -> bool { + Command::new("which") + .arg(bin) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +fn resolve_terminal() -> Option<(String, Vec)> { + if let Ok(val) = std::env::var("TERM_CMD") { + let val = val.trim().to_string(); + if !val.is_empty() { + let (bin, args) = parse_term_cmd(&val); + if !bin.is_empty() { + return Some((bin, args)); + } + } + } + if let Ok(val) = std::env::var("TERMINAL") { + let bin = val.trim().to_string(); + if !bin.is_empty() { + return Some((bin, vec!["-e".to_string()])); + } + } + for (bin, flag) in &[ + ("foot", "-e"), + ("kitty", "-e"), + ("alacritty", "-e"), + ("wezterm", "start"), + ("konsole", "-e"), + ("xterm", "-e"), + ] { + if which(bin) { + return Some((bin.to_string(), vec![flag.to_string()])); + } + } + None +} + +pub struct UnixAppLauncher; + +impl UnixAppLauncher { + pub fn new() -> Self { + Self + } +} + +impl Default for UnixAppLauncher { + fn default() -> Self { + Self::new() + } +} + +impl AppLauncher for UnixAppLauncher { + fn execute(&self, action: &LaunchAction) { + match action { + LaunchAction::SpawnProcess(cmd) => { + let parts: Vec<&str> = cmd.split_whitespace().collect(); + if let Some((bin, args)) = parts.split_first() { + let _ = unsafe { + Command::new(bin) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .pre_exec(|| { libc::setsid(); Ok(()) }) + .spawn() + }; + } + } + LaunchAction::SpawnInTerminal(cmd) => { + let Some((term_bin, term_args)) = resolve_terminal() else { return }; + let _ = unsafe { + Command::new(&term_bin) + .args(&term_args) + .arg("sh").arg("-c").arg(cmd) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .pre_exec(|| { libc::setsid(); Ok(()) }) + .spawn() + }; + } + LaunchAction::OpenPath(path) => { + let _ = Command::new("xdg-open").arg(path).spawn(); + } + LaunchAction::CopyToClipboard(val) => { + if Command::new("wl-copy").arg(val).spawn().is_err() { + use std::io::Write; + if let Ok(mut child) = Command::new("xclip") + .args(["-selection", "clipboard"]) + .stdin(Stdio::piped()) + .spawn() + { + if let Some(stdin) = child.stdin.as_mut() { + let _ = stdin.write_all(val.as_bytes()); + } + } + } + } + LaunchAction::Custom(f) => f(), + } + } +} diff --git a/crates/k-launcher-ui/src/app.rs b/crates/k-launcher-ui/src/app.rs index 3076a6d..5a7bd2f 100644 --- a/crates/k-launcher-ui/src/app.rs +++ b/crates/k-launcher-ui/src/app.rs @@ -8,7 +8,7 @@ use iced::{ window, }; -use k_launcher_kernel::{Kernel, SearchResult}; +use k_launcher_kernel::{AppLauncher, SearchEngine, SearchResult}; use k_launcher_os_bridge::WindowConfig; use crate::theme; @@ -17,16 +17,18 @@ static INPUT_ID: std::sync::LazyLock = std::sync::LazyLock::new(|| iced::widget::Id::new("search")); pub struct KLauncherApp { - kernel: Arc, + engine: Arc, + launcher: Arc, query: String, results: Arc>, selected: usize, } impl KLauncherApp { - fn new(kernel: Arc) -> Self { + fn new(engine: Arc, launcher: Arc) -> Self { Self { - kernel, + engine, + launcher, query: String::new(), results: Arc::new(vec![]), selected: 0, @@ -46,9 +48,9 @@ fn update(state: &mut KLauncherApp, message: Message) -> Task { Message::QueryChanged(q) => { state.query = q.clone(); state.selected = 0; - let kernel = state.kernel.clone(); + let engine = state.engine.clone(); Task::perform( - async move { kernel.search(&q).await }, + async move { engine.search(&q).await }, |results| Message::ResultsReady(Arc::new(results)), ) } @@ -79,7 +81,10 @@ fn update(state: &mut KLauncherApp, message: Message) -> Task { } Named::Enter => { if let Some(result) = state.results.get(state.selected) { - (result.on_execute)(); + if let Some(on_select) = &result.on_select { + on_select(); + } + state.launcher.execute(&result.action); } std::process::exit(0); } @@ -192,11 +197,11 @@ fn subscription(_state: &KLauncherApp) -> Subscription { }) } -pub fn run(kernel: Arc) -> iced::Result { +pub fn run(engine: Arc, launcher: Arc) -> iced::Result { let wc = WindowConfig::launcher(); iced::application( move || { - let app = KLauncherApp::new(kernel.clone()); + let app = KLauncherApp::new(engine.clone(), launcher.clone()); let focus = iced::widget::operation::focus(INPUT_ID.clone()); (app, focus) }, diff --git a/crates/k-launcher-ui/src/lib.rs b/crates/k-launcher-ui/src/lib.rs index 528de40..aea8bbc 100644 --- a/crates/k-launcher-ui/src/lib.rs +++ b/crates/k-launcher-ui/src/lib.rs @@ -3,9 +3,8 @@ pub mod theme; use std::sync::Arc; -use k_launcher_kernel::Kernel; +use k_launcher_kernel::{AppLauncher, SearchEngine}; - -pub fn run(kernel: Arc) -> iced::Result { - app::run(kernel) +pub fn run(engine: Arc, launcher: Arc) -> iced::Result { + app::run(engine, launcher) } diff --git a/crates/k-launcher/Cargo.toml b/crates/k-launcher/Cargo.toml index 4c9c163..a1ea625 100644 --- a/crates/k-launcher/Cargo.toml +++ b/crates/k-launcher/Cargo.toml @@ -11,6 +11,7 @@ path = "src/main.rs" [dependencies] iced = { workspace = true } k-launcher-kernel = { path = "../k-launcher-kernel" } +k-launcher-os-bridge = { path = "../k-launcher-os-bridge" } k-launcher-ui = { path = "../k-launcher-ui" } plugin-apps = { path = "../plugins/plugin-apps" } plugin-calc = { path = "../plugins/plugin-calc" } diff --git a/crates/k-launcher/src/main.rs b/crates/k-launcher/src/main.rs index b0d3663..2c8379b 100644 --- a/crates/k-launcher/src/main.rs +++ b/crates/k-launcher/src/main.rs @@ -1,18 +1,22 @@ use std::sync::Arc; use k_launcher_kernel::Kernel; -use plugin_apps::{AppsPlugin, FsDesktopEntrySource, frecency::FrecencyStore}; +use k_launcher_os_bridge::UnixAppLauncher; +use plugin_apps::{AppsPlugin, frecency::FrecencyStore}; +#[cfg(target_os = "linux")] +use plugin_apps::linux::FsDesktopEntrySource; use plugin_calc::CalcPlugin; use plugin_cmd::CmdPlugin; use plugin_files::FilesPlugin; fn main() -> iced::Result { + let launcher = Arc::new(UnixAppLauncher::new()); let frecency = FrecencyStore::load(); - let kernel = Arc::new(Kernel::new(vec![ + let kernel: Arc = Arc::new(Kernel::new(vec![ Arc::new(CmdPlugin::new()), Arc::new(CalcPlugin::new()), Arc::new(FilesPlugin::new()), Arc::new(AppsPlugin::new(FsDesktopEntrySource::new(), frecency)), ])); - k_launcher_ui::run(kernel) + k_launcher_ui::run(kernel, launcher) } diff --git a/crates/plugins/plugin-apps/Cargo.toml b/crates/plugins/plugin-apps/Cargo.toml index 19fc5e7..aee1597 100644 --- a/crates/plugins/plugin-apps/Cargo.toml +++ b/crates/plugins/plugin-apps/Cargo.toml @@ -7,15 +7,12 @@ edition = "2024" name = "plugin_apps" path = "src/lib.rs" -[[bin]] -name = "plugin-apps" -path = "src/main.rs" - [dependencies] async-trait = { workspace = true } k-launcher-kernel = { path = "../../k-launcher-kernel" } -libc = "0.2" serde = { workspace = true } serde_json = "1.0" tokio = { workspace = true } + +[target.'cfg(target_os = "linux")'.dependencies] xdg = "2" diff --git a/crates/plugins/plugin-apps/src/lib.rs b/crates/plugins/plugin-apps/src/lib.rs index 16f6f89..294236e 100644 --- a/crates/plugins/plugin-apps/src/lib.rs +++ b/crates/plugins/plugin-apps/src/lib.rs @@ -1,10 +1,11 @@ pub mod frecency; +#[cfg(target_os = "linux")] +pub mod linux; -use std::{collections::HashMap, path::Path, process::{Command, Stdio}, sync::Arc}; -use std::os::unix::process::CommandExt; +use std::{collections::HashMap, sync::Arc}; use async_trait::async_trait; -use k_launcher_kernel::{Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult}; +use k_launcher_kernel::{LaunchAction, Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult}; use crate::frecency::FrecencyStore; @@ -71,7 +72,8 @@ struct CachedEntry { keywords_lc: Vec, category: Option, icon: Option, - on_execute: Arc, + exec: String, + on_select: Arc, } // --- Plugin --- @@ -90,27 +92,15 @@ impl AppsPlugin { let id = format!("app-{}", e.name.as_str()); let name_lc = e.name.as_str().to_lowercase(); let keywords_lc = e.keywords.iter().map(|k| k.to_lowercase()).collect(); - let icon = e.icon.as_ref().and_then(|p| resolve_icon_path(p.as_str())); - let exec = e.exec.clone(); + #[cfg(target_os = "linux")] + let icon = e.icon.as_ref().and_then(|p| linux::resolve_icon_path(p.as_str())); + #[cfg(not(target_os = "linux"))] + let icon: Option = None; + let exec = e.exec.as_str().to_string(); let store = Arc::clone(&frecency); let record_id = id.clone(); - let on_execute: Arc = Arc::new(move || { + let on_select: Arc = Arc::new(move || { store.record(&record_id); - let parts: Vec<&str> = exec.as_str().split_whitespace().collect(); - if let Some((cmd, args)) = parts.split_first() { - let _ = unsafe { - Command::new(cmd) - .args(args) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .pre_exec(|| { - libc::setsid(); - Ok(()) - }) - .spawn() - }; - } }); let cached = CachedEntry { id: id.clone(), @@ -118,7 +108,8 @@ impl AppsPlugin { keywords_lc, category: e.category, icon, - on_execute, + exec, + on_select, name: e.name, }; (id, cached) @@ -128,19 +119,6 @@ impl AppsPlugin { } } -fn resolve_icon_path(name: &str) -> Option { - if name.starts_with('/') && Path::new(name).exists() { - return Some(name.to_string()); - } - let candidates = [ - format!("/usr/share/pixmaps/{name}.png"), - format!("/usr/share/pixmaps/{name}.svg"), - format!("/usr/share/icons/hicolor/48x48/apps/{name}.png"), - format!("/usr/share/icons/hicolor/scalable/apps/{name}.svg"), - ]; - candidates.into_iter().find(|p| Path::new(p).exists()) -} - fn initials(name_lc: &str) -> String { name_lc.split_whitespace().filter_map(|w| w.chars().next()).collect() } @@ -153,7 +131,7 @@ fn score_match(name_lc: &str, query_lc: &str) -> Option { None } -fn humanize_category(s: &str) -> String { +pub(crate) fn humanize_category(s: &str) -> String { let mut result = String::new(); for ch in s.chars() { if ch.is_uppercase() && !result.is_empty() { @@ -183,7 +161,8 @@ impl Plugin for AppsPlugin { description: e.category.clone(), icon: e.icon.clone(), score: Score::new(score), - on_execute: Arc::clone(&e.on_execute), + action: LaunchAction::SpawnProcess(e.exec.clone()), + on_select: Some(Arc::clone(&e.on_select)), }) }) .collect(); @@ -202,129 +181,14 @@ impl Plugin for AppsPlugin { description: e.category.clone(), icon: e.icon.clone(), score: Score::new(score), - on_execute: Arc::clone(&e.on_execute), + action: LaunchAction::SpawnProcess(e.exec.clone()), + on_select: Some(Arc::clone(&e.on_select)), }) }) .collect() } } -// --- Filesystem source --- - -pub struct FsDesktopEntrySource; - -impl FsDesktopEntrySource { - pub fn new() -> Self { - Self - } -} - -impl Default for FsDesktopEntrySource { - fn default() -> Self { - Self::new() - } -} - -impl DesktopEntrySource for FsDesktopEntrySource { - fn entries(&self) -> Vec { - let mut dirs = Vec::new(); - if let Ok(xdg) = xdg::BaseDirectories::new() { - dirs.push(xdg.get_data_home().join("applications")); - for d in xdg.get_data_dirs() { - dirs.push(d.join("applications")); - } - } - let mut entries = Vec::new(); - for dir in &dirs { - if let Ok(read_dir) = std::fs::read_dir(dir) { - for entry in read_dir.flatten() { - let path = entry.path(); - if path.extension().and_then(|e| e.to_str()) != Some("desktop") { - continue; - } - if let Some(de) = parse_desktop_file(&path) { - entries.push(de); - } - } - } - } - entries - } -} - -fn parse_desktop_file(path: &Path) -> Option { - let content = std::fs::read_to_string(path).ok()?; - let mut in_section = false; - let mut name: Option = None; - let mut exec: Option = None; - let mut icon: Option = None; - let mut category: Option = None; - let mut keywords: Vec = Vec::new(); - let mut is_application = false; - let mut no_display = false; - - for line in content.lines() { - let line = line.trim(); - if line == "[Desktop Entry]" { - in_section = true; - continue; - } - if line.starts_with('[') { - in_section = false; - continue; - } - if !in_section || line.starts_with('#') || line.is_empty() { - continue; - } - if let Some((key, value)) = line.split_once('=') { - match key.trim() { - "Name" if name.is_none() => name = Some(value.trim().to_string()), - "Exec" if exec.is_none() => exec = Some(value.trim().to_string()), - "Icon" if icon.is_none() => icon = Some(value.trim().to_string()), - "Type" if !is_application => is_application = value.trim() == "Application", - "NoDisplay" => no_display = value.trim().eq_ignore_ascii_case("true"), - "Categories" if category.is_none() => { - category = value.trim() - .split(';') - .find(|s| !s.is_empty()) - .map(|s| humanize_category(s.trim())); - } - "Keywords" if keywords.is_empty() => { - keywords = value.trim() - .split(';') - .filter(|s| !s.is_empty()) - .map(|s| s.trim().to_string()) - .collect(); - } - _ => {} - } - } - } - - if !is_application || no_display { - return None; - } - - let exec_clean: String = exec? - .split_whitespace() - .filter(|s| !s.starts_with('%')) - .fold(String::new(), |mut acc, s| { - if !acc.is_empty() { - acc.push(' '); - } - acc.push_str(s); - acc - }); - - Some(DesktopEntry { - name: AppName::new(name?), - exec: ExecCommand::new(exec_clean), - icon: icon.map(IconPath::new), - category, - keywords, - }) -} - // --- Tests --- #[cfg(test)] diff --git a/crates/plugins/plugin-apps/src/linux.rs b/crates/plugins/plugin-apps/src/linux.rs new file mode 100644 index 0000000..55dfce4 --- /dev/null +++ b/crates/plugins/plugin-apps/src/linux.rs @@ -0,0 +1,133 @@ +#![cfg(target_os = "linux")] + +use std::path::Path; + +use crate::{AppName, DesktopEntry, DesktopEntrySource, ExecCommand, IconPath}; +use crate::humanize_category; + +pub struct FsDesktopEntrySource; + +impl FsDesktopEntrySource { + pub fn new() -> Self { + Self + } +} + +impl Default for FsDesktopEntrySource { + fn default() -> Self { + Self::new() + } +} + +impl DesktopEntrySource for FsDesktopEntrySource { + fn entries(&self) -> Vec { + let mut dirs = Vec::new(); + if let Ok(xdg) = xdg::BaseDirectories::new() { + dirs.push(xdg.get_data_home().join("applications")); + for d in xdg.get_data_dirs() { + dirs.push(d.join("applications")); + } + } + let mut entries = Vec::new(); + for dir in &dirs { + if let Ok(read_dir) = std::fs::read_dir(dir) { + for entry in read_dir.flatten() { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) != Some("desktop") { + continue; + } + if let Some(de) = parse_desktop_file(&path) { + entries.push(de); + } + } + } + } + entries + } +} + +pub fn resolve_icon_path(name: &str) -> Option { + if name.starts_with('/') && Path::new(name).exists() { + return Some(name.to_string()); + } + let candidates = [ + format!("/usr/share/pixmaps/{name}.png"), + format!("/usr/share/pixmaps/{name}.svg"), + format!("/usr/share/icons/hicolor/48x48/apps/{name}.png"), + format!("/usr/share/icons/hicolor/scalable/apps/{name}.svg"), + ]; + candidates.into_iter().find(|p| Path::new(p).exists()) +} + +fn parse_desktop_file(path: &Path) -> Option { + let content = std::fs::read_to_string(path).ok()?; + let mut in_section = false; + let mut name: Option = None; + let mut exec: Option = None; + let mut icon: Option = None; + let mut category: Option = None; + let mut keywords: Vec = Vec::new(); + let mut is_application = false; + let mut no_display = false; + + for line in content.lines() { + let line = line.trim(); + if line == "[Desktop Entry]" { + in_section = true; + continue; + } + if line.starts_with('[') { + in_section = false; + continue; + } + if !in_section || line.starts_with('#') || line.is_empty() { + continue; + } + if let Some((key, value)) = line.split_once('=') { + match key.trim() { + "Name" if name.is_none() => name = Some(value.trim().to_string()), + "Exec" if exec.is_none() => exec = Some(value.trim().to_string()), + "Icon" if icon.is_none() => icon = Some(value.trim().to_string()), + "Type" if !is_application => is_application = value.trim() == "Application", + "NoDisplay" => no_display = value.trim().eq_ignore_ascii_case("true"), + "Categories" if category.is_none() => { + category = value.trim() + .split(';') + .find(|s| !s.is_empty()) + .map(|s| humanize_category(s.trim())); + } + "Keywords" if keywords.is_empty() => { + keywords = value.trim() + .split(';') + .filter(|s| !s.is_empty()) + .map(|s| s.trim().to_string()) + .collect(); + } + _ => {} + } + } + } + + if !is_application || no_display { + return None; + } + + let exec_clean: String = exec? + .split_whitespace() + .filter(|s| !s.starts_with('%')) + .fold(String::new(), |mut acc, s| { + if !acc.is_empty() { + acc.push(' '); + } + acc.push_str(s); + acc + }); + + Some(DesktopEntry { + name: AppName::new(name?), + exec: ExecCommand::new(exec_clean), + icon: icon.map(IconPath::new), + category, + keywords, + }) +} diff --git a/crates/plugins/plugin-apps/src/main.rs b/crates/plugins/plugin-apps/src/main.rs deleted file mode 100644 index f328e4d..0000000 --- a/crates/plugins/plugin-apps/src/main.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/crates/plugins/plugin-calc/src/lib.rs b/crates/plugins/plugin-calc/src/lib.rs index efd8b87..8430107 100644 --- a/crates/plugins/plugin-calc/src/lib.rs +++ b/crates/plugins/plugin-calc/src/lib.rs @@ -1,7 +1,5 @@ -use std::sync::Arc; - use async_trait::async_trait; -use k_launcher_kernel::{Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult}; +use k_launcher_kernel::{LaunchAction, Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult}; pub struct CalcPlugin; @@ -46,27 +44,14 @@ impl Plugin for CalcPlugin { }; let display = format!("= {value_str}"); let expr_owned = expr.to_string(); - let clipboard_val = value_str; vec![SearchResult { id: ResultId::new("calc-result"), title: ResultTitle::new(display), description: Some(format!("{expr_owned} ยท Enter to copy")), icon: None, score: Score::new(90), - on_execute: Arc::new(move || { - if std::process::Command::new("wl-copy").arg(&clipboard_val).spawn().is_err() { - use std::io::Write; - if let Ok(mut child) = std::process::Command::new("xclip") - .args(["-selection", "clipboard"]) - .stdin(std::process::Stdio::piped()) - .spawn() - { - if let Some(stdin) = child.stdin.as_mut() { - let _ = stdin.write_all(clipboard_val.as_bytes()); - } - } - } - }), + action: LaunchAction::CopyToClipboard(value_str), + on_select: None, }] } _ => vec![], diff --git a/crates/plugins/plugin-cmd/Cargo.toml b/crates/plugins/plugin-cmd/Cargo.toml index 582aeee..fe3ead2 100644 --- a/crates/plugins/plugin-cmd/Cargo.toml +++ b/crates/plugins/plugin-cmd/Cargo.toml @@ -10,7 +10,6 @@ path = "src/lib.rs" [dependencies] async-trait = { workspace = true } k-launcher-kernel = { path = "../../k-launcher-kernel" } -libc = "0.2" [dev-dependencies] tokio = { workspace = true } diff --git a/crates/plugins/plugin-cmd/src/lib.rs b/crates/plugins/plugin-cmd/src/lib.rs index 3981759..8e2d286 100644 --- a/crates/plugins/plugin-cmd/src/lib.rs +++ b/crates/plugins/plugin-cmd/src/lib.rs @@ -1,56 +1,5 @@ -use std::{process::{Command, Stdio}, sync::Arc}; -use std::os::unix::process::CommandExt; - use async_trait::async_trait; -use k_launcher_kernel::{Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult}; - -fn parse_term_cmd(s: &str) -> (String, Vec) { - let mut parts = s.split_whitespace(); - let bin = parts.next().unwrap_or("").to_string(); - let args = parts.map(str::to_string).collect(); - (bin, args) -} - -fn which(bin: &str) -> bool { - Command::new("which") - .arg(bin) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .map(|s| s.success()) - .unwrap_or(false) -} - -fn resolve_terminal() -> Option<(String, Vec)> { - if let Ok(val) = std::env::var("TERM_CMD") { - let val = val.trim().to_string(); - if !val.is_empty() { - let (bin, args) = parse_term_cmd(&val); - if !bin.is_empty() { - return Some((bin, args)); - } - } - } - if let Ok(val) = std::env::var("TERMINAL") { - let bin = val.trim().to_string(); - if !bin.is_empty() { - return Some((bin, vec!["-e".to_string()])); - } - } - for (bin, flag) in &[ - ("foot", "-e"), - ("kitty", "-e"), - ("alacritty", "-e"), - ("wezterm", "start"), - ("konsole", "-e"), - ("xterm", "-e"), - ] { - if which(bin) { - return Some((bin.to_string(), vec![flag.to_string()])); - } - } - None -} +use k_launcher_kernel::{LaunchAction, Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult}; pub struct CmdPlugin; @@ -80,26 +29,14 @@ impl Plugin for CmdPlugin { if cmd.is_empty() { return vec![]; } - let cmd_owned = cmd.to_string(); vec![SearchResult { id: ResultId::new(format!("cmd-{cmd}")), title: ResultTitle::new(format!("Run: {cmd}")), description: None, icon: None, score: Score::new(95), - on_execute: Arc::new(move || { - let Some((term_bin, term_args)) = resolve_terminal() else { return }; - let _ = unsafe { - Command::new(&term_bin) - .args(&term_args) - .arg("sh").arg("-c").arg(&cmd_owned) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .pre_exec(|| { libc::setsid(); Ok(()) }) - .spawn() - }; - }), + action: LaunchAction::SpawnInTerminal(cmd.to_string()), + on_select: None, }] } } @@ -130,25 +67,4 @@ mod tests { assert!(p.search("echo hello").await.is_empty()); assert!(p.search("firefox").await.is_empty()); } - - #[test] - fn parse_term_cmd_single_flag() { - let (bin, args) = parse_term_cmd("foot -e"); - assert_eq!(bin, "foot"); - assert_eq!(args, vec!["-e"]); - } - - #[test] - fn parse_term_cmd_multiword() { - let (bin, args) = parse_term_cmd("wezterm start"); - assert_eq!(bin, "wezterm"); - assert_eq!(args, vec!["start"]); - } - - #[test] - fn parse_term_cmd_no_args() { - let (bin, args) = parse_term_cmd("xterm"); - assert_eq!(bin, "xterm"); - assert!(args.is_empty()); - } } diff --git a/crates/plugins/plugin-files/src/lib.rs b/crates/plugins/plugin-files/src/lib.rs index eca77f6..bbf93e8 100644 --- a/crates/plugins/plugin-files/src/lib.rs +++ b/crates/plugins/plugin-files/src/lib.rs @@ -1,8 +1,9 @@ +mod platform; + use std::path::Path; -use std::sync::Arc; use async_trait::async_trait; -use k_launcher_kernel::{Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult}; +use k_launcher_kernel::{LaunchAction, Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult}; pub struct FilesPlugin; @@ -20,7 +21,7 @@ impl Default for FilesPlugin { fn expand_query(query: &str) -> Option { if query.starts_with("~/") { - let home = std::env::var("HOME").ok()?; + let home = platform::home_dir()?; Some(format!("{}{}", home, &query[1..])) } else if query.starts_with('/') { Some(query.to_string()) @@ -87,9 +88,8 @@ impl Plugin for FilesPlugin { description: Some(path_str.clone()), icon: None, score: Score::new(50), - on_execute: Arc::new(move || { - let _ = std::process::Command::new("xdg-open").arg(&path_str).spawn(); - }), + action: LaunchAction::OpenPath(path_str), + on_select: None, } }) .collect() diff --git a/crates/plugins/plugin-files/src/platform.rs b/crates/plugins/plugin-files/src/platform.rs new file mode 100644 index 0000000..ffaecf1 --- /dev/null +++ b/crates/plugins/plugin-files/src/platform.rs @@ -0,0 +1,9 @@ +#[cfg(unix)] +pub fn home_dir() -> Option { + std::env::var("HOME").ok() +} + +#[cfg(windows)] +pub fn home_dir() -> Option { + std::env::var("USERPROFILE").ok() +}