v0.2.0
Some checks failed
CI / test (push) Failing after 5m16s
CI / fmt (push) Has been cancelled
CI / clippy (push) Has been cancelled
Release / build (push) Failing after 5m31s

clean architecture refactor, performance, resilience, DX/UX

architecture:
- 13 crates with proper domain/application/infrastructure layers
- domain crate: newtypes, ports (Plugin, AppLauncher), constants
- kernel: pure orchestrator
- shared UI state machine (k-launcher-ui-core)
- merged plugin-api into domain as ports module
- granular file structure (no monolithic lib.rs)
- all tests extracted to tests/ directories

features:
- frecency boost in search results
- empty query shows top frecent apps
- append-only frecency log with configurable compaction
- config-driven styling (all colors, sizes, debounce)
- configurable terminal emulator, external plugin timeout
- log rotation with max_log_files
- loading indicator, descriptive placeholder text
- graceful shutdown via iced::exit() + Plugin::shutdown()
- --version flag, panic hook, signal handling (SIGINT/SIGTERM)
- SpawnInTerminal in external plugin protocol

performance:
- ~1500 -> ~50 heap allocs per keystroke
- reused Matcher, Pattern, char buffer across entries
- Arc<str> for shared result fields
- pre-filter before fuzzy matching
- partial sort for top frecent IDs
- cached lowercase names in entries

resilience:
- parking_lot (no mutex poisoning)
- thiserror hierarchy (PluginError, ConfigError, AppError)
- all silent error swallowing replaced with tracing::warn
- config parse errors logged

quality:
- named constants (no magic strings/numbers)
- named types (no anonymous tuples)
- Rgba newtype with validation
- domain newtype validation (debug_assert non-empty)
- man page, LICENSE (MIT), PKGBUILD, example config
- plugin development guide updated
- make check (fmt + clippy + test), make dev (RUST_LOG=debug)

style: format code for better readability in tests and function signatures

fix: update build_entries function signature to ignore frecency parameter

fix(review): bugs, arch violations, design smells

P1 bugs:
- unix_launcher: shell_split respects quoted args (was split_whitespace)
- plugin-host: 5s timeout on external plugin search
- ui: handle engine init panic, wire error state
- ui-egui: read window config instead of always using defaults
- plugin-url: use OpenPath action instead of SpawnProcess+xdg-open

Architecture:
- remove WindowConfig (mirror of WindowCfg); use WindowCfg directly
- remove on_select closure from SearchResult (domain leakage)
- remove LaunchAction::Custom; add Plugin::on_selected + SearchEngine::on_selected
- apps: record frecency via on_selected instead of embedded closure

Design smells:
- frecency: extract decay_factor helper, write outside mutex
- apps: remove cfg(test) cache_path hack; add new_for_test ctor
- apps: stable ResultId using name+exec to prevent collision
- files: stable ResultId using full path instead of index
- plugin-host: remove k-launcher-os-bridge dep (WindowConfig gone)

Update iced dependency in Cargo.toml to disable default features and add additional ones

feat(app): enhance engine initialization with EngineHandle and update run function signature

feat: production hardening (panic isolation, file logging, apps cache)

- Kernel::search wraps each plugin in catch_unwind; panics are logged and return []
- init_logging() adds daily rolling file at ~/.local/share/k-launcher/logs/
- AppsPlugin caches entries to ~/.cache/k-launcher/apps.bin via bincode; stale-while-revalidate on subsequent launches
- 57 tests pass

refactor: remove client module and associated show command logic

fix(app): format code for clarity in update function

chore: update .gitignore and enhance README with compositor setup instructions

chore(docs): remove unused screenshot file

