feat: initial owlry application launcher
Owl-themed Wayland application launcher with GTK4 and layer-shell. Features: - Provider-based architecture (apps, commands, systemd user services) - Filter tabs and prefix shortcuts (:app, :cmd, :uuctl) - Submenu actions for systemd services (start/stop/restart/status/journal) - Smart terminal detection with fallback chain - CLI options for mode selection (--mode, --providers) - Fuzzy search with configurable max results - Custom owl-inspired dark theme 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
237
src/filter.rs
Normal file
237
src/filter.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::config::ProvidersConfig;
|
||||
use crate::providers::ProviderType;
|
||||
|
||||
/// Tracks which providers are enabled and handles prefix-based filtering
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProviderFilter {
|
||||
enabled: HashSet<ProviderType>,
|
||||
active_prefix: Option<ProviderType>,
|
||||
}
|
||||
|
||||
/// Result of parsing a query for prefix syntax
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedQuery {
|
||||
pub prefix: Option<ProviderType>,
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
impl ProviderFilter {
|
||||
/// Create filter from CLI args and config
|
||||
pub fn new(
|
||||
cli_mode: Option<ProviderType>,
|
||||
cli_providers: Option<Vec<ProviderType>>,
|
||||
config_providers: &ProvidersConfig,
|
||||
) -> Self {
|
||||
let enabled = if let Some(mode) = cli_mode {
|
||||
// --mode overrides everything: single provider
|
||||
HashSet::from([mode])
|
||||
} else if let Some(providers) = cli_providers {
|
||||
// --providers overrides config
|
||||
providers.into_iter().collect()
|
||||
} else {
|
||||
// Use config file settings, default to apps only
|
||||
let mut set = HashSet::new();
|
||||
if config_providers.applications {
|
||||
set.insert(ProviderType::Application);
|
||||
}
|
||||
if config_providers.commands {
|
||||
set.insert(ProviderType::Command);
|
||||
}
|
||||
if config_providers.uuctl {
|
||||
set.insert(ProviderType::Uuctl);
|
||||
}
|
||||
// Default to apps if nothing enabled
|
||||
if set.is_empty() {
|
||||
set.insert(ProviderType::Application);
|
||||
}
|
||||
set
|
||||
};
|
||||
|
||||
Self {
|
||||
enabled,
|
||||
active_prefix: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Default filter: apps only
|
||||
pub fn apps_only() -> Self {
|
||||
Self {
|
||||
enabled: HashSet::from([ProviderType::Application]),
|
||||
active_prefix: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle a provider on/off
|
||||
pub fn toggle(&mut self, provider: ProviderType) {
|
||||
if self.enabled.contains(&provider) {
|
||||
self.enabled.remove(&provider);
|
||||
// Ensure at least one provider is always enabled
|
||||
if self.enabled.is_empty() {
|
||||
self.enabled.insert(ProviderType::Application);
|
||||
}
|
||||
} else {
|
||||
self.enabled.insert(provider);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable a specific provider
|
||||
pub fn enable(&mut self, provider: ProviderType) {
|
||||
self.enabled.insert(provider);
|
||||
}
|
||||
|
||||
/// Disable a specific provider (ensures at least one remains)
|
||||
pub fn disable(&mut self, provider: ProviderType) {
|
||||
self.enabled.remove(&provider);
|
||||
if self.enabled.is_empty() {
|
||||
self.enabled.insert(ProviderType::Application);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set to single provider mode
|
||||
pub fn set_single_mode(&mut self, provider: ProviderType) {
|
||||
self.enabled.clear();
|
||||
self.enabled.insert(provider);
|
||||
}
|
||||
|
||||
/// Set prefix mode (from :app, :cmd, etc.)
|
||||
pub fn set_prefix(&mut self, prefix: Option<ProviderType>) {
|
||||
self.active_prefix = prefix;
|
||||
}
|
||||
|
||||
/// Check if a provider should be searched
|
||||
pub fn is_active(&self, provider: ProviderType) -> bool {
|
||||
if let Some(prefix) = self.active_prefix {
|
||||
provider == prefix
|
||||
} else {
|
||||
self.enabled.contains(&provider)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if provider is in enabled set (ignoring prefix)
|
||||
pub fn is_enabled(&self, provider: ProviderType) -> bool {
|
||||
self.enabled.contains(&provider)
|
||||
}
|
||||
|
||||
/// Get current active prefix if any
|
||||
pub fn active_prefix(&self) -> Option<ProviderType> {
|
||||
self.active_prefix
|
||||
}
|
||||
|
||||
/// Parse query for prefix syntax
|
||||
pub fn parse_query(query: &str) -> ParsedQuery {
|
||||
let trimmed = query.trim_start();
|
||||
|
||||
// Check for prefix patterns (with trailing space)
|
||||
let prefixes = [
|
||||
(":app ", ProviderType::Application),
|
||||
(":apps ", ProviderType::Application),
|
||||
(":cmd ", ProviderType::Command),
|
||||
(":command ", ProviderType::Command),
|
||||
(":uuctl ", ProviderType::Uuctl),
|
||||
];
|
||||
|
||||
for (prefix_str, provider) in prefixes {
|
||||
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
|
||||
return ParsedQuery {
|
||||
prefix: Some(provider),
|
||||
query: rest.to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle prefix without trailing space (still typing)
|
||||
let partial_prefixes = [
|
||||
(":app", ProviderType::Application),
|
||||
(":apps", ProviderType::Application),
|
||||
(":cmd", ProviderType::Command),
|
||||
(":command", ProviderType::Command),
|
||||
(":uuctl", ProviderType::Uuctl),
|
||||
];
|
||||
|
||||
for (prefix_str, provider) in partial_prefixes {
|
||||
if trimmed == prefix_str {
|
||||
return ParsedQuery {
|
||||
prefix: Some(provider),
|
||||
query: String::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ParsedQuery {
|
||||
prefix: None,
|
||||
query: query.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get enabled providers for UI display (sorted)
|
||||
pub fn enabled_providers(&self) -> Vec<ProviderType> {
|
||||
let mut providers: Vec<_> = self.enabled.iter().copied().collect();
|
||||
providers.sort_by_key(|p| match p {
|
||||
ProviderType::Application => 0,
|
||||
ProviderType::Command => 1,
|
||||
ProviderType::Uuctl => 2,
|
||||
ProviderType::Dmenu => 3,
|
||||
});
|
||||
providers
|
||||
}
|
||||
|
||||
/// Get display name for current mode
|
||||
pub fn mode_display_name(&self) -> &'static str {
|
||||
if let Some(prefix) = self.active_prefix {
|
||||
return match prefix {
|
||||
ProviderType::Application => "Apps",
|
||||
ProviderType::Command => "Commands",
|
||||
ProviderType::Uuctl => "uuctl",
|
||||
ProviderType::Dmenu => "dmenu",
|
||||
};
|
||||
}
|
||||
|
||||
let enabled: Vec<_> = self.enabled_providers();
|
||||
if enabled.len() == 1 {
|
||||
match enabled[0] {
|
||||
ProviderType::Application => "Apps",
|
||||
ProviderType::Command => "Commands",
|
||||
ProviderType::Uuctl => "uuctl",
|
||||
ProviderType::Dmenu => "dmenu",
|
||||
}
|
||||
} else {
|
||||
"All"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_query_with_prefix() {
|
||||
let result = ProviderFilter::parse_query(":app firefox");
|
||||
assert_eq!(result.prefix, Some(ProviderType::Application));
|
||||
assert_eq!(result.query, "firefox");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_query_without_prefix() {
|
||||
let result = ProviderFilter::parse_query("firefox");
|
||||
assert_eq!(result.prefix, None);
|
||||
assert_eq!(result.query, "firefox");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_query_partial_prefix() {
|
||||
let result = ProviderFilter::parse_query(":cmd");
|
||||
assert_eq!(result.prefix, Some(ProviderType::Command));
|
||||
assert_eq!(result.query, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_ensures_one_enabled() {
|
||||
let mut filter = ProviderFilter::apps_only();
|
||||
filter.toggle(ProviderType::Application);
|
||||
// Should still have apps enabled as fallback
|
||||
assert!(filter.is_enabled(ProviderType::Application));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user