feat: restructure k-launcher workspace and add core functionality
- Updated Cargo.toml to include a new k-launcher crate and reorganized workspace members. - Introduced a README.md file detailing the project philosophy, architecture, and technical specifications. - Implemented a new Kernel struct in k-launcher-kernel for managing plugins and search functionality. - Created a Plugin trait for plugins to implement, allowing for asynchronous search operations. - Developed k-launcher-ui with an Iced-based UI for user interaction, including search input and result display. - Added AppsPlugin and CalcPlugin to handle application launching and basic calculations, respectively. - Established a theme module for UI styling, focusing on an Aero aesthetic. - Removed unnecessary main.rs files from plugin crates, streamlining the project structure.
This commit is contained in:
@@ -1,14 +1,168 @@
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::future::join_all;
|
||||
|
||||
pub type PluginName = &'static str;
|
||||
|
||||
// --- Newtypes ---
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ResultId(String);
|
||||
|
||||
impl ResultId {
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
Self(id.into())
|
||||
}
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ResultTitle(String);
|
||||
|
||||
impl ResultTitle {
|
||||
pub fn new(title: impl Into<String>) -> Self {
|
||||
Self(title.into())
|
||||
}
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Score(u32);
|
||||
|
||||
impl Score {
|
||||
pub fn new(value: u32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
pub fn value(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
// --- SearchResult ---
|
||||
|
||||
pub struct SearchResult {
|
||||
pub id: ResultId,
|
||||
pub title: ResultTitle,
|
||||
pub description: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub score: Score,
|
||||
pub on_execute: Arc<dyn Fn() + Send + Sync>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SearchResult {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SearchResult")
|
||||
.field("id", &self.id)
|
||||
.field("title", &self.title)
|
||||
.field("icon", &self.icon)
|
||||
.field("score", &self.score)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Plugin trait ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait Plugin: Send + Sync {
|
||||
fn name(&self) -> PluginName;
|
||||
async fn search(&self, query: &str) -> Vec<SearchResult>;
|
||||
}
|
||||
|
||||
// --- Kernel (Application use case) ---
|
||||
|
||||
pub struct Kernel {
|
||||
plugins: Vec<Arc<dyn Plugin>>,
|
||||
}
|
||||
|
||||
impl Kernel {
|
||||
pub fn new(plugins: Vec<Arc<dyn Plugin>>) -> Self {
|
||||
Self { plugins }
|
||||
}
|
||||
|
||||
pub async fn search(&self, query: &str) -> Vec<SearchResult> {
|
||||
let futures = self.plugins.iter().map(|p| p.search(query));
|
||||
let nested: Vec<Vec<SearchResult>> = join_all(futures).await;
|
||||
let mut flat: Vec<SearchResult> = nested.into_iter().flatten().collect();
|
||||
flat.sort_by(|a, b| b.score.cmp(&a.score));
|
||||
flat
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct MockPlugin {
|
||||
results: Vec<(&'static str, u32)>,
|
||||
}
|
||||
|
||||
impl MockPlugin {
|
||||
fn returns(results: Vec<(&'static str, u32)>) -> Self {
|
||||
Self { results }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Plugin for MockPlugin {
|
||||
fn name(&self) -> PluginName {
|
||||
"mock"
|
||||
}
|
||||
|
||||
async fn search(&self, _query: &str) -> Vec<SearchResult> {
|
||||
self.results
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (title, score))| SearchResult {
|
||||
id: ResultId::new(format!("id-{i}")),
|
||||
title: ResultTitle::new(*title),
|
||||
description: None,
|
||||
icon: None,
|
||||
score: Score::new(*score),
|
||||
on_execute: Arc::new(|| {}),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
fn newtype_result_id() {
|
||||
assert_eq!(ResultId::new("x").as_str(), "x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newtype_score() {
|
||||
assert_eq!(Score::new(42).value(), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newtype_title() {
|
||||
assert_eq!(ResultTitle::new("hello").as_str(), "hello");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_kernel_returns_empty() {
|
||||
let k = Kernel::new(vec![]);
|
||||
assert!(k.search("x").await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn kernel_sorts_by_score_desc() {
|
||||
let plugin = Arc::new(MockPlugin::returns(vec![
|
||||
("lower", 5),
|
||||
("higher", 10),
|
||||
("middle", 7),
|
||||
]));
|
||||
let k = Kernel::new(vec![plugin]);
|
||||
let results = k.search("q").await;
|
||||
assert_eq!(results[0].score.value(), 10);
|
||||
assert_eq!(results[1].score.value(), 7);
|
||||
assert_eq!(results[2].score.value(), 5);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user