feat: implement OS bridge and enhance app launcher functionality
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -1764,6 +1764,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"iced",
|
"iced",
|
||||||
"k-launcher-kernel",
|
"k-launcher-kernel",
|
||||||
|
"k-launcher-os-bridge",
|
||||||
"k-launcher-ui",
|
"k-launcher-ui",
|
||||||
"plugin-apps",
|
"plugin-apps",
|
||||||
"plugin-calc",
|
"plugin-calc",
|
||||||
@@ -1785,6 +1786,10 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "k-launcher-os-bridge"
|
name = "k-launcher-os-bridge"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"k-launcher-kernel",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "k-launcher-ui"
|
name = "k-launcher-ui"
|
||||||
@@ -2738,7 +2743,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"k-launcher-kernel",
|
"k-launcher-kernel",
|
||||||
"libc",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2761,7 +2765,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"k-launcher-kernel",
|
"k-launcher-kernel",
|
||||||
"libc",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,22 @@ impl Score {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- LaunchAction (port) ---
|
||||||
|
|
||||||
|
pub enum LaunchAction {
|
||||||
|
SpawnProcess(String),
|
||||||
|
SpawnInTerminal(String),
|
||||||
|
OpenPath(String),
|
||||||
|
CopyToClipboard(String),
|
||||||
|
Custom(Arc<dyn Fn() + Send + Sync>),
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AppLauncher port trait ---
|
||||||
|
|
||||||
|
pub trait AppLauncher: Send + Sync {
|
||||||
|
fn execute(&self, action: &LaunchAction);
|
||||||
|
}
|
||||||
|
|
||||||
// --- SearchResult ---
|
// --- SearchResult ---
|
||||||
|
|
||||||
pub struct SearchResult {
|
pub struct SearchResult {
|
||||||
@@ -51,7 +67,8 @@ pub struct SearchResult {
|
|||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
pub score: Score,
|
pub score: Score,
|
||||||
pub on_execute: Arc<dyn Fn() + Send + Sync>,
|
pub action: LaunchAction,
|
||||||
|
pub on_select: Option<Arc<dyn Fn() + Send + Sync>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for SearchResult {
|
impl std::fmt::Debug for SearchResult {
|
||||||
@@ -73,6 +90,13 @@ pub trait Plugin: Send + Sync {
|
|||||||
async fn search(&self, query: &str) -> Vec<SearchResult>;
|
async fn search(&self, query: &str) -> Vec<SearchResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- SearchEngine port trait ---
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait SearchEngine: Send + Sync {
|
||||||
|
async fn search(&self, query: &str) -> Vec<SearchResult>;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Kernel (Application use case) ---
|
// --- Kernel (Application use case) ---
|
||||||
|
|
||||||
pub struct Kernel {
|
pub struct Kernel {
|
||||||
@@ -94,6 +118,13 @@ impl Kernel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SearchEngine for Kernel {
|
||||||
|
async fn search(&self, query: &str) -> Vec<SearchResult> {
|
||||||
|
self.search(query).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Tests ---
|
// --- Tests ---
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -126,7 +157,8 @@ mod tests {
|
|||||||
description: None,
|
description: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
score: Score::new(*score),
|
score: Score::new(*score),
|
||||||
on_execute: Arc::new(|| {}),
|
action: LaunchAction::Custom(Arc::new(|| {})),
|
||||||
|
on_select: None,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
k-launcher-kernel = { path = "../k-launcher-kernel" }
|
||||||
|
libc = "0.2"
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
/// Configuration for the launcher window.
|
mod unix_launcher;
|
||||||
|
|
||||||
|
pub use unix_launcher::UnixAppLauncher;
|
||||||
|
|
||||||
pub struct WindowConfig {
|
pub struct WindowConfig {
|
||||||
pub width: f32,
|
pub width: f32,
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
|
|||||||
118
crates/k-launcher-os-bridge/src/unix_launcher.rs
Normal file
118
crates/k-launcher-os-bridge/src/unix_launcher.rs
Normal file
@@ -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<String>) {
|
||||||
|
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<String>)> {
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ use iced::{
|
|||||||
window,
|
window,
|
||||||
};
|
};
|
||||||
|
|
||||||
use k_launcher_kernel::{Kernel, SearchResult};
|
use k_launcher_kernel::{AppLauncher, SearchEngine, SearchResult};
|
||||||
use k_launcher_os_bridge::WindowConfig;
|
use k_launcher_os_bridge::WindowConfig;
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
@@ -17,16 +17,18 @@ static INPUT_ID: std::sync::LazyLock<iced::widget::Id> =
|
|||||||
std::sync::LazyLock::new(|| iced::widget::Id::new("search"));
|
std::sync::LazyLock::new(|| iced::widget::Id::new("search"));
|
||||||
|
|
||||||
pub struct KLauncherApp {
|
pub struct KLauncherApp {
|
||||||
kernel: Arc<Kernel>,
|
engine: Arc<dyn SearchEngine>,
|
||||||
|
launcher: Arc<dyn AppLauncher>,
|
||||||
query: String,
|
query: String,
|
||||||
results: Arc<Vec<SearchResult>>,
|
results: Arc<Vec<SearchResult>>,
|
||||||
selected: usize,
|
selected: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KLauncherApp {
|
impl KLauncherApp {
|
||||||
fn new(kernel: Arc<Kernel>) -> Self {
|
fn new(engine: Arc<dyn SearchEngine>, launcher: Arc<dyn AppLauncher>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
kernel,
|
engine,
|
||||||
|
launcher,
|
||||||
query: String::new(),
|
query: String::new(),
|
||||||
results: Arc::new(vec![]),
|
results: Arc::new(vec![]),
|
||||||
selected: 0,
|
selected: 0,
|
||||||
@@ -46,9 +48,9 @@ fn update(state: &mut KLauncherApp, message: Message) -> Task<Message> {
|
|||||||
Message::QueryChanged(q) => {
|
Message::QueryChanged(q) => {
|
||||||
state.query = q.clone();
|
state.query = q.clone();
|
||||||
state.selected = 0;
|
state.selected = 0;
|
||||||
let kernel = state.kernel.clone();
|
let engine = state.engine.clone();
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move { kernel.search(&q).await },
|
async move { engine.search(&q).await },
|
||||||
|results| Message::ResultsReady(Arc::new(results)),
|
|results| Message::ResultsReady(Arc::new(results)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -79,7 +81,10 @@ fn update(state: &mut KLauncherApp, message: Message) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
Named::Enter => {
|
Named::Enter => {
|
||||||
if let Some(result) = state.results.get(state.selected) {
|
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);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
@@ -192,11 +197,11 @@ fn subscription(_state: &KLauncherApp) -> Subscription<Message> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(kernel: Arc<Kernel>) -> iced::Result {
|
pub fn run(engine: Arc<dyn SearchEngine>, launcher: Arc<dyn AppLauncher>) -> iced::Result {
|
||||||
let wc = WindowConfig::launcher();
|
let wc = WindowConfig::launcher();
|
||||||
iced::application(
|
iced::application(
|
||||||
move || {
|
move || {
|
||||||
let app = KLauncherApp::new(kernel.clone());
|
let app = KLauncherApp::new(engine.clone(), launcher.clone());
|
||||||
let focus = iced::widget::operation::focus(INPUT_ID.clone());
|
let focus = iced::widget::operation::focus(INPUT_ID.clone());
|
||||||
(app, focus)
|
(app, focus)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ pub mod theme;
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use k_launcher_kernel::Kernel;
|
use k_launcher_kernel::{AppLauncher, SearchEngine};
|
||||||
|
|
||||||
|
pub fn run(engine: Arc<dyn SearchEngine>, launcher: Arc<dyn AppLauncher>) -> iced::Result {
|
||||||
pub fn run(kernel: Arc<Kernel>) -> iced::Result {
|
app::run(engine, launcher)
|
||||||
app::run(kernel)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
iced = { workspace = true }
|
iced = { workspace = true }
|
||||||
k-launcher-kernel = { path = "../k-launcher-kernel" }
|
k-launcher-kernel = { path = "../k-launcher-kernel" }
|
||||||
|
k-launcher-os-bridge = { path = "../k-launcher-os-bridge" }
|
||||||
k-launcher-ui = { path = "../k-launcher-ui" }
|
k-launcher-ui = { path = "../k-launcher-ui" }
|
||||||
plugin-apps = { path = "../plugins/plugin-apps" }
|
plugin-apps = { path = "../plugins/plugin-apps" }
|
||||||
plugin-calc = { path = "../plugins/plugin-calc" }
|
plugin-calc = { path = "../plugins/plugin-calc" }
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use k_launcher_kernel::Kernel;
|
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_calc::CalcPlugin;
|
||||||
use plugin_cmd::CmdPlugin;
|
use plugin_cmd::CmdPlugin;
|
||||||
use plugin_files::FilesPlugin;
|
use plugin_files::FilesPlugin;
|
||||||
|
|
||||||
fn main() -> iced::Result {
|
fn main() -> iced::Result {
|
||||||
|
let launcher = Arc::new(UnixAppLauncher::new());
|
||||||
let frecency = FrecencyStore::load();
|
let frecency = FrecencyStore::load();
|
||||||
let kernel = Arc::new(Kernel::new(vec![
|
let kernel: Arc<dyn k_launcher_kernel::SearchEngine> = Arc::new(Kernel::new(vec![
|
||||||
Arc::new(CmdPlugin::new()),
|
Arc::new(CmdPlugin::new()),
|
||||||
Arc::new(CalcPlugin::new()),
|
Arc::new(CalcPlugin::new()),
|
||||||
Arc::new(FilesPlugin::new()),
|
Arc::new(FilesPlugin::new()),
|
||||||
Arc::new(AppsPlugin::new(FsDesktopEntrySource::new(), frecency)),
|
Arc::new(AppsPlugin::new(FsDesktopEntrySource::new(), frecency)),
|
||||||
]));
|
]));
|
||||||
k_launcher_ui::run(kernel)
|
k_launcher_ui::run(kernel, launcher)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,12 @@ edition = "2024"
|
|||||||
name = "plugin_apps"
|
name = "plugin_apps"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "plugin-apps"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
k-launcher-kernel = { path = "../../k-launcher-kernel" }
|
k-launcher-kernel = { path = "../../k-launcher-kernel" }
|
||||||
libc = "0.2"
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
xdg = "2"
|
xdg = "2"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
pub mod frecency;
|
pub mod frecency;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub mod linux;
|
||||||
|
|
||||||
use std::{collections::HashMap, path::Path, process::{Command, Stdio}, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use std::os::unix::process::CommandExt;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
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;
|
use crate::frecency::FrecencyStore;
|
||||||
|
|
||||||
@@ -71,7 +72,8 @@ struct CachedEntry {
|
|||||||
keywords_lc: Vec<String>,
|
keywords_lc: Vec<String>,
|
||||||
category: Option<String>,
|
category: Option<String>,
|
||||||
icon: Option<String>,
|
icon: Option<String>,
|
||||||
on_execute: Arc<dyn Fn() + Send + Sync>,
|
exec: String,
|
||||||
|
on_select: Arc<dyn Fn() + Send + Sync>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Plugin ---
|
// --- Plugin ---
|
||||||
@@ -90,27 +92,15 @@ impl AppsPlugin {
|
|||||||
let id = format!("app-{}", e.name.as_str());
|
let id = format!("app-{}", e.name.as_str());
|
||||||
let name_lc = e.name.as_str().to_lowercase();
|
let name_lc = e.name.as_str().to_lowercase();
|
||||||
let keywords_lc = e.keywords.iter().map(|k| k.to_lowercase()).collect();
|
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()));
|
#[cfg(target_os = "linux")]
|
||||||
let exec = e.exec.clone();
|
let icon = e.icon.as_ref().and_then(|p| linux::resolve_icon_path(p.as_str()));
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
let icon: Option<String> = None;
|
||||||
|
let exec = e.exec.as_str().to_string();
|
||||||
let store = Arc::clone(&frecency);
|
let store = Arc::clone(&frecency);
|
||||||
let record_id = id.clone();
|
let record_id = id.clone();
|
||||||
let on_execute: Arc<dyn Fn() + Send + Sync> = Arc::new(move || {
|
let on_select: Arc<dyn Fn() + Send + Sync> = Arc::new(move || {
|
||||||
store.record(&record_id);
|
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 {
|
let cached = CachedEntry {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
@@ -118,7 +108,8 @@ impl AppsPlugin {
|
|||||||
keywords_lc,
|
keywords_lc,
|
||||||
category: e.category,
|
category: e.category,
|
||||||
icon,
|
icon,
|
||||||
on_execute,
|
exec,
|
||||||
|
on_select,
|
||||||
name: e.name,
|
name: e.name,
|
||||||
};
|
};
|
||||||
(id, cached)
|
(id, cached)
|
||||||
@@ -128,19 +119,6 @@ impl AppsPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_icon_path(name: &str) -> Option<String> {
|
|
||||||
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 {
|
fn initials(name_lc: &str) -> String {
|
||||||
name_lc.split_whitespace().filter_map(|w| w.chars().next()).collect()
|
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<u32> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn humanize_category(s: &str) -> String {
|
pub(crate) fn humanize_category(s: &str) -> String {
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
for ch in s.chars() {
|
for ch in s.chars() {
|
||||||
if ch.is_uppercase() && !result.is_empty() {
|
if ch.is_uppercase() && !result.is_empty() {
|
||||||
@@ -183,7 +161,8 @@ impl Plugin for AppsPlugin {
|
|||||||
description: e.category.clone(),
|
description: e.category.clone(),
|
||||||
icon: e.icon.clone(),
|
icon: e.icon.clone(),
|
||||||
score: Score::new(score),
|
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();
|
.collect();
|
||||||
@@ -202,129 +181,14 @@ impl Plugin for AppsPlugin {
|
|||||||
description: e.category.clone(),
|
description: e.category.clone(),
|
||||||
icon: e.icon.clone(),
|
icon: e.icon.clone(),
|
||||||
score: Score::new(score),
|
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()
|
.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<DesktopEntry> {
|
|
||||||
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<DesktopEntry> {
|
|
||||||
let content = std::fs::read_to_string(path).ok()?;
|
|
||||||
let mut in_section = false;
|
|
||||||
let mut name: Option<String> = None;
|
|
||||||
let mut exec: Option<String> = None;
|
|
||||||
let mut icon: Option<String> = None;
|
|
||||||
let mut category: Option<String> = None;
|
|
||||||
let mut keywords: Vec<String> = 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 ---
|
// --- Tests ---
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
133
crates/plugins/plugin-apps/src/linux.rs
Normal file
133
crates/plugins/plugin-apps/src/linux.rs
Normal file
@@ -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<DesktopEntry> {
|
||||||
|
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<String> {
|
||||||
|
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<DesktopEntry> {
|
||||||
|
let content = std::fs::read_to_string(path).ok()?;
|
||||||
|
let mut in_section = false;
|
||||||
|
let mut name: Option<String> = None;
|
||||||
|
let mut exec: Option<String> = None;
|
||||||
|
let mut icon: Option<String> = None;
|
||||||
|
let mut category: Option<String> = None;
|
||||||
|
let mut keywords: Vec<String> = 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
fn main() {}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
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;
|
pub struct CalcPlugin;
|
||||||
|
|
||||||
@@ -46,27 +44,14 @@ impl Plugin for CalcPlugin {
|
|||||||
};
|
};
|
||||||
let display = format!("= {value_str}");
|
let display = format!("= {value_str}");
|
||||||
let expr_owned = expr.to_string();
|
let expr_owned = expr.to_string();
|
||||||
let clipboard_val = value_str;
|
|
||||||
vec![SearchResult {
|
vec![SearchResult {
|
||||||
id: ResultId::new("calc-result"),
|
id: ResultId::new("calc-result"),
|
||||||
title: ResultTitle::new(display),
|
title: ResultTitle::new(display),
|
||||||
description: Some(format!("{expr_owned} · Enter to copy")),
|
description: Some(format!("{expr_owned} · Enter to copy")),
|
||||||
icon: None,
|
icon: None,
|
||||||
score: Score::new(90),
|
score: Score::new(90),
|
||||||
on_execute: Arc::new(move || {
|
action: LaunchAction::CopyToClipboard(value_str),
|
||||||
if std::process::Command::new("wl-copy").arg(&clipboard_val).spawn().is_err() {
|
on_select: None,
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ path = "src/lib.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
k-launcher-kernel = { path = "../../k-launcher-kernel" }
|
k-launcher-kernel = { path = "../../k-launcher-kernel" }
|
||||||
libc = "0.2"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|||||||
@@ -1,56 +1,5 @@
|
|||||||
use std::{process::{Command, Stdio}, sync::Arc};
|
|
||||||
use std::os::unix::process::CommandExt;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
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};
|
||||||
|
|
||||||
fn parse_term_cmd(s: &str) -> (String, Vec<String>) {
|
|
||||||
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<String>)> {
|
|
||||||
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 CmdPlugin;
|
pub struct CmdPlugin;
|
||||||
|
|
||||||
@@ -80,26 +29,14 @@ impl Plugin for CmdPlugin {
|
|||||||
if cmd.is_empty() {
|
if cmd.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let cmd_owned = cmd.to_string();
|
|
||||||
vec![SearchResult {
|
vec![SearchResult {
|
||||||
id: ResultId::new(format!("cmd-{cmd}")),
|
id: ResultId::new(format!("cmd-{cmd}")),
|
||||||
title: ResultTitle::new(format!("Run: {cmd}")),
|
title: ResultTitle::new(format!("Run: {cmd}")),
|
||||||
description: None,
|
description: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
score: Score::new(95),
|
score: Score::new(95),
|
||||||
on_execute: Arc::new(move || {
|
action: LaunchAction::SpawnInTerminal(cmd.to_string()),
|
||||||
let Some((term_bin, term_args)) = resolve_terminal() else { return };
|
on_select: None,
|
||||||
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()
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,25 +67,4 @@ mod tests {
|
|||||||
assert!(p.search("echo hello").await.is_empty());
|
assert!(p.search("echo hello").await.is_empty());
|
||||||
assert!(p.search("firefox").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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
mod platform;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
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;
|
pub struct FilesPlugin;
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ impl Default for FilesPlugin {
|
|||||||
|
|
||||||
fn expand_query(query: &str) -> Option<String> {
|
fn expand_query(query: &str) -> Option<String> {
|
||||||
if query.starts_with("~/") {
|
if query.starts_with("~/") {
|
||||||
let home = std::env::var("HOME").ok()?;
|
let home = platform::home_dir()?;
|
||||||
Some(format!("{}{}", home, &query[1..]))
|
Some(format!("{}{}", home, &query[1..]))
|
||||||
} else if query.starts_with('/') {
|
} else if query.starts_with('/') {
|
||||||
Some(query.to_string())
|
Some(query.to_string())
|
||||||
@@ -87,9 +88,8 @@ impl Plugin for FilesPlugin {
|
|||||||
description: Some(path_str.clone()),
|
description: Some(path_str.clone()),
|
||||||
icon: None,
|
icon: None,
|
||||||
score: Score::new(50),
|
score: Score::new(50),
|
||||||
on_execute: Arc::new(move || {
|
action: LaunchAction::OpenPath(path_str),
|
||||||
let _ = std::process::Command::new("xdg-open").arg(&path_str).spawn();
|
on_select: None,
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
9
crates/plugins/plugin-files/src/platform.rs
Normal file
9
crates/plugins/plugin-files/src/platform.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#[cfg(unix)]
|
||||||
|
pub fn home_dir() -> Option<String> {
|
||||||
|
std::env::var("HOME").ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn home_dir() -> Option<String> {
|
||||||
|
std::env::var("USERPROFILE").ok()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user