diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..1206e91 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,110 @@ +# k-launcher Architecture + +## Philosophy + +- **TDD:** Red-Green-Refactor is mandatory. No functional code without a failing test first. +- **Clean Architecture:** Strict layer separation — Domain, Application, Infrastructure, Main. +- **Newtype Pattern:** All domain primitives wrapped (e.g. `struct Score(f64)`). +- **Small Traits / ISP:** Many focused traits over one "God" trait. +- **No Cyclic Dependencies:** Use IoC (define traits in higher-level modules, implement in lower-level). + +--- + +## Workspace Structure + +| Crate | Layer | Responsibility | +|---|---|---| +| `k-launcher-kernel` | Domain + Application | Newtypes (`ResultId`, `ResultTitle`, `Score`), `Plugin` trait, `SearchEngine` trait, `AppLauncher` port, `Kernel` use case | +| `k-launcher-config` | Infrastructure | TOML config loading; `Config`, `WindowCfg`, `AppearanceCfg`, `PluginsCfg` structs | +| `k-launcher-os-bridge` | Infrastructure | `UnixAppLauncher` (process spawning), `WindowConfig` adapter | +| `k-launcher-plugin-host` | Infrastructure | `ExternalPlugin` — JSON-newline IPC protocol for out-of-process plugins | +| `k-launcher-ui` | Infrastructure | iced 0.14 Elm-like UI (`KLauncherApp`, debounced async search, keyboard nav) | +| `k-launcher-ui-egui` | Infrastructure | Alternative egui UI (feature-gated) | +| `plugins/plugin-apps` | Infrastructure | XDG `.desktop` parser, frecency scoring, nucleo fuzzy matching | +| `plugins/plugin-calc` | Infrastructure | `evalexpr`-based calculator | +| `plugins/plugin-cmd` | Infrastructure | Shell command runner | +| `plugins/plugin-files` | Infrastructure | File path search | +| `plugins/plugin-url` | Infrastructure | URL opener | +| `k-launcher` | Main/Entry | DI wiring, CLI arg parsing (`show` command), `run_ui()` composition root | + +--- + +## Dependency Graph + +``` +k-launcher (main) + ├── k-launcher-kernel (Domain/Application) + ├── k-launcher-config (Infrastructure — pure data, no kernel dep) + ├── k-launcher-os-bridge (Infrastructure) + ├── k-launcher-plugin-host (Infrastructure) + ├── k-launcher-ui (Infrastructure) + └── plugins/* (Infrastructure) + └── k-launcher-kernel +``` + +All arrows point inward toward the kernel. The kernel has no external dependencies. + +--- + +## Core Abstractions (kernel) + +```rust +// Plugin trait — implemented by every plugin +async fn search(&self, query: &str) -> Vec; + +// SearchEngine trait — implemented by Kernel +async fn search(&self, query: &str) -> Vec; + +// AppLauncher port — implemented by UnixAppLauncher in os-bridge +fn execute(&self, action: &LaunchAction); + +// DesktopEntrySource trait (plugin-apps) — swappable .desktop file source +``` + +--- + +## Plugin System + +Two kinds of plugins: + +1. **In-process** — implement `Plugin` in Rust, linked at compile time. + - `plugin-calc`, `plugin-apps`, `plugin-cmd`, `plugin-files`, `plugin-url` + +2. **External / out-of-process** — `ExternalPlugin` in `k-launcher-plugin-host` communicates via JSON newline protocol over stdin/stdout. + - Query: `{"query": "..."}` + - Response: `[{"id": "...", "title": "...", "score": 1.0, "description": "...", "icon": "...", "action": "..."}]` + +Plugins are enabled/disabled via `~/.config/k-launcher/config.toml`. + +--- + +## Kernel (Application Use Case) + +`Kernel::search` fans out to all registered plugins concurrently via `join_all`, merges results, sorts by `Score` descending, truncates to `max_results`. + +--- + +## UI Architecture (iced 0.14 — Elm model) + +- **State:** `KLauncherApp` holds engine ref, launcher ref, query string, results, selected index, appearance config. +- **Messages:** `QueryChanged`, `ResultsReady`, `KeyPressed` +- **Update:** + - `QueryChanged` → spawns debounced async task (50 ms) → `ResultsReady` + - Epoch guard prevents stale results from out-of-order responses +- **View:** search bar + scrollable result list with icon support (SVG/raster) +- **Subscription:** keyboard events — `Esc` = quit, `Enter` = launch, arrows = navigate +- **Window:** transparent, undecorated, centered (Wayland-compatible) + +--- + +## Frecency (plugin-apps) + +`FrecencyStore` records app launches by ID. On empty query, returns top-5 frecent apps instead of search results. + +--- + +## Configuration + +`~/.config/k-launcher/config.toml` — sections: `[window]`, `[appearance]`, `[search]`, `[plugins]`. + +All fields have sane defaults; a missing file yields defaults without error. diff --git a/crates/k-launcher/src/client.rs b/crates/k-launcher/src/client.rs deleted file mode 100644 index 658b3d2..0000000 --- a/crates/k-launcher/src/client.rs +++ /dev/null @@ -1,10 +0,0 @@ -use std::io::Write; - -pub fn send_show() -> Result<(), Box> { - let runtime_dir = - std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/run/user/1000".to_string()); - let socket_path = format!("{runtime_dir}/k-launcher.sock"); - let mut stream = std::os::unix::net::UnixStream::connect(&socket_path)?; - stream.write_all(b"show\n")?; - Ok(()) -} diff --git a/crates/k-launcher/src/main.rs b/crates/k-launcher/src/main.rs index 76d0189..73ef394 100644 --- a/crates/k-launcher/src/main.rs +++ b/crates/k-launcher/src/main.rs @@ -1,5 +1,3 @@ -mod client; - use std::sync::Arc; use k_launcher_kernel::Kernel; @@ -15,15 +13,6 @@ use plugin_files::FilesPlugin; fn main() { tracing_subscriber::fmt::init(); - let args: Vec = std::env::args().collect(); - if args.get(1).map(|s| s.as_str()) == Some("show") { - if let Err(e) = client::send_show() { - eprintln!("error: failed to send show command: {e}"); - std::process::exit(1); - } - return; - } - if let Err(e) = run_ui() { eprintln!("error: UI: {e}"); std::process::exit(1);