feat: add FilesPlugin for file searching and integrate into KLauncher
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -1768,6 +1768,7 @@ dependencies = [
|
|||||||
"plugin-apps",
|
"plugin-apps",
|
||||||
"plugin-calc",
|
"plugin-calc",
|
||||||
"plugin-cmd",
|
"plugin-cmd",
|
||||||
|
"plugin-files",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1791,6 +1792,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"iced",
|
"iced",
|
||||||
"k-launcher-kernel",
|
"k-launcher-kernel",
|
||||||
|
"k-launcher-os-bridge",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2766,6 +2768,11 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "plugin-files"
|
name = "plugin-files"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"k-launcher-kernel",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "png"
|
name = "png"
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ impl Kernel {
|
|||||||
let nested: Vec<Vec<SearchResult>> = join_all(futures).await;
|
let nested: Vec<Vec<SearchResult>> = join_all(futures).await;
|
||||||
let mut flat: Vec<SearchResult> = nested.into_iter().flatten().collect();
|
let mut flat: Vec<SearchResult> = nested.into_iter().flatten().collect();
|
||||||
flat.sort_by(|a, b| b.score.cmp(&a.score));
|
flat.sort_by(|a, b| b.score.cmp(&a.score));
|
||||||
|
flat.truncate(8);
|
||||||
flat
|
flat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ path = "src/lib.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" }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use iced::{
|
use iced::{
|
||||||
Color, Element, Length, Size, Subscription, Task,
|
Border, Color, Element, Length, Size, Subscription, Task,
|
||||||
event,
|
event,
|
||||||
keyboard::{Event as KeyEvent, Key, key::Named},
|
keyboard::{Event as KeyEvent, Key, key::Named},
|
||||||
widget::{column, container, image, row, scrollable, svg, text, text_input, Space},
|
widget::{column, container, image, row, scrollable, svg, text, text_input, Space},
|
||||||
@@ -9,6 +9,7 @@ use iced::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use k_launcher_kernel::{Kernel, SearchResult};
|
use k_launcher_kernel::{Kernel, SearchResult};
|
||||||
|
use k_launcher_os_bridge::WindowConfig;
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
|||||||
let title_col: Element<'_, Message> = if let Some(desc) = &result.description {
|
let title_col: Element<'_, Message> = if let Some(desc) = &result.description {
|
||||||
column![
|
column![
|
||||||
text(result.title.as_str()).size(15),
|
text(result.title.as_str()).size(15),
|
||||||
text(desc).size(11).color(Color::from_rgba8(180, 180, 200, 0.8)),
|
text(desc).size(12).color(Color::from_rgba8(210, 215, 230, 1.0)),
|
||||||
]
|
]
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
@@ -134,14 +135,32 @@ fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
|||||||
.padding([6, 12])
|
.padding([6, 12])
|
||||||
.style(move |_theme| container::Style {
|
.style(move |_theme| container::Style {
|
||||||
background: Some(iced::Background::Color(bg_color)),
|
background: Some(iced::Background::Color(bg_color)),
|
||||||
|
border: Border {
|
||||||
|
color: Color::TRANSPARENT,
|
||||||
|
width: 0.0,
|
||||||
|
radius: 4.0.into(),
|
||||||
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let results_list =
|
let results_list = if state.results.is_empty() && !state.query.is_empty() {
|
||||||
scrollable(column(result_rows).spacing(2).width(Length::Fill)).height(Length::Fill);
|
scrollable(
|
||||||
|
container(
|
||||||
|
text("No results")
|
||||||
|
.size(15)
|
||||||
|
.color(Color::from_rgba8(180, 180, 200, 0.5)),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(iced::Center)
|
||||||
|
.padding([20, 0]),
|
||||||
|
)
|
||||||
|
.height(Length::Fill)
|
||||||
|
} else {
|
||||||
|
scrollable(column(result_rows).spacing(2).width(Length::Fill)).height(Length::Fill)
|
||||||
|
};
|
||||||
|
|
||||||
let content = column![search_bar, results_list]
|
let content = column![search_bar, results_list]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
@@ -156,6 +175,11 @@ fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
|||||||
background: Some(iced::Background::Color(Color::from_rgba8(
|
background: Some(iced::Background::Color(Color::from_rgba8(
|
||||||
20, 20, 30, 0.9,
|
20, 20, 30, 0.9,
|
||||||
))),
|
))),
|
||||||
|
border: Border {
|
||||||
|
color: theme::AERO.border_cyan,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 8.0.into(),
|
||||||
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
@@ -169,6 +193,7 @@ fn subscription(_state: &KLauncherApp) -> Subscription<Message> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(kernel: Arc<Kernel>) -> iced::Result {
|
pub fn run(kernel: Arc<Kernel>) -> iced::Result {
|
||||||
|
let wc = WindowConfig::launcher();
|
||||||
iced::application(
|
iced::application(
|
||||||
move || {
|
move || {
|
||||||
let app = KLauncherApp::new(kernel.clone());
|
let app = KLauncherApp::new(kernel.clone());
|
||||||
@@ -181,11 +206,11 @@ pub fn run(kernel: Arc<Kernel>) -> iced::Result {
|
|||||||
.title("K-Launcher")
|
.title("K-Launcher")
|
||||||
.subscription(subscription)
|
.subscription(subscription)
|
||||||
.window(window::Settings {
|
.window(window::Settings {
|
||||||
size: Size::new(600.0, 400.0),
|
size: Size::new(wc.width, wc.height),
|
||||||
position: window::Position::Centered,
|
position: window::Position::Centered,
|
||||||
decorations: false,
|
decorations: wc.decorations,
|
||||||
transparent: true,
|
transparent: wc.transparent,
|
||||||
resizable: false,
|
resizable: wc.resizable,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.run()
|
.run()
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ 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" }
|
||||||
plugin-cmd = { path = "../plugins/plugin-cmd" }
|
plugin-cmd = { path = "../plugins/plugin-cmd" }
|
||||||
|
plugin-files = { path = "../plugins/plugin-files" }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ use k_launcher_kernel::Kernel;
|
|||||||
use plugin_apps::{AppsPlugin, FsDesktopEntrySource, frecency::FrecencyStore};
|
use plugin_apps::{AppsPlugin, FsDesktopEntrySource, frecency::FrecencyStore};
|
||||||
use plugin_calc::CalcPlugin;
|
use plugin_calc::CalcPlugin;
|
||||||
use plugin_cmd::CmdPlugin;
|
use plugin_cmd::CmdPlugin;
|
||||||
|
use plugin_files::FilesPlugin;
|
||||||
|
|
||||||
fn main() -> iced::Result {
|
fn main() -> iced::Result {
|
||||||
let frecency = FrecencyStore::load();
|
let frecency = FrecencyStore::load();
|
||||||
let kernel = Arc::new(Kernel::new(vec![
|
let kernel = 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(AppsPlugin::new(FsDesktopEntrySource::new(), frecency)),
|
Arc::new(AppsPlugin::new(FsDesktopEntrySource::new(), frecency)),
|
||||||
]));
|
]));
|
||||||
k_launcher_ui::run(kernel)
|
k_launcher_ui::run(kernel)
|
||||||
|
|||||||
@@ -39,18 +39,34 @@ impl Plugin for CalcPlugin {
|
|||||||
let expr = query.strip_prefix('=').unwrap_or(query);
|
let expr = query.strip_prefix('=').unwrap_or(query);
|
||||||
match evalexpr::eval_number(expr) {
|
match evalexpr::eval_number(expr) {
|
||||||
Ok(n) if n.is_finite() => {
|
Ok(n) if n.is_finite() => {
|
||||||
let display = if n.fract() == 0.0 {
|
let value_str = if n.fract() == 0.0 {
|
||||||
format!("= {}", n as i64)
|
format!("{}", n as i64)
|
||||||
} else {
|
} else {
|
||||||
format!("= {n}")
|
format!("{n}")
|
||||||
};
|
};
|
||||||
|
let display = format!("= {value_str}");
|
||||||
|
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: None,
|
description: Some(format!("{expr_owned} · Enter to copy")),
|
||||||
icon: None,
|
icon: None,
|
||||||
score: Score::new(90),
|
score: Score::new(90),
|
||||||
on_execute: Arc::new(|| {}),
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
|
|||||||
@@ -8,3 +8,6 @@ name = "plugin_files"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
async-trait = { workspace = true }
|
||||||
|
k-launcher-kernel = { path = "../../k-launcher-kernel" }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use k_launcher_kernel::{Plugin, PluginName, ResultId, ResultTitle, Score, SearchResult};
|
||||||
|
|
||||||
|
pub struct FilesPlugin;
|
||||||
|
|
||||||
|
impl FilesPlugin {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FilesPlugin {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_query(query: &str) -> Option<String> {
|
||||||
|
if query.starts_with("~/") {
|
||||||
|
let home = std::env::var("HOME").ok()?;
|
||||||
|
Some(format!("{}{}", home, &query[1..]))
|
||||||
|
} else if query.starts_with('/') {
|
||||||
|
Some(query.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Plugin for FilesPlugin {
|
||||||
|
fn name(&self) -> PluginName {
|
||||||
|
"files"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn search(&self, query: &str) -> Vec<SearchResult> {
|
||||||
|
let expanded = match expand_query(query) {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return vec![],
|
||||||
|
};
|
||||||
|
let path = Path::new(&expanded);
|
||||||
|
let (parent, prefix) = if path.is_dir() {
|
||||||
|
(path.to_path_buf(), String::new())
|
||||||
|
} else {
|
||||||
|
let parent = path.parent().unwrap_or(Path::new("/")).to_path_buf();
|
||||||
|
let prefix = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_lowercase();
|
||||||
|
(parent, prefix)
|
||||||
|
};
|
||||||
|
|
||||||
|
let entries = match std::fs::read_dir(&parent) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(_) => return vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
entries
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.filter(|e| {
|
||||||
|
if prefix.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
e.file_name()
|
||||||
|
.to_str()
|
||||||
|
.map(|n| n.to_lowercase().starts_with(&prefix))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.take(20)
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, entry)| {
|
||||||
|
let full_path = entry.path();
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
let is_dir = full_path.is_dir();
|
||||||
|
let title = if is_dir {
|
||||||
|
format!("{name}/")
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
};
|
||||||
|
let path_str = full_path.to_string_lossy().to_string();
|
||||||
|
SearchResult {
|
||||||
|
id: ResultId::new(format!("file-{i}")),
|
||||||
|
title: ResultTitle::new(title),
|
||||||
|
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();
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn files_ignores_non_path_query() {
|
||||||
|
let p = FilesPlugin::new();
|
||||||
|
assert!(p.search("firefox").await.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn files_handles_root() {
|
||||||
|
let p = FilesPlugin::new();
|
||||||
|
let results = p.search("/").await;
|
||||||
|
assert!(!results.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user