feature/prod-ready (#1)

Reviewed-on: #1

fix(calc): remove ambiguous log alias, use ln/log2/log10 explicitly

fix(calc): fix log/ln naming, cache math context, strengthen sin(pi) test

feat(calc): add math functions (sqrt, sin, cos, etc.) and pi/e constants

refactor(calc): rename preprocess, extend underscore test assertions

feat(calc): strip underscore digit separators

feat: update dependencies for improved compatibility and performance

feat: add plugin-url for URL handling and open in browser functionality

feat: add support for external plugins and enhance plugin management

feat: add Makefile for build, run, and installation commands

feat: add required features for k-launcher-egui and update dependencies

feat: update README and add documentation for installation, configuration, usage, and plugin development

feat: enhance configuration management and UI styling, remove unused theme module

feat: add k-launcher-config crate for configuration management and integrate with existing components

feat: add k-launcher-ui-egui crate for enhanced UI

- Introduced a new crate `k-launcher-ui-egui` to provide a graphical user interface using eframe and egui.
- Updated the workspace configuration in `Cargo.toml` to include the new crate.
- Implemented the main application logic in `src/app.rs`, handling search functionality and user interactions.
- Created a library entry point in `src/lib.rs` to expose the `run` function for launching the UI.
- Modified the `k-launcher` crate to include a new binary target for the egui-based launcher.
- Added a new main file `src/main_egui.rs` to initialize and run the egui UI with the existing kernel and launcher components.

feat: implement OS bridge and enhance app launcher functionality

feat: add FilesPlugin for file searching and integrate into KLauncher

feat: implement frecency tracking for app usage and enhance search functionality

feat: add CmdPlugin for executing terminal commands and update workspace configuration

refactor: update dependencies and improve keyboard event handling in KLauncherApp

refactor: simplify theme usage and enhance AppsPlugin structure

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.

Initialize k-launcher project structure with multiple crates and basic configurations
This commit is contained in:
2026-07-24 13:42:14 +02:00
parent 2e773cdeaf
commit 051d19d878
95 changed files with 4129 additions and 2591 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "k-launcher-config"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
[lib]
@@ -9,5 +9,11 @@ path = "src/lib.rs"
[dependencies]
dirs = { workspace = true }
k-launcher-domain = { workspace = true }
serde = { workspace = true }
toml = { workspace = true }
thiserror = { workspace = true }
toml = "1.0"
tracing = { workspace = true }
[dev-dependencies]
toml = "1.0"

View File

@@ -0,0 +1,14 @@
use serde::Deserialize;
use crate::types::*;
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub struct Config {
pub window: WindowCfg,
pub appearance: AppearanceCfg,
pub search: SearchCfg,
pub plugins: PluginsCfg,
pub logging: LoggingCfg,
pub terminal: TerminalCfg,
}

View File

@@ -0,0 +1,18 @@
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("config directory not found")]
NoDirFound,
#[error("failed to read config at {path}: {source}")]
ReadFailed {
path: PathBuf,
source: std::io::Error,
},
#[error("failed to parse config at {path}: {source}")]
ParseFailed {
path: PathBuf,
source: toml::de::Error,
},
}

View File

@@ -1,191 +1,8 @@
use serde::Deserialize;
mod config;
pub mod error;
mod load;
mod types;
// RGBA: [r, g, b, a] where r/g/b are 0255 as f32, a is 0.01.0
pub type Rgba = [f32; 4];
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub struct Config {
pub window: WindowCfg,
pub appearance: AppearanceCfg,
pub search: SearchCfg,
pub plugins: PluginsCfg,
}
#[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: [229.0, 125.0, 33.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, Default)]
pub struct ExternalPluginCfg {
pub name: String,
pub path: String,
#[serde(default)]
pub args: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct PluginsCfg {
pub calc: bool,
pub cmd: bool,
pub files: bool,
pub apps: bool,
pub external: Vec<ExternalPluginCfg>,
}
impl Default for PluginsCfg {
fn default() -> Self {
Self {
calc: true,
cmd: true,
files: true,
apps: true,
external: vec![],
}
}
}
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);
}
}
pub use config::*;
pub use load::*;
pub use types::*;

View File

@@ -0,0 +1,25 @@
use k_launcher_domain::constants::{APP_NAME, CONFIG_FILENAME};
use crate::config::Config;
use crate::error::ConfigError;
pub fn load() -> Config {
match try_load() {
Ok(cfg) => cfg,
Err(ConfigError::NoDirFound | ConfigError::ReadFailed { .. }) => Config::default(),
Err(e @ ConfigError::ParseFailed { .. }) => {
tracing::warn!("{e}");
Config::default()
}
}
}
pub fn try_load() -> Result<Config, ConfigError> {
let dir = dirs::config_dir().ok_or(ConfigError::NoDirFound)?;
let path = dir.join(APP_NAME).join(CONFIG_FILENAME);
let content = std::fs::read_to_string(&path).map_err(|e| ConfigError::ReadFailed {
path: path.clone(),
source: e,
})?;
toml::from_str(&content).map_err(|e| ConfigError::ParseFailed { path, source: e })
}

