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
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
[package]
|
||||
name = "k-launcher-os-bridge"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
k-launcher-kernel = { path = "../k-launcher-kernel" }
|
||||
k-launcher-domain = { workspace = true }
|
||||
libc = "0.2"
|
||||
tracing = { workspace = true }
|
||||
|
||||
50
crates/k-launcher-os-bridge/src/launcher.rs
Normal file
50
crates/k-launcher-os-bridge/src/launcher.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use k_launcher_domain::{AppLauncher, LaunchAction};
|
||||
|
||||
use crate::shell::shell_split;
|
||||
use crate::spawn::{copy_to_clipboard, open_path, spawn_detached};
|
||||
use crate::terminal::resolve_terminal;
|
||||
|
||||
pub struct UnixAppLauncher {
|
||||
terminal_cmd: Option<String>,
|
||||
}
|
||||
|
||||
impl UnixAppLauncher {
|
||||
pub fn new(terminal_cmd: Option<String>) -> Self {
|
||||
Self { terminal_cmd }
|
||||
}
|
||||
}
|
||||
|
||||
impl AppLauncher for UnixAppLauncher {
|
||||
fn execute(&self, action: &LaunchAction) {
|
||||
match action {
|
||||
LaunchAction::SpawnProcess(cmd) => spawn_command(cmd),
|
||||
LaunchAction::SpawnInTerminal(cmd) => {
|
||||
spawn_in_terminal(cmd, self.terminal_cmd.as_deref())
|
||||
}
|
||||
LaunchAction::OpenPath(path) => open_path(path),
|
||||
LaunchAction::CopyToClipboard(val) => copy_to_clipboard(val),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_command(cmd: &str) {
|
||||
let parts = shell_split(cmd);
|
||||
if let Some((bin, args)) = parts.split_first() {
|
||||
spawn_detached(bin, args);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_in_terminal(cmd: &str, configured: Option<&str>) {
|
||||
let Some(terminal) = resolve_terminal(configured) else {
|
||||
return;
|
||||
};
|
||||
let mut args = terminal.exec_flag;
|
||||
const SHELL: &str = "sh";
|
||||
const SHELL_CMD_FLAG: &str = "-c";
|
||||
args.extend([
|
||||
SHELL.to_string(),
|
||||
SHELL_CMD_FLAG.to_string(),
|
||||
cmd.to_string(),
|
||||
]);
|
||||
spawn_detached(&terminal.bin, &args);
|
||||
}
|
||||
@@ -1,2 +1,7 @@
|
||||
mod unix_launcher;
|
||||
pub use unix_launcher::UnixAppLauncher;
|
||||
mod launcher;
|
||||
mod shell;
|
||||
mod spawn;
|
||||
mod terminal;
|
||||
|
||||
pub use launcher::UnixAppLauncher;
|
||||
pub use shell::shell_split;
|
||||
|
||||
22
crates/k-launcher-os-bridge/src/shell.rs
Normal file
22
crates/k-launcher-os-bridge/src/shell.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
pub fn shell_split(cmd: &str) -> Vec<String> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut in_quotes = false;
|
||||
|
||||
for ch in cmd.chars() {
|
||||
match ch {
|
||||
'"' => in_quotes = !in_quotes,
|
||||
' ' | '\t' if !in_quotes => {
|
||||
if !current.is_empty() {
|
||||
tokens.push(current.clone());
|
||||
current.clear();
|
||||
}
|
||||
}
|
||||
_ => current.push(ch),
|
||||
}
|
||||
}
|
||||
if !current.is_empty() {
|
||||
tokens.push(current);
|
||||
}
|
||||
tokens
|
||||
}
|
||||
49
crates/k-launcher-os-bridge/src/spawn.rs
Normal file
49
crates/k-launcher-os-bridge/src/spawn.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use std::io::Write;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
const XDG_OPEN: &str = "xdg-open";
|
||||
const WL_COPY: &str = "wl-copy";
|
||||
const XCLIP: &str = "xclip";
|
||||
|
||||
pub(crate) fn spawn_detached(bin: &str, args: &[String]) {
|
||||
// SAFETY: setsid() is async-signal-safe; called in forked child before exec
|
||||
if let Err(e) = unsafe {
|
||||
Command::new(bin)
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.pre_exec(|| {
|
||||
libc::setsid();
|
||||
Ok(())
|
||||
})
|
||||
.spawn()
|
||||
} {
|
||||
tracing::warn!("failed to spawn detached process '{bin}': {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn open_path(path: &str) {
|
||||
if let Err(e) = Command::new(XDG_OPEN).arg(path).spawn() {
|
||||
tracing::warn!("failed to open path '{path}': {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn copy_to_clipboard(val: &str) {
|
||||
if Command::new(WL_COPY).arg(val).spawn().is_err() {
|
||||
copy_to_clipboard_xclip(val);
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_to_clipboard_xclip(val: &str) {
|
||||
if let Ok(mut child) = Command::new(XCLIP)
|
||||
.args(["-selection", "clipboard"])
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
&& let Some(stdin) = child.stdin.as_mut()
|
||||
&& let Err(e) = stdin.write_all(val.as_bytes())
|
||||
{
|
||||
tracing::warn!("failed to write to xclip stdin: {e}");
|
||||
}
|
||||
}
|
||||
86
crates/k-launcher-os-bridge/src/terminal.rs
Normal file
86
crates/k-launcher-os-bridge/src/terminal.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
pub(crate) struct TerminalCommand {
|
||||
pub bin: String,
|
||||
pub exec_flag: Vec<String>,
|
||||
}
|
||||
|
||||
struct KnownTerminal {
|
||||
bin: &'static str,
|
||||
exec_flag: &'static str,
|
||||
}
|
||||
|
||||
const KNOWN_TERMINALS: &[KnownTerminal] = &[
|
||||
KnownTerminal {
|
||||
bin: "foot",
|
||||
exec_flag: "-e",
|
||||
},
|
||||
KnownTerminal {
|
||||
bin: "kitty",
|
||||
exec_flag: "-e",
|
||||
},
|
||||
KnownTerminal {
|
||||
bin: "alacritty",
|
||||
exec_flag: "-e",
|
||||
},
|
||||
KnownTerminal {
|
||||
bin: "wezterm",
|
||||
exec_flag: "start",
|
||||
},
|
||||
KnownTerminal {
|
||||
bin: "konsole",
|
||||
exec_flag: "-e",
|
||||
},
|
||||
KnownTerminal {
|
||||
bin: "xterm",
|
||||
exec_flag: "-e",
|
||||
},
|
||||
];
|
||||
|
||||
fn find_in_path(bin: &str) -> bool {
|
||||
std::env::var_os("PATH")
|
||||
.iter()
|
||||
.flat_map(|p| std::env::split_paths(p))
|
||||
.any(|dir| dir.join(bin).is_file())
|
||||
}
|
||||
|
||||
fn parse_term_cmd(s: &str) -> TerminalCommand {
|
||||
let mut parts = s.split_whitespace();
|
||||
let bin = parts.next().unwrap_or("").to_string();
|
||||
let exec_flag = parts.map(str::to_string).collect();
|
||||
TerminalCommand { bin, exec_flag }
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_terminal(configured: Option<&str>) -> Option<TerminalCommand> {
|
||||
if let Some(cmd) = configured.filter(|s| !s.is_empty()) {
|
||||
let term = parse_term_cmd(cmd);
|
||||
if !term.bin.is_empty() {
|
||||
return Some(term);
|
||||
}
|
||||
}
|
||||
if let Ok(val) = std::env::var("TERM_CMD") {
|
||||
let val = val.trim().to_string();
|
||||
if !val.is_empty() {
|
||||
let term = parse_term_cmd(&val);
|
||||
if !term.bin.is_empty() {
|
||||
return Some(term);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(val) = std::env::var("TERMINAL") {
|
||||
let bin = val.trim().to_string();
|
||||
if !bin.is_empty() {
|
||||
return Some(TerminalCommand {
|
||||
bin,
|
||||
exec_flag: vec!["-e".to_string()],
|
||||
});
|
||||
}
|
||||
}
|
||||
for terminal in KNOWN_TERMINALS {
|
||||
if find_in_path(terminal.bin) {
|
||||
return Some(TerminalCommand {
|
||||
bin: terminal.bin.to_string(),
|
||||
exec_flag: vec![terminal.exec_flag.to_string()],
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use k_launcher_kernel::{AppLauncher, LaunchAction};
|
||||
|
||||
fn shell_split(cmd: &str) -> Vec<String> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut in_quotes = false;
|
||||
|
||||
for ch in cmd.chars() {
|
||||
match ch {
|
||||
'"' => in_quotes = !in_quotes,
|
||||
' ' | '\t' if !in_quotes => {
|
||||
if !current.is_empty() {
|
||||
tokens.push(current.clone());
|
||||
current.clear();
|
||||
}
|
||||
}
|
||||
_ => current.push(ch),
|
||||
}
|
||||
}
|
||||
if !current.is_empty() {
|
||||
tokens.push(current);
|
||||
}
|
||||
tokens
|
||||
}
|
||||
|
||||
fn parse_term_cmd(s: &str) -> (String, Vec<String>) {
|
||||
let mut parts = s.split_whitespace();
|
||||
let bin = parts.next().unwrap_or("").to_string();
|
||||
let args = parts.map(str::to_string).collect();
|
||||
(bin, args)
|
||||
}
|
||||
|
||||
fn which(bin: &str) -> bool {
|
||||
Command::new("which")
|
||||
.arg(bin)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn resolve_terminal() -> Option<(String, Vec<String>)> {
|
||||
if let Ok(val) = std::env::var("TERM_CMD") {
|
||||
let val = val.trim().to_string();
|
||||
if !val.is_empty() {
|
||||
let (bin, args) = parse_term_cmd(&val);
|
||||
if !bin.is_empty() {
|
||||
return Some((bin, args));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(val) = std::env::var("TERMINAL") {
|
||||
let bin = val.trim().to_string();
|
||||
if !bin.is_empty() {
|
||||
return Some((bin, vec!["-e".to_string()]));
|
||||
}
|
||||
}
|
||||
for (bin, flag) in &[
|
||||
("foot", "-e"),
|
||||
("kitty", "-e"),
|
||||
("alacritty", "-e"),
|
||||
("wezterm", "start"),
|
||||
("konsole", "-e"),
|
||||
("xterm", "-e"),
|
||||
] {
|
||||
if which(bin) {
|
||||
return Some((bin.to_string(), vec![flag.to_string()]));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub struct UnixAppLauncher;
|
||||
|
||||
impl UnixAppLauncher {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UnixAppLauncher {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AppLauncher for UnixAppLauncher {
|
||||
fn execute(&self, action: &LaunchAction) {
|
||||
match action {
|
||||
LaunchAction::SpawnProcess(cmd) => {
|
||||
let parts = shell_split(cmd);
|
||||
if let Some((bin, args)) = parts.split_first() {
|
||||
let _ = unsafe {
|
||||
Command::new(bin)
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.pre_exec(|| {
|
||||
libc::setsid();
|
||||
Ok(())
|
||||
})
|
||||
.spawn()
|
||||
};
|
||||
}
|
||||
}
|
||||
LaunchAction::SpawnInTerminal(cmd) => {
|
||||
let Some((term_bin, term_args)) = resolve_terminal() else {
|
||||
return;
|
||||
};
|
||||
let _ = unsafe {
|
||||
Command::new(&term_bin)
|
||||
.args(&term_args)
|
||||
.arg("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.pre_exec(|| {
|
||||
libc::setsid();
|
||||
Ok(())
|
||||
})
|
||||
.spawn()
|
||||
};
|
||||
}
|
||||
LaunchAction::OpenPath(path) => {
|
||||
let _ = Command::new("xdg-open").arg(path).spawn();
|
||||
}
|
||||
LaunchAction::CopyToClipboard(val) => {
|
||||
if Command::new("wl-copy").arg(val).spawn().is_err() {
|
||||
use std::io::Write;
|
||||
if let Ok(mut child) = Command::new("xclip")
|
||||
.args(["-selection", "clipboard"])
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
&& let Some(stdin) = child.stdin.as_mut()
|
||||
{
|
||||
let _ = stdin.write_all(val.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::shell_split;
|
||||
|
||||
#[test]
|
||||
fn split_simple() {
|
||||
assert_eq!(shell_split("firefox"), vec!["firefox"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_with_args() {
|
||||
assert_eq!(
|
||||
shell_split("firefox --new-window"),
|
||||
vec!["firefox", "--new-window"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_quoted_path() {
|
||||
assert_eq!(shell_split(r#""My App" --flag"#), vec!["My App", "--flag"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_quoted_with_spaces() {
|
||||
assert_eq!(
|
||||
shell_split(r#"env "FOO BAR" baz"#),
|
||||
vec!["env", "FOO BAR", "baz"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_empty() {
|
||||
assert!(shell_split("").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_extra_whitespace() {
|
||||
assert_eq!(shell_split(" a b "), vec!["a", "b"]);
|
||||
}
|
||||
}
|
||||
37
crates/k-launcher-os-bridge/tests/shell_split.rs
Normal file
37
crates/k-launcher-os-bridge/tests/shell_split.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use k_launcher_os_bridge::shell_split;
|
||||
|
||||
#[test]
|
||||
fn split_simple() {
|
||||
assert_eq!(shell_split("firefox"), vec!["firefox"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_with_args() {
|
||||
assert_eq!(
|
||||
shell_split("firefox --new-window"),
|
||||
vec!["firefox", "--new-window"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_quoted_path() {
|
||||
assert_eq!(shell_split(r#""My App" --flag"#), vec!["My App", "--flag"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_quoted_with_spaces() {
|
||||
assert_eq!(
|
||||
shell_split(r#"env "FOO BAR" baz"#),
|
||||
vec!["env", "FOO BAR", "baz"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_empty() {
|
||||
assert!(shell_split("").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_extra_whitespace() {
|
||||
assert_eq!(shell_split(" a b "), vec!["a", "b"]);
|
||||
}
|
||||
Reference in New Issue
Block a user