feat: add k-launcher-config crate for configuration management and integrate with existing components
This commit is contained in:
13
crates/k-launcher-config/Cargo.toml
Normal file
13
crates/k-launcher-config/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "k-launcher-config"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "k_launcher_config"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
dirs = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
193
crates/k-launcher-config/src/lib.rs
Normal file
193
crates/k-launcher-config/src/lib.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
// RGBA: [r, g, b, a] where r/g/b are 0–255 as f32, a is 0.0–1.0
|
||||
pub type Rgba = [f32; 4];
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
pub window: WindowCfg,
|
||||
pub appearance: AppearanceCfg,
|
||||
pub search: SearchCfg,
|
||||
pub plugins: PluginsCfg,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
window: WindowCfg::default(),
|
||||
appearance: AppearanceCfg::default(),
|
||||
search: SearchCfg::default(),
|
||||
plugins: PluginsCfg::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct WindowCfg {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub decorations: bool,
|
||||
pub transparent: bool,
|
||||
pub resizable: bool,
|
||||
}
|
||||
|
||||
impl Default for WindowCfg {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
width: 600.0,
|
||||
height: 400.0,
|
||||
decorations: false,
|
||||
transparent: true,
|
||||
resizable: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct AppearanceCfg {
|
||||
pub background_rgba: Rgba,
|
||||
pub border_rgba: Rgba,
|
||||
pub border_width: f32,
|
||||
pub border_radius: f32,
|
||||
pub search_font_size: f32,
|
||||
pub title_size: f32,
|
||||
pub desc_size: f32,
|
||||
pub row_radius: f32,
|
||||
pub placeholder: String,
|
||||
}
|
||||
|
||||
impl Default for AppearanceCfg {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
background_rgba: [20.0, 20.0, 30.0, 0.9],
|
||||
border_rgba: [0.0, 183.0, 235.0, 1.0],
|
||||
border_width: 1.0,
|
||||
border_radius: 8.0,
|
||||
search_font_size: 18.0,
|
||||
title_size: 15.0,
|
||||
desc_size: 12.0,
|
||||
row_radius: 4.0,
|
||||
placeholder: "Search...".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct SearchCfg {
|
||||
pub max_results: usize,
|
||||
}
|
||||
|
||||
impl Default for SearchCfg {
|
||||
fn default() -> Self {
|
||||
Self { max_results: 8 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct PluginsCfg {
|
||||
pub calc: bool,
|
||||
pub cmd: bool,
|
||||
pub files: bool,
|
||||
pub apps: bool,
|
||||
}
|
||||
|
||||
impl Default for PluginsCfg {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
calc: true,
|
||||
cmd: true,
|
||||
files: true,
|
||||
apps: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load() -> Config {
|
||||
let path = dirs::config_dir()
|
||||
.map(|d| d.join("k-launcher").join("config.toml"));
|
||||
let Some(path) = path else {
|
||||
return Config::default();
|
||||
};
|
||||
let Ok(content) = std::fs::read_to_string(&path) else {
|
||||
return Config::default();
|
||||
};
|
||||
toml::from_str(&content).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_config_has_sane_values() {
|
||||
let cfg = Config::default();
|
||||
assert_eq!(cfg.search.max_results, 8);
|
||||
assert_eq!(cfg.window.width, 600.0);
|
||||
assert_eq!(cfg.window.height, 400.0);
|
||||
assert!(!cfg.window.decorations);
|
||||
assert!(cfg.window.transparent);
|
||||
assert!(!cfg.window.resizable);
|
||||
assert!(cfg.plugins.calc);
|
||||
assert!(cfg.plugins.apps);
|
||||
assert_eq!(cfg.appearance.search_font_size, 18.0);
|
||||
assert_eq!(cfg.appearance.placeholder, "Search...");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_partial_toml_uses_defaults() {
|
||||
let toml = "[search]\nmax_results = 5\n";
|
||||
let cfg: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(cfg.search.max_results, 5);
|
||||
assert_eq!(cfg.window.width, 600.0);
|
||||
assert_eq!(cfg.appearance.search_font_size, 18.0);
|
||||
assert!(cfg.plugins.apps);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_full_toml_roundtrip() {
|
||||
let toml = r#"
|
||||
[window]
|
||||
width = 800.0
|
||||
height = 500.0
|
||||
decorations = true
|
||||
transparent = false
|
||||
resizable = true
|
||||
|
||||
[appearance]
|
||||
background_rgba = [10.0, 10.0, 20.0, 0.8]
|
||||
border_rgba = [100.0, 200.0, 255.0, 1.0]
|
||||
border_width = 2.0
|
||||
border_radius = 12.0
|
||||
search_font_size = 20.0
|
||||
title_size = 16.0
|
||||
desc_size = 13.0
|
||||
row_radius = 6.0
|
||||
placeholder = "Type here..."
|
||||
|
||||
[search]
|
||||
max_results = 12
|
||||
|
||||
[plugins]
|
||||
calc = false
|
||||
cmd = true
|
||||
files = false
|
||||
apps = true
|
||||
"#;
|
||||
let cfg: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(cfg.window.width, 800.0);
|
||||
assert_eq!(cfg.window.height, 500.0);
|
||||
assert!(cfg.window.decorations);
|
||||
assert!(!cfg.window.transparent);
|
||||
assert_eq!(cfg.appearance.background_rgba, [10.0, 10.0, 20.0, 0.8]);
|
||||
assert_eq!(cfg.appearance.search_font_size, 20.0);
|
||||
assert_eq!(cfg.appearance.placeholder, "Type here...");
|
||||
assert_eq!(cfg.search.max_results, 12);
|
||||
assert!(!cfg.plugins.calc);
|
||||
assert!(!cfg.plugins.files);
|
||||
}
|
||||
}
|
||||
@@ -101,11 +101,12 @@ pub trait SearchEngine: Send + Sync {
|
||||
|
||||
pub struct Kernel {
|
||||
plugins: Vec<Arc<dyn Plugin>>,
|
||||
max_results: usize,
|
||||
}
|
||||
|
||||
impl Kernel {
|
||||
pub fn new(plugins: Vec<Arc<dyn Plugin>>) -> Self {
|
||||
Self { plugins }
|
||||
pub fn new(plugins: Vec<Arc<dyn Plugin>>, max_results: usize) -> Self {
|
||||
Self { plugins, max_results }
|
||||
}
|
||||
|
||||
pub async fn search(&self, query: &str) -> Vec<SearchResult> {
|
||||
@@ -113,7 +114,7 @@ impl Kernel {
|
||||
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.truncate(8);
|
||||
flat.truncate(self.max_results);
|
||||
flat
|
||||
}
|
||||
}
|
||||
@@ -181,7 +182,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_kernel_returns_empty() {
|
||||
let k = Kernel::new(vec![]);
|
||||
let k = Kernel::new(vec![], 8);
|
||||
assert!(k.search("x").await.is_empty());
|
||||
}
|
||||
|
||||
@@ -192,10 +193,26 @@ mod tests {
|
||||
("higher", 10),
|
||||
("middle", 7),
|
||||
]));
|
||||
let k = Kernel::new(vec![plugin]);
|
||||
let k = Kernel::new(vec![plugin], 8);
|
||||
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);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn kernel_truncates_at_max_results() {
|
||||
let plugin = Arc::new(MockPlugin::returns(vec![
|
||||
("a", 10),
|
||||
("b", 9),
|
||||
("c", 8),
|
||||
("d", 7),
|
||||
("e", 6),
|
||||
]));
|
||||
let k = Kernel::new(vec![plugin], 3);
|
||||
let results = k.search("q").await;
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results[0].score.value(), 10);
|
||||
assert_eq!(results[2].score.value(), 8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
k-launcher-config = { path = "../k-launcher-config" }
|
||||
k-launcher-kernel = { path = "../k-launcher-kernel" }
|
||||
libc = "0.2"
|
||||
|
||||
@@ -11,13 +11,13 @@ pub struct WindowConfig {
|
||||
}
|
||||
|
||||
impl WindowConfig {
|
||||
pub fn launcher() -> Self {
|
||||
pub fn from_cfg(w: &k_launcher_config::WindowCfg) -> Self {
|
||||
Self {
|
||||
width: 600.0,
|
||||
height: 400.0,
|
||||
decorations: false,
|
||||
transparent: true,
|
||||
resizable: false,
|
||||
width: w.width,
|
||||
height: w.height,
|
||||
decorations: w.decorations,
|
||||
transparent: w.transparent,
|
||||
resizable: w.resizable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ path = "src/lib.rs"
|
||||
[dependencies]
|
||||
eframe = { version = "0.31", default-features = false, features = ["default_fonts", "wayland", "x11", "glow"] }
|
||||
egui = "0.31"
|
||||
k-launcher-config = { path = "../k-launcher-config" }
|
||||
k-launcher-kernel = { path = "../k-launcher-kernel" }
|
||||
k-launcher-os-bridge = { path = "../k-launcher-os-bridge" }
|
||||
tokio = { workspace = true }
|
||||
|
||||
@@ -160,7 +160,7 @@ pub fn run(
|
||||
engine: Arc<dyn SearchEngine>,
|
||||
launcher: Arc<dyn AppLauncher>,
|
||||
) -> Result<(), eframe::Error> {
|
||||
let wc = WindowConfig::launcher();
|
||||
let wc = WindowConfig::from_cfg(&k_launcher_config::WindowCfg::default());
|
||||
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
|
||||
let handle = rt.handle().clone();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
iced = { workspace = true }
|
||||
k-launcher-config = { path = "../k-launcher-config" }
|
||||
k-launcher-kernel = { path = "../k-launcher-kernel" }
|
||||
k-launcher-os-bridge = { path = "../k-launcher-os-bridge" }
|
||||
tokio = { workspace = true }
|
||||
|
||||
@@ -8,30 +8,39 @@ use iced::{
|
||||
window,
|
||||
};
|
||||
|
||||
use k_launcher_config::AppearanceCfg;
|
||||
use k_launcher_kernel::{AppLauncher, SearchEngine, SearchResult};
|
||||
use k_launcher_os_bridge::WindowConfig;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
static INPUT_ID: std::sync::LazyLock<iced::widget::Id> =
|
||||
std::sync::LazyLock::new(|| iced::widget::Id::new("search"));
|
||||
|
||||
fn rgba(c: &[f32; 4]) -> Color {
|
||||
Color::from_rgba8(c[0] as u8, c[1] as u8, c[2] as u8, c[3])
|
||||
}
|
||||
|
||||
pub struct KLauncherApp {
|
||||
engine: Arc<dyn SearchEngine>,
|
||||
launcher: Arc<dyn AppLauncher>,
|
||||
query: String,
|
||||
results: Arc<Vec<SearchResult>>,
|
||||
selected: usize,
|
||||
cfg: AppearanceCfg,
|
||||
}
|
||||
|
||||
impl KLauncherApp {
|
||||
fn new(engine: Arc<dyn SearchEngine>, launcher: Arc<dyn AppLauncher>) -> Self {
|
||||
fn new(
|
||||
engine: Arc<dyn SearchEngine>,
|
||||
launcher: Arc<dyn AppLauncher>,
|
||||
cfg: AppearanceCfg,
|
||||
) -> Self {
|
||||
Self {
|
||||
engine,
|
||||
launcher,
|
||||
query: String::new(),
|
||||
results: Arc::new(vec![]),
|
||||
selected: 0,
|
||||
cfg,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,13 +105,18 @@ fn update(state: &mut KLauncherApp, message: Message) -> Task<Message> {
|
||||
}
|
||||
|
||||
fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
||||
let colors = &*theme::AERO;
|
||||
let cfg = &state.cfg;
|
||||
let border_color = rgba(&cfg.border_rgba);
|
||||
|
||||
let search_bar = text_input("Search...", &state.query)
|
||||
let search_bar = text_input(&cfg.placeholder, &state.query)
|
||||
.id(INPUT_ID.clone())
|
||||
.on_input(Message::QueryChanged)
|
||||
.padding(12)
|
||||
.size(18);
|
||||
.size(cfg.search_font_size);
|
||||
|
||||
let row_radius: f32 = cfg.row_radius;
|
||||
let title_size: f32 = cfg.title_size;
|
||||
let desc_size: f32 = cfg.desc_size;
|
||||
|
||||
let result_rows: Vec<Element<'_, Message>> = state
|
||||
.results
|
||||
@@ -111,7 +125,7 @@ fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
||||
.map(|(i, result)| {
|
||||
let is_selected = i == state.selected;
|
||||
let bg_color = if is_selected {
|
||||
colors.border_cyan
|
||||
border_color
|
||||
} else {
|
||||
Color::from_rgba8(255, 255, 255, 0.07)
|
||||
};
|
||||
@@ -124,12 +138,12 @@ fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
||||
};
|
||||
let title_col: Element<'_, Message> = if let Some(desc) = &result.description {
|
||||
column![
|
||||
text(result.title.as_str()).size(15),
|
||||
text(desc).size(12).color(Color::from_rgba8(210, 215, 230, 1.0)),
|
||||
text(result.title.as_str()).size(title_size),
|
||||
text(desc).size(desc_size).color(Color::from_rgba8(210, 215, 230, 1.0)),
|
||||
]
|
||||
.into()
|
||||
} else {
|
||||
text(result.title.as_str()).size(15).into()
|
||||
text(result.title.as_str()).size(title_size).into()
|
||||
};
|
||||
container(
|
||||
row![icon_el, title_col]
|
||||
@@ -143,7 +157,7 @@ fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 4.0.into(),
|
||||
radius: row_radius.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
@@ -155,7 +169,7 @@ fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
||||
scrollable(
|
||||
container(
|
||||
text("No results")
|
||||
.size(15)
|
||||
.size(title_size)
|
||||
.color(Color::from_rgba8(180, 180, 200, 0.5)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
@@ -173,17 +187,19 @@ fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
|
||||
let bg_color = rgba(&cfg.background_rgba);
|
||||
let border_width = cfg.border_width;
|
||||
let border_radius = cfg.border_radius;
|
||||
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(|_theme| container::Style {
|
||||
background: Some(iced::Background::Color(Color::from_rgba8(
|
||||
20, 20, 30, 0.9,
|
||||
))),
|
||||
.style(move |_theme| container::Style {
|
||||
background: Some(iced::Background::Color(bg_color)),
|
||||
border: Border {
|
||||
color: theme::AERO.border_cyan,
|
||||
width: 1.0,
|
||||
radius: 8.0.into(),
|
||||
color: border_color,
|
||||
width: border_width,
|
||||
radius: border_radius.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
@@ -197,11 +213,16 @@ fn subscription(_state: &KLauncherApp) -> Subscription<Message> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(engine: Arc<dyn SearchEngine>, launcher: Arc<dyn AppLauncher>) -> iced::Result {
|
||||
let wc = WindowConfig::launcher();
|
||||
pub fn run(
|
||||
engine: Arc<dyn SearchEngine>,
|
||||
launcher: Arc<dyn AppLauncher>,
|
||||
window_cfg: &k_launcher_config::WindowCfg,
|
||||
appearance_cfg: AppearanceCfg,
|
||||
) -> iced::Result {
|
||||
let wc = WindowConfig::from_cfg(window_cfg);
|
||||
iced::application(
|
||||
move || {
|
||||
let app = KLauncherApp::new(engine.clone(), launcher.clone());
|
||||
let app = KLauncherApp::new(engine.clone(), launcher.clone(), appearance_cfg.clone());
|
||||
let focus = iced::widget::operation::focus(INPUT_ID.clone());
|
||||
(app, focus)
|
||||
},
|
||||
|
||||
@@ -3,8 +3,14 @@ pub mod theme;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use k_launcher_config::{AppearanceCfg, WindowCfg};
|
||||
use k_launcher_kernel::{AppLauncher, SearchEngine};
|
||||
|
||||
pub fn run(engine: Arc<dyn SearchEngine>, launcher: Arc<dyn AppLauncher>) -> iced::Result {
|
||||
app::run(engine, launcher)
|
||||
pub fn run(
|
||||
engine: Arc<dyn SearchEngine>,
|
||||
launcher: Arc<dyn AppLauncher>,
|
||||
window_cfg: &WindowCfg,
|
||||
appearance_cfg: AppearanceCfg,
|
||||
) -> iced::Result {
|
||||
app::run(engine, launcher, window_cfg, appearance_cfg)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ path = "src/main_egui.rs"
|
||||
|
||||
[dependencies]
|
||||
iced = { workspace = true }
|
||||
k-launcher-config = { path = "../k-launcher-config" }
|
||||
k-launcher-kernel = { path = "../k-launcher-kernel" }
|
||||
k-launcher-os-bridge = { path = "../k-launcher-os-bridge" }
|
||||
k-launcher-ui = { path = "../k-launcher-ui" }
|
||||
|
||||
@@ -10,13 +10,20 @@ use plugin_cmd::CmdPlugin;
|
||||
use plugin_files::FilesPlugin;
|
||||
|
||||
fn main() -> iced::Result {
|
||||
let cfg = k_launcher_config::load();
|
||||
let launcher = Arc::new(UnixAppLauncher::new());
|
||||
let frecency = FrecencyStore::load();
|
||||
let kernel: Arc<dyn k_launcher_kernel::SearchEngine> = 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, launcher)
|
||||
|
||||
let mut plugins: Vec<Arc<dyn k_launcher_kernel::Plugin>> = vec![];
|
||||
if cfg.plugins.cmd { plugins.push(Arc::new(CmdPlugin::new())); }
|
||||
if cfg.plugins.calc { plugins.push(Arc::new(CalcPlugin::new())); }
|
||||
if cfg.plugins.files { plugins.push(Arc::new(FilesPlugin::new())); }
|
||||
if cfg.plugins.apps {
|
||||
plugins.push(Arc::new(AppsPlugin::new(FsDesktopEntrySource::new(), frecency)));
|
||||
}
|
||||
|
||||
let kernel: Arc<dyn k_launcher_kernel::SearchEngine> =
|
||||
Arc::new(Kernel::new(plugins, cfg.search.max_results));
|
||||
|
||||
k_launcher_ui::run(kernel, launcher, &cfg.window, cfg.appearance)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Arc::new(CalcPlugin::new()),
|
||||
Arc::new(FilesPlugin::new()),
|
||||
Arc::new(AppsPlugin::new(FsDesktopEntrySource::new(), frecency)),
|
||||
]));
|
||||
], 8));
|
||||
k_launcher_ui_egui::run(kernel, launcher)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user