View File

@@ -0,0 +1,200 @@
use serde::Deserialize;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rgba {
red: f32,
green: f32,
blue: f32,
alpha: f32,
}
impl<'de> serde::Deserialize<'de> for Rgba {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let [red, green, blue, alpha] = <[f32; 4]>::deserialize(deserializer)?;
Ok(Self::new(red, green, blue, alpha))
}
}
impl Rgba {
pub fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
Self {
red: red.clamp(0.0, 255.0),
green: green.clamp(0.0, 255.0),
blue: blue.clamp(0.0, 255.0),
alpha: alpha.clamp(0.0, 1.0),
}
}
pub fn red(&self) -> f32 {
self.red
}
pub fn green(&self) -> f32 {
self.green
}
pub fn blue(&self) -> f32 {
self.blue
}
pub fn alpha(&self) -> f32 {
self.alpha
}
pub fn red_u8(&self) -> u8 {
self.red as u8
}
pub fn green_u8(&self) -> u8 {
self.green as u8
}
pub fn blue_u8(&self) -> u8 {
self.blue as u8
}
pub fn alpha_byte(&self) -> u8 {
(self.alpha * 255.0) as u8
}
}
#[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,
pub selected_row_rgba: Rgba,
pub unselected_row_rgba: Rgba,
pub description_rgba: Rgba,
pub no_results_rgba: Rgba,
pub error_rgba: Rgba,
pub icon_size: f32,
}
impl Default for AppearanceCfg {
fn default() -> Self {
Self {
background_rgba: Rgba::new(20.0, 20.0, 30.0, 0.9),
border_rgba: Rgba::new(229.0, 125.0, 33.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 apps, type > for commands, = for math".to_string(),
selected_row_rgba: Rgba::new(229.0, 125.0, 33.0, 1.0),
unselected_row_rgba: Rgba::new(255.0, 255.0, 255.0, 0.07),
description_rgba: Rgba::new(210.0, 215.0, 230.0, 1.0),
no_results_rgba: Rgba::new(180.0, 180.0, 200.0, 0.5),
error_rgba: Rgba::new(255.0, 80.0, 80.0, 1.0),
icon_size: 24.0,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct SearchCfg {
pub max_results: usize,
pub debounce_ms: u64,
pub frecency_compact_threshold: usize,
}
impl Default for SearchCfg {
fn default() -> Self {
Self {
max_results: 8,
debounce_ms: 50,
frecency_compact_threshold: 50,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct LoggingCfg {
pub max_log_files: usize,
}
impl Default for LoggingCfg {
fn default() -> Self {
Self { max_log_files: 7 }
}
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct TerminalCfg {
pub cmd: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct ExternalPluginCfg {
pub name: String,
pub path: String,
pub args: Vec<String>,
pub timeout_secs: u64,
}
impl Default for ExternalPluginCfg {
fn default() -> Self {
Self {
name: String::new(),
path: String::new(),
args: vec![],
timeout_secs: 5,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct PluginsCfg {
pub calc: bool,
pub cmd: bool,
pub files: bool,
pub apps: bool,
pub external: Vec<ExternalPluginCfg>,
}
impl Default for PluginsCfg {
fn default() -> Self {
Self {
calc: true,
cmd: true,
files: true,
apps: true,
external: vec![],
}
}
}

View File

@@ -0,0 +1,75 @@
use k_launcher_config::Config;
#[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 apps, type > for commands, = for math"
);
}
#[test]
fn parse_partial_toml_uses_defaults() {
let toml_str = "[search]\nmax_results = 5\n";
let cfg: Config = toml::from_str(toml_str).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_str = 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_str).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,
k_launcher_config::Rgba::new(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);
}