From 0a4a09037ece73ad8e100bd0d6f3ad0ee378eef7 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Wed, 13 May 2026 02:05:26 +0200 Subject: [PATCH] refactor(v2): collapse owlry-core into owlry single crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspace shrinks from 2 members to 1. The daemon, IPC layer, providers, config, frecency store, GTK4 UI, and CLI now live in a single `crates/owlry` crate exposing both a library (so integration tests can reach daemon types) and a binary. Structural changes: - crates/owlry-core/ deleted; all source moved into crates/owlry/src/ via git mv to preserve history - crates/owlry/src/lib.rs added with module declarations - crates/owlry/src/main.rs rewritten as thin entry that uses owlry::* - crates/owlry/src/providers/mod.rs absorbs owlry-core's providers/mod.rs and pulls dmenu into the same module tree - All owlry_core:: refs in src/ rewritten to crate:: - All owlry_core:: refs in tests/ rewritten to owlry:: - systemd/owlryd.service: ExecStart=/usr/bin/owlry -d (single binary) - justfile: drop owlry-core/owlry-lua/owlry-rune build steps; daemon runs via 'cargo run -p owlry -- -d' - owlry version: 1.0.10 -> 2.0.0-dev Tests: 178 still pass (156 lib + 14 ipc + 8 server). No test changes needed — moved files retained their inline test modules. Task #2 complete. --- Cargo.lock | 26 +- Cargo.toml | 1 - crates/owlry-core/Cargo.toml | 51 - crates/owlry-core/src/lib.rs | 8 - crates/owlry-core/src/main.rs | 33 - crates/owlry-core/src/providers/mod.rs | 1096 ----------------- crates/owlry/Cargo.toml | 50 +- crates/owlry/src/app.rs | 12 +- crates/owlry/src/backend.rs | 10 +- crates/owlry/src/cli.rs | 2 +- crates/owlry/src/client.rs | 6 +- .../{owlry-core => owlry}/src/config/mod.rs | 0 .../src/data/frecency.rs | 0 crates/{owlry-core => owlry}/src/data/mod.rs | 0 crates/{owlry-core => owlry}/src/filter.rs | 0 crates/{owlry-core => owlry}/src/ipc.rs | 0 crates/owlry/src/lib.rs | 20 + crates/owlry/src/main.rs | 34 +- crates/{owlry-core => owlry}/src/notify.rs | 0 crates/{owlry-core => owlry}/src/paths.rs | 0 .../src/providers/application.rs | 0 .../src/providers/calculator.rs | 0 .../src/providers/command.rs | 0 .../src/providers/converter/currency.rs | 0 .../src/providers/converter/mod.rs | 0 .../src/providers/converter/parser.rs | 0 .../src/providers/converter/units.rs | 0 crates/owlry/src/providers/dmenu.rs | 2 +- crates/owlry/src/providers/mod.rs | 1096 +++++++++++++++++ .../src/providers/system.rs | 0 crates/{owlry-core => owlry}/src/server.rs | 0 crates/owlry/src/theme.rs | 2 +- crates/owlry/src/ui/main_window.rs | 20 +- crates/owlry/src/ui/provider_meta.rs | 4 +- crates/owlry/src/ui/result_row.rs | 10 +- crates/owlry/src/ui/submenu.rs | 4 +- .../{owlry-core => owlry}/tests/ipc_test.rs | 2 +- .../tests/server_test.rs | 4 +- docs/RESTRUCTURE-V2.md | 3 +- justfile | 30 +- systemd/owlryd.service | 2 +- 41 files changed, 1223 insertions(+), 1305 deletions(-) delete mode 100644 crates/owlry-core/Cargo.toml delete mode 100644 crates/owlry-core/src/lib.rs delete mode 100644 crates/owlry-core/src/main.rs delete mode 100644 crates/owlry-core/src/providers/mod.rs rename crates/{owlry-core => owlry}/src/config/mod.rs (100%) rename crates/{owlry-core => owlry}/src/data/frecency.rs (100%) rename crates/{owlry-core => owlry}/src/data/mod.rs (100%) rename crates/{owlry-core => owlry}/src/filter.rs (100%) rename crates/{owlry-core => owlry}/src/ipc.rs (100%) create mode 100644 crates/owlry/src/lib.rs rename crates/{owlry-core => owlry}/src/notify.rs (100%) rename crates/{owlry-core => owlry}/src/paths.rs (100%) rename crates/{owlry-core => owlry}/src/providers/application.rs (100%) rename crates/{owlry-core => owlry}/src/providers/calculator.rs (100%) rename crates/{owlry-core => owlry}/src/providers/command.rs (100%) rename crates/{owlry-core => owlry}/src/providers/converter/currency.rs (100%) rename crates/{owlry-core => owlry}/src/providers/converter/mod.rs (100%) rename crates/{owlry-core => owlry}/src/providers/converter/parser.rs (100%) rename crates/{owlry-core => owlry}/src/providers/converter/units.rs (100%) rename crates/{owlry-core => owlry}/src/providers/system.rs (100%) rename crates/{owlry-core => owlry}/src/server.rs (100%) rename crates/{owlry-core => owlry}/tests/ipc_test.rs (98%) rename crates/{owlry-core => owlry}/tests/server_test.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index 752b379..e975510 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1823,33 +1823,21 @@ dependencies = [ [[package]] name = "owlry" -version = "1.0.10" -dependencies = [ - "clap", - "env_logger", - "futures-channel", - "glib-build-tools", - "gtk4", - "gtk4-layer-shell", - "libc", - "log", - "owlry-core", - "serde", - "serde_json", - "toml 0.8.23", -] - -[[package]] -name = "owlry-core" -version = "1.3.6" +version = "2.0.0-dev" dependencies = [ "chrono", + "clap", "dirs", "env_logger", "expr-solver-lib", "freedesktop-desktop-entry", "fs2", + "futures-channel", "fuzzy-matcher", + "glib-build-tools", + "gtk4", + "gtk4-layer-shell", + "libc", "log", "notify-rust", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 8010327..49ea3b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ resolver = "2" members = [ "crates/owlry", - "crates/owlry-core", ] # Shared workspace settings diff --git a/crates/owlry-core/Cargo.toml b/crates/owlry-core/Cargo.toml deleted file mode 100644 index 9a406f1..0000000 --- a/crates/owlry-core/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "owlry-core" -version = "1.3.6" -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -description = "Core daemon for the Owlry application launcher" - -[lib] -name = "owlry_core" -path = "src/lib.rs" - -[[bin]] -name = "owlryd" -path = "src/main.rs" - -[dependencies] -# Provider system -fuzzy-matcher = "0.3" -freedesktop-desktop-entry = "0.8" - -# Data & config -serde = { version = "1", features = ["derive"] } -serde_json = "1" -toml = "0.8" -fs2 = "0.4" -chrono = { version = "0.4", features = ["serde"] } -dirs = "5" - -# Error handling -thiserror = "2" - -# Signal handling -signal-hook = "0.3" - -# Logging & notifications -log = "0.4" -env_logger = "0.11" -notify-rust = "4" - -# Built-in providers -expr-solver-lib = "1" -reqwest = { version = "0.13", default-features = false, features = ["native-tls", "json", "blocking"] } - -[dev-dependencies] -tempfile = "3" - -[features] -default = [] -dev-logging = [] diff --git a/crates/owlry-core/src/lib.rs b/crates/owlry-core/src/lib.rs deleted file mode 100644 index 3ef0be7..0000000 --- a/crates/owlry-core/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod config; -pub mod data; -pub mod filter; -pub mod ipc; -pub mod notify; -pub mod paths; -pub mod providers; -pub mod server; diff --git a/crates/owlry-core/src/main.rs b/crates/owlry-core/src/main.rs deleted file mode 100644 index 8931098..0000000 --- a/crates/owlry-core/src/main.rs +++ /dev/null @@ -1,33 +0,0 @@ -use log::info; - -use owlry_core::paths; -use owlry_core::server::Server; - -fn main() { - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); - - let sock = paths::socket_path(); - info!("Starting owlryd daemon..."); - - // Ensure the socket parent directory exists - if let Err(e) = paths::ensure_parent_dir(&sock) { - eprintln!("Failed to create socket directory: {e}"); - std::process::exit(1); - } - - let server = match Server::bind(&sock) { - Ok(s) => s, - Err(e) => { - eprintln!("Failed to start owlryd: {e}"); - std::process::exit(1); - } - }; - - // SIGTERM/SIGINT are handled inside Server::run() via signal-hook, - // which saves frecency before exiting. - - if let Err(e) = server.run() { - eprintln!("Server error: {e}"); - std::process::exit(1); - } -} diff --git a/crates/owlry-core/src/providers/mod.rs b/crates/owlry-core/src/providers/mod.rs deleted file mode 100644 index 7e6c75e..0000000 --- a/crates/owlry-core/src/providers/mod.rs +++ /dev/null @@ -1,1096 +0,0 @@ -// Core providers (compiled in) -mod application; -mod command; -pub(crate) mod calculator; -pub(crate) mod converter; -pub(crate) mod system; - -// Re-exports for core providers -pub use application::ApplicationProvider; -pub use command::CommandProvider; - -use chrono::Utc; -use fuzzy_matcher::FuzzyMatcher; -use fuzzy_matcher::skim::SkimMatcherV2; -use log::info; - -#[cfg(feature = "dev-logging")] -use log::debug; - -use std::sync::{Arc, RwLock}; - -use crate::config::Config; -use crate::data::FrecencyStore; - -/// Where a provider sits in the UI. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ProviderPosition { - /// Normal results list. - Normal, - /// Widget — rendered at the top of the results, outside the normal flow. - Widget, -} - -impl ProviderPosition { - pub fn as_str(&self) -> &'static str { - match self { - ProviderPosition::Normal => "normal", - ProviderPosition::Widget => "widget", - } - } -} - -/// Metadata descriptor for an available provider (used by IPC/daemon API) -#[derive(Debug, Clone)] -pub struct ProviderDescriptor { - pub id: String, - pub name: String, - pub prefix: Option, - pub icon: String, - pub position: String, - pub tab_label: Option, - pub search_noun: Option, -} - -/// Trust level of a [`LaunchItem`]'s command, used to gate `sh -c` execution. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ItemSource { - /// Built-in provider compiled into the binary (trusted). - Core, - /// Script-defined provider (Lua, in Phase 3+) — user-installed, untrusted. - ScriptPlugin, -} - -impl ItemSource { - pub fn as_str(&self) -> &'static str { - match self { - ItemSource::Core => "core", - ItemSource::ScriptPlugin => "script_plugin", - } - } -} - -impl std::str::FromStr for ItemSource { - type Err = (); - fn from_str(s: &str) -> Result { - match s { - "script_plugin" => Ok(ItemSource::ScriptPlugin), - _ => Ok(ItemSource::Core), - } - } -} - -/// Represents a single searchable/launchable item -#[derive(Debug, Clone)] -pub struct LaunchItem { - #[allow(dead_code)] - pub id: String, - pub name: String, - pub description: Option, - pub icon: Option, - pub provider: ProviderType, - pub command: String, - pub terminal: bool, - /// Tags/categories for filtering (e.g., from .desktop Categories) - pub tags: Vec, - /// Trust level — gates `sh -c` execution for script plugin items. - pub source: ItemSource, -} - -/// Provider type identifier for filtering and badge display. -/// -/// - `Application`, `Command`, `Dmenu`: built-in core providers -/// - `Plugin(type_id)`: any other provider (compiled-in feature module or -/// future Lua-registered provider). The `type_id` is the provider's -/// stable identifier (e.g. `"bookmarks"`, `"uuctl"`, `"calc"`). -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum ProviderType { - Application, - Command, - Dmenu, - Plugin(String), -} - -impl std::str::FromStr for ProviderType { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "app" | "apps" | "application" | "applications" => Ok(ProviderType::Application), - "cmd" | "cmds" | "command" | "commands" => Ok(ProviderType::Command), - "dmenu" => Ok(ProviderType::Dmenu), - other => Ok(ProviderType::Plugin(other.to_string())), - } - } -} - -impl std::fmt::Display for ProviderType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ProviderType::Application => write!(f, "app"), - ProviderType::Command => write!(f, "cmd"), - ProviderType::Dmenu => write!(f, "dmenu"), - ProviderType::Plugin(type_id) => write!(f, "{}", type_id), - } - } -} - -/// Trait for all search providers. -/// -/// Static providers hold a refreshable item cache. Submenu and action support -/// are optional methods — implement them only if the provider produces -/// `SUBMENU:` items or handles plugin-defined action commands. -pub trait Provider: Send + Sync { - #[allow(dead_code)] - fn name(&self) -> &str; - fn provider_type(&self) -> ProviderType; - fn refresh(&mut self); - fn items(&self) -> &[LaunchItem]; - - /// Short label for UI tab button (e.g., "Shutdown"). None = use default. - fn tab_label(&self) -> Option<&str> { - None - } - /// Noun for search placeholder (e.g., "shutdown actions"). None = use default. - fn search_noun(&self) -> Option<&str> { - None - } - /// Optional search prefix (e.g. ":bm"). None = no prefix. - fn prefix(&self) -> Option<&str> { - None - } - /// Icon name (XDG icon theme). - fn icon(&self) -> &str { - "application-x-addon" - } - /// UI placement. - fn position(&self) -> ProviderPosition { - ProviderPosition::Normal - } - /// Priority hint for ordering. - fn priority(&self) -> u32 { - 0 - } - - /// Generate submenu actions for an item whose command starts with `SUBMENU:`. - /// `data` is everything after the type_id prefix. - /// Default: no submenu support. - fn submenu_actions(&self, _data: &str) -> Vec { - Vec::new() - } - - /// Handle a plugin-defined action command (e.g. `"POMODORO:start"`). - /// Returns true if the command was handled. - /// Default: no action support. - fn execute_action(&self, _command: &str) -> bool { - false - } -} - -/// Trait for built-in providers that produce results per-keystroke. -/// Unlike static `Provider`s which cache items via `refresh()`/`items()`, -/// dynamic providers generate results on every query. -pub trait DynamicProvider: Send + Sync { - #[allow(dead_code)] - fn name(&self) -> &str; - fn provider_type(&self) -> ProviderType; - fn query(&self, query: &str) -> Vec; - fn priority(&self) -> u32; - - /// Handle a plugin action command. Returns true if handled. - fn execute_action(&self, _command: &str) -> bool { - false - } -} - -/// Manages all providers and handles searching. -pub struct ProviderManager { - /// Static providers (apps, commands, systemd, etc.). - providers: Vec>, - /// Dynamic providers (calculator, converter, websearch, filesearch). - /// Queried per-keystroke, not cached. - builtin_dynamic: Vec>, - /// Fuzzy matcher for search. - matcher: SkimMatcherV2, -} - -impl ProviderManager { - /// Create a new ProviderManager from a pre-built list of providers. - /// - /// Used by tests and by the dmenu-mode client. The daemon uses - /// [`Self::new_with_config`] which builds the provider set from config. - pub fn new( - core_providers: Vec>, - dynamic: Vec>, - ) -> Self { - let mut manager = Self { - providers: core_providers, - builtin_dynamic: dynamic, - matcher: SkimMatcherV2::default(), - }; - manager.refresh_all(); - manager - } - - /// Build a ProviderManager for the daemon, sourcing enabled providers from config. - /// - /// Only built-in / compiled-in providers are registered here. Future Lua-defined - /// providers (Phase 3+) are added via [`Self::add_provider`] after construction. - pub fn new_with_config(config: Arc>) -> Self { - let (calc_enabled, conv_enabled, sys_enabled) = match config.read() { - Ok(cfg) => ( - cfg.providers.calculator, - cfg.providers.converter, - cfg.providers.system, - ), - Err(_) => { - log::warn!("Config lock poisoned during provider init; using defaults"); - (true, true, true) - } - }; - - let mut core_providers: Vec> = vec![ - Box::new(ApplicationProvider::new()), - Box::new(CommandProvider::new()), - ]; - - if sys_enabled { - core_providers.push(Box::new(system::SystemProvider::new())); - info!("Registered built-in system provider"); - } - - let mut builtin_dynamic: Vec> = Vec::new(); - if calc_enabled { - builtin_dynamic.push(Box::new(calculator::CalculatorProvider)); - info!("Registered built-in calculator provider"); - } - if conv_enabled { - builtin_dynamic.push(Box::new(converter::ConverterProvider::new())); - info!("Registered built-in converter provider"); - } - - Self::new(core_providers, builtin_dynamic) - } - - #[allow(dead_code)] - pub fn is_dmenu_mode(&self) -> bool { - self.providers - .iter() - .any(|p| p.provider_type() == ProviderType::Dmenu) - } - - pub fn refresh_all(&mut self) { - for provider in &mut self.providers { - provider.refresh(); - info!( - "Provider '{}' loaded {} items", - provider.name(), - provider.items().len() - ); - } - // Dynamic providers don't need refresh (they query on demand). - } - - /// Register an additional provider at runtime. - /// - /// Used by Phase 3+ Lua config to add user-defined providers after the - /// daemon has booted. The provider's `refresh()` is called immediately. - #[allow(dead_code)] - pub fn add_provider(&mut self, mut provider: Box) { - provider.refresh(); - info!("Registered provider: {}", provider.name()); - self.providers.push(provider); - } - - /// Execute a plugin-defined action command. - /// - /// Command format: `PLUGIN_ID:action_data` (e.g. `"POMODORO:start"`). - /// Returns true if a provider handled the command. - pub fn execute_plugin_action(&self, command: &str) -> bool { - for provider in &self.providers { - if provider.execute_action(command) { - return true; - } - } - for provider in &self.builtin_dynamic { - if provider.execute_action(command) { - return true; - } - } - false - } - - #[allow(dead_code)] - pub fn search(&self, query: &str, max_results: usize) -> Vec<(LaunchItem, i64)> { - if query.is_empty() { - return self - .providers - .iter() - .flat_map(|p| p.items().iter().cloned()) - .take(max_results) - .map(|item| (item, 0)) - .collect(); - } - - let mut results: Vec<(LaunchItem, i64)> = self - .providers - .iter() - .flat_map(|p| p.items().iter()) - .filter_map(|item| { - let name_score = self.matcher.fuzzy_match(&item.name, query); - let desc_score = item - .description - .as_ref() - .and_then(|d| self.matcher.fuzzy_match(d, query)); - let score = match (name_score, desc_score) { - (Some(n), Some(d)) => Some(n.max(d)), - (Some(n), None) => Some(n), - (None, Some(d)) => Some(d / 2), - (None, None) => None, - }; - score.map(|s| (item.clone(), s)) - }) - .collect(); - - results.sort_by(|a, b| b.1.cmp(&a.1)); - results.truncate(max_results); - results - } - - /// Search with provider filtering. - #[allow(dead_code)] - pub fn search_filtered( - &self, - query: &str, - max_results: usize, - filter: &crate::filter::ProviderFilter, - tag_filter: Option<&str>, - ) -> Vec<(LaunchItem, i64)> { - let all_items = self - .providers - .iter() - .filter(|p| filter.is_active(p.provider_type())) - .flat_map(|p| p.items().iter().cloned()) - .filter(|item| tag_filter.is_none_or(|t| item.tags.iter().any(|it| it == t))); - - if query.is_empty() { - return all_items.take(max_results).map(|item| (item, 0)).collect(); - } - - let mut results: Vec<(LaunchItem, i64)> = all_items - .filter_map(|item| { - let name_score = self.matcher.fuzzy_match(&item.name, query); - let desc_score = item - .description - .as_ref() - .and_then(|d| self.matcher.fuzzy_match(d, query)); - let score = match (name_score, desc_score) { - (Some(n), Some(d)) => Some(n.max(d)), - (Some(n), None) => Some(n), - (None, Some(d)) => Some(d / 2), - (None, None) => None, - }; - score.map(|s| (item, s)) - }) - .collect(); - - results.sort_by(|a, b| b.1.cmp(&a.1)); - results.truncate(max_results); - results - } - - /// Search with frecency boosting, dynamic providers, and tag filtering. - pub fn search_with_frecency( - &self, - query: &str, - max_results: usize, - filter: &crate::filter::ProviderFilter, - frecency: &FrecencyStore, - frecency_weight: f64, - tag_filter: Option<&str>, - ) -> Vec<(LaunchItem, i64)> { - #[cfg(feature = "dev-logging")] - debug!( - "[Search] query={:?}, max={}, frecency_weight={}", - query, max_results, frecency_weight - ); - - let now = Utc::now(); - let mut results: Vec<(LaunchItem, i64)> = Vec::new(); - - // Widget providers contribute on empty query (no prefix active). - if filter.active_prefix().is_none() && query.is_empty() { - for provider in &self.providers { - if provider.position() != ProviderPosition::Widget { - continue; - } - let base_score = provider.priority() as i64; - for (idx, item) in provider.items().iter().enumerate() { - results.push((item.clone(), base_score - idx as i64)); - } - } - } - - // Dynamic providers (calculator, converter, etc.) — only when there's a query. - if !query.is_empty() { - for provider in &self.builtin_dynamic { - if !filter.is_active(provider.provider_type()) { - continue; - } - let dynamic_results = provider.query(query); - let base_score = provider.priority() as i64; - let grouping_bonus: i64 = match provider.provider_type() { - ProviderType::Plugin(ref id) if matches!(id.as_str(), "calc" | "conv") => { - 10_000 - } - _ => 0, - }; - for (idx, item) in dynamic_results.into_iter().enumerate() { - results.push((item, base_score + grouping_bonus - idx as i64)); - } - } - } - - // Empty query (after widgets) — frecency-sorted items. - if query.is_empty() { - let mut scored_refs: Vec<(&LaunchItem, i64)> = self - .providers - .iter() - .filter(|p| p.position() != ProviderPosition::Widget) - .filter(|p| filter.is_active(p.provider_type())) - .flat_map(|p| p.items().iter()) - .filter(|item| { - if let Some(tag) = tag_filter { - item.tags.iter().any(|t| t.to_lowercase().contains(tag)) - } else { - true - } - }) - .map(|item| { - let frecency_score = frecency.get_score_at(&item.id, now); - let boosted = (frecency_score * frecency_weight * 100.0) as i64; - (item, boosted) - }) - .collect(); - - if scored_refs.len() > max_results { - scored_refs.select_nth_unstable_by(max_results, |a, b| b.1.cmp(&a.1)); - scored_refs.truncate(max_results); - } - scored_refs.sort_by(|a, b| b.1.cmp(&a.1)); - - results.extend(scored_refs.into_iter().map(|(item, score)| (item.clone(), score))); - results.sort_by(|a, b| b.1.cmp(&a.1)); - results.truncate(max_results); - return results; - } - - // Regular search with frecency boost and tag matching. - let score_item = |item: &LaunchItem| -> Option { - if let Some(tag) = tag_filter - && !item.tags.iter().any(|t| t.to_lowercase().contains(tag)) - { - return None; - } - let name_score = self.matcher.fuzzy_match(&item.name, query); - let desc_score = item - .description - .as_ref() - .and_then(|d| self.matcher.fuzzy_match(d, query)); - let tag_score = item - .tags - .iter() - .filter_map(|t| self.matcher.fuzzy_match(t, query)) - .max() - .map(|s| s / 3); - - let base_score = match (name_score, desc_score, tag_score) { - (Some(n), Some(d), Some(t)) => Some(n.max(d).max(t)), - (Some(n), Some(d), None) => Some(n.max(d)), - (Some(n), None, Some(t)) => Some(n.max(t)), - (Some(n), None, None) => Some(n), - (None, Some(d), Some(t)) => Some((d / 2).max(t)), - (None, Some(d), None) => Some(d / 2), - (None, None, Some(t)) => Some(t), - (None, None, None) => None, - }; - - base_score.map(|s| { - let frecency_score = frecency.get_score_at(&item.id, now); - let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64; - let exact_match_boost = if item.name.eq_ignore_ascii_case(query) { - match &item.provider { - ProviderType::Application => 50_000, - _ => 30_000, - } - } else { - 0 - }; - s + frecency_boost + exact_match_boost - }) - }; - - let mut scored_refs: Vec<(&LaunchItem, i64)> = Vec::new(); - for provider in &self.providers { - if provider.position() == ProviderPosition::Widget { - continue; - } - if !filter.is_active(provider.provider_type()) { - continue; - } - for item in provider.items() { - if let Some(score) = score_item(item) { - scored_refs.push((item, score)); - } - } - } - - if scored_refs.len() > max_results { - scored_refs.select_nth_unstable_by(max_results, |a, b| b.1.cmp(&a.1)); - scored_refs.truncate(max_results); - } - scored_refs.sort_by(|a, b| b.1.cmp(&a.1)); - - results.extend(scored_refs.into_iter().map(|(item, score)| (item.clone(), score))); - results.sort_by(|a, b| b.1.cmp(&a.1)); - results.truncate(max_results); - - #[cfg(feature = "dev-logging")] - { - debug!("[Search] Returning {} results", results.len()); - for (i, (item, score)) in results.iter().take(5).enumerate() { - debug!( - "[Search] #{}: {} (score={}, provider={:?})", - i + 1, - item.name, - score, - item.provider - ); - } - if results.len() > 5 { - debug!("[Search] ... and {} more", results.len() - 5); - } - } - - results - } - - /// Get all available provider types (for UI tabs). - #[allow(dead_code)] - pub fn available_provider_types(&self) -> Vec { - self.providers.iter().map(|p| p.provider_type()).collect() - } - - /// Get descriptors for all registered providers. - /// - /// Used by the IPC server to report what providers are available to clients. - pub fn available_providers(&self) -> Vec { - let mut descs = Vec::new(); - - for provider in &self.providers { - let (id, default_prefix, default_icon) = match provider.provider_type() { - ProviderType::Application => ( - "app".to_string(), - Some(":app".to_string()), - "application-x-executable", - ), - ProviderType::Command => ( - "cmd".to_string(), - Some(":cmd".to_string()), - "utilities-terminal", - ), - ProviderType::Dmenu => ("dmenu".to_string(), None, "view-list-symbolic"), - ProviderType::Plugin(type_id) => (type_id, None, "application-x-addon"), - }; - descs.push(ProviderDescriptor { - id, - name: provider.name().to_string(), - prefix: provider.prefix().map(String::from).or(default_prefix), - icon: { - let trait_icon = provider.icon(); - if trait_icon == "application-x-addon" { - default_icon.to_string() - } else { - trait_icon.to_string() - } - }, - position: provider.position().as_str().to_string(), - tab_label: provider.tab_label().map(String::from), - search_noun: provider.search_noun().map(String::from), - }); - } - - descs - } - - /// Refresh a specific provider by its type_id. - pub fn refresh_provider(&mut self, provider_id: &str) { - for provider in &mut self.providers { - let matches = match provider.provider_type() { - ProviderType::Application => provider_id == "app", - ProviderType::Command => provider_id == "cmd", - ProviderType::Dmenu => provider_id == "dmenu", - ProviderType::Plugin(ref id) => provider_id == id, - }; - if matches { - provider.refresh(); - info!("Refreshed provider '{}'", provider.name()); - return; - } - } - info!("Provider '{}' not found for refresh", provider_id); - } - - /// Query a provider for submenu actions. - /// - /// Called when a user selects a `SUBMENU:plugin_id:data` item. The provider's - /// `submenu_actions(data)` is invoked to produce the action list. - pub fn query_submenu_actions( - &self, - plugin_id: &str, - data: &str, - display_name: &str, - ) -> Option<(String, Vec)> { - #[cfg(feature = "dev-logging")] - debug!("[Submenu] Querying provider '{}' with data: {}", plugin_id, data); - - for provider in &self.providers { - let matches = match provider.provider_type() { - ProviderType::Plugin(ref id) => id == plugin_id, - _ => false, - }; - if matches { - let actions = provider.submenu_actions(data); - if !actions.is_empty() { - return Some((display_name.to_string(), actions)); - } - } - } - - #[cfg(feature = "dev-logging")] - debug!("[Submenu] No submenu actions for provider '{}'", plugin_id); - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Minimal mock provider for testing ProviderManager - struct MockProvider { - name: String, - provider_type: ProviderType, - items: Vec, - refresh_count: usize, - } - - impl MockProvider { - fn new(name: &str, provider_type: ProviderType) -> Self { - Self { - name: name.to_string(), - provider_type, - items: Vec::new(), - refresh_count: 0, - } - } - - fn with_items(mut self, items: Vec) -> Self { - self.items = items; - self - } - } - - impl Provider for MockProvider { - fn name(&self) -> &str { - &self.name - } - - fn provider_type(&self) -> ProviderType { - self.provider_type.clone() - } - - fn refresh(&mut self) { - self.refresh_count += 1; - } - - fn items(&self) -> &[LaunchItem] { - &self.items - } - } - - fn make_item(id: &str, name: &str, provider: ProviderType) -> LaunchItem { - LaunchItem { - id: id.to_string(), - name: name.to_string(), - description: None, - icon: None, - provider, - command: format!("run-{}", id), - terminal: false, - tags: Vec::new(), - source: ItemSource::Core, - } - } - - #[test] - fn test_available_providers_core_only() { - let providers: Vec> = vec![ - Box::new(MockProvider::new("Applications", ProviderType::Application)), - Box::new(MockProvider::new("Commands", ProviderType::Command)), - ]; - let pm = ProviderManager::new(providers, Vec::new()); - let descs = pm.available_providers(); - assert_eq!(descs.len(), 2); - assert_eq!(descs[0].id, "app"); - assert_eq!(descs[0].name, "Applications"); - assert_eq!(descs[0].prefix, Some(":app".to_string())); - assert_eq!(descs[0].icon, "application-x-executable"); - assert_eq!(descs[0].position, "normal"); - assert_eq!(descs[1].id, "cmd"); - assert_eq!(descs[1].name, "Commands"); - } - - #[test] - fn test_available_providers_dmenu() { - let providers: Vec> = - vec![Box::new(MockProvider::new("dmenu", ProviderType::Dmenu))]; - let pm = ProviderManager::new(providers, Vec::new()); - let descs = pm.available_providers(); - assert_eq!(descs.len(), 1); - assert_eq!(descs[0].id, "dmenu"); - assert!(descs[0].prefix.is_none()); - } - - #[test] - fn test_available_provider_types() { - let providers: Vec> = vec![ - Box::new(MockProvider::new("Applications", ProviderType::Application)), - Box::new(MockProvider::new("Commands", ProviderType::Command)), - ]; - let pm = ProviderManager::new(providers, Vec::new()); - let types = pm.available_provider_types(); - assert_eq!(types.len(), 2); - assert!(types.contains(&ProviderType::Application)); - assert!(types.contains(&ProviderType::Command)); - } - - #[test] - fn test_refresh_provider_core() { - let app = MockProvider::new("Applications", ProviderType::Application); - let cmd = MockProvider::new("Commands", ProviderType::Command); - let providers: Vec> = vec![Box::new(app), Box::new(cmd)]; - let mut pm = ProviderManager::new(providers, Vec::new()); - - pm.refresh_provider("app"); - pm.refresh_provider("cmd"); - // Just verifying it doesn't panic. - } - - #[test] - fn test_refresh_provider_unknown_does_not_panic() { - let providers: Vec> = vec![Box::new(MockProvider::new( - "Applications", - ProviderType::Application, - ))]; - let mut pm = ProviderManager::new(providers, Vec::new()); - pm.refresh_provider("nonexistent"); - } - - #[test] - fn test_search_with_core_providers() { - let items = vec![ - make_item("firefox", "Firefox", ProviderType::Application), - make_item("vim", "Vim", ProviderType::Application), - ]; - let provider = - MockProvider::new("Applications", ProviderType::Application).with_items(items); - let providers: Vec> = vec![Box::new(provider)]; - let pm = ProviderManager::new(providers, Vec::new()); - - let results = pm.search("fire", 10); - assert_eq!(results.len(), 1); - assert_eq!(results[0].0.name, "Firefox"); - } - - // ========================================================================= - // Tests for behavior introduced in the v2 C-ABI demolition (commit ae4a903) - // ========================================================================= - - /// Provider impl that overrides every trait method to verify customization - /// flows through `ProviderManager::available_providers()` and submenu/action - /// dispatch. - struct RichMockProvider { - type_id: String, - items: Vec, - submenu: Vec, - action_handled_for: Option, - } - - impl Provider for RichMockProvider { - fn name(&self) -> &str { - "Rich" - } - fn provider_type(&self) -> ProviderType { - ProviderType::Plugin(self.type_id.clone()) - } - fn refresh(&mut self) {} - fn items(&self) -> &[LaunchItem] { - &self.items - } - fn prefix(&self) -> Option<&str> { - Some(":rich") - } - fn icon(&self) -> &str { - "rich-icon" - } - fn position(&self) -> ProviderPosition { - ProviderPosition::Widget - } - fn priority(&self) -> u32 { - 42 - } - fn tab_label(&self) -> Option<&str> { - Some("Rich") - } - fn search_noun(&self) -> Option<&str> { - Some("rich things") - } - fn submenu_actions(&self, _data: &str) -> Vec { - self.submenu.clone() - } - fn execute_action(&self, command: &str) -> bool { - self.action_handled_for - .as_ref() - .map(|prefix| command.starts_with(prefix.as_str())) - .unwrap_or(false) - } - } - - #[test] - fn provider_trait_default_methods_return_documented_values() { - // MockProvider does not override any optional method; defaults must hold. - let p = MockProvider::new("M", ProviderType::Application); - assert_eq!(p.prefix(), None); - assert_eq!(p.icon(), "application-x-addon"); - assert_eq!(p.position(), ProviderPosition::Normal); - assert_eq!(p.priority(), 0); - assert_eq!(p.tab_label(), None); - assert_eq!(p.search_noun(), None); - assert!(p.submenu_actions("anything").is_empty()); - assert!(!p.execute_action("MOCK:anything")); - } - - #[test] - fn provider_position_as_str_matches_ipc_strings() { - assert_eq!(ProviderPosition::Normal.as_str(), "normal"); - assert_eq!(ProviderPosition::Widget.as_str(), "widget"); - } - - #[test] - fn provider_type_from_str_accepts_plural_aliases() { - // After the demolition, FromStr accepts both singular and plural aliases. - use std::str::FromStr; - assert_eq!(ProviderType::from_str("app").unwrap(), ProviderType::Application); - assert_eq!(ProviderType::from_str("apps").unwrap(), ProviderType::Application); - assert_eq!( - ProviderType::from_str("application").unwrap(), - ProviderType::Application - ); - assert_eq!( - ProviderType::from_str("applications").unwrap(), - ProviderType::Application - ); - assert_eq!(ProviderType::from_str("cmd").unwrap(), ProviderType::Command); - assert_eq!(ProviderType::from_str("cmds").unwrap(), ProviderType::Command); - assert_eq!( - ProviderType::from_str("command").unwrap(), - ProviderType::Command - ); - assert_eq!( - ProviderType::from_str("commands").unwrap(), - ProviderType::Command - ); - assert_eq!(ProviderType::from_str("dmenu").unwrap(), ProviderType::Dmenu); - // Anything unknown becomes Plugin(s) — preserves user-defined provider IDs. - assert_eq!( - ProviderType::from_str("bookmarks").unwrap(), - ProviderType::Plugin("bookmarks".into()) - ); - assert_eq!( - ProviderType::from_str("uuctl").unwrap(), - ProviderType::Plugin("uuctl".into()) - ); - } - - #[test] - fn item_source_from_str_maps_unknown_to_core() { - // NativePlugin variant is gone; unknown strings (including the old - // "native_plugin" tag from pre-2.0 daemons) decode to Core. - use std::str::FromStr; - assert_eq!(ItemSource::from_str("core"), Ok(ItemSource::Core)); - assert_eq!( - ItemSource::from_str("script_plugin"), - Ok(ItemSource::ScriptPlugin) - ); - assert_eq!(ItemSource::from_str("native_plugin"), Ok(ItemSource::Core)); - assert_eq!(ItemSource::from_str(""), Ok(ItemSource::Core)); - assert_eq!(ItemSource::from_str("anything-else"), Ok(ItemSource::Core)); - } - - #[test] - fn item_source_as_str_only_emits_supported_variants() { - assert_eq!(ItemSource::Core.as_str(), "core"); - assert_eq!(ItemSource::ScriptPlugin.as_str(), "script_plugin"); - } - - #[test] - fn add_provider_refreshes_and_appends() { - let mut pm = ProviderManager::new(Vec::new(), Vec::new()); - assert_eq!(pm.available_provider_types().len(), 0); - - let prov = MockProvider::new("Late", ProviderType::Plugin("late".into())); - pm.add_provider(Box::new(prov)); - - let types = pm.available_provider_types(); - assert_eq!(types.len(), 1); - assert_eq!(types[0], ProviderType::Plugin("late".into())); - // add_provider must call refresh() — checked indirectly by the impl - // contract; refresh_count on MockProvider was bumped, but we can't - // peek through the Box. The public observable is that items() - // is callable without panic, which we exercise here. - assert!(pm.available_providers()[0].id == "late"); - } - - #[test] - fn available_providers_uses_trait_overrides_for_plugin_type() { - let prov = RichMockProvider { - type_id: "rich".into(), - items: Vec::new(), - submenu: Vec::new(), - action_handled_for: None, - }; - let pm = ProviderManager::new(vec![Box::new(prov)], Vec::new()); - let descs = pm.available_providers(); - assert_eq!(descs.len(), 1); - let d = &descs[0]; - assert_eq!(d.id, "rich"); - assert_eq!(d.prefix.as_deref(), Some(":rich")); - assert_eq!(d.icon, "rich-icon"); - assert_eq!(d.position, "widget"); - assert_eq!(d.tab_label.as_deref(), Some("Rich")); - assert_eq!(d.search_noun.as_deref(), Some("rich things")); - } - - #[test] - fn query_submenu_actions_returns_some_when_provider_matches_and_has_actions() { - let prov = RichMockProvider { - type_id: "uuctl".into(), - items: Vec::new(), - submenu: vec![make_item( - "start", - "Start service", - ProviderType::Plugin("uuctl".into()), - )], - action_handled_for: None, - }; - let pm = ProviderManager::new(vec![Box::new(prov)], Vec::new()); - - let result = pm.query_submenu_actions("uuctl", "foo.service:true", "foo"); - assert!(result.is_some(), "expected Some when provider matches and returns actions"); - let (display, actions) = result.unwrap(); - assert_eq!(display, "foo"); - assert_eq!(actions.len(), 1); - assert_eq!(actions[0].name, "Start service"); - } - - #[test] - fn query_submenu_actions_returns_none_when_no_provider_matches() { - let prov = RichMockProvider { - type_id: "uuctl".into(), - items: Vec::new(), - submenu: vec![make_item("x", "x", ProviderType::Plugin("uuctl".into()))], - action_handled_for: None, - }; - let pm = ProviderManager::new(vec![Box::new(prov)], Vec::new()); - assert!(pm.query_submenu_actions("does-not-exist", "data", "name").is_none()); - } - - #[test] - fn query_submenu_actions_returns_none_when_provider_returns_empty_actions() { - let prov = RichMockProvider { - type_id: "uuctl".into(), - items: Vec::new(), - submenu: Vec::new(), // matching provider but no actions - action_handled_for: None, - }; - let pm = ProviderManager::new(vec![Box::new(prov)], Vec::new()); - assert!(pm.query_submenu_actions("uuctl", "data", "name").is_none()); - } - - #[test] - fn execute_plugin_action_returns_true_when_static_provider_handles() { - let prov = RichMockProvider { - type_id: "pomodoro".into(), - items: Vec::new(), - submenu: Vec::new(), - action_handled_for: Some("POMODORO:".into()), - }; - let pm = ProviderManager::new(vec![Box::new(prov)], Vec::new()); - assert!(pm.execute_plugin_action("POMODORO:start")); - } - - #[test] - fn execute_plugin_action_returns_true_when_dynamic_provider_handles() { - struct DynStub { - handles: String, - } - impl DynamicProvider for DynStub { - fn name(&self) -> &str { - "dyn" - } - fn provider_type(&self) -> ProviderType { - ProviderType::Plugin("dyn".into()) - } - fn query(&self, _q: &str) -> Vec { - Vec::new() - } - fn priority(&self) -> u32 { - 0 - } - fn execute_action(&self, command: &str) -> bool { - command.starts_with(&self.handles) - } - } - - let pm = ProviderManager::new( - Vec::new(), - vec![Box::new(DynStub { - handles: "DYN:".into(), - })], - ); - assert!(pm.execute_plugin_action("DYN:thing")); - } - - #[test] - fn execute_plugin_action_returns_false_when_nothing_handles() { - let prov = RichMockProvider { - type_id: "x".into(), - items: Vec::new(), - submenu: Vec::new(), - action_handled_for: Some("X:".into()), - }; - let pm = ProviderManager::new(vec![Box::new(prov)], Vec::new()); - assert!(!pm.execute_plugin_action("UNRELATED:command")); - } - - #[test] - fn provider_manager_new_with_no_providers_does_not_panic() { - // Regression guard: the daemon may be configured to disable every - // provider; construction must still succeed. - let pm = ProviderManager::new(Vec::new(), Vec::new()); - assert_eq!(pm.available_providers().len(), 0); - assert!(!pm.execute_plugin_action("ANYTHING:foo")); - assert!(pm.query_submenu_actions("anything", "data", "n").is_none()); - } -} diff --git a/crates/owlry/Cargo.toml b/crates/owlry/Cargo.toml index 506acad..da5350a 100644 --- a/crates/owlry/Cargo.toml +++ b/crates/owlry/Cargo.toml @@ -1,19 +1,24 @@ [package] name = "owlry" -version = "1.0.10" +version = "2.0.0-dev" edition = "2024" rust-version = "1.90" description = "A lightweight, owl-themed application launcher for Wayland" -authors = ["Your Name "] +authors = ["Owlibou"] license = "GPL-3.0-or-later" repository = "https://somegit.dev/Owlibou/owlry" keywords = ["launcher", "wayland", "gtk4", "linux"] categories = ["gui"] -[dependencies] -# Core backend library -owlry-core = { path = "../owlry-core" } +[lib] +name = "owlry" +path = "src/lib.rs" +[[bin]] +name = "owlry" +path = "src/main.rs" + +[dependencies] # GTK4 for the UI gtk4 = { version = "0.10", features = ["v4_12"] } @@ -23,19 +28,37 @@ gtk4-layer-shell = "0.7" # Low-level syscalls for stdin detection (dmenu mode) libc = "0.2" -# Logging +# Logging & notifications log = "0.4" env_logger = "0.11" - -# Configuration (needed for config types used in app.rs/theme.rs) -serde = { version = "1", features = ["derive"] } -toml = "0.8" +notify-rust = "4" # CLI argument parsing clap = { version = "4", features = ["derive"] } -# IPC (Request/Response serialization) +# Configuration & serialization +serde = { version = "1", features = ["derive"] } serde_json = "1" +toml = "0.8" + +# Provider system & search +fuzzy-matcher = "0.3" +freedesktop-desktop-entry = "0.8" + +# Data & filesystem +fs2 = "0.4" +chrono = { version = "0.4", features = ["serde"] } +dirs = "5" + +# Error handling +thiserror = "2" + +# Signal handling (daemon) +signal-hook = "0.3" + +# Built-in providers +expr-solver-lib = "1" +reqwest = { version = "0.13", default-features = false, features = ["native-tls", "json", "blocking"] } # Async oneshot channel (background thread -> main loop) futures-channel = "0.3" @@ -44,7 +67,10 @@ futures-channel = "0.3" # GResource compilation for bundled icons glib-build-tools = "0.20" +[dev-dependencies] +tempfile = "3" + [features] default = [] # Enable verbose debug logging (for development/testing builds) -dev-logging = ["owlry-core/dev-logging"] +dev-logging = [] diff --git a/crates/owlry/src/app.rs b/crates/owlry/src/app.rs index 77fb6e3..75148a4 100644 --- a/crates/owlry/src/app.rs +++ b/crates/owlry/src/app.rs @@ -8,11 +8,11 @@ use gtk4::prelude::*; use gtk4::{Application, CssProvider, gio}; use gtk4_layer_shell::{Edge, Layer, LayerShell}; use log::{debug, info, warn}; -use owlry_core::config::Config; -use owlry_core::data::FrecencyStore; -use owlry_core::filter::ProviderFilter; -use owlry_core::paths; -use owlry_core::providers::{Provider, ProviderManager, ProviderType}; +use crate::config::Config; +use crate::data::FrecencyStore; +use crate::filter::ProviderFilter; +use crate::paths; +use crate::providers::{Provider, ProviderManager, ProviderType}; use std::cell::RefCell; use std::rc::Rc; @@ -149,7 +149,7 @@ impl OwlryApp { /// All other providers belong to the daemon (Phase 1 keeps the daemon as the /// primary path). fn create_local_backend(_config: &Config) -> SearchBackend { - use owlry_core::providers::{ApplicationProvider, CommandProvider}; + use crate::providers::{ApplicationProvider, CommandProvider}; let core_providers: Vec> = vec![ Box::new(ApplicationProvider::new()), diff --git a/crates/owlry/src/backend.rs b/crates/owlry/src/backend.rs index aa36184..5c2f9fa 100644 --- a/crates/owlry/src/backend.rs +++ b/crates/owlry/src/backend.rs @@ -5,11 +5,11 @@ use crate::client::CoreClient; use log::warn; -use owlry_core::config::Config; -use owlry_core::data::FrecencyStore; -use owlry_core::filter::ProviderFilter; -use owlry_core::ipc::{ProviderDesc, ResultItem}; -use owlry_core::providers::{ItemSource, LaunchItem, ProviderManager, ProviderType}; +use crate::config::Config; +use crate::data::FrecencyStore; +use crate::filter::ProviderFilter; +use crate::ipc::{ProviderDesc, ResultItem}; +use crate::providers::{ItemSource, LaunchItem, ProviderManager, ProviderType}; use std::sync::{Arc, Mutex}; /// Parameters needed to run a search query on a background thread. diff --git a/crates/owlry/src/cli.rs b/crates/owlry/src/cli.rs index 1fa4640..8757d77 100644 --- a/crates/owlry/src/cli.rs +++ b/crates/owlry/src/cli.rs @@ -2,7 +2,7 @@ use clap::Parser; -use owlry_core::providers::ProviderType; +use crate::providers::ProviderType; #[derive(Parser, Debug, Clone)] #[command( diff --git a/crates/owlry/src/client.rs b/crates/owlry/src/client.rs index 898ba6f..de3ebb1 100644 --- a/crates/owlry/src/client.rs +++ b/crates/owlry/src/client.rs @@ -3,7 +3,7 @@ use std::os::unix::net::UnixStream; use std::path::{Path, PathBuf}; use std::time::Duration; -use owlry_core::ipc::{ProviderDesc, Request, Response, ResultItem}; +use crate::ipc::{ProviderDesc, Request, Response, ResultItem}; /// Maximum allowed size for a single IPC response line (4 MiB). /// Larger than the request limit because responses carry result sets. @@ -111,10 +111,10 @@ impl CoreClient { /// Default socket path: `$XDG_RUNTIME_DIR/owlry/owlry.sock`. /// - /// Delegates to `owlry_core::paths::socket_path()` to keep a single + /// Delegates to `crate::paths::socket_path()` to keep a single /// source of truth. pub fn socket_path() -> PathBuf { - owlry_core::paths::socket_path() + crate::paths::socket_path() } /// Send a search query and return matching results. diff --git a/crates/owlry-core/src/config/mod.rs b/crates/owlry/src/config/mod.rs similarity index 100% rename from crates/owlry-core/src/config/mod.rs rename to crates/owlry/src/config/mod.rs diff --git a/crates/owlry-core/src/data/frecency.rs b/crates/owlry/src/data/frecency.rs similarity index 100% rename from crates/owlry-core/src/data/frecency.rs rename to crates/owlry/src/data/frecency.rs diff --git a/crates/owlry-core/src/data/mod.rs b/crates/owlry/src/data/mod.rs similarity index 100% rename from crates/owlry-core/src/data/mod.rs rename to crates/owlry/src/data/mod.rs diff --git a/crates/owlry-core/src/filter.rs b/crates/owlry/src/filter.rs similarity index 100% rename from crates/owlry-core/src/filter.rs rename to crates/owlry/src/filter.rs diff --git a/crates/owlry-core/src/ipc.rs b/crates/owlry/src/ipc.rs similarity index 100% rename from crates/owlry-core/src/ipc.rs rename to crates/owlry/src/ipc.rs diff --git a/crates/owlry/src/lib.rs b/crates/owlry/src/lib.rs new file mode 100644 index 0000000..7b26b4c --- /dev/null +++ b/crates/owlry/src/lib.rs @@ -0,0 +1,20 @@ +//! Owlry — Wayland application launcher. +//! +//! Single-crate layout (v2): the daemon, IPC layer, providers, and GTK4 UI +//! all live here. The binary entry point in `main.rs` selects between UI +//! launch (default) and daemon mode (`-d` / `--daemon`). + +pub mod app; +pub mod backend; +pub mod cli; +pub mod client; +pub mod config; +pub mod data; +pub mod filter; +pub mod ipc; +pub mod notify; +pub mod paths; +pub mod providers; +pub mod server; +pub mod theme; +pub mod ui; diff --git a/crates/owlry/src/main.rs b/crates/owlry/src/main.rs index d3e501f..96c1c11 100644 --- a/crates/owlry/src/main.rs +++ b/crates/owlry/src/main.rs @@ -1,16 +1,10 @@ -mod app; -mod backend; -mod cli; -pub mod client; -mod providers; -mod theme; -mod ui; - -use app::OwlryApp; -use cli::CliArgs; use log::{info, warn}; use std::os::unix::io::AsRawFd; +use owlry::app::OwlryApp; +use owlry::cli::CliArgs; +use owlry::{client, paths, server}; + #[cfg(feature = "dev-logging")] use log::debug; @@ -22,12 +16,11 @@ use log::debug; fn try_acquire_lock() -> Option { use std::os::unix::fs::OpenOptionsExt; - let lock_path = owlry_core::paths::socket_path() + let lock_path = paths::socket_path() .parent() .unwrap() .join("owlry-ui.lock"); - // Ensure the parent directory exists if let Some(parent) = lock_path.parent() { let _ = std::fs::create_dir_all(parent); } @@ -51,17 +44,21 @@ fn main() { // -d / --daemon: run the daemon in-process and exit when it stops. if args.daemon { - let default_level = if cfg!(feature = "dev-logging") { "debug" } else { "info" }; + let default_level = if cfg!(feature = "dev-logging") { + "debug" + } else { + "info" + }; env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default_level)) .format_timestamp_millis() .init(); - let sock = owlry_core::paths::socket_path(); - if let Err(e) = owlry_core::paths::ensure_parent_dir(&sock) { + let sock = paths::socket_path(); + if let Err(e) = paths::ensure_parent_dir(&sock) { eprintln!("Failed to create socket directory: {e}"); std::process::exit(1); } - match owlry_core::server::Server::bind(&sock) { + match server::Server::bind(&sock) { Ok(server) => { if let Err(e) = server.run() { eprintln!("Server error: {e}"); @@ -76,7 +73,7 @@ fn main() { } } - // No subcommand - launch the app + // Default: launch the UI. let default_level = if cfg!(feature = "dev-logging") { "debug" } else { @@ -100,7 +97,6 @@ fn main() { let _lock_guard = match try_acquire_lock() { Some(file) => file, None => { - // Another instance holds the lock — send toggle to daemon and exit info!("Another owlry instance detected, sending toggle"); let socket_path = client::CoreClient::socket_path(); if let Ok(mut client) = client::CoreClient::connect(&socket_path) { @@ -118,7 +114,7 @@ fn main() { info!("Starting Owlry launcher"); - // Diagnostic: log critical environment variables + // Diagnostic: log critical environment variables. let home = std::env::var("HOME").unwrap_or_else(|_| "".to_string()); let path = std::env::var("PATH").unwrap_or_else(|_| "".to_string()); let xdg_data = std::env::var("XDG_DATA_HOME").unwrap_or_else(|_| "".to_string()); diff --git a/crates/owlry-core/src/notify.rs b/crates/owlry/src/notify.rs similarity index 100% rename from crates/owlry-core/src/notify.rs rename to crates/owlry/src/notify.rs diff --git a/crates/owlry-core/src/paths.rs b/crates/owlry/src/paths.rs similarity index 100% rename from crates/owlry-core/src/paths.rs rename to crates/owlry/src/paths.rs diff --git a/crates/owlry-core/src/providers/application.rs b/crates/owlry/src/providers/application.rs similarity index 100% rename from crates/owlry-core/src/providers/application.rs rename to crates/owlry/src/providers/application.rs diff --git a/crates/owlry-core/src/providers/calculator.rs b/crates/owlry/src/providers/calculator.rs similarity index 100% rename from crates/owlry-core/src/providers/calculator.rs rename to crates/owlry/src/providers/calculator.rs diff --git a/crates/owlry-core/src/providers/command.rs b/crates/owlry/src/providers/command.rs similarity index 100% rename from crates/owlry-core/src/providers/command.rs rename to crates/owlry/src/providers/command.rs diff --git a/crates/owlry-core/src/providers/converter/currency.rs b/crates/owlry/src/providers/converter/currency.rs similarity index 100% rename from crates/owlry-core/src/providers/converter/currency.rs rename to crates/owlry/src/providers/converter/currency.rs diff --git a/crates/owlry-core/src/providers/converter/mod.rs b/crates/owlry/src/providers/converter/mod.rs similarity index 100% rename from crates/owlry-core/src/providers/converter/mod.rs rename to crates/owlry/src/providers/converter/mod.rs diff --git a/crates/owlry-core/src/providers/converter/parser.rs b/crates/owlry/src/providers/converter/parser.rs similarity index 100% rename from crates/owlry-core/src/providers/converter/parser.rs rename to crates/owlry/src/providers/converter/parser.rs diff --git a/crates/owlry-core/src/providers/converter/units.rs b/crates/owlry/src/providers/converter/units.rs similarity index 100% rename from crates/owlry-core/src/providers/converter/units.rs rename to crates/owlry/src/providers/converter/units.rs diff --git a/crates/owlry/src/providers/dmenu.rs b/crates/owlry/src/providers/dmenu.rs index 12eccf9..e148520 100644 --- a/crates/owlry/src/providers/dmenu.rs +++ b/crates/owlry/src/providers/dmenu.rs @@ -1,5 +1,5 @@ use log::debug; -use owlry_core::providers::{ItemSource, LaunchItem, Provider, ProviderType}; +use crate::providers::{ItemSource, LaunchItem, Provider, ProviderType}; use std::io::{self, BufRead}; /// Provider for dmenu-style input from stdin diff --git a/crates/owlry/src/providers/mod.rs b/crates/owlry/src/providers/mod.rs index bbb7ad5..4e17b19 100644 --- a/crates/owlry/src/providers/mod.rs +++ b/crates/owlry/src/providers/mod.rs @@ -1,2 +1,1098 @@ +// Core providers (compiled in) +mod application; +mod command; pub mod dmenu; +pub(crate) mod calculator; +pub(crate) mod converter; +pub(crate) mod system; + +// Re-exports for core providers +pub use application::ApplicationProvider; +pub use command::CommandProvider; pub use dmenu::DmenuProvider; + +use chrono::Utc; +use fuzzy_matcher::FuzzyMatcher; +use fuzzy_matcher::skim::SkimMatcherV2; +use log::info; + +#[cfg(feature = "dev-logging")] +use log::debug; + +use std::sync::{Arc, RwLock}; + +use crate::config::Config; +use crate::data::FrecencyStore; + +/// Where a provider sits in the UI. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProviderPosition { + /// Normal results list. + Normal, + /// Widget — rendered at the top of the results, outside the normal flow. + Widget, +} + +impl ProviderPosition { + pub fn as_str(&self) -> &'static str { + match self { + ProviderPosition::Normal => "normal", + ProviderPosition::Widget => "widget", + } + } +} + +/// Metadata descriptor for an available provider (used by IPC/daemon API) +#[derive(Debug, Clone)] +pub struct ProviderDescriptor { + pub id: String, + pub name: String, + pub prefix: Option, + pub icon: String, + pub position: String, + pub tab_label: Option, + pub search_noun: Option, +} + +/// Trust level of a [`LaunchItem`]'s command, used to gate `sh -c` execution. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ItemSource { + /// Built-in provider compiled into the binary (trusted). + Core, + /// Script-defined provider (Lua, in Phase 3+) — user-installed, untrusted. + ScriptPlugin, +} + +impl ItemSource { + pub fn as_str(&self) -> &'static str { + match self { + ItemSource::Core => "core", + ItemSource::ScriptPlugin => "script_plugin", + } + } +} + +impl std::str::FromStr for ItemSource { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "script_plugin" => Ok(ItemSource::ScriptPlugin), + _ => Ok(ItemSource::Core), + } + } +} + +/// Represents a single searchable/launchable item +#[derive(Debug, Clone)] +pub struct LaunchItem { + #[allow(dead_code)] + pub id: String, + pub name: String, + pub description: Option, + pub icon: Option, + pub provider: ProviderType, + pub command: String, + pub terminal: bool, + /// Tags/categories for filtering (e.g., from .desktop Categories) + pub tags: Vec, + /// Trust level — gates `sh -c` execution for script plugin items. + pub source: ItemSource, +} + +/// Provider type identifier for filtering and badge display. +/// +/// - `Application`, `Command`, `Dmenu`: built-in core providers +/// - `Plugin(type_id)`: any other provider (compiled-in feature module or +/// future Lua-registered provider). The `type_id` is the provider's +/// stable identifier (e.g. `"bookmarks"`, `"uuctl"`, `"calc"`). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ProviderType { + Application, + Command, + Dmenu, + Plugin(String), +} + +impl std::str::FromStr for ProviderType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "app" | "apps" | "application" | "applications" => Ok(ProviderType::Application), + "cmd" | "cmds" | "command" | "commands" => Ok(ProviderType::Command), + "dmenu" => Ok(ProviderType::Dmenu), + other => Ok(ProviderType::Plugin(other.to_string())), + } + } +} + +impl std::fmt::Display for ProviderType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProviderType::Application => write!(f, "app"), + ProviderType::Command => write!(f, "cmd"), + ProviderType::Dmenu => write!(f, "dmenu"), + ProviderType::Plugin(type_id) => write!(f, "{}", type_id), + } + } +} + +/// Trait for all search providers. +/// +/// Static providers hold a refreshable item cache. Submenu and action support +/// are optional methods — implement them only if the provider produces +/// `SUBMENU:` items or handles plugin-defined action commands. +pub trait Provider: Send + Sync { + #[allow(dead_code)] + fn name(&self) -> &str; + fn provider_type(&self) -> ProviderType; + fn refresh(&mut self); + fn items(&self) -> &[LaunchItem]; + + /// Short label for UI tab button (e.g., "Shutdown"). None = use default. + fn tab_label(&self) -> Option<&str> { + None + } + /// Noun for search placeholder (e.g., "shutdown actions"). None = use default. + fn search_noun(&self) -> Option<&str> { + None + } + /// Optional search prefix (e.g. ":bm"). None = no prefix. + fn prefix(&self) -> Option<&str> { + None + } + /// Icon name (XDG icon theme). + fn icon(&self) -> &str { + "application-x-addon" + } + /// UI placement. + fn position(&self) -> ProviderPosition { + ProviderPosition::Normal + } + /// Priority hint for ordering. + fn priority(&self) -> u32 { + 0 + } + + /// Generate submenu actions for an item whose command starts with `SUBMENU:`. + /// `data` is everything after the type_id prefix. + /// Default: no submenu support. + fn submenu_actions(&self, _data: &str) -> Vec { + Vec::new() + } + + /// Handle a plugin-defined action command (e.g. `"POMODORO:start"`). + /// Returns true if the command was handled. + /// Default: no action support. + fn execute_action(&self, _command: &str) -> bool { + false + } +} + +/// Trait for built-in providers that produce results per-keystroke. +/// Unlike static `Provider`s which cache items via `refresh()`/`items()`, +/// dynamic providers generate results on every query. +pub trait DynamicProvider: Send + Sync { + #[allow(dead_code)] + fn name(&self) -> &str; + fn provider_type(&self) -> ProviderType; + fn query(&self, query: &str) -> Vec; + fn priority(&self) -> u32; + + /// Handle a plugin action command. Returns true if handled. + fn execute_action(&self, _command: &str) -> bool { + false + } +} + +/// Manages all providers and handles searching. +pub struct ProviderManager { + /// Static providers (apps, commands, systemd, etc.). + providers: Vec>, + /// Dynamic providers (calculator, converter, websearch, filesearch). + /// Queried per-keystroke, not cached. + builtin_dynamic: Vec>, + /// Fuzzy matcher for search. + matcher: SkimMatcherV2, +} + +impl ProviderManager { + /// Create a new ProviderManager from a pre-built list of providers. + /// + /// Used by tests and by the dmenu-mode client. The daemon uses + /// [`Self::new_with_config`] which builds the provider set from config. + pub fn new( + core_providers: Vec>, + dynamic: Vec>, + ) -> Self { + let mut manager = Self { + providers: core_providers, + builtin_dynamic: dynamic, + matcher: SkimMatcherV2::default(), + }; + manager.refresh_all(); + manager + } + + /// Build a ProviderManager for the daemon, sourcing enabled providers from config. + /// + /// Only built-in / compiled-in providers are registered here. Future Lua-defined + /// providers (Phase 3+) are added via [`Self::add_provider`] after construction. + pub fn new_with_config(config: Arc>) -> Self { + let (calc_enabled, conv_enabled, sys_enabled) = match config.read() { + Ok(cfg) => ( + cfg.providers.calculator, + cfg.providers.converter, + cfg.providers.system, + ), + Err(_) => { + log::warn!("Config lock poisoned during provider init; using defaults"); + (true, true, true) + } + }; + + let mut core_providers: Vec> = vec![ + Box::new(ApplicationProvider::new()), + Box::new(CommandProvider::new()), + ]; + + if sys_enabled { + core_providers.push(Box::new(system::SystemProvider::new())); + info!("Registered built-in system provider"); + } + + let mut builtin_dynamic: Vec> = Vec::new(); + if calc_enabled { + builtin_dynamic.push(Box::new(calculator::CalculatorProvider)); + info!("Registered built-in calculator provider"); + } + if conv_enabled { + builtin_dynamic.push(Box::new(converter::ConverterProvider::new())); + info!("Registered built-in converter provider"); + } + + Self::new(core_providers, builtin_dynamic) + } + + #[allow(dead_code)] + pub fn is_dmenu_mode(&self) -> bool { + self.providers + .iter() + .any(|p| p.provider_type() == ProviderType::Dmenu) + } + + pub fn refresh_all(&mut self) { + for provider in &mut self.providers { + provider.refresh(); + info!( + "Provider '{}' loaded {} items", + provider.name(), + provider.items().len() + ); + } + // Dynamic providers don't need refresh (they query on demand). + } + + /// Register an additional provider at runtime. + /// + /// Used by Phase 3+ Lua config to add user-defined providers after the + /// daemon has booted. The provider's `refresh()` is called immediately. + #[allow(dead_code)] + pub fn add_provider(&mut self, mut provider: Box) { + provider.refresh(); + info!("Registered provider: {}", provider.name()); + self.providers.push(provider); + } + + /// Execute a plugin-defined action command. + /// + /// Command format: `PLUGIN_ID:action_data` (e.g. `"POMODORO:start"`). + /// Returns true if a provider handled the command. + pub fn execute_plugin_action(&self, command: &str) -> bool { + for provider in &self.providers { + if provider.execute_action(command) { + return true; + } + } + for provider in &self.builtin_dynamic { + if provider.execute_action(command) { + return true; + } + } + false + } + + #[allow(dead_code)] + pub fn search(&self, query: &str, max_results: usize) -> Vec<(LaunchItem, i64)> { + if query.is_empty() { + return self + .providers + .iter() + .flat_map(|p| p.items().iter().cloned()) + .take(max_results) + .map(|item| (item, 0)) + .collect(); + } + + let mut results: Vec<(LaunchItem, i64)> = self + .providers + .iter() + .flat_map(|p| p.items().iter()) + .filter_map(|item| { + let name_score = self.matcher.fuzzy_match(&item.name, query); + let desc_score = item + .description + .as_ref() + .and_then(|d| self.matcher.fuzzy_match(d, query)); + let score = match (name_score, desc_score) { + (Some(n), Some(d)) => Some(n.max(d)), + (Some(n), None) => Some(n), + (None, Some(d)) => Some(d / 2), + (None, None) => None, + }; + score.map(|s| (item.clone(), s)) + }) + .collect(); + + results.sort_by(|a, b| b.1.cmp(&a.1)); + results.truncate(max_results); + results + } + + /// Search with provider filtering. + #[allow(dead_code)] + pub fn search_filtered( + &self, + query: &str, + max_results: usize, + filter: &crate::filter::ProviderFilter, + tag_filter: Option<&str>, + ) -> Vec<(LaunchItem, i64)> { + let all_items = self + .providers + .iter() + .filter(|p| filter.is_active(p.provider_type())) + .flat_map(|p| p.items().iter().cloned()) + .filter(|item| tag_filter.is_none_or(|t| item.tags.iter().any(|it| it == t))); + + if query.is_empty() { + return all_items.take(max_results).map(|item| (item, 0)).collect(); + } + + let mut results: Vec<(LaunchItem, i64)> = all_items + .filter_map(|item| { + let name_score = self.matcher.fuzzy_match(&item.name, query); + let desc_score = item + .description + .as_ref() + .and_then(|d| self.matcher.fuzzy_match(d, query)); + let score = match (name_score, desc_score) { + (Some(n), Some(d)) => Some(n.max(d)), + (Some(n), None) => Some(n), + (None, Some(d)) => Some(d / 2), + (None, None) => None, + }; + score.map(|s| (item, s)) + }) + .collect(); + + results.sort_by(|a, b| b.1.cmp(&a.1)); + results.truncate(max_results); + results + } + + /// Search with frecency boosting, dynamic providers, and tag filtering. + pub fn search_with_frecency( + &self, + query: &str, + max_results: usize, + filter: &crate::filter::ProviderFilter, + frecency: &FrecencyStore, + frecency_weight: f64, + tag_filter: Option<&str>, + ) -> Vec<(LaunchItem, i64)> { + #[cfg(feature = "dev-logging")] + debug!( + "[Search] query={:?}, max={}, frecency_weight={}", + query, max_results, frecency_weight + ); + + let now = Utc::now(); + let mut results: Vec<(LaunchItem, i64)> = Vec::new(); + + // Widget providers contribute on empty query (no prefix active). + if filter.active_prefix().is_none() && query.is_empty() { + for provider in &self.providers { + if provider.position() != ProviderPosition::Widget { + continue; + } + let base_score = provider.priority() as i64; + for (idx, item) in provider.items().iter().enumerate() { + results.push((item.clone(), base_score - idx as i64)); + } + } + } + + // Dynamic providers (calculator, converter, etc.) — only when there's a query. + if !query.is_empty() { + for provider in &self.builtin_dynamic { + if !filter.is_active(provider.provider_type()) { + continue; + } + let dynamic_results = provider.query(query); + let base_score = provider.priority() as i64; + let grouping_bonus: i64 = match provider.provider_type() { + ProviderType::Plugin(ref id) if matches!(id.as_str(), "calc" | "conv") => { + 10_000 + } + _ => 0, + }; + for (idx, item) in dynamic_results.into_iter().enumerate() { + results.push((item, base_score + grouping_bonus - idx as i64)); + } + } + } + + // Empty query (after widgets) — frecency-sorted items. + if query.is_empty() { + let mut scored_refs: Vec<(&LaunchItem, i64)> = self + .providers + .iter() + .filter(|p| p.position() != ProviderPosition::Widget) + .filter(|p| filter.is_active(p.provider_type())) + .flat_map(|p| p.items().iter()) + .filter(|item| { + if let Some(tag) = tag_filter { + item.tags.iter().any(|t| t.to_lowercase().contains(tag)) + } else { + true + } + }) + .map(|item| { + let frecency_score = frecency.get_score_at(&item.id, now); + let boosted = (frecency_score * frecency_weight * 100.0) as i64; + (item, boosted) + }) + .collect(); + + if scored_refs.len() > max_results { + scored_refs.select_nth_unstable_by(max_results, |a, b| b.1.cmp(&a.1)); + scored_refs.truncate(max_results); + } + scored_refs.sort_by(|a, b| b.1.cmp(&a.1)); + + results.extend(scored_refs.into_iter().map(|(item, score)| (item.clone(), score))); + results.sort_by(|a, b| b.1.cmp(&a.1)); + results.truncate(max_results); + return results; + } + + // Regular search with frecency boost and tag matching. + let score_item = |item: &LaunchItem| -> Option { + if let Some(tag) = tag_filter + && !item.tags.iter().any(|t| t.to_lowercase().contains(tag)) + { + return None; + } + let name_score = self.matcher.fuzzy_match(&item.name, query); + let desc_score = item + .description + .as_ref() + .and_then(|d| self.matcher.fuzzy_match(d, query)); + let tag_score = item + .tags + .iter() + .filter_map(|t| self.matcher.fuzzy_match(t, query)) + .max() + .map(|s| s / 3); + + let base_score = match (name_score, desc_score, tag_score) { + (Some(n), Some(d), Some(t)) => Some(n.max(d).max(t)), + (Some(n), Some(d), None) => Some(n.max(d)), + (Some(n), None, Some(t)) => Some(n.max(t)), + (Some(n), None, None) => Some(n), + (None, Some(d), Some(t)) => Some((d / 2).max(t)), + (None, Some(d), None) => Some(d / 2), + (None, None, Some(t)) => Some(t), + (None, None, None) => None, + }; + + base_score.map(|s| { + let frecency_score = frecency.get_score_at(&item.id, now); + let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64; + let exact_match_boost = if item.name.eq_ignore_ascii_case(query) { + match &item.provider { + ProviderType::Application => 50_000, + _ => 30_000, + } + } else { + 0 + }; + s + frecency_boost + exact_match_boost + }) + }; + + let mut scored_refs: Vec<(&LaunchItem, i64)> = Vec::new(); + for provider in &self.providers { + if provider.position() == ProviderPosition::Widget { + continue; + } + if !filter.is_active(provider.provider_type()) { + continue; + } + for item in provider.items() { + if let Some(score) = score_item(item) { + scored_refs.push((item, score)); + } + } + } + + if scored_refs.len() > max_results { + scored_refs.select_nth_unstable_by(max_results, |a, b| b.1.cmp(&a.1)); + scored_refs.truncate(max_results); + } + scored_refs.sort_by(|a, b| b.1.cmp(&a.1)); + + results.extend(scored_refs.into_iter().map(|(item, score)| (item.clone(), score))); + results.sort_by(|a, b| b.1.cmp(&a.1)); + results.truncate(max_results); + + #[cfg(feature = "dev-logging")] + { + debug!("[Search] Returning {} results", results.len()); + for (i, (item, score)) in results.iter().take(5).enumerate() { + debug!( + "[Search] #{}: {} (score={}, provider={:?})", + i + 1, + item.name, + score, + item.provider + ); + } + if results.len() > 5 { + debug!("[Search] ... and {} more", results.len() - 5); + } + } + + results + } + + /// Get all available provider types (for UI tabs). + #[allow(dead_code)] + pub fn available_provider_types(&self) -> Vec { + self.providers.iter().map(|p| p.provider_type()).collect() + } + + /// Get descriptors for all registered providers. + /// + /// Used by the IPC server to report what providers are available to clients. + pub fn available_providers(&self) -> Vec { + let mut descs = Vec::new(); + + for provider in &self.providers { + let (id, default_prefix, default_icon) = match provider.provider_type() { + ProviderType::Application => ( + "app".to_string(), + Some(":app".to_string()), + "application-x-executable", + ), + ProviderType::Command => ( + "cmd".to_string(), + Some(":cmd".to_string()), + "utilities-terminal", + ), + ProviderType::Dmenu => ("dmenu".to_string(), None, "view-list-symbolic"), + ProviderType::Plugin(type_id) => (type_id, None, "application-x-addon"), + }; + descs.push(ProviderDescriptor { + id, + name: provider.name().to_string(), + prefix: provider.prefix().map(String::from).or(default_prefix), + icon: { + let trait_icon = provider.icon(); + if trait_icon == "application-x-addon" { + default_icon.to_string() + } else { + trait_icon.to_string() + } + }, + position: provider.position().as_str().to_string(), + tab_label: provider.tab_label().map(String::from), + search_noun: provider.search_noun().map(String::from), + }); + } + + descs + } + + /// Refresh a specific provider by its type_id. + pub fn refresh_provider(&mut self, provider_id: &str) { + for provider in &mut self.providers { + let matches = match provider.provider_type() { + ProviderType::Application => provider_id == "app", + ProviderType::Command => provider_id == "cmd", + ProviderType::Dmenu => provider_id == "dmenu", + ProviderType::Plugin(ref id) => provider_id == id, + }; + if matches { + provider.refresh(); + info!("Refreshed provider '{}'", provider.name()); + return; + } + } + info!("Provider '{}' not found for refresh", provider_id); + } + + /// Query a provider for submenu actions. + /// + /// Called when a user selects a `SUBMENU:plugin_id:data` item. The provider's + /// `submenu_actions(data)` is invoked to produce the action list. + pub fn query_submenu_actions( + &self, + plugin_id: &str, + data: &str, + display_name: &str, + ) -> Option<(String, Vec)> { + #[cfg(feature = "dev-logging")] + debug!("[Submenu] Querying provider '{}' with data: {}", plugin_id, data); + + for provider in &self.providers { + let matches = match provider.provider_type() { + ProviderType::Plugin(ref id) => id == plugin_id, + _ => false, + }; + if matches { + let actions = provider.submenu_actions(data); + if !actions.is_empty() { + return Some((display_name.to_string(), actions)); + } + } + } + + #[cfg(feature = "dev-logging")] + debug!("[Submenu] No submenu actions for provider '{}'", plugin_id); + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Minimal mock provider for testing ProviderManager + struct MockProvider { + name: String, + provider_type: ProviderType, + items: Vec, + refresh_count: usize, + } + + impl MockProvider { + fn new(name: &str, provider_type: ProviderType) -> Self { + Self { + name: name.to_string(), + provider_type, + items: Vec::new(), + refresh_count: 0, + } + } + + fn with_items(mut self, items: Vec) -> Self { + self.items = items; + self + } + } + + impl Provider for MockProvider { + fn name(&self) -> &str { + &self.name + } + + fn provider_type(&self) -> ProviderType { + self.provider_type.clone() + } + + fn refresh(&mut self) { + self.refresh_count += 1; + } + + fn items(&self) -> &[LaunchItem] { + &self.items + } + } + + fn make_item(id: &str, name: &str, provider: ProviderType) -> LaunchItem { + LaunchItem { + id: id.to_string(), + name: name.to_string(), + description: None, + icon: None, + provider, + command: format!("run-{}", id), + terminal: false, + tags: Vec::new(), + source: ItemSource::Core, + } + } + + #[test] + fn test_available_providers_core_only() { + let providers: Vec> = vec![ + Box::new(MockProvider::new("Applications", ProviderType::Application)), + Box::new(MockProvider::new("Commands", ProviderType::Command)), + ]; + let pm = ProviderManager::new(providers, Vec::new()); + let descs = pm.available_providers(); + assert_eq!(descs.len(), 2); + assert_eq!(descs[0].id, "app"); + assert_eq!(descs[0].name, "Applications"); + assert_eq!(descs[0].prefix, Some(":app".to_string())); + assert_eq!(descs[0].icon, "application-x-executable"); + assert_eq!(descs[0].position, "normal"); + assert_eq!(descs[1].id, "cmd"); + assert_eq!(descs[1].name, "Commands"); + } + + #[test] + fn test_available_providers_dmenu() { + let providers: Vec> = + vec![Box::new(MockProvider::new("dmenu", ProviderType::Dmenu))]; + let pm = ProviderManager::new(providers, Vec::new()); + let descs = pm.available_providers(); + assert_eq!(descs.len(), 1); + assert_eq!(descs[0].id, "dmenu"); + assert!(descs[0].prefix.is_none()); + } + + #[test] + fn test_available_provider_types() { + let providers: Vec> = vec![ + Box::new(MockProvider::new("Applications", ProviderType::Application)), + Box::new(MockProvider::new("Commands", ProviderType::Command)), + ]; + let pm = ProviderManager::new(providers, Vec::new()); + let types = pm.available_provider_types(); + assert_eq!(types.len(), 2); + assert!(types.contains(&ProviderType::Application)); + assert!(types.contains(&ProviderType::Command)); + } + + #[test] + fn test_refresh_provider_core() { + let app = MockProvider::new("Applications", ProviderType::Application); + let cmd = MockProvider::new("Commands", ProviderType::Command); + let providers: Vec> = vec![Box::new(app), Box::new(cmd)]; + let mut pm = ProviderManager::new(providers, Vec::new()); + + pm.refresh_provider("app"); + pm.refresh_provider("cmd"); + // Just verifying it doesn't panic. + } + + #[test] + fn test_refresh_provider_unknown_does_not_panic() { + let providers: Vec> = vec![Box::new(MockProvider::new( + "Applications", + ProviderType::Application, + ))]; + let mut pm = ProviderManager::new(providers, Vec::new()); + pm.refresh_provider("nonexistent"); + } + + #[test] + fn test_search_with_core_providers() { + let items = vec![ + make_item("firefox", "Firefox", ProviderType::Application), + make_item("vim", "Vim", ProviderType::Application), + ]; + let provider = + MockProvider::new("Applications", ProviderType::Application).with_items(items); + let providers: Vec> = vec![Box::new(provider)]; + let pm = ProviderManager::new(providers, Vec::new()); + + let results = pm.search("fire", 10); + assert_eq!(results.len(), 1); + assert_eq!(results[0].0.name, "Firefox"); + } + + // ========================================================================= + // Tests for behavior introduced in the v2 C-ABI demolition (commit ae4a903) + // ========================================================================= + + /// Provider impl that overrides every trait method to verify customization + /// flows through `ProviderManager::available_providers()` and submenu/action + /// dispatch. + struct RichMockProvider { + type_id: String, + items: Vec, + submenu: Vec, + action_handled_for: Option, + } + + impl Provider for RichMockProvider { + fn name(&self) -> &str { + "Rich" + } + fn provider_type(&self) -> ProviderType { + ProviderType::Plugin(self.type_id.clone()) + } + fn refresh(&mut self) {} + fn items(&self) -> &[LaunchItem] { + &self.items + } + fn prefix(&self) -> Option<&str> { + Some(":rich") + } + fn icon(&self) -> &str { + "rich-icon" + } + fn position(&self) -> ProviderPosition { + ProviderPosition::Widget + } + fn priority(&self) -> u32 { + 42 + } + fn tab_label(&self) -> Option<&str> { + Some("Rich") + } + fn search_noun(&self) -> Option<&str> { + Some("rich things") + } + fn submenu_actions(&self, _data: &str) -> Vec { + self.submenu.clone() + } + fn execute_action(&self, command: &str) -> bool { + self.action_handled_for + .as_ref() + .map(|prefix| command.starts_with(prefix.as_str())) + .unwrap_or(false) + } + } + + #[test] + fn provider_trait_default_methods_return_documented_values() { + // MockProvider does not override any optional method; defaults must hold. + let p = MockProvider::new("M", ProviderType::Application); + assert_eq!(p.prefix(), None); + assert_eq!(p.icon(), "application-x-addon"); + assert_eq!(p.position(), ProviderPosition::Normal); + assert_eq!(p.priority(), 0); + assert_eq!(p.tab_label(), None); + assert_eq!(p.search_noun(), None); + assert!(p.submenu_actions("anything").is_empty()); + assert!(!p.execute_action("MOCK:anything")); + } + + #[test] + fn provider_position_as_str_matches_ipc_strings() { + assert_eq!(ProviderPosition::Normal.as_str(), "normal"); + assert_eq!(ProviderPosition::Widget.as_str(), "widget"); + } + + #[test] + fn provider_type_from_str_accepts_plural_aliases() { + // After the demolition, FromStr accepts both singular and plural aliases. + use std::str::FromStr; + assert_eq!(ProviderType::from_str("app").unwrap(), ProviderType::Application); + assert_eq!(ProviderType::from_str("apps").unwrap(), ProviderType::Application); + assert_eq!( + ProviderType::from_str("application").unwrap(), + ProviderType::Application + ); + assert_eq!( + ProviderType::from_str("applications").unwrap(), + ProviderType::Application + ); + assert_eq!(ProviderType::from_str("cmd").unwrap(), ProviderType::Command); + assert_eq!(ProviderType::from_str("cmds").unwrap(), ProviderType::Command); + assert_eq!( + ProviderType::from_str("command").unwrap(), + ProviderType::Command + ); + assert_eq!( + ProviderType::from_str("commands").unwrap(), + ProviderType::Command + ); + assert_eq!(ProviderType::from_str("dmenu").unwrap(), ProviderType::Dmenu); + // Anything unknown becomes Plugin(s) — preserves user-defined provider IDs. + assert_eq!( + ProviderType::from_str("bookmarks").unwrap(), + ProviderType::Plugin("bookmarks".into()) + ); + assert_eq!( + ProviderType::from_str("uuctl").unwrap(), + ProviderType::Plugin("uuctl".into()) + ); + } + + #[test] + fn item_source_from_str_maps_unknown_to_core() { + // NativePlugin variant is gone; unknown strings (including the old + // "native_plugin" tag from pre-2.0 daemons) decode to Core. + use std::str::FromStr; + assert_eq!(ItemSource::from_str("core"), Ok(ItemSource::Core)); + assert_eq!( + ItemSource::from_str("script_plugin"), + Ok(ItemSource::ScriptPlugin) + ); + assert_eq!(ItemSource::from_str("native_plugin"), Ok(ItemSource::Core)); + assert_eq!(ItemSource::from_str(""), Ok(ItemSource::Core)); + assert_eq!(ItemSource::from_str("anything-else"), Ok(ItemSource::Core)); + } + + #[test] + fn item_source_as_str_only_emits_supported_variants() { + assert_eq!(ItemSource::Core.as_str(), "core"); + assert_eq!(ItemSource::ScriptPlugin.as_str(), "script_plugin"); + } + + #[test] + fn add_provider_refreshes_and_appends() { + let mut pm = ProviderManager::new(Vec::new(), Vec::new()); + assert_eq!(pm.available_provider_types().len(), 0); + + let prov = MockProvider::new("Late", ProviderType::Plugin("late".into())); + pm.add_provider(Box::new(prov)); + + let types = pm.available_provider_types(); + assert_eq!(types.len(), 1); + assert_eq!(types[0], ProviderType::Plugin("late".into())); + // add_provider must call refresh() — checked indirectly by the impl + // contract; refresh_count on MockProvider was bumped, but we can't + // peek through the Box. The public observable is that items() + // is callable without panic, which we exercise here. + assert!(pm.available_providers()[0].id == "late"); + } + + #[test] + fn available_providers_uses_trait_overrides_for_plugin_type() { + let prov = RichMockProvider { + type_id: "rich".into(), + items: Vec::new(), + submenu: Vec::new(), + action_handled_for: None, + }; + let pm = ProviderManager::new(vec![Box::new(prov)], Vec::new()); + let descs = pm.available_providers(); + assert_eq!(descs.len(), 1); + let d = &descs[0]; + assert_eq!(d.id, "rich"); + assert_eq!(d.prefix.as_deref(), Some(":rich")); + assert_eq!(d.icon, "rich-icon"); + assert_eq!(d.position, "widget"); + assert_eq!(d.tab_label.as_deref(), Some("Rich")); + assert_eq!(d.search_noun.as_deref(), Some("rich things")); + } + + #[test] + fn query_submenu_actions_returns_some_when_provider_matches_and_has_actions() { + let prov = RichMockProvider { + type_id: "uuctl".into(), + items: Vec::new(), + submenu: vec![make_item( + "start", + "Start service", + ProviderType::Plugin("uuctl".into()), + )], + action_handled_for: None, + }; + let pm = ProviderManager::new(vec![Box::new(prov)], Vec::new()); + + let result = pm.query_submenu_actions("uuctl", "foo.service:true", "foo"); + assert!(result.is_some(), "expected Some when provider matches and returns actions"); + let (display, actions) = result.unwrap(); + assert_eq!(display, "foo"); + assert_eq!(actions.len(), 1); + assert_eq!(actions[0].name, "Start service"); + } + + #[test] + fn query_submenu_actions_returns_none_when_no_provider_matches() { + let prov = RichMockProvider { + type_id: "uuctl".into(), + items: Vec::new(), + submenu: vec![make_item("x", "x", ProviderType::Plugin("uuctl".into()))], + action_handled_for: None, + }; + let pm = ProviderManager::new(vec![Box::new(prov)], Vec::new()); + assert!(pm.query_submenu_actions("does-not-exist", "data", "name").is_none()); + } + + #[test] + fn query_submenu_actions_returns_none_when_provider_returns_empty_actions() { + let prov = RichMockProvider { + type_id: "uuctl".into(), + items: Vec::new(), + submenu: Vec::new(), // matching provider but no actions + action_handled_for: None, + }; + let pm = ProviderManager::new(vec![Box::new(prov)], Vec::new()); + assert!(pm.query_submenu_actions("uuctl", "data", "name").is_none()); + } + + #[test] + fn execute_plugin_action_returns_true_when_static_provider_handles() { + let prov = RichMockProvider { + type_id: "pomodoro".into(), + items: Vec::new(), + submenu: Vec::new(), + action_handled_for: Some("POMODORO:".into()), + }; + let pm = ProviderManager::new(vec![Box::new(prov)], Vec::new()); + assert!(pm.execute_plugin_action("POMODORO:start")); + } + + #[test] + fn execute_plugin_action_returns_true_when_dynamic_provider_handles() { + struct DynStub { + handles: String, + } + impl DynamicProvider for DynStub { + fn name(&self) -> &str { + "dyn" + } + fn provider_type(&self) -> ProviderType { + ProviderType::Plugin("dyn".into()) + } + fn query(&self, _q: &str) -> Vec { + Vec::new() + } + fn priority(&self) -> u32 { + 0 + } + fn execute_action(&self, command: &str) -> bool { + command.starts_with(&self.handles) + } + } + + let pm = ProviderManager::new( + Vec::new(), + vec![Box::new(DynStub { + handles: "DYN:".into(), + })], + ); + assert!(pm.execute_plugin_action("DYN:thing")); + } + + #[test] + fn execute_plugin_action_returns_false_when_nothing_handles() { + let prov = RichMockProvider { + type_id: "x".into(), + items: Vec::new(), + submenu: Vec::new(), + action_handled_for: Some("X:".into()), + }; + let pm = ProviderManager::new(vec![Box::new(prov)], Vec::new()); + assert!(!pm.execute_plugin_action("UNRELATED:command")); + } + + #[test] + fn provider_manager_new_with_no_providers_does_not_panic() { + // Regression guard: the daemon may be configured to disable every + // provider; construction must still succeed. + let pm = ProviderManager::new(Vec::new(), Vec::new()); + assert_eq!(pm.available_providers().len(), 0); + assert!(!pm.execute_plugin_action("ANYTHING:foo")); + assert!(pm.query_submenu_actions("anything", "data", "n").is_none()); + } +} diff --git a/crates/owlry-core/src/providers/system.rs b/crates/owlry/src/providers/system.rs similarity index 100% rename from crates/owlry-core/src/providers/system.rs rename to crates/owlry/src/providers/system.rs diff --git a/crates/owlry-core/src/server.rs b/crates/owlry/src/server.rs similarity index 100% rename from crates/owlry-core/src/server.rs rename to crates/owlry/src/server.rs diff --git a/crates/owlry/src/theme.rs b/crates/owlry/src/theme.rs index f0b11e8..b3f4db6 100644 --- a/crates/owlry/src/theme.rs +++ b/crates/owlry/src/theme.rs @@ -1,4 +1,4 @@ -use owlry_core::config::AppearanceConfig; +use crate::config::AppearanceConfig; /// Generate CSS with :root variables from config settings pub fn generate_variables_css(config: &AppearanceConfig) -> String { diff --git a/crates/owlry/src/ui/main_window.rs b/crates/owlry/src/ui/main_window.rs index 33a3ed4..291edce 100644 --- a/crates/owlry/src/ui/main_window.rs +++ b/crates/owlry/src/ui/main_window.rs @@ -9,10 +9,10 @@ use gtk4::{ ListBoxRow, Orientation, ScrolledWindow, SelectionMode, ToggleButton, }; use log::info; -use owlry_core::config::Config; -use owlry_core::filter::ProviderFilter; -use owlry_core::ipc::ProviderDesc; -use owlry_core::providers::{ItemSource, LaunchItem, ProviderType}; +use crate::config::Config; +use crate::filter::ProviderFilter; +use crate::ipc::ProviderDesc; +use crate::providers::{ItemSource, LaunchItem, ProviderType}; #[cfg(feature = "dev-logging")] use log::debug; @@ -382,7 +382,7 @@ impl MainWindow { /// Build hints string for the status bar based on enabled built-in providers. /// Plugin trigger hints (? web, / files, etc.) are not included here since /// plugin availability is not tracked in ProvidersConfig. - fn build_hints(config: &owlry_core::config::ProvidersConfig) -> String { + fn build_hints(config: &crate::config::ProvidersConfig) -> String { let mut parts: Vec = vec![ "Tab: cycle".to_string(), "↑↓: nav".to_string(), @@ -1366,7 +1366,7 @@ impl MainWindow { item.name, cmd ); log::warn!("{}", msg); - owlry_core::notify::notify("Command blocked", &msg); + crate::notify::notify("Command blocked", &msg); return; } } @@ -1375,7 +1375,7 @@ impl MainWindow { if item.command.is_empty() && !matches!(item.provider, ProviderType::Application) { let msg = format!("Item '{}' has no command; cannot launch", item.name); log::warn!("{}", msg); - owlry_core::notify::notify("Launch failed", &msg); + crate::notify::notify("Launch failed", &msg); return; } @@ -1397,7 +1397,7 @@ impl MainWindow { if let Err(e) = result { let msg = format!("Failed to launch '{}': {}", item.name, e); log::error!("{}", msg); - owlry_core::notify::notify("Launch failed", &msg); + crate::notify::notify("Launch failed", &msg); } } @@ -1418,7 +1418,7 @@ impl MainWindow { if !Path::new(desktop_path).exists() { let msg = format!("Desktop file not found: {}", desktop_path); log::error!("{}", msg); - owlry_core::notify::notify("Launch failed", &msg); + crate::notify::notify("Launch failed", &msg); return Err(std::io::Error::new(std::io::ErrorKind::NotFound, msg)); } @@ -1435,7 +1435,7 @@ impl MainWindow { if !uwsm_available { let msg = "uwsm is enabled in config but not installed"; log::error!("{}", msg); - owlry_core::notify::notify("Launch failed", msg); + crate::notify::notify("Launch failed", msg); return Err(std::io::Error::new(std::io::ErrorKind::NotFound, msg)); } diff --git a/crates/owlry/src/ui/provider_meta.rs b/crates/owlry/src/ui/provider_meta.rs index 60ffee5..2f4c517 100644 --- a/crates/owlry/src/ui/provider_meta.rs +++ b/crates/owlry/src/ui/provider_meta.rs @@ -1,5 +1,5 @@ -use owlry_core::ipc::ProviderDesc; -use owlry_core::providers::ProviderType; +use crate::ipc::ProviderDesc; +use crate::providers::ProviderType; /// Display metadata for a provider. pub struct ProviderMeta { diff --git a/crates/owlry/src/ui/result_row.rs b/crates/owlry/src/ui/result_row.rs index 866f3e3..ab4b796 100644 --- a/crates/owlry/src/ui/result_row.rs +++ b/crates/owlry/src/ui/result_row.rs @@ -1,6 +1,6 @@ use gtk4::prelude::*; use gtk4::{Box as GtkBox, Image, Label, ListBoxRow, Orientation, Widget}; -use owlry_core::providers::{LaunchItem, ProviderType}; +use crate::providers::{LaunchItem, ProviderType}; #[allow(dead_code)] pub struct ResultRow { @@ -107,13 +107,13 @@ impl ResultRow { } else { // Default icon based on provider type (only core types, plugins should provide icons) let default_icon = match &item.provider { - owlry_core::providers::ProviderType::Application => { + crate::providers::ProviderType::Application => { "application-x-executable-symbolic" } - owlry_core::providers::ProviderType::Command => "utilities-terminal-symbolic", - owlry_core::providers::ProviderType::Dmenu => "view-list-symbolic", + crate::providers::ProviderType::Command => "utilities-terminal-symbolic", + crate::providers::ProviderType::Dmenu => "view-list-symbolic", // Plugins should provide their own icon; fallback to generic addon icon - owlry_core::providers::ProviderType::Plugin(_) => "application-x-addon-symbolic", + crate::providers::ProviderType::Plugin(_) => "application-x-addon-symbolic", }; let img = Image::from_icon_name(default_icon); img.set_pixel_size(32); diff --git a/crates/owlry/src/ui/submenu.rs b/crates/owlry/src/ui/submenu.rs index 7a809ca..35512d3 100644 --- a/crates/owlry/src/ui/submenu.rs +++ b/crates/owlry/src/ui/submenu.rs @@ -46,7 +46,7 @@ //! } //! ``` -use owlry_core::providers::LaunchItem; +use crate::providers::LaunchItem; /// Parse a submenu command and extract plugin_id and data /// Returns (plugin_id, data) if command matches SUBMENU: format @@ -66,7 +66,7 @@ pub fn is_submenu_item(item: &LaunchItem) -> bool { #[cfg(test)] mod tests { use super::*; - use owlry_core::providers::{ItemSource, ProviderType}; + use crate::providers::{ItemSource, ProviderType}; #[test] fn test_parse_submenu_command() { diff --git a/crates/owlry-core/tests/ipc_test.rs b/crates/owlry/tests/ipc_test.rs similarity index 98% rename from crates/owlry-core/tests/ipc_test.rs rename to crates/owlry/tests/ipc_test.rs index ce3c62b..79fc10a 100644 --- a/crates/owlry-core/tests/ipc_test.rs +++ b/crates/owlry/tests/ipc_test.rs @@ -1,4 +1,4 @@ -use owlry_core::ipc::{ProviderDesc, Request, Response, ResultItem}; +use owlry::ipc::{ProviderDesc, Request, Response, ResultItem}; #[test] fn test_query_request_roundtrip() { diff --git a/crates/owlry-core/tests/server_test.rs b/crates/owlry/tests/server_test.rs similarity index 98% rename from crates/owlry-core/tests/server_test.rs rename to crates/owlry/tests/server_test.rs index b80ee13..4870ee0 100644 --- a/crates/owlry-core/tests/server_test.rs +++ b/crates/owlry/tests/server_test.rs @@ -2,8 +2,8 @@ use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::UnixStream; use std::thread; -use owlry_core::ipc::{Request, Response}; -use owlry_core::server::Server; +use owlry::ipc::{Request, Response}; +use owlry::server::Server; /// Helper: send a JSON request line and read the JSON response line. fn roundtrip(stream: &mut UnixStream, request: &Request) -> Response { diff --git a/docs/RESTRUCTURE-V2.md b/docs/RESTRUCTURE-V2.md index 8d74ddd..1c65885 100644 --- a/docs/RESTRUCTURE-V2.md +++ b/docs/RESTRUCTURE-V2.md @@ -451,7 +451,8 @@ This section captures in-progress state. Update freely as work proceeds. - `163e68a` — plan doc - `2fc976b` — D15–D21 resolutions - `ae4a903` — C-ABI demolition: tasks #3/#4/#5 done in one commit - - (next) — TDD characterization pass for demolition (Provider trait defaults, ProviderManager submenu/action dispatch, FromStr aliases, ItemSource simplification, CLI `-d` flag, plugin_list IPC rejection) + - `1d20754` — TDD characterization pass (+36 tests) + - (next) — Workspace collapse: owlry-core merged into owlry (task #2) - **Tasks done:** #1 inventory, #3 delete C-ABI, #4 delete Rune+Lua crates, #5 delete config_editor (scripts never lived in this repo) - **Tasks remaining (Phase 1):** #2 workspace collapse, #6 convert 8 plugins, #7 cargo features, #8 sys→power rename, #9 CLI subcommands, #10 auto-mode test, #11 final build+smoke - **Stray processes from inventory phase:** diff --git a/justfile b/justfile index af32b12..55625f6 100644 --- a/justfile +++ b/justfile @@ -8,25 +8,16 @@ default: build: cargo build --workspace -build-ui: - cargo build -p owlry - -build-daemon: - cargo build -p owlry-core - release: cargo build --workspace --release -release-daemon: - cargo build -p owlry-core --release - # === Run === run *ARGS: cargo run -p owlry -- {{ARGS}} run-daemon *ARGS: - cargo run -p owlry-core -- {{ARGS}} + cargo run -p owlry -- -d {{ARGS}} # === Quality === @@ -50,25 +41,14 @@ install-local: set -euo pipefail echo "Building release..." - cargo build -p owlry --release --no-default-features - cargo build -p owlry-core --release - cargo build -p owlry-lua -p owlry-rune --release + cargo build -p owlry --release - echo "Creating directories..." - sudo mkdir -p /usr/lib/owlry/plugins - sudo mkdir -p /usr/lib/owlry/runtimes - - echo "Installing binaries..." + echo "Installing binary..." sudo install -Dm755 target/release/owlry /usr/bin/owlry - sudo install -Dm755 target/release/owlryd /usr/bin/owlryd - - echo "Installing runtimes..." - [ -f target/release/libowlry_lua.so ] && sudo install -Dm755 target/release/libowlry_lua.so /usr/lib/owlry/runtimes/liblua.so - [ -f target/release/libowlry_rune.so ] && sudo install -Dm755 target/release/libowlry_rune.so /usr/lib/owlry/runtimes/librune.so echo "Installing systemd service files..." - [ -f systemd/owlryd.service ] && sudo install -Dm644 systemd/owlryd.service /usr/lib/systemd/user/owlryd.service - [ -f systemd/owlryd.socket ] && sudo install -Dm644 systemd/owlryd.socket /usr/lib/systemd/user/owlryd.socket + sudo install -Dm644 systemd/owlryd.service /usr/lib/systemd/user/owlryd.service + sudo install -Dm644 systemd/owlryd.socket /usr/lib/systemd/user/owlryd.socket echo "Done. Start daemon: systemctl --user enable --now owlryd.service" diff --git a/systemd/owlryd.service b/systemd/owlryd.service index 12a6454..b551eb6 100644 --- a/systemd/owlryd.service +++ b/systemd/owlryd.service @@ -5,7 +5,7 @@ After=graphical-session.target [Service] Type=simple -ExecStart=/usr/bin/owlryd +ExecStart=/usr/bin/owlry -d ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=3