From 7cdb97d743e94b7cb30c36bf49bf6e4496cf3727 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Sun, 28 Dec 2025 18:55:27 +0100 Subject: [PATCH] feat: add 7 new providers (system, ssh, clipboard, files, bookmarks, emoji, scripts) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New providers: - System: shutdown, reboot, suspend, hibernate, lock, logout, reboot into BIOS - SSH: parse ~/.ssh/config for quick host connections - Clipboard: integrate with cliphist for clipboard history - Files: search files using fd or locate (/ or find prefix) - Bookmarks: read Chrome/Chromium/Brave/Edge browser bookmarks - Emoji: searchable emoji picker with wl-copy integration - Scripts: run user scripts from ~/.config/owlry/scripts/ Filter prefixes: :sys, :ssh, :clip, :file, :bm, :emoji, :script ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- resources/base.css | 89 +++++++- src/app.rs | 3 +- src/config/mod.rs | 7 + src/filter.rs | 65 +++++- src/providers/bookmarks.rs | 242 ++++++++++++++++++++ src/providers/clipboard.rs | 137 +++++++++++ src/providers/emoji.rs | 450 +++++++++++++++++++++++++++++++++++++ src/providers/files.rs | 224 ++++++++++++++++++ src/providers/mod.rs | 69 +++++- src/providers/scripts.rs | 181 +++++++++++++++ src/providers/ssh.rs | 197 ++++++++++++++++ src/providers/system.rs | 115 ++++++++++ src/theme.rs | 21 ++ src/ui/main_window.rs | 27 ++- src/ui/result_row.rs | 7 + 15 files changed, 1816 insertions(+), 18 deletions(-) create mode 100644 src/providers/bookmarks.rs create mode 100644 src/providers/clipboard.rs create mode 100644 src/providers/emoji.rs create mode 100644 src/providers/files.rs create mode 100644 src/providers/scripts.rs create mode 100644 src/providers/ssh.rs create mode 100644 src/providers/system.rs diff --git a/resources/base.css b/resources/base.css index bb0f15f..b1ab191 100644 --- a/resources/base.css +++ b/resources/base.css @@ -106,11 +106,21 @@ color: var(--owlry-badge-app, @blue_3); } +.owlry-badge-bookmark { + background-color: alpha(var(--owlry-badge-bookmark, #f5a623), 0.2); + color: var(--owlry-badge-bookmark, #f5a623); +} + .owlry-badge-calc { background-color: alpha(var(--owlry-badge-calc, @yellow_3), 0.2); color: var(--owlry-badge-calc, @yellow_3); } +.owlry-badge-clip { + background-color: alpha(var(--owlry-badge-clip, #8b5cf6), 0.2); + color: var(--owlry-badge-clip, #8b5cf6); +} + .owlry-badge-cmd { background-color: alpha(var(--owlry-badge-cmd, @purple_3), 0.2); color: var(--owlry-badge-cmd, @purple_3); @@ -121,6 +131,31 @@ color: var(--owlry-badge-dmenu, @green_3); } +.owlry-badge-emoji { + background-color: alpha(var(--owlry-badge-emoji, #f472b6), 0.2); + color: var(--owlry-badge-emoji, #f472b6); +} + +.owlry-badge-file { + background-color: alpha(var(--owlry-badge-file, #22d3ee), 0.2); + color: var(--owlry-badge-file, #22d3ee); +} + +.owlry-badge-script { + background-color: alpha(var(--owlry-badge-script, #a3e635), 0.2); + color: var(--owlry-badge-script, #a3e635); +} + +.owlry-badge-ssh { + background-color: alpha(var(--owlry-badge-ssh, #2dd4bf), 0.2); + color: var(--owlry-badge-ssh, #2dd4bf); +} + +.owlry-badge-sys { + background-color: alpha(var(--owlry-badge-sys, #ef4444), 0.2); + color: var(--owlry-badge-sys, #ef4444); +} + .owlry-badge-uuctl { background-color: alpha(var(--owlry-badge-uuctl, @orange_3), 0.2); color: var(--owlry-badge-uuctl, @orange_3); @@ -176,30 +211,72 @@ border-color: alpha(var(--owlry-badge-app, @blue_3), 0.4); } +.owlry-filter-bookmark:checked { + background-color: alpha(var(--owlry-badge-bookmark, #f5a623), 0.2); + color: var(--owlry-badge-bookmark, #f5a623); + border-color: alpha(var(--owlry-badge-bookmark, #f5a623), 0.4); +} + .owlry-filter-calc:checked { background-color: alpha(var(--owlry-badge-calc, @yellow_3), 0.2); color: var(--owlry-badge-calc, @yellow_3); border-color: alpha(var(--owlry-badge-calc, @yellow_3), 0.4); } +.owlry-filter-clip:checked { + background-color: alpha(var(--owlry-badge-clip, #8b5cf6), 0.2); + color: var(--owlry-badge-clip, #8b5cf6); + border-color: alpha(var(--owlry-badge-clip, #8b5cf6), 0.4); +} + .owlry-filter-cmd:checked { background-color: alpha(var(--owlry-badge-cmd, @purple_3), 0.2); color: var(--owlry-badge-cmd, @purple_3); border-color: alpha(var(--owlry-badge-cmd, @purple_3), 0.4); } -.owlry-filter-uuctl:checked { - background-color: alpha(var(--owlry-badge-uuctl, @orange_3), 0.2); - color: var(--owlry-badge-uuctl, @orange_3); - border-color: alpha(var(--owlry-badge-uuctl, @orange_3), 0.4); -} - .owlry-filter-dmenu:checked { background-color: alpha(var(--owlry-badge-dmenu, @green_3), 0.2); color: var(--owlry-badge-dmenu, @green_3); border-color: alpha(var(--owlry-badge-dmenu, @green_3), 0.4); } +.owlry-filter-emoji:checked { + background-color: alpha(var(--owlry-badge-emoji, #f472b6), 0.2); + color: var(--owlry-badge-emoji, #f472b6); + border-color: alpha(var(--owlry-badge-emoji, #f472b6), 0.4); +} + +.owlry-filter-file:checked { + background-color: alpha(var(--owlry-badge-file, #22d3ee), 0.2); + color: var(--owlry-badge-file, #22d3ee); + border-color: alpha(var(--owlry-badge-file, #22d3ee), 0.4); +} + +.owlry-filter-script:checked { + background-color: alpha(var(--owlry-badge-script, #a3e635), 0.2); + color: var(--owlry-badge-script, #a3e635); + border-color: alpha(var(--owlry-badge-script, #a3e635), 0.4); +} + +.owlry-filter-ssh:checked { + background-color: alpha(var(--owlry-badge-ssh, #2dd4bf), 0.2); + color: var(--owlry-badge-ssh, #2dd4bf); + border-color: alpha(var(--owlry-badge-ssh, #2dd4bf), 0.4); +} + +.owlry-filter-sys:checked { + background-color: alpha(var(--owlry-badge-sys, #ef4444), 0.2); + color: var(--owlry-badge-sys, #ef4444); + border-color: alpha(var(--owlry-badge-sys, #ef4444), 0.4); +} + +.owlry-filter-uuctl:checked { + background-color: alpha(var(--owlry-badge-uuctl, @orange_3), 0.2); + color: var(--owlry-badge-uuctl, @orange_3); + border-color: alpha(var(--owlry-badge-uuctl, @orange_3), 0.4); +} + .owlry-filter-web:checked { background-color: alpha(var(--owlry-badge-web, @teal_3), 0.2); color: var(--owlry-badge-web, @teal_3); diff --git a/src/app.rs b/src/app.rs index d33dc54..57a7218 100644 --- a/src/app.rs +++ b/src/app.rs @@ -41,7 +41,8 @@ impl OwlryApp { let config = Rc::new(RefCell::new(Config::load_or_default())); let search_engine = config.borrow().providers.search_engine.clone(); - let providers = Rc::new(RefCell::new(ProviderManager::with_search_engine(&search_engine))); + let terminal = config.borrow().general.terminal_command.clone(); + let providers = Rc::new(RefCell::new(ProviderManager::with_config(&search_engine, &terminal))); let frecency = Rc::new(RefCell::new(FrecencyStore::load_or_default())); // Create filter from CLI args and config diff --git a/src/config/mod.rs b/src/config/mod.rs index e182623..0cef0a7 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -35,9 +35,16 @@ pub struct ThemeColors { pub accent_bright: Option, // Provider badge colors pub badge_app: Option, + pub badge_bookmark: Option, pub badge_calc: Option, + pub badge_clip: Option, pub badge_cmd: Option, pub badge_dmenu: Option, + pub badge_emoji: Option, + pub badge_file: Option, + pub badge_script: Option, + pub badge_ssh: Option, + pub badge_sys: Option, pub badge_uuctl: Option, pub badge_web: Option, } diff --git a/src/filter.rs b/src/filter.rs index bbab1cd..a4a3c27 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -129,10 +129,26 @@ impl ProviderFilter { let prefixes = [ (":app ", ProviderType::Application), (":apps ", ProviderType::Application), + (":bm ", ProviderType::Bookmarks), + (":bookmark ", ProviderType::Bookmarks), + (":bookmarks ", ProviderType::Bookmarks), (":calc ", ProviderType::Calculator), (":calculator ", ProviderType::Calculator), + (":clip ", ProviderType::Clipboard), + (":clipboard ", ProviderType::Clipboard), (":cmd ", ProviderType::Command), (":command ", ProviderType::Command), + (":emoji ", ProviderType::Emoji), + (":emojis ", ProviderType::Emoji), + (":file ", ProviderType::Files), + (":files ", ProviderType::Files), + (":find ", ProviderType::Files), + (":script ", ProviderType::Scripts), + (":scripts ", ProviderType::Scripts), + (":ssh ", ProviderType::Ssh), + (":sys ", ProviderType::System), + (":system ", ProviderType::System), + (":power ", ProviderType::System), (":uuctl ", ProviderType::Uuctl), (":web ", ProviderType::WebSearch), (":search ", ProviderType::WebSearch), @@ -151,10 +167,26 @@ impl ProviderFilter { let partial_prefixes = [ (":app", ProviderType::Application), (":apps", ProviderType::Application), + (":bm", ProviderType::Bookmarks), + (":bookmark", ProviderType::Bookmarks), + (":bookmarks", ProviderType::Bookmarks), (":calc", ProviderType::Calculator), (":calculator", ProviderType::Calculator), + (":clip", ProviderType::Clipboard), + (":clipboard", ProviderType::Clipboard), (":cmd", ProviderType::Command), (":command", ProviderType::Command), + (":emoji", ProviderType::Emoji), + (":emojis", ProviderType::Emoji), + (":file", ProviderType::Files), + (":files", ProviderType::Files), + (":find", ProviderType::Files), + (":script", ProviderType::Scripts), + (":scripts", ProviderType::Scripts), + (":ssh", ProviderType::Ssh), + (":sys", ProviderType::System), + (":system", ProviderType::System), + (":power", ProviderType::System), (":uuctl", ProviderType::Uuctl), (":web", ProviderType::WebSearch), (":search", ProviderType::WebSearch), @@ -180,11 +212,18 @@ impl ProviderFilter { let mut providers: Vec<_> = self.enabled.iter().copied().collect(); providers.sort_by_key(|p| match p { ProviderType::Application => 0, - ProviderType::Calculator => 1, - ProviderType::Command => 2, - ProviderType::Uuctl => 3, - ProviderType::WebSearch => 4, + ProviderType::Bookmarks => 1, + ProviderType::Calculator => 2, + ProviderType::Clipboard => 3, + ProviderType::Command => 4, ProviderType::Dmenu => 5, + ProviderType::Emoji => 6, + ProviderType::Files => 7, + ProviderType::Scripts => 8, + ProviderType::Ssh => 9, + ProviderType::System => 10, + ProviderType::Uuctl => 11, + ProviderType::WebSearch => 12, }); providers } @@ -194,11 +233,18 @@ impl ProviderFilter { if let Some(prefix) = self.active_prefix { return match prefix { ProviderType::Application => "Apps", + ProviderType::Bookmarks => "Bookmarks", ProviderType::Calculator => "Calc", + ProviderType::Clipboard => "Clipboard", ProviderType::Command => "Commands", + ProviderType::Dmenu => "dmenu", + ProviderType::Emoji => "Emoji", + ProviderType::Files => "Files", + ProviderType::Scripts => "Scripts", + ProviderType::Ssh => "SSH", + ProviderType::System => "System", ProviderType::Uuctl => "uuctl", ProviderType::WebSearch => "Web", - ProviderType::Dmenu => "dmenu", }; } @@ -206,11 +252,18 @@ impl ProviderFilter { if enabled.len() == 1 { match enabled[0] { ProviderType::Application => "Apps", + ProviderType::Bookmarks => "Bookmarks", ProviderType::Calculator => "Calc", + ProviderType::Clipboard => "Clipboard", ProviderType::Command => "Commands", + ProviderType::Dmenu => "dmenu", + ProviderType::Emoji => "Emoji", + ProviderType::Files => "Files", + ProviderType::Scripts => "Scripts", + ProviderType::Ssh => "SSH", + ProviderType::System => "System", ProviderType::Uuctl => "uuctl", ProviderType::WebSearch => "Web", - ProviderType::Dmenu => "dmenu", } } else { "All" diff --git a/src/providers/bookmarks.rs b/src/providers/bookmarks.rs new file mode 100644 index 0000000..df98ae0 --- /dev/null +++ b/src/providers/bookmarks.rs @@ -0,0 +1,242 @@ +use crate::providers::{LaunchItem, Provider, ProviderType}; +use log::{debug, warn}; +use serde::Deserialize; +use std::fs; +use std::path::PathBuf; + +/// Browser bookmarks provider - reads Firefox and Chrome bookmarks +pub struct BookmarksProvider { + items: Vec, +} + +impl BookmarksProvider { + pub fn new() -> Self { + Self { items: Vec::new() } + } + + fn load_bookmarks(&mut self) { + self.items.clear(); + + // Try Firefox first, then Chrome/Chromium + self.load_firefox_bookmarks(); + self.load_chrome_bookmarks(); + + debug!("Loaded {} bookmarks total", self.items.len()); + } + + fn load_firefox_bookmarks(&mut self) { + // Firefox stores bookmarks in places.sqlite + // The file is locked when Firefox is running, so we read from backup + let firefox_dir = match dirs::home_dir() { + Some(h) => h.join(".mozilla").join("firefox"), + None => return, + }; + + if !firefox_dir.exists() { + debug!("Firefox directory not found"); + return; + } + + // Find default profile (ends with .default-release or .default) + let profile_dir = match Self::find_firefox_profile(&firefox_dir) { + Some(p) => p, + None => { + debug!("No Firefox profile found"); + return; + } + }; + + // Try to read bookmarkbackups (JSON format, not locked) + let backup_dir = profile_dir.join("bookmarkbackups"); + if backup_dir.exists() { + if let Some(latest_backup) = Self::find_latest_file(&backup_dir, "jsonlz4") { + // jsonlz4 files need decompression - skip for now, try places.sqlite + debug!("Found Firefox backup at {:?}, but jsonlz4 not supported", latest_backup); + } + } + + // Try places.sqlite directly (may fail if Firefox is running) + let places_db = profile_dir.join("places.sqlite"); + if places_db.exists() { + self.read_firefox_places(&places_db); + } + } + + fn find_firefox_profile(firefox_dir: &PathBuf) -> Option { + let entries = fs::read_dir(firefox_dir).ok()?; + + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.ends_with(".default-release") || name.ends_with(".default") { + return Some(entry.path()); + } + } + None + } + + fn find_latest_file(dir: &PathBuf, extension: &str) -> Option { + let entries = fs::read_dir(dir).ok()?; + + entries + .flatten() + .filter(|e| { + e.path() + .extension() + .map(|ext| ext == extension) + .unwrap_or(false) + }) + .max_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok())) + .map(|e| e.path()) + } + + fn read_firefox_places(&mut self, db_path: &PathBuf) { + // Note: This requires the rusqlite crate which we don't have + // For now, skip Firefox SQLite reading + debug!( + "Firefox places.sqlite found at {:?}, but SQLite reading not implemented", + db_path + ); + } + + fn load_chrome_bookmarks(&mut self) { + // Chrome/Chromium bookmarks are in JSON format + let home = match dirs::home_dir() { + Some(h) => h, + None => return, + }; + + // Try multiple browser paths + let bookmark_paths = [ + // Chrome + home.join(".config/google-chrome/Default/Bookmarks"), + // Chromium + home.join(".config/chromium/Default/Bookmarks"), + // Brave + home.join(".config/BraveSoftware/Brave-Browser/Default/Bookmarks"), + // Edge + home.join(".config/microsoft-edge/Default/Bookmarks"), + // Vivaldi + home.join(".config/vivaldi/Default/Bookmarks"), + ]; + + for path in &bookmark_paths { + if path.exists() { + self.read_chrome_bookmarks(path); + } + } + } + + fn read_chrome_bookmarks(&mut self, path: &PathBuf) { + let content = match fs::read_to_string(path) { + Ok(c) => c, + Err(e) => { + warn!("Failed to read Chrome bookmarks from {:?}: {}", path, e); + return; + } + }; + + let bookmarks: ChromeBookmarks = match serde_json::from_str(&content) { + Ok(b) => b, + Err(e) => { + warn!("Failed to parse Chrome bookmarks: {}", e); + return; + } + }; + + // Process bookmark bar and other folders + if let Some(roots) = bookmarks.roots { + if let Some(bar) = roots.bookmark_bar { + self.process_chrome_folder(&bar); + } + if let Some(other) = roots.other { + self.process_chrome_folder(&other); + } + if let Some(synced) = roots.synced { + self.process_chrome_folder(&synced); + } + } + + debug!("Loaded Chrome bookmarks from {:?}", path); + } + + fn process_chrome_folder(&mut self, folder: &ChromeBookmarkNode) { + if let Some(ref children) = folder.children { + for child in children { + match child.node_type.as_deref() { + Some("url") => { + if let Some(ref url) = child.url { + let name = child.name.clone().unwrap_or_else(|| url.clone()); + + self.items.push(LaunchItem { + id: format!("bookmark:{}", url), + name, + description: Some(url.clone()), + icon: Some("web-browser".to_string()), + provider: ProviderType::Bookmarks, + command: format!("xdg-open '{}'", url.replace('\'', "'\\''")), + terminal: false, + }); + } + } + Some("folder") => { + // Recursively process subfolders + self.process_chrome_folder(child); + } + _ => {} + } + } + } + } +} + +// Chrome bookmark JSON structures +#[derive(Debug, Deserialize)] +struct ChromeBookmarks { + roots: Option, +} + +#[derive(Debug, Deserialize)] +struct ChromeBookmarkRoots { + bookmark_bar: Option, + other: Option, + synced: Option, +} + +#[derive(Debug, Deserialize)] +struct ChromeBookmarkNode { + name: Option, + url: Option, + #[serde(rename = "type")] + node_type: Option, + children: Option>, +} + +impl Provider for BookmarksProvider { + fn name(&self) -> &str { + "Bookmarks" + } + + fn provider_type(&self) -> ProviderType { + ProviderType::Bookmarks + } + + fn refresh(&mut self) { + self.load_bookmarks(); + } + + fn items(&self) -> &[LaunchItem] { + &self.items + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bookmarks_provider() { + let mut provider = BookmarksProvider::new(); + provider.refresh(); + // Just ensure it doesn't panic + } +} diff --git a/src/providers/clipboard.rs b/src/providers/clipboard.rs new file mode 100644 index 0000000..3ddde93 --- /dev/null +++ b/src/providers/clipboard.rs @@ -0,0 +1,137 @@ +use crate::providers::{LaunchItem, Provider, ProviderType}; +use log::{debug, warn}; +use std::process::Command; + +/// Clipboard history provider - integrates with cliphist +pub struct ClipboardProvider { + items: Vec, + max_entries: usize, +} + +impl ClipboardProvider { + pub fn new() -> Self { + Self { + items: Vec::new(), + max_entries: 50, + } + } + + #[allow(dead_code)] + pub fn with_max_entries(max_entries: usize) -> Self { + Self { + items: Vec::new(), + max_entries, + } + } + + /// Check if cliphist is available + fn has_cliphist() -> bool { + Command::new("which") + .arg("cliphist") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + } + + fn load_clipboard_history(&mut self) { + self.items.clear(); + + if !Self::has_cliphist() { + debug!("cliphist not found, clipboard provider disabled"); + return; + } + + // Get clipboard history from cliphist + let output = match Command::new("cliphist").arg("list").output() { + Ok(o) => o, + Err(e) => { + warn!("Failed to run cliphist: {}", e); + return; + } + }; + + if !output.status.success() { + debug!("cliphist list returned non-zero status"); + return; + } + + let content = String::from_utf8_lossy(&output.stdout); + + for (idx, line) in content.lines().take(self.max_entries).enumerate() { + // cliphist format: "id\tpreview" + let parts: Vec<&str> = line.splitn(2, '\t').collect(); + + if parts.is_empty() { + continue; + } + + let clip_id = parts[0]; + let preview = if parts.len() > 1 { + // Truncate long previews + let p = parts[1]; + if p.len() > 80 { + format!("{}...", &p[..77]) + } else { + p.to_string() + } + } else { + "[binary data]".to_string() + }; + + // Clean up preview - replace newlines with spaces + let preview_clean = preview + .replace('\n', " ") + .replace('\r', "") + .replace('\t', " "); + + // Command to paste this entry + // echo "id" | cliphist decode | wl-copy + let command = format!( + "echo '{}' | cliphist decode | wl-copy", + clip_id.replace('\'', "'\\''") + ); + + self.items.push(LaunchItem { + id: format!("clipboard:{}", idx), + name: preview_clean, + description: Some("Copy to clipboard".to_string()), + icon: Some("edit-paste".to_string()), + provider: ProviderType::Clipboard, + command, + terminal: false, + }); + } + + debug!("Loaded {} clipboard entries", self.items.len()); + } +} + +impl Provider for ClipboardProvider { + fn name(&self) -> &str { + "Clipboard" + } + + fn provider_type(&self) -> ProviderType { + ProviderType::Clipboard + } + + fn refresh(&mut self) { + self.load_clipboard_history(); + } + + fn items(&self) -> &[LaunchItem] { + &self.items + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_clipboard_provider() { + let mut provider = ClipboardProvider::new(); + provider.refresh(); + // Just ensure it doesn't panic - cliphist may not be installed + } +} diff --git a/src/providers/emoji.rs b/src/providers/emoji.rs new file mode 100644 index 0000000..4d3d711 --- /dev/null +++ b/src/providers/emoji.rs @@ -0,0 +1,450 @@ +use crate::providers::{LaunchItem, Provider, ProviderType}; + +/// Emoji picker provider - search and copy emojis +pub struct EmojiProvider { + items: Vec, +} + +impl EmojiProvider { + pub fn new() -> Self { + Self { items: Vec::new() } + } + + fn load_emojis(&mut self) { + self.items.clear(); + + // Common emojis with searchable names + // Format: (emoji, name, keywords) + let emojis: &[(&str, &str, &str)] = &[ + // Smileys & Emotion + ("๐Ÿ˜€", "grinning face", "smile happy"), + ("๐Ÿ˜ƒ", "grinning face with big eyes", "smile happy"), + ("๐Ÿ˜„", "grinning face with smiling eyes", "smile happy laugh"), + ("๐Ÿ˜", "beaming face with smiling eyes", "smile happy grin"), + ("๐Ÿ˜…", "grinning face with sweat", "smile nervous"), + ("๐Ÿคฃ", "rolling on the floor laughing", "lol rofl funny"), + ("๐Ÿ˜‚", "face with tears of joy", "laugh cry funny lol"), + ("๐Ÿ™‚", "slightly smiling face", "smile"), + ("๐Ÿ˜Š", "smiling face with smiling eyes", "blush happy"), + ("๐Ÿ˜‡", "smiling face with halo", "angel innocent"), + ("๐Ÿฅฐ", "smiling face with hearts", "love adore"), + ("๐Ÿ˜", "smiling face with heart-eyes", "love crush"), + ("๐Ÿคฉ", "star-struck", "excited wow amazing"), + ("๐Ÿ˜˜", "face blowing a kiss", "kiss love"), + ("๐Ÿ˜œ", "winking face with tongue", "playful silly"), + ("๐Ÿคช", "zany face", "crazy silly wild"), + ("๐Ÿ˜Ž", "smiling face with sunglasses", "cool"), + ("๐Ÿค“", "nerd face", "geek glasses"), + ("๐Ÿง", "face with monocle", "thinking inspect"), + ("๐Ÿ˜", "smirking face", "smug"), + ("๐Ÿ˜’", "unamused face", "meh annoyed"), + ("๐Ÿ™„", "face with rolling eyes", "whatever annoyed"), + ("๐Ÿ˜ฌ", "grimacing face", "awkward nervous"), + ("๐Ÿ˜ฎโ€๐Ÿ’จ", "face exhaling", "sigh relief"), + ("๐Ÿคฅ", "lying face", "pinocchio lie"), + ("๐Ÿ˜Œ", "relieved face", "relaxed peaceful"), + ("๐Ÿ˜”", "pensive face", "sad thoughtful"), + ("๐Ÿ˜ช", "sleepy face", "tired"), + ("๐Ÿคค", "drooling face", "hungry yummy"), + ("๐Ÿ˜ด", "sleeping face", "zzz tired"), + ("๐Ÿ˜ท", "face with medical mask", "sick covid"), + ("๐Ÿค’", "face with thermometer", "sick fever"), + ("๐Ÿค•", "face with head-bandage", "hurt injured"), + ("๐Ÿคข", "nauseated face", "sick gross"), + ("๐Ÿคฎ", "face vomiting", "sick puke"), + ("๐Ÿคง", "sneezing face", "achoo sick"), + ("๐Ÿฅต", "hot face", "sweating heat"), + ("๐Ÿฅถ", "cold face", "freezing"), + ("๐Ÿ˜ต", "face with crossed-out eyes", "dizzy dead"), + ("๐Ÿคฏ", "exploding head", "mind blown wow"), + ("๐Ÿค ", "cowboy hat face", "yeehaw western"), + ("๐Ÿฅณ", "partying face", "celebration party"), + ("๐Ÿฅธ", "disguised face", "incognito"), + ("๐Ÿ˜Ž", "cool face", "sunglasses"), + ("๐Ÿคก", "clown face", "circus"), + ("๐Ÿ‘ป", "ghost", "halloween spooky"), + ("๐Ÿ’€", "skull", "dead death"), + ("โ˜ ๏ธ", "skull and crossbones", "danger death"), + ("๐Ÿ‘ฝ", "alien", "ufo extraterrestrial"), + ("๐Ÿค–", "robot", "bot android"), + ("๐Ÿ’ฉ", "pile of poo", "poop shit"), + ("๐Ÿ˜ˆ", "smiling face with horns", "devil evil"), + ("๐Ÿ‘ฟ", "angry face with horns", "devil evil"), + // Gestures & People + ("๐Ÿ‘‹", "waving hand", "hello hi bye wave"), + ("๐Ÿคš", "raised back of hand", "stop"), + ("๐Ÿ–๏ธ", "hand with fingers splayed", "five high"), + ("โœ‹", "raised hand", "stop high five"), + ("๐Ÿ––", "vulcan salute", "spock trek"), + ("๐Ÿ‘Œ", "ok hand", "okay perfect"), + ("๐ŸคŒ", "pinched fingers", "italian"), + ("๐Ÿค", "pinching hand", "small tiny"), + ("โœŒ๏ธ", "victory hand", "peace two"), + ("๐Ÿคž", "crossed fingers", "luck hope"), + ("๐ŸคŸ", "love-you gesture", "ily rock"), + ("๐Ÿค˜", "sign of the horns", "rock metal"), + ("๐Ÿค™", "call me hand", "shaka hang loose"), + ("๐Ÿ‘ˆ", "backhand index pointing left", "left point"), + ("๐Ÿ‘‰", "backhand index pointing right", "right point"), + ("๐Ÿ‘†", "backhand index pointing up", "up point"), + ("๐Ÿ‘‡", "backhand index pointing down", "down point"), + ("โ˜๏ธ", "index pointing up", "one point"), + ("๐Ÿ‘", "thumbs up", "like yes good approve"), + ("๐Ÿ‘Ž", "thumbs down", "dislike no bad"), + ("โœŠ", "raised fist", "power solidarity"), + ("๐Ÿ‘Š", "oncoming fist", "punch bump"), + ("๐Ÿค›", "left-facing fist", "fist bump"), + ("๐Ÿคœ", "right-facing fist", "fist bump"), + ("๐Ÿ‘", "clapping hands", "applause bravo"), + ("๐Ÿ™Œ", "raising hands", "hooray celebrate"), + ("๐Ÿ‘", "open hands", "hug"), + ("๐Ÿคฒ", "palms up together", "prayer"), + ("๐Ÿค", "handshake", "agreement deal"), + ("๐Ÿ™", "folded hands", "prayer please thanks"), + ("โœ๏ธ", "writing hand", "write"), + ("๐Ÿ’ช", "flexed biceps", "strong muscle"), + ("๐Ÿฆพ", "mechanical arm", "robot prosthetic"), + ("๐Ÿฆต", "leg", "kick"), + ("๐Ÿฆถ", "foot", "kick"), + ("๐Ÿ‘‚", "ear", "listen hear"), + ("๐Ÿ‘ƒ", "nose", "smell"), + ("๐Ÿง ", "brain", "smart think"), + ("๐Ÿ‘€", "eyes", "look see watch"), + ("๐Ÿ‘๏ธ", "eye", "see look"), + ("๐Ÿ‘…", "tongue", "taste lick"), + ("๐Ÿ‘„", "mouth", "lips kiss"), + // Hearts & Love + ("โค๏ธ", "red heart", "love"), + ("๐Ÿงก", "orange heart", "love"), + ("๐Ÿ’›", "yellow heart", "love friendship"), + ("๐Ÿ’š", "green heart", "love"), + ("๐Ÿ’™", "blue heart", "love"), + ("๐Ÿ’œ", "purple heart", "love"), + ("๐Ÿ–ค", "black heart", "love dark"), + ("๐Ÿค", "white heart", "love pure"), + ("๐ŸคŽ", "brown heart", "love"), + ("๐Ÿ’”", "broken heart", "heartbreak sad"), + ("โค๏ธโ€๐Ÿ”ฅ", "heart on fire", "passion love"), + ("โค๏ธโ€๐Ÿฉน", "mending heart", "healing recovery"), + ("๐Ÿ’•", "two hearts", "love"), + ("๐Ÿ’ž", "revolving hearts", "love"), + ("๐Ÿ’“", "beating heart", "love"), + ("๐Ÿ’—", "growing heart", "love"), + ("๐Ÿ’–", "sparkling heart", "love"), + ("๐Ÿ’˜", "heart with arrow", "love cupid"), + ("๐Ÿ’", "heart with ribbon", "love gift"), + ("๐Ÿ’Ÿ", "heart decoration", "love"), + // Animals + ("๐Ÿถ", "dog face", "puppy"), + ("๐Ÿฑ", "cat face", "kitty"), + ("๐Ÿญ", "mouse face", ""), + ("๐Ÿน", "hamster", ""), + ("๐Ÿฐ", "rabbit face", "bunny"), + ("๐ŸฆŠ", "fox", ""), + ("๐Ÿป", "bear", ""), + ("๐Ÿผ", "panda", ""), + ("๐Ÿจ", "koala", ""), + ("๐Ÿฏ", "tiger face", ""), + ("๐Ÿฆ", "lion", ""), + ("๐Ÿฎ", "cow face", ""), + ("๐Ÿท", "pig face", ""), + ("๐Ÿธ", "frog", ""), + ("๐Ÿต", "monkey face", ""), + ("๐Ÿฆ„", "unicorn", "magic"), + ("๐Ÿ", "bee", "honeybee"), + ("๐Ÿฆ‹", "butterfly", ""), + ("๐ŸŒ", "snail", "slow"), + ("๐Ÿ›", "bug", "caterpillar"), + ("๐Ÿฆ€", "crab", ""), + ("๐Ÿ™", "octopus", ""), + ("๐Ÿ ", "tropical fish", ""), + ("๐ŸŸ", "fish", ""), + ("๐Ÿฌ", "dolphin", ""), + ("๐Ÿณ", "whale", ""), + ("๐Ÿฆˆ", "shark", ""), + ("๐ŸŠ", "crocodile", "alligator"), + ("๐Ÿข", "turtle", ""), + ("๐ŸฆŽ", "lizard", ""), + ("๐Ÿ", "snake", ""), + ("๐Ÿฆ–", "t-rex", "dinosaur"), + ("๐Ÿฆ•", "sauropod", "dinosaur"), + ("๐Ÿ”", "chicken", ""), + ("๐Ÿง", "penguin", ""), + ("๐Ÿฆ…", "eagle", "bird"), + ("๐Ÿฆ†", "duck", ""), + ("๐Ÿฆ‰", "owl", ""), + // Food & Drink + ("๐ŸŽ", "red apple", "fruit"), + ("๐Ÿ", "pear", "fruit"), + ("๐ŸŠ", "orange", "tangerine fruit"), + ("๐Ÿ‹", "lemon", "fruit"), + ("๐ŸŒ", "banana", "fruit"), + ("๐Ÿ‰", "watermelon", "fruit"), + ("๐Ÿ‡", "grapes", "fruit"), + ("๐Ÿ“", "strawberry", "fruit"), + ("๐Ÿ’", "cherries", "fruit"), + ("๐Ÿ‘", "peach", "fruit"), + ("๐Ÿฅญ", "mango", "fruit"), + ("๐Ÿ", "pineapple", "fruit"), + ("๐Ÿฅฅ", "coconut", "fruit"), + ("๐Ÿฅ", "kiwi", "fruit"), + ("๐Ÿ…", "tomato", "vegetable"), + ("๐Ÿฅ‘", "avocado", ""), + ("๐Ÿฅฆ", "broccoli", "vegetable"), + ("๐Ÿฅฌ", "leafy green", "vegetable salad"), + ("๐Ÿฅ’", "cucumber", "vegetable"), + ("๐ŸŒถ๏ธ", "hot pepper", "spicy chili"), + ("๐ŸŒฝ", "corn", ""), + ("๐Ÿฅ•", "carrot", "vegetable"), + ("๐Ÿง„", "garlic", ""), + ("๐Ÿง…", "onion", ""), + ("๐Ÿฅ”", "potato", ""), + ("๐Ÿž", "bread", ""), + ("๐Ÿฅ", "croissant", ""), + ("๐Ÿฅ–", "baguette", "bread french"), + ("๐Ÿฅจ", "pretzel", ""), + ("๐Ÿง€", "cheese", ""), + ("๐Ÿฅš", "egg", ""), + ("๐Ÿณ", "cooking", "frying pan egg"), + ("๐Ÿฅž", "pancakes", "breakfast"), + ("๐Ÿง‡", "waffle", "breakfast"), + ("๐Ÿฅ“", "bacon", "breakfast"), + ("๐Ÿ”", "hamburger", "burger"), + ("๐ŸŸ", "french fries", ""), + ("๐Ÿ•", "pizza", ""), + ("๐ŸŒญ", "hot dog", ""), + ("๐Ÿฅช", "sandwich", ""), + ("๐ŸŒฎ", "taco", "mexican"), + ("๐ŸŒฏ", "burrito", "mexican"), + ("๐Ÿœ", "steaming bowl", "ramen noodles"), + ("๐Ÿ", "spaghetti", "pasta"), + ("๐Ÿฃ", "sushi", "japanese"), + ("๐Ÿฑ", "bento box", "japanese"), + ("๐Ÿฉ", "doughnut", "donut dessert"), + ("๐Ÿช", "cookie", "dessert"), + ("๐ŸŽ‚", "birthday cake", "dessert"), + ("๐Ÿฐ", "shortcake", "dessert"), + ("๐Ÿง", "cupcake", "dessert"), + ("๐Ÿซ", "chocolate bar", "dessert"), + ("๐Ÿฌ", "candy", "sweet"), + ("๐Ÿญ", "lollipop", "candy sweet"), + ("๐Ÿฆ", "soft ice cream", "dessert"), + ("๐Ÿจ", "ice cream", "dessert"), + ("โ˜•", "hot beverage", "coffee tea"), + ("๐Ÿต", "teacup", "tea"), + ("๐Ÿงƒ", "juice box", ""), + ("๐Ÿฅค", "cup with straw", "soda drink"), + ("๐Ÿบ", "beer mug", "drink alcohol"), + ("๐Ÿป", "clinking beer mugs", "cheers drink"), + ("๐Ÿฅ‚", "clinking glasses", "champagne cheers"), + ("๐Ÿท", "wine glass", "drink alcohol"), + ("๐Ÿฅƒ", "tumbler glass", "whiskey drink"), + ("๐Ÿธ", "cocktail glass", "martini drink"), + // Objects & Symbols + ("๐Ÿ’ป", "laptop", "computer"), + ("๐Ÿ–ฅ๏ธ", "desktop computer", "pc"), + ("โŒจ๏ธ", "keyboard", ""), + ("๐Ÿ–ฑ๏ธ", "computer mouse", ""), + ("๐Ÿ’พ", "floppy disk", "save"), + ("๐Ÿ’ฟ", "optical disk", "cd"), + ("๐Ÿ“ฑ", "mobile phone", "smartphone"), + ("โ˜Ž๏ธ", "telephone", "phone"), + ("๐Ÿ“ง", "email", "mail"), + ("๐Ÿ“จ", "incoming envelope", "email"), + ("๐Ÿ“ฉ", "envelope with arrow", "email send"), + ("๐Ÿ“", "memo", "note write"), + ("๐Ÿ“„", "page facing up", "document"), + ("๐Ÿ“ƒ", "page with curl", "document"), + ("๐Ÿ“‘", "bookmark tabs", ""), + ("๐Ÿ“š", "books", "library read"), + ("๐Ÿ“–", "open book", "read"), + ("๐Ÿ”—", "link", "chain url"), + ("๐Ÿ“Ž", "paperclip", "attachment"), + ("๐Ÿ”’", "locked", "security"), + ("๐Ÿ”“", "unlocked", "security open"), + ("๐Ÿ”‘", "key", "password"), + ("๐Ÿ”ง", "wrench", "tool fix"), + ("๐Ÿ”จ", "hammer", "tool"), + ("โš™๏ธ", "gear", "settings"), + ("๐Ÿงฒ", "magnet", ""), + ("๐Ÿ’ก", "light bulb", "idea"), + ("๐Ÿ”ฆ", "flashlight", ""), + ("๐Ÿ”‹", "battery", "power"), + ("๐Ÿ”Œ", "electric plug", "power"), + ("๐Ÿ’ฐ", "money bag", ""), + ("๐Ÿ’ต", "dollar", "money cash"), + ("๐Ÿ’ณ", "credit card", "payment"), + ("โฐ", "alarm clock", "time"), + ("โฑ๏ธ", "stopwatch", "timer"), + ("๐Ÿ“…", "calendar", "date"), + ("๐Ÿ“†", "tear-off calendar", "date"), + ("โœ…", "check mark", "done yes"), + ("โŒ", "cross mark", "no wrong delete"), + ("โ“", "question mark", "help"), + ("โ—", "exclamation mark", "important warning"), + ("โš ๏ธ", "warning", "caution alert"), + ("๐Ÿšซ", "prohibited", "no ban forbidden"), + ("โญ•", "hollow circle", ""), + ("๐Ÿ”ด", "red circle", ""), + ("๐ŸŸ ", "orange circle", ""), + ("๐ŸŸก", "yellow circle", ""), + ("๐ŸŸข", "green circle", ""), + ("๐Ÿ”ต", "blue circle", ""), + ("๐ŸŸฃ", "purple circle", ""), + ("โšซ", "black circle", ""), + ("โšช", "white circle", ""), + ("๐ŸŸค", "brown circle", ""), + ("โฌ›", "black square", ""), + ("โฌœ", "white square", ""), + ("๐Ÿ”ถ", "large orange diamond", ""), + ("๐Ÿ”ท", "large blue diamond", ""), + ("โญ", "star", "favorite"), + ("๐ŸŒŸ", "glowing star", "sparkle"), + ("โœจ", "sparkles", "magic shine"), + ("๐Ÿ’ซ", "dizzy", "star"), + ("๐Ÿ”ฅ", "fire", "hot lit"), + ("๐Ÿ’ง", "droplet", "water"), + ("๐ŸŒŠ", "wave", "water ocean"), + ("๐ŸŽต", "musical note", "music"), + ("๐ŸŽถ", "musical notes", "music"), + ("๐ŸŽค", "microphone", "sing karaoke"), + ("๐ŸŽง", "headphones", "music"), + ("๐ŸŽฎ", "video game", "gaming controller"), + ("๐Ÿ•น๏ธ", "joystick", "gaming"), + ("๐ŸŽฏ", "direct hit", "target bullseye"), + ("๐Ÿ†", "trophy", "winner award"), + ("๐Ÿฅ‡", "1st place medal", "gold winner"), + ("๐Ÿฅˆ", "2nd place medal", "silver"), + ("๐Ÿฅ‰", "3rd place medal", "bronze"), + ("๐ŸŽ", "wrapped gift", "present"), + ("๐ŸŽˆ", "balloon", "party"), + ("๐ŸŽ‰", "party popper", "celebration tada"), + ("๐ŸŽŠ", "confetti ball", "celebration"), + // Arrows & Misc + ("โžก๏ธ", "right arrow", ""), + ("โฌ…๏ธ", "left arrow", ""), + ("โฌ†๏ธ", "up arrow", ""), + ("โฌ‡๏ธ", "down arrow", ""), + ("โ†—๏ธ", "up-right arrow", ""), + ("โ†˜๏ธ", "down-right arrow", ""), + ("โ†™๏ธ", "down-left arrow", ""), + ("โ†–๏ธ", "up-left arrow", ""), + ("โ†•๏ธ", "up-down arrow", ""), + ("โ†”๏ธ", "left-right arrow", ""), + ("๐Ÿ”„", "counterclockwise arrows", "refresh reload"), + ("๐Ÿ”ƒ", "clockwise arrows", "refresh reload"), + ("โž•", "plus", "add"), + ("โž–", "minus", "subtract"), + ("โž—", "division", "divide"), + ("โœ–๏ธ", "multiply", "times"), + ("โ™พ๏ธ", "infinity", "forever"), + ("๐Ÿ’ฏ", "hundred points", "100 perfect"), + ("๐Ÿ†—", "ok button", "okay"), + ("๐Ÿ†•", "new button", ""), + ("๐Ÿ†“", "free button", ""), + ("โ„น๏ธ", "information", "info"), + ("๐Ÿ…ฟ๏ธ", "parking", ""), + ("๐Ÿš€", "rocket", "launch startup"), + ("โœˆ๏ธ", "airplane", "travel flight"), + ("๐Ÿš—", "car", "automobile"), + ("๐Ÿš•", "taxi", "cab"), + ("๐ŸšŒ", "bus", ""), + ("๐Ÿš‚", "locomotive", "train"), + ("๐Ÿ ", "house", "home"), + ("๐Ÿข", "office building", "work"), + ("๐Ÿฅ", "hospital", ""), + ("๐Ÿซ", "school", ""), + ("๐Ÿ›๏ธ", "classical building", ""), + ("โ›ช", "church", ""), + ("๐Ÿ•Œ", "mosque", ""), + ("๐Ÿ•", "synagogue", ""), + ("๐Ÿ—ฝ", "statue of liberty", "usa america"), + ("๐Ÿ—ผ", "tokyo tower", "japan"), + ("๐Ÿ—พ", "map of japan", ""), + ("๐ŸŒ", "globe europe-africa", "earth world"), + ("๐ŸŒŽ", "globe americas", "earth world"), + ("๐ŸŒ", "globe asia-australia", "earth world"), + ("๐ŸŒ‘", "new moon", ""), + ("๐ŸŒ•", "full moon", ""), + ("โ˜€๏ธ", "sun", "sunny"), + ("๐ŸŒ™", "crescent moon", "night"), + ("โญ", "star", ""), + ("โ˜๏ธ", "cloud", ""), + ("๐ŸŒง๏ธ", "cloud with rain", "rainy"), + ("โ›ˆ๏ธ", "cloud with lightning", "storm thunder"), + ("๐ŸŒˆ", "rainbow", ""), + ("โ„๏ธ", "snowflake", "cold winter"), + ("โ˜ƒ๏ธ", "snowman", "winter"), + ("๐ŸŽ„", "christmas tree", "xmas holiday"), + ("๐ŸŽƒ", "jack-o-lantern", "halloween pumpkin"), + ("๐Ÿš", "shell", "beach"), + ("๐ŸŒธ", "cherry blossom", "flower spring"), + ("๐ŸŒบ", "hibiscus", "flower"), + ("๐ŸŒป", "sunflower", "flower"), + ("๐ŸŒน", "rose", "flower love"), + ("๐ŸŒท", "tulip", "flower"), + ("๐ŸŒฑ", "seedling", "plant grow"), + ("๐ŸŒฒ", "evergreen tree", ""), + ("๐ŸŒณ", "deciduous tree", ""), + ("๐ŸŒด", "palm tree", "tropical"), + ("๐ŸŒต", "cactus", "desert"), + ("๐Ÿ€", "four leaf clover", "luck irish"), + ("๐Ÿ", "maple leaf", "fall autumn canada"), + ("๐Ÿ‚", "fallen leaf", "fall autumn"), + ]; + + for (emoji, name, keywords) in emojis { + // Combine name and keywords for better searching + let search_text = format!("{} {}", name, keywords); + + self.items.push(LaunchItem { + id: format!("emoji:{}", emoji), + name: format!("{} {}", emoji, name), + description: if keywords.is_empty() { + None + } else { + Some(keywords.to_string()) + }, + icon: None, // Emoji is shown in name + provider: ProviderType::Emoji, + // Copy emoji to clipboard using wl-copy + command: format!("printf '%s' '{}' | wl-copy", emoji), + terminal: false, + }); + + // Store the search text for matching (not used directly but could be) + let _ = search_text; + } + } +} + +impl Provider for EmojiProvider { + fn name(&self) -> &str { + "Emoji" + } + + fn provider_type(&self) -> ProviderType { + ProviderType::Emoji + } + + fn refresh(&mut self) { + self.load_emojis(); + } + + fn items(&self) -> &[LaunchItem] { + &self.items + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_emoji_provider() { + let mut provider = EmojiProvider::new(); + provider.refresh(); + assert!(provider.items().len() > 100); + assert!(provider.items().iter().any(|i| i.name.contains("๐Ÿ˜€"))); + } +} diff --git a/src/providers/files.rs b/src/providers/files.rs new file mode 100644 index 0000000..3cad950 --- /dev/null +++ b/src/providers/files.rs @@ -0,0 +1,224 @@ +use crate::providers::{LaunchItem, ProviderType}; +use log::{debug, warn}; +use std::process::Command; + +/// File search provider - uses fd or locate for fast file finding +pub struct FileSearchProvider { + search_tool: SearchTool, + max_results: usize, +} + +#[derive(Debug, Clone, Copy)] +enum SearchTool { + Fd, + Locate, + None, +} + +impl FileSearchProvider { + pub fn new() -> Self { + let search_tool = Self::detect_search_tool(); + debug!("File search using: {:?}", search_tool); + + Self { + search_tool, + max_results: 20, + } + } + + fn detect_search_tool() -> SearchTool { + // Prefer fd (faster, respects .gitignore) + if Self::command_exists("fd") { + return SearchTool::Fd; + } + // Fall back to locate (requires updatedb) + if Self::command_exists("locate") { + return SearchTool::Locate; + } + SearchTool::None + } + + fn command_exists(cmd: &str) -> bool { + Command::new("which") + .arg(cmd) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + } + + /// Check if query is a file search query + /// Triggers on: `/ query`, `file query`, `find query` + pub fn is_file_query(query: &str) -> bool { + let trimmed = query.trim(); + trimmed.starts_with("/ ") + || trimmed.starts_with("/") + || trimmed.to_lowercase().starts_with("file ") + || trimmed.to_lowercase().starts_with("find ") + } + + /// Extract the search term from the query + fn extract_search_term(query: &str) -> Option<&str> { + let trimmed = query.trim(); + + if let Some(rest) = trimmed.strip_prefix("/ ") { + Some(rest.trim()) + } else if let Some(rest) = trimmed.strip_prefix("/") { + Some(rest.trim()) + } else if trimmed.to_lowercase().starts_with("file ") { + Some(trimmed[5..].trim()) + } else if trimmed.to_lowercase().starts_with("find ") { + Some(trimmed[5..].trim()) + } else { + None + } + } + + /// Evaluate a file search query + pub fn evaluate(&self, query: &str) -> Vec { + let search_term = match Self::extract_search_term(query) { + Some(t) if !t.is_empty() => t, + _ => return Vec::new(), + }; + + self.search_files(search_term) + } + + /// Evaluate a raw search term (for :file filter mode) + pub fn evaluate_raw(&self, search_term: &str) -> Vec { + let trimmed = search_term.trim(); + if trimmed.is_empty() { + return Vec::new(); + } + + self.search_files(trimmed) + } + + fn search_files(&self, pattern: &str) -> Vec { + match self.search_tool { + SearchTool::Fd => self.search_with_fd(pattern), + SearchTool::Locate => self.search_with_locate(pattern), + SearchTool::None => { + debug!("No file search tool available"); + Vec::new() + } + } + } + + fn search_with_fd(&self, pattern: &str) -> Vec { + // fd searches from home directory by default + let home = dirs::home_dir().unwrap_or_default(); + + let output = match Command::new("fd") + .args([ + "--max-results", + &self.max_results.to_string(), + "--type", + "f", // Files only + "--type", + "d", // And directories + pattern, + ]) + .current_dir(&home) + .output() + { + Ok(o) => o, + Err(e) => { + warn!("Failed to run fd: {}", e); + return Vec::new(); + } + }; + + self.parse_file_results(&String::from_utf8_lossy(&output.stdout), &home) + } + + fn search_with_locate(&self, pattern: &str) -> Vec { + let home = dirs::home_dir().unwrap_or_default(); + + let output = match Command::new("locate") + .args([ + "--limit", + &self.max_results.to_string(), + "--ignore-case", + pattern, + ]) + .output() + { + Ok(o) => o, + Err(e) => { + warn!("Failed to run locate: {}", e); + return Vec::new(); + } + }; + + self.parse_file_results(&String::from_utf8_lossy(&output.stdout), &home) + } + + fn parse_file_results(&self, output: &str, home: &std::path::Path) -> Vec { + output + .lines() + .filter(|line| !line.is_empty()) + .map(|path| { + let path = path.trim(); + let full_path = if path.starts_with('/') { + path.to_string() + } else { + home.join(path).to_string_lossy().to_string() + }; + + // Get filename for display + let filename = std::path::Path::new(&full_path) + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| full_path.clone()); + + // Determine icon based on whether it's a directory + let is_dir = std::path::Path::new(&full_path).is_dir(); + let icon = if is_dir { + "folder" + } else { + "text-x-generic" + }; + + // Command to open with xdg-open + let command = format!("xdg-open '{}'", full_path.replace('\'', "'\\''")); + + LaunchItem { + id: format!("file:{}", full_path), + name: filename, + description: Some(full_path.clone()), + icon: Some(icon.to_string()), + provider: ProviderType::Files, + command, + terminal: false, + } + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_file_query() { + assert!(FileSearchProvider::is_file_query("/ config")); + assert!(FileSearchProvider::is_file_query("/config")); + assert!(FileSearchProvider::is_file_query("file config")); + assert!(FileSearchProvider::is_file_query("find config")); + assert!(!FileSearchProvider::is_file_query("config")); + assert!(!FileSearchProvider::is_file_query("? search")); + } + + #[test] + fn test_extract_search_term() { + assert_eq!( + FileSearchProvider::extract_search_term("/ config.toml"), + Some("config.toml") + ); + assert_eq!( + FileSearchProvider::extract_search_term("file bashrc"), + Some("bashrc") + ); + } +} diff --git a/src/providers/mod.rs b/src/providers/mod.rs index ded847f..bd4698e 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -1,14 +1,28 @@ mod application; +mod bookmarks; mod calculator; +mod clipboard; mod command; mod dmenu; +mod emoji; +mod files; +mod scripts; +mod ssh; +mod system; mod uuctl; mod websearch; pub use application::ApplicationProvider; +pub use bookmarks::BookmarksProvider; pub use calculator::CalculatorProvider; +pub use clipboard::ClipboardProvider; pub use command::CommandProvider; pub use dmenu::DmenuProvider; +pub use emoji::EmojiProvider; +pub use files::FileSearchProvider; +pub use scripts::ScriptsProvider; +pub use ssh::SshProvider; +pub use system::SystemProvider; pub use uuctl::UuctlProvider; pub use websearch::WebSearchProvider; @@ -34,9 +48,16 @@ pub struct LaunchItem { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ProviderType { Application, + Bookmarks, Calculator, + Clipboard, Command, Dmenu, + Emoji, + Files, + Scripts, + Ssh, + System, Uuctl, WebSearch, } @@ -47,13 +68,20 @@ impl std::str::FromStr for ProviderType { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "app" | "apps" | "application" | "applications" => Ok(ProviderType::Application), + "bookmark" | "bookmarks" | "bm" => Ok(ProviderType::Bookmarks), "calc" | "calculator" => Ok(ProviderType::Calculator), + "clip" | "clipboard" => Ok(ProviderType::Clipboard), "cmd" | "command" | "commands" => Ok(ProviderType::Command), - "uuctl" => Ok(ProviderType::Uuctl), "dmenu" => Ok(ProviderType::Dmenu), + "emoji" | "emojis" => Ok(ProviderType::Emoji), + "file" | "files" | "find" => Ok(ProviderType::Files), + "script" | "scripts" => Ok(ProviderType::Scripts), + "ssh" => Ok(ProviderType::Ssh), + "sys" | "system" | "power" => Ok(ProviderType::System), + "uuctl" => Ok(ProviderType::Uuctl), "web" | "websearch" | "search" => Ok(ProviderType::WebSearch), _ => Err(format!( - "Unknown provider: '{}'. Valid: app, calc, cmd, uuctl", + "Unknown provider: '{}'. Valid: app, bookmark, calc, clip, cmd, emoji, file, script, ssh, sys, web", s )), } @@ -64,9 +92,16 @@ 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::Bookmarks => write!(f, "bookmark"), ProviderType::Calculator => write!(f, "calc"), + ProviderType::Clipboard => write!(f, "clip"), ProviderType::Command => write!(f, "cmd"), ProviderType::Dmenu => write!(f, "dmenu"), + ProviderType::Emoji => write!(f, "emoji"), + ProviderType::Files => write!(f, "file"), + ProviderType::Scripts => write!(f, "script"), + ProviderType::Ssh => write!(f, "ssh"), + ProviderType::System => write!(f, "sys"), ProviderType::Uuctl => write!(f, "uuctl"), ProviderType::WebSearch => write!(f, "web"), } @@ -87,6 +122,7 @@ pub struct ProviderManager { providers: Vec>, calculator: CalculatorProvider, websearch: WebSearchProvider, + filesearch: FileSearchProvider, matcher: SkimMatcherV2, } @@ -97,10 +133,15 @@ impl ProviderManager { } pub fn with_search_engine(search_engine: &str) -> Self { + Self::with_config(search_engine, "kitty") + } + + pub fn with_config(search_engine: &str, terminal: &str) -> Self { let mut manager = Self { providers: Vec::new(), calculator: CalculatorProvider::new(), websearch: WebSearchProvider::with_engine(search_engine), + filesearch: FileSearchProvider::new(), matcher: SkimMatcherV2::default(), }; @@ -117,6 +158,14 @@ impl ProviderManager { manager.providers.push(Box::new(ApplicationProvider::new())); manager.providers.push(Box::new(CommandProvider::new())); manager.providers.push(Box::new(UuctlProvider::new())); + + // New providers + manager.providers.push(Box::new(SystemProvider::new())); + manager.providers.push(Box::new(SshProvider::with_terminal(terminal))); + manager.providers.push(Box::new(ClipboardProvider::new())); + manager.providers.push(Box::new(BookmarksProvider::new())); + manager.providers.push(Box::new(EmojiProvider::new())); + manager.providers.push(Box::new(ScriptsProvider::new())); } // Initial refresh @@ -271,6 +320,22 @@ impl ProviderManager { } } + // Check for file search query + if FileSearchProvider::is_file_query(query) { + let file_results = self.filesearch.evaluate(query); + for (idx, item) in file_results.into_iter().enumerate() { + // Score decreases for each result to maintain order + results.push((item, 8000 - idx as i64)); + } + } + // Also check for raw query when in :file filter mode + else if filter.active_prefix() == Some(ProviderType::Files) && !query.is_empty() { + let file_results = self.filesearch.evaluate_raw(query); + for (idx, item) in file_results.into_iter().enumerate() { + results.push((item, 8000 - idx as i64)); + } + } + // Empty query (after checking special providers) - return frecency-sorted items if query.is_empty() { let mut items: Vec<(LaunchItem, i64)> = self diff --git a/src/providers/scripts.rs b/src/providers/scripts.rs new file mode 100644 index 0000000..121eaf3 --- /dev/null +++ b/src/providers/scripts.rs @@ -0,0 +1,181 @@ +use crate::providers::{LaunchItem, Provider, ProviderType}; +use log::{debug, warn}; +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; + +/// Custom scripts provider - runs user scripts from ~/.config/owlry/scripts/ +pub struct ScriptsProvider { + items: Vec, +} + +impl ScriptsProvider { + pub fn new() -> Self { + Self { items: Vec::new() } + } + + fn scripts_dir() -> Option { + dirs::config_dir().map(|p| p.join("owlry").join("scripts")) + } + + fn load_scripts(&mut self) { + self.items.clear(); + + let scripts_dir = match Self::scripts_dir() { + Some(p) => p, + None => { + debug!("Could not determine scripts directory"); + return; + } + }; + + if !scripts_dir.exists() { + debug!("Scripts directory not found at {:?}", scripts_dir); + // Create the directory for the user + if let Err(e) = fs::create_dir_all(&scripts_dir) { + warn!("Failed to create scripts directory: {}", e); + } + return; + } + + let entries = match fs::read_dir(&scripts_dir) { + Ok(e) => e, + Err(e) => { + warn!("Failed to read scripts directory: {}", e); + return; + } + }; + + for entry in entries.flatten() { + let path = entry.path(); + + // Skip directories + if path.is_dir() { + continue; + } + + // Check if executable + let metadata = match path.metadata() { + Ok(m) => m, + Err(_) => continue, + }; + + let is_executable = metadata.permissions().mode() & 0o111 != 0; + if !is_executable { + continue; + } + + // Get script name without extension + let filename = path + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default(); + + let name = path + .file_stem() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or(filename.clone()); + + // Try to read description from first line comment + let description = Self::read_script_description(&path); + + // Determine icon based on extension or shebang + let icon = Self::determine_icon(&path); + + self.items.push(LaunchItem { + id: format!("script:{}", filename), + name: format!("Script: {}", name), + description, + icon: Some(icon), + provider: ProviderType::Scripts, + command: path.to_string_lossy().to_string(), + terminal: false, + }); + } + + debug!("Loaded {} scripts from {:?}", self.items.len(), scripts_dir); + } + + fn read_script_description(path: &PathBuf) -> Option { + let content = fs::read_to_string(path).ok()?; + let mut lines = content.lines(); + + // Skip shebang if present + let first_line = lines.next()?; + let check_line = if first_line.starts_with("#!") { + lines.next()? + } else { + first_line + }; + + // Look for a comment description + if check_line.starts_with("# ") { + Some(check_line[2..].trim().to_string()) + } else if check_line.starts_with("// ") { + Some(check_line[3..].trim().to_string()) + } else { + None + } + } + + fn determine_icon(path: &PathBuf) -> String { + // Check extension first + if let Some(ext) = path.extension() { + match ext.to_string_lossy().as_ref() { + "sh" | "bash" | "zsh" => return "utilities-terminal".to_string(), + "py" | "python" => return "text-x-python".to_string(), + "js" | "ts" => return "text-x-javascript".to_string(), + "rb" => return "text-x-ruby".to_string(), + "pl" => return "text-x-perl".to_string(), + _ => {} + } + } + + // Check shebang + if let Ok(content) = fs::read_to_string(path) { + if let Some(first_line) = content.lines().next() { + if first_line.contains("bash") || first_line.contains("sh") { + return "utilities-terminal".to_string(); + } else if first_line.contains("python") { + return "text-x-python".to_string(); + } else if first_line.contains("node") { + return "text-x-javascript".to_string(); + } else if first_line.contains("ruby") { + return "text-x-ruby".to_string(); + } + } + } + + "application-x-executable".to_string() + } +} + +impl Provider for ScriptsProvider { + fn name(&self) -> &str { + "Scripts" + } + + fn provider_type(&self) -> ProviderType { + ProviderType::Scripts + } + + fn refresh(&mut self) { + self.load_scripts(); + } + + fn items(&self) -> &[LaunchItem] { + &self.items + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_scripts_provider() { + let mut provider = ScriptsProvider::new(); + provider.refresh(); + // Just ensure it doesn't panic + } +} diff --git a/src/providers/ssh.rs b/src/providers/ssh.rs new file mode 100644 index 0000000..25e09a6 --- /dev/null +++ b/src/providers/ssh.rs @@ -0,0 +1,197 @@ +use crate::providers::{LaunchItem, Provider, ProviderType}; +use log::{debug, warn}; +use std::fs; +use std::path::PathBuf; + +/// SSH connections provider - parses ~/.ssh/config +pub struct SshProvider { + items: Vec, + terminal_command: String, +} + +impl SshProvider { + #[allow(dead_code)] + pub fn new() -> Self { + Self::with_terminal("kitty") + } + + pub fn with_terminal(terminal: &str) -> Self { + Self { + items: Vec::new(), + terminal_command: terminal.to_string(), + } + } + + #[allow(dead_code)] + pub fn set_terminal(&mut self, terminal: &str) { + self.terminal_command = terminal.to_string(); + } + + fn ssh_config_path() -> Option { + dirs::home_dir().map(|p| p.join(".ssh").join("config")) + } + + fn parse_ssh_config(&mut self) { + self.items.clear(); + + let config_path = match Self::ssh_config_path() { + Some(p) => p, + None => { + debug!("Could not determine SSH config path"); + return; + } + }; + + if !config_path.exists() { + debug!("SSH config not found at {:?}", config_path); + return; + } + + let content = match fs::read_to_string(&config_path) { + Ok(c) => c, + Err(e) => { + warn!("Failed to read SSH config: {}", e); + return; + } + }; + + let mut current_host: Option = None; + let mut current_hostname: Option = None; + let mut current_user: Option = None; + let mut current_port: Option = None; + + for line in content.lines() { + let line = line.trim(); + + // Skip comments and empty lines + if line.is_empty() || line.starts_with('#') { + continue; + } + + // Split on whitespace or '=' + let parts: Vec<&str> = line.splitn(2, |c: char| c.is_whitespace() || c == '=') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect(); + + if parts.len() < 2 { + continue; + } + + let key = parts[0].to_lowercase(); + let value = parts[1]; + + match key.as_str() { + "host" => { + // Save previous host if exists + if let Some(host) = current_host.take() { + self.add_host_item( + &host, + current_hostname.take(), + current_user.take(), + current_port.take(), + ); + } + + // Skip wildcards and patterns + if !value.contains('*') && !value.contains('?') && value != "*" { + current_host = Some(value.to_string()); + } + current_hostname = None; + current_user = None; + current_port = None; + } + "hostname" => { + current_hostname = Some(value.to_string()); + } + "user" => { + current_user = Some(value.to_string()); + } + "port" => { + current_port = Some(value.to_string()); + } + _ => {} + } + } + + // Don't forget the last host + if let Some(host) = current_host.take() { + self.add_host_item(&host, current_hostname, current_user, current_port); + } + + debug!("Loaded {} SSH hosts", self.items.len()); + } + + fn add_host_item( + &mut self, + host: &str, + hostname: Option, + user: Option, + port: Option, + ) { + // Build description + let mut desc_parts = Vec::new(); + if let Some(ref h) = hostname { + desc_parts.push(h.clone()); + } + if let Some(ref u) = user { + desc_parts.push(format!("user: {}", u)); + } + if let Some(ref p) = port { + desc_parts.push(format!("port: {}", p)); + } + + let description = if desc_parts.is_empty() { + None + } else { + Some(desc_parts.join(", ")) + }; + + // Build SSH command - just use the host alias, SSH will resolve the rest + let ssh_command = format!("ssh {}", host); + + // Wrap in terminal + let command = format!("{} -e {}", self.terminal_command, ssh_command); + + self.items.push(LaunchItem { + id: format!("ssh:{}", host), + name: format!("SSH: {}", host), + description, + icon: Some("utilities-terminal".to_string()), + provider: ProviderType::Ssh, + command, + terminal: false, // We're already wrapping in terminal + }); + } +} + +impl Provider for SshProvider { + fn name(&self) -> &str { + "SSH" + } + + fn provider_type(&self) -> ProviderType { + ProviderType::Ssh + } + + fn refresh(&mut self) { + self.parse_ssh_config(); + } + + fn items(&self) -> &[LaunchItem] { + &self.items + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_ssh_config() { + // This test will only work if the user has an SSH config + let mut provider = SshProvider::new(); + provider.refresh(); + // Just ensure it doesn't panic + } +} diff --git a/src/providers/system.rs b/src/providers/system.rs new file mode 100644 index 0000000..76f29bc --- /dev/null +++ b/src/providers/system.rs @@ -0,0 +1,115 @@ +use crate::providers::{LaunchItem, Provider, ProviderType}; + +/// System commands provider - shutdown, reboot, lock, etc. +pub struct SystemProvider { + items: Vec, +} + +impl SystemProvider { + pub fn new() -> Self { + Self { items: Vec::new() } + } + + fn load_commands(&mut self) { + self.items.clear(); + + // Define system commands + // Format: (id, name, description, icon, command) + let commands: Vec<(&str, &str, &str, &str, &str)> = vec![ + ( + "system:shutdown", + "Shutdown", + "Power off the system", + "system-shutdown", + "systemctl poweroff", + ), + ( + "system:reboot", + "Reboot", + "Restart the system", + "system-reboot", + "systemctl reboot", + ), + ( + "system:reboot-bios", + "Reboot into BIOS", + "Restart into UEFI/BIOS setup", + "system-reboot", + "systemctl reboot --firmware-setup", + ), + ( + "system:suspend", + "Suspend", + "Suspend to RAM", + "system-suspend", + "systemctl suspend", + ), + ( + "system:hibernate", + "Hibernate", + "Suspend to disk", + "system-suspend-hibernate", + "systemctl hibernate", + ), + ( + "system:lock", + "Lock Screen", + "Lock the session", + "system-lock-screen", + "loginctl lock-session", + ), + ( + "system:logout", + "Log Out", + "End the current session", + "system-log-out", + "loginctl terminate-session self", + ), + ]; + + for (id, name, description, icon, command) in commands { + self.items.push(LaunchItem { + id: id.to_string(), + name: name.to_string(), + description: Some(description.to_string()), + icon: Some(icon.to_string()), + provider: ProviderType::System, + command: command.to_string(), + terminal: false, + }); + } + } +} + +impl Provider for SystemProvider { + fn name(&self) -> &str { + "System" + } + + fn provider_type(&self) -> ProviderType { + ProviderType::System + } + + fn refresh(&mut self) { + self.load_commands(); + } + + fn items(&self) -> &[LaunchItem] { + &self.items + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_system_provider() { + let mut provider = SystemProvider::new(); + provider.refresh(); + + assert!(provider.items().len() >= 6); + assert!(provider.items().iter().any(|i| i.name == "Shutdown")); + assert!(provider.items().iter().any(|i| i.name == "Reboot into BIOS")); + } +} diff --git a/src/theme.rs b/src/theme.rs index b738d97..a3ac251 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -35,15 +35,36 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String { if let Some(ref badge_app) = config.colors.badge_app { css.push_str(&format!(" --owlry-badge-app: {};\n", badge_app)); } + if let Some(ref badge_bookmark) = config.colors.badge_bookmark { + css.push_str(&format!(" --owlry-badge-bookmark: {};\n", badge_bookmark)); + } if let Some(ref badge_calc) = config.colors.badge_calc { css.push_str(&format!(" --owlry-badge-calc: {};\n", badge_calc)); } + if let Some(ref badge_clip) = config.colors.badge_clip { + css.push_str(&format!(" --owlry-badge-clip: {};\n", badge_clip)); + } if let Some(ref badge_cmd) = config.colors.badge_cmd { css.push_str(&format!(" --owlry-badge-cmd: {};\n", badge_cmd)); } if let Some(ref badge_dmenu) = config.colors.badge_dmenu { css.push_str(&format!(" --owlry-badge-dmenu: {};\n", badge_dmenu)); } + if let Some(ref badge_emoji) = config.colors.badge_emoji { + css.push_str(&format!(" --owlry-badge-emoji: {};\n", badge_emoji)); + } + if let Some(ref badge_file) = config.colors.badge_file { + css.push_str(&format!(" --owlry-badge-file: {};\n", badge_file)); + } + if let Some(ref badge_script) = config.colors.badge_script { + css.push_str(&format!(" --owlry-badge-script: {};\n", badge_script)); + } + if let Some(ref badge_ssh) = config.colors.badge_ssh { + css.push_str(&format!(" --owlry-badge-ssh: {};\n", badge_ssh)); + } + if let Some(ref badge_sys) = config.colors.badge_sys { + css.push_str(&format!(" --owlry-badge-sys: {};\n", badge_sys)); + } if let Some(ref badge_uuctl) = config.colors.badge_uuctl { css.push_str(&format!(" --owlry-badge-uuctl: {};\n", badge_uuctl)); } diff --git a/src/ui/main_window.rs b/src/ui/main_window.rs index 5b79ce9..5c279be 100644 --- a/src/ui/main_window.rs +++ b/src/ui/main_window.rs @@ -206,11 +206,18 @@ impl MainWindow { button.add_css_class("owlry-filter-button"); let css_class = match provider_type { ProviderType::Application => "owlry-filter-app", + ProviderType::Bookmarks => "owlry-filter-bookmark", ProviderType::Calculator => "owlry-filter-calc", + ProviderType::Clipboard => "owlry-filter-clip", ProviderType::Command => "owlry-filter-cmd", + ProviderType::Dmenu => "owlry-filter-dmenu", + ProviderType::Emoji => "owlry-filter-emoji", + ProviderType::Files => "owlry-filter-file", + ProviderType::Scripts => "owlry-filter-script", + ProviderType::Ssh => "owlry-filter-ssh", + ProviderType::System => "owlry-filter-sys", ProviderType::Uuctl => "owlry-filter-uuctl", ProviderType::WebSearch => "owlry-filter-web", - ProviderType::Dmenu => "owlry-filter-dmenu", }; button.add_css_class(css_class); @@ -227,11 +234,18 @@ impl MainWindow { .iter() .map(|p| match p { ProviderType::Application => "applications", + ProviderType::Bookmarks => "bookmarks", ProviderType::Calculator => "calculator", + ProviderType::Clipboard => "clipboard", ProviderType::Command => "commands", + ProviderType::Dmenu => "options", + ProviderType::Emoji => "emoji", + ProviderType::Files => "files", + ProviderType::Scripts => "scripts", + ProviderType::Ssh => "SSH hosts", + ProviderType::System => "system", ProviderType::Uuctl => "uuctl units", ProviderType::WebSearch => "web", - ProviderType::Dmenu => "options", }) .collect(); @@ -410,11 +424,18 @@ impl MainWindow { if parsed.prefix.is_some() { let prefix_name = match parsed.prefix.unwrap() { ProviderType::Application => "applications", + ProviderType::Bookmarks => "bookmarks", ProviderType::Calculator => "calculator", + ProviderType::Clipboard => "clipboard", ProviderType::Command => "commands", + ProviderType::Dmenu => "options", + ProviderType::Emoji => "emoji", + ProviderType::Files => "files", + ProviderType::Scripts => "scripts", + ProviderType::Ssh => "SSH hosts", + ProviderType::System => "system", ProviderType::Uuctl => "uuctl units", ProviderType::WebSearch => "web", - ProviderType::Dmenu => "options", }; search_entry_for_change .set_placeholder_text(Some(&format!("Search {}...", prefix_name))); diff --git a/src/ui/result_row.rs b/src/ui/result_row.rs index 4d4ff10..e169c80 100644 --- a/src/ui/result_row.rs +++ b/src/ui/result_row.rs @@ -32,9 +32,16 @@ impl ResultRow { // Default icon based on provider type let default_icon = match item.provider { crate::providers::ProviderType::Application => "application-x-executable", + crate::providers::ProviderType::Bookmarks => "user-bookmarks", crate::providers::ProviderType::Calculator => "accessories-calculator", + crate::providers::ProviderType::Clipboard => "edit-paste", crate::providers::ProviderType::Command => "utilities-terminal", crate::providers::ProviderType::Dmenu => "view-list-symbolic", + crate::providers::ProviderType::Emoji => "face-smile", + crate::providers::ProviderType::Files => "folder", + crate::providers::ProviderType::Scripts => "application-x-executable", + crate::providers::ProviderType::Ssh => "network-server", + crate::providers::ProviderType::System => "system-shutdown", crate::providers::ProviderType::Uuctl => "system-run", crate::providers::ProviderType::WebSearch => "web-browser", };