Files
k-launcher/crates/k-launcher-ui-core/tests/state.rs
Gabriel Kaszewski 051d19d878
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
v0.2.0
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
2026-07-24 13:42:14 +02:00

159 lines
4.1 KiB
Rust

use std::sync::Arc;
use k_launcher_config::AppearanceCfg;
use k_launcher_domain::AppLauncher;
use k_launcher_domain::*;
use k_launcher_kernel::Kernel;
use k_launcher_ui_core::{Action, Effect, LauncherState};
struct NoopLauncher;
impl AppLauncher for NoopLauncher {
fn execute(&self, _action: &LaunchAction) {}
}
fn make_state() -> LauncherState {
LauncherState::new(Arc::new(NoopLauncher), AppearanceCfg::default(), 50)
}
fn make_result(id: &str) -> SearchResult {
SearchResult {
id: ResultId::new(id),
title: ResultTitle::new(id),
description: None,
icon: None,
score: Score::new(100),
action: LaunchAction::CopyToClipboard(id.to_string()),
}
}
fn make_state_with_engine() -> LauncherState {
let kernel = Arc::new(Kernel::new(vec![], 10));
let mut state = make_state();
let _ = state.handle(Action::EngineReady(kernel));
state
}
#[test]
fn move_down_clamps_to_last_result() {
let mut state = make_state();
state.handle(Action::ResultsReady {
epoch: 0,
results: vec![make_result("a"), make_result("b"), make_result("c")],
});
state.handle(Action::MoveDown);
state.handle(Action::MoveDown);
state.handle(Action::MoveDown);
state.handle(Action::MoveDown);
assert_eq!(state.selected(), 2);
}
#[test]
fn move_up_does_not_go_below_zero() {
let mut state = make_state();
state.handle(Action::ResultsReady {
epoch: 0,
results: vec![make_result("a"), make_result("b")],
});
state.handle(Action::MoveUp);
state.handle(Action::MoveUp);
assert_eq!(state.selected(), 0);
}
#[test]
fn query_changed_resets_selected() {
let mut state = make_state_with_engine();
state.handle(Action::ResultsReady {
epoch: state.search_epoch(),
results: vec![make_result("a"), make_result("b"), make_result("c")],
});
state.handle(Action::MoveDown);
state.handle(Action::MoveDown);
assert_eq!(state.selected(), 2);
state.handle(Action::QueryChanged("new".to_string()));
assert_eq!(state.selected(), 0);
}
#[test]
fn engine_ready_returns_trigger_search() {
let mut state = make_state();
let kernel = Arc::new(Kernel::new(vec![], 10));
let effect = state.handle(Action::EngineReady(kernel));
assert!(matches!(effect, Effect::TriggerSearch(_)));
}
#[test]
fn launch_selected_with_no_results_returns_exit() {
let mut state = make_state_with_engine();
let effect = state.handle(Action::LaunchSelected);
assert!(matches!(effect, Effect::Exit));
}
#[test]
fn results_ready_with_wrong_epoch_is_ignored() {
let mut state = make_state_with_engine();
let current_epoch = state.search_epoch();
state.handle(Action::ResultsReady {
epoch: current_epoch + 999,
results: vec![make_result("stale")],
});
assert!(state.results().is_empty());
}
#[test]
fn query_changed_without_engine_returns_none() {
let mut state = make_state();
let effect = state.handle(Action::QueryChanged("hello".to_string()));
assert!(matches!(effect, Effect::None));
assert_eq!(state.query(), "hello");
}
#[test]
fn query_changed_with_engine_returns_search_after_debounce() {
let mut state = make_state_with_engine();
let effect = state.handle(Action::QueryChanged("test".to_string()));
match effect {
Effect::SearchAfterDebounce {
query,
debounce_ms,
epoch,
} => {
assert_eq!(query, "test");
assert_eq!(debounce_ms, 50);
assert_eq!(epoch, state.search_epoch());
}
_ => panic!("expected SearchAfterDebounce"),
}
}
#[test]
fn launch_selected_with_results_returns_launch_and_exit() {
let mut state = make_state_with_engine();
state.handle(Action::ResultsReady {
epoch: state.search_epoch(),
results: vec![make_result("app1")],
});
let effect = state.handle(Action::LaunchSelected);
assert!(matches!(effect, Effect::LaunchAndExit(_)));
}
#[test]
fn engine_init_failed_sets_error() {
let mut state = make_state();
state.handle(Action::EngineInitFailed("boom".to_string()));
assert_eq!(state.error(), Some("boom"));
}