diff --git a/data/config.example.toml b/data/config.example.toml index 89b8e6e..42cf860 100644 --- a/data/config.example.toml +++ b/data/config.example.toml @@ -13,6 +13,10 @@ terminal_command = "kitty" # Auto-detected if not set # "" # Direct execution # launch_wrapper = "uwsm app --" +# Provider tabs shown in header bar (Ctrl+1, Ctrl+2, etc. to toggle) +# Valid values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web +tabs = ["app", "cmd", "uuctl"] + [appearance] width = 600 height = 400 diff --git a/src/config/mod.rs b/src/config/mod.rs index 1ae1c21..4f3a34c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -22,6 +22,18 @@ pub struct GeneralConfig { /// If None or empty, launches directly via sh -c #[serde(default)] pub launch_wrapper: Option, + /// Provider tabs shown in the header bar. + /// Valid values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web + #[serde(default = "default_tabs")] + pub tabs: Vec, +} + +fn default_tabs() -> Vec { + vec![ + "app".to_string(), + "cmd".to_string(), + "uuctl".to_string(), + ] } /// User-customizable theme colors @@ -220,6 +232,7 @@ impl Default for Config { max_results: 10, terminal_command: terminal, launch_wrapper: detect_launch_wrapper(), + tabs: default_tabs(), }, appearance: AppearanceConfig { width: 600, diff --git a/src/filter.rs b/src/filter.rs index 02dc59e..a16096e 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -17,6 +17,7 @@ pub struct ProviderFilter { #[derive(Debug, Clone)] pub struct ParsedQuery { pub prefix: Option, + pub tag_filter: Option, pub query: String, } @@ -161,6 +162,30 @@ impl ProviderFilter { pub fn parse_query(query: &str) -> ParsedQuery { let trimmed = query.trim_start(); + // Check for tag filter pattern: ":tag:XXX query" or ":tag:XXX" + if let Some(rest) = trimmed.strip_prefix(":tag:") { + // Find the end of the tag (space or end of string) + if let Some(space_idx) = rest.find(' ') { + let tag = rest[..space_idx].to_lowercase(); + let query_part = rest[space_idx + 1..].to_string(); + #[cfg(feature = "dev-logging")] + debug!("[Filter] parse_query({:?}) -> tag={:?}, query={:?}", query, tag, query_part); + return ParsedQuery { + prefix: None, + tag_filter: Some(tag), + query: query_part, + }; + } else { + // Just the tag, no query yet + let tag = rest.to_lowercase(); + return ParsedQuery { + prefix: None, + tag_filter: Some(tag), + query: String::new(), + }; + } + } + // Check for prefix patterns (with trailing space) let prefixes = [ (":app ", ProviderType::Application), @@ -196,6 +221,7 @@ impl ProviderFilter { debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest); return ParsedQuery { prefix: Some(provider), + tag_filter: None, query: rest.to_string(), }; } @@ -236,6 +262,7 @@ impl ProviderFilter { debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider); return ParsedQuery { prefix: Some(provider), + tag_filter: None, query: String::new(), }; } @@ -243,11 +270,12 @@ impl ProviderFilter { let result = ParsedQuery { prefix: None, + tag_filter: None, query: query.to_string(), }; #[cfg(feature = "dev-logging")] - debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, result.prefix, result.query); + debug!("[Filter] parse_query({:?}) -> prefix={:?}, tag={:?}, query={:?}", query, result.prefix, result.tag_filter, result.query); result } diff --git a/src/providers/application.rs b/src/providers/application.rs index 3c7c1b8..9415120 100644 --- a/src/providers/application.rs +++ b/src/providers/application.rs @@ -135,6 +135,12 @@ impl Provider for ApplicationProvider { None => continue, }; + // Extract categories as tags (lowercase for consistency) + let tags: Vec = desktop_entry + .categories() + .map(|cats| cats.into_iter().map(|s| s.to_lowercase()).collect()) + .unwrap_or_default(); + let item = LaunchItem { id: path.to_string_lossy().to_string(), name, @@ -143,6 +149,7 @@ impl Provider for ApplicationProvider { provider: ProviderType::Application, command: run_cmd, terminal: desktop_entry.terminal(), + tags, }; self.items.push(item); diff --git a/src/providers/bookmarks.rs b/src/providers/bookmarks.rs index c064f6e..c349193 100644 --- a/src/providers/bookmarks.rs +++ b/src/providers/bookmarks.rs @@ -157,6 +157,7 @@ impl BookmarksProvider { provider: ProviderType::Bookmarks, command: format!("xdg-open '{}'", url.replace('\'', "'\\''")), terminal: false, + tags: Vec::new(), }); } } diff --git a/src/providers/calculator.rs b/src/providers/calculator.rs index 79a4032..a81c65f 100644 --- a/src/providers/calculator.rs +++ b/src/providers/calculator.rs @@ -73,6 +73,7 @@ impl CalculatorProvider { provider: ProviderType::Calculator, command: format!("echo -n '{}' | wl-copy", result_str), terminal: false, + tags: vec!["math".to_string()], }) } Err(_) => None, @@ -111,6 +112,7 @@ impl CalculatorProvider { // Copy result to clipboard using wl-copy command: format!("sh -c 'echo -n \"{}\" | wl-copy'", result_str), terminal: false, + tags: vec!["math".to_string()], }; debug!("Calculator result: {} = {}", expr, result_str); diff --git a/src/providers/clipboard.rs b/src/providers/clipboard.rs index 3ddde93..21c8b61 100644 --- a/src/providers/clipboard.rs +++ b/src/providers/clipboard.rs @@ -99,6 +99,7 @@ impl ClipboardProvider { provider: ProviderType::Clipboard, command, terminal: false, + tags: Vec::new(), }); } diff --git a/src/providers/command.rs b/src/providers/command.rs index abad066..0df024f 100644 --- a/src/providers/command.rs +++ b/src/providers/command.rs @@ -87,6 +87,7 @@ impl Provider for CommandProvider { provider: ProviderType::Command, command: name, terminal: false, + tags: Vec::new(), }; self.items.push(item); diff --git a/src/providers/dmenu.rs b/src/providers/dmenu.rs index dfc8b0d..84a1022 100644 --- a/src/providers/dmenu.rs +++ b/src/providers/dmenu.rs @@ -101,6 +101,7 @@ impl Provider for DmenuProvider { provider: ProviderType::Dmenu, command: line.to_string(), terminal: false, + tags: Vec::new(), }; self.items.push(item); diff --git a/src/providers/emoji.rs b/src/providers/emoji.rs index d8fd978..810c047 100644 --- a/src/providers/emoji.rs +++ b/src/providers/emoji.rs @@ -406,6 +406,7 @@ impl EmojiProvider { // Copy emoji to clipboard using wl-copy command: format!("printf '%s' '{}' | wl-copy", emoji), terminal: false, + tags: Vec::new(), // TODO: Extract category from emoji data }); // Store the search text for matching (not used directly but could be) diff --git a/src/providers/files.rs b/src/providers/files.rs index 93a65ba..2194044 100644 --- a/src/providers/files.rs +++ b/src/providers/files.rs @@ -191,6 +191,7 @@ impl FileSearchProvider { provider: ProviderType::Files, command, terminal: false, + tags: Vec::new(), } }) .collect() diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 13526ab..7a19925 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -46,6 +46,8 @@ pub struct LaunchItem { pub provider: ProviderType, pub command: String, pub terminal: bool, + /// Tags/categories for filtering (e.g., from .desktop Categories) + pub tags: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -282,7 +284,7 @@ impl ProviderManager { results } - /// Search with frecency boosting and calculator support + /// Search with frecency boosting, calculator support, and tag filtering pub fn search_with_frecency( &mut self, query: &str, @@ -290,6 +292,7 @@ impl ProviderManager { 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); @@ -352,6 +355,14 @@ impl ProviderManager { .iter() .filter(|p| filter.is_active(p.provider_type())) .flat_map(|p| p.items().iter().cloned()) + .filter(|item| { + // Apply tag filter if present + 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(&item.id); let boosted = (frecency_score * frecency_weight * 100.0) as i64; @@ -364,24 +375,43 @@ impl ProviderManager { return items; } - // Regular search with frecency boost + // Regular search with frecency boost and tag matching let search_results: Vec<(LaunchItem, i64)> = self .providers .iter() .filter(|provider| filter.is_active(provider.provider_type())) .flat_map(|provider| { provider.items().iter().filter_map(|item| { + // Apply tag filter if present + if let Some(tag) = tag_filter { + if !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 base_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, + // Also match against tags (lower weight) + let tag_score = item + .tags + .iter() + .filter_map(|t| self.matcher.fuzzy_match(t, query)) + .max() + .map(|s| s / 3); // Lower weight for tag matches + + 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| { diff --git a/src/providers/scripts.rs b/src/providers/scripts.rs index c30978e..928d4e3 100644 --- a/src/providers/scripts.rs +++ b/src/providers/scripts.rs @@ -87,6 +87,7 @@ impl ScriptsProvider { provider: ProviderType::Scripts, command: path.to_string_lossy().to_string(), terminal: false, + tags: vec!["script".to_string()], }); } diff --git a/src/providers/ssh.rs b/src/providers/ssh.rs index 54da2ed..bc310f2 100644 --- a/src/providers/ssh.rs +++ b/src/providers/ssh.rs @@ -161,6 +161,7 @@ impl SshProvider { provider: ProviderType::Ssh, command, terminal: false, // We're already wrapping in terminal + tags: vec!["ssh".to_string()], }); } } diff --git a/src/providers/system.rs b/src/providers/system.rs index 76f29bc..9600692 100644 --- a/src/providers/system.rs +++ b/src/providers/system.rs @@ -76,6 +76,7 @@ impl SystemProvider { provider: ProviderType::System, command: command.to_string(), terminal: false, + tags: vec!["power".to_string(), "system".to_string()], }); } } diff --git a/src/providers/uuctl.rs b/src/providers/uuctl.rs index 64e6573..a14a188 100644 --- a/src/providers/uuctl.rs +++ b/src/providers/uuctl.rs @@ -46,6 +46,7 @@ impl UuctlProvider { provider: ProviderType::Uuctl, command: format!("systemctl --user restart {}", unit_name), terminal: false, + tags: vec!["systemd".to_string(), "service".to_string()], }); actions.push(LaunchItem { @@ -56,6 +57,7 @@ impl UuctlProvider { provider: ProviderType::Uuctl, command: format!("systemctl --user stop {}", unit_name), terminal: false, + tags: vec!["systemd".to_string(), "service".to_string()], }); actions.push(LaunchItem { @@ -66,6 +68,7 @@ impl UuctlProvider { provider: ProviderType::Uuctl, command: format!("systemctl --user reload {}", unit_name), terminal: false, + tags: vec!["systemd".to_string(), "service".to_string()], }); actions.push(LaunchItem { @@ -76,6 +79,7 @@ impl UuctlProvider { provider: ProviderType::Uuctl, command: format!("systemctl --user kill {}", unit_name), terminal: false, + tags: vec!["systemd".to_string(), "service".to_string()], }); } else { actions.push(LaunchItem { @@ -86,6 +90,7 @@ impl UuctlProvider { provider: ProviderType::Uuctl, command: format!("systemctl --user start {}", unit_name), terminal: false, + tags: vec!["systemd".to_string(), "service".to_string()], }); } @@ -98,6 +103,7 @@ impl UuctlProvider { provider: ProviderType::Uuctl, command: format!("systemctl --user status {}", unit_name), terminal: true, + tags: vec!["systemd".to_string(), "service".to_string()], }); actions.push(LaunchItem { @@ -108,6 +114,7 @@ impl UuctlProvider { provider: ProviderType::Uuctl, command: format!("journalctl --user -u {} -f", unit_name), terminal: true, + tags: vec!["systemd".to_string(), "service".to_string()], }); actions.push(LaunchItem { @@ -118,6 +125,7 @@ impl UuctlProvider { provider: ProviderType::Uuctl, command: format!("systemctl --user enable {}", unit_name), terminal: false, + tags: vec!["systemd".to_string(), "service".to_string()], }); actions.push(LaunchItem { @@ -128,6 +136,7 @@ impl UuctlProvider { provider: ProviderType::Uuctl, command: format!("systemctl --user disable {}", unit_name), terminal: false, + tags: vec!["systemd".to_string(), "service".to_string()], }); actions @@ -189,6 +198,7 @@ impl UuctlProvider { provider: ProviderType::Uuctl, command: submenu_data, // Special marker for submenu terminal: false, + tags: vec!["systemd".to_string(), "service".to_string()], }); } diff --git a/src/providers/websearch.rs b/src/providers/websearch.rs index 813195b..4e37814 100644 --- a/src/providers/websearch.rs +++ b/src/providers/websearch.rs @@ -136,6 +136,7 @@ impl WebSearchProvider { provider: ProviderType::WebSearch, command, terminal: false, + tags: vec!["web".to_string(), "search".to_string()], }) } } diff --git a/src/resources/base.css b/src/resources/base.css index b1ab191..f82c2e4 100644 --- a/src/resources/base.css +++ b/src/resources/base.css @@ -315,6 +315,22 @@ scrollbar slider:active { background-color: var(--owlry-accent, @theme_selected_bg_color); } +/* Tag badges */ +.owlry-tag-badge { + font-size: calc(var(--owlry-font-size, 14px) - 4px); + font-weight: 500; + padding: 1px 6px; + border-radius: 4px; + background-color: alpha(var(--owlry-border, @borders), 0.3); + color: var(--owlry-text-secondary, alpha(@theme_fg_color, 0.6)); + margin-top: 4px; +} + +.owlry-result-row:selected .owlry-tag-badge { + background-color: alpha(var(--owlry-accent-bright, @theme_selected_fg_color), 0.2); + color: var(--owlry-accent-bright, @theme_selected_fg_color); +} + /* Text selection */ selection { background-color: alpha(var(--owlry-accent, @theme_selected_bg_color), 0.3); diff --git a/src/ui/main_window.rs b/src/ui/main_window.rs index 50bcbd8..37742ea 100644 --- a/src/ui/main_window.rs +++ b/src/ui/main_window.rs @@ -48,6 +48,8 @@ pub struct MainWindow { hints_label: Label, filter_buttons: Rc>>, submenu_state: Rc>, + /// Parsed tab config (ProviderTypes for cycling) + tab_order: Rc>, } impl MainWindow { @@ -108,8 +110,17 @@ impl MainWindow { .build(); filter_tabs.add_css_class("owlry-filter-tabs"); - // Create toggle buttons for each provider - let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter); + // Parse tabs config to ProviderTypes + let tab_order: Vec = cfg + .general + .tabs + .iter() + .filter_map(|s| s.parse().ok()) + .collect(); + let tab_order = Rc::new(tab_order); + + // Create toggle buttons for each provider (from config) + let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter, &cfg.general.tabs); let filter_buttons = Rc::new(RefCell::new(filter_buttons)); header_box.append(&mode_label); @@ -177,6 +188,7 @@ impl MainWindow { hints_label, filter_buttons, submenu_state: Rc::new(RefCell::new(SubmenuState::default())), + tab_order, }; main_window.setup_signals(); @@ -191,38 +203,31 @@ impl MainWindow { fn create_filter_buttons( container: &GtkBox, filter: &Rc>, + tabs: &[String], ) -> HashMap { - let providers = [ - (ProviderType::Application, "Apps", "Ctrl+1"), - (ProviderType::Command, "Cmds", "Ctrl+2"), - (ProviderType::Uuctl, "uuctl", "Ctrl+3"), - ]; - let mut buttons = HashMap::new(); - for (provider_type, label, shortcut) in providers { + // Parse tab strings to ProviderType and create buttons + for (idx, tab_str) in tabs.iter().enumerate() { + let provider_type: ProviderType = match tab_str.parse() { + Ok(pt) => pt, + Err(e) => { + log::warn!("Invalid tab config '{}': {}", tab_str, e); + continue; + } + }; + + let label = Self::provider_tab_label(provider_type); + let shortcut = format!("Ctrl+{}", idx + 1); + let button = ToggleButton::builder() .label(label) - .tooltip_text(shortcut) + .tooltip_text(&shortcut) .active(filter.borrow().is_enabled(provider_type)) .build(); 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", - }; + let css_class = Self::provider_css_class(provider_type); button.add_css_class(css_class); container.append(&button); @@ -232,6 +237,44 @@ impl MainWindow { buttons } + /// Get display label for a provider tab + fn provider_tab_label(provider: ProviderType) -> &'static str { + match provider { + ProviderType::Application => "Apps", + ProviderType::Bookmarks => "Bookmarks", + ProviderType::Calculator => "Calc", + ProviderType::Clipboard => "Clip", + ProviderType::Command => "Cmds", + ProviderType::Dmenu => "Dmenu", + ProviderType::Emoji => "Emoji", + ProviderType::Files => "Files", + ProviderType::Scripts => "Scripts", + ProviderType::Ssh => "SSH", + ProviderType::System => "System", + ProviderType::Uuctl => "uuctl", + ProviderType::WebSearch => "Web", + } + } + + /// Get CSS class for a provider + fn provider_css_class(provider: ProviderType) -> &'static str { + match provider { + 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", + } + } + fn build_placeholder(filter: &ProviderFilter) -> String { let active: Vec<&str> = filter .enabled_providers() @@ -507,7 +550,7 @@ impl MainWindow { let results: Vec = if use_frecency { providers .borrow_mut() - .search_with_frecency(&parsed.query, max_results, &filter.borrow(), &frecency.borrow(), frecency_weight) + .search_with_frecency(&parsed.query, max_results, &filter.borrow(), &frecency.borrow(), frecency_weight, parsed.tag_filter.as_deref()) .into_iter() .map(|(item, _)| item) .collect() @@ -616,6 +659,7 @@ impl MainWindow { let mode_label = self.mode_label.clone(); let hints_label = self.hints_label.clone(); let submenu_state = self.submenu_state.clone(); + let tab_order = self.tab_order.clone(); key_controller.connect_key_pressed(move |_, key, _, modifiers| { let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK); @@ -693,6 +737,7 @@ impl MainWindow { &filter_buttons, &search_entry, &mode_label, + &tab_order, !shift, ); } @@ -705,45 +750,37 @@ impl MainWindow { &filter_buttons, &search_entry, &mode_label, + &tab_order, false, ); } gtk4::glib::Propagation::Stop } - // Ctrl+1/2/3 toggle specific providers (only when not in submenu) - Key::_1 if ctrl => { + // Ctrl+1-9 toggle specific providers based on tab order (only when not in submenu) + Key::_1 | Key::_2 | Key::_3 | Key::_4 | Key::_5 | + Key::_6 | Key::_7 | Key::_8 | Key::_9 if ctrl => { if !submenu_state.borrow().active { - Self::toggle_provider_button( - ProviderType::Application, - &filter, - &filter_buttons, - &search_entry, - &mode_label, - ); - } - gtk4::glib::Propagation::Stop - } - Key::_2 if ctrl => { - if !submenu_state.borrow().active { - Self::toggle_provider_button( - ProviderType::Command, - &filter, - &filter_buttons, - &search_entry, - &mode_label, - ); - } - gtk4::glib::Propagation::Stop - } - Key::_3 if ctrl => { - if !submenu_state.borrow().active { - Self::toggle_provider_button( - ProviderType::Uuctl, - &filter, - &filter_buttons, - &search_entry, - &mode_label, - ); + let idx = match key { + Key::_1 => 0, + Key::_2 => 1, + Key::_3 => 2, + Key::_4 => 3, + Key::_5 => 4, + Key::_6 => 5, + Key::_7 => 6, + Key::_8 => 7, + Key::_9 => 8, + _ => return gtk4::glib::Propagation::Proceed, + }; + if let Some(&provider) = tab_order.get(idx) { + Self::toggle_provider_button( + provider, + &filter, + &filter_buttons, + &search_entry, + &mode_label, + ); + } } gtk4::glib::Propagation::Stop } @@ -797,24 +834,24 @@ impl MainWindow { buttons: &Rc>>, entry: &Entry, mode_label: &Label, + tab_order: &[ProviderType], forward: bool, ) { - let order = [ - ProviderType::Application, - ProviderType::Command, - ProviderType::Uuctl, - ]; + if tab_order.is_empty() { + return; + } + let current = filter.borrow().enabled_providers(); let next = if current.len() == 1 { - let idx = order.iter().position(|p| p == ¤t[0]).unwrap_or(0); + let idx = tab_order.iter().position(|p| p == ¤t[0]).unwrap_or(0); if forward { - order[(idx + 1) % order.len()] + tab_order[(idx + 1) % tab_order.len()] } else { - order[(idx + order.len() - 1) % order.len()] + tab_order[(idx + tab_order.len() - 1) % tab_order.len()] } } else { - ProviderType::Application + tab_order[0] }; { @@ -862,7 +899,7 @@ impl MainWindow { let results: Vec = if use_frecency { self.providers .borrow_mut() - .search_with_frecency(query, max_results, &self.filter.borrow(), &self.frecency.borrow(), frecency_weight) + .search_with_frecency(query, max_results, &self.filter.borrow(), &self.frecency.borrow(), frecency_weight, None) .into_iter() .map(|(item, _)| item) .collect() diff --git a/src/ui/result_row.rs b/src/ui/result_row.rs index e169c80..3ebb98e 100644 --- a/src/ui/result_row.rs +++ b/src/ui/result_row.rs @@ -82,6 +82,25 @@ impl ResultRow { text_box.append(&name_label); } + // Tag badges (show first 3 tags) + if !item.tags.is_empty() { + let tags_box = GtkBox::builder() + .orientation(Orientation::Horizontal) + .spacing(4) + .halign(gtk4::Align::Start) + .build(); + + for tag in item.tags.iter().take(3) { + let tag_label = Label::builder() + .label(tag) + .build(); + tag_label.add_css_class("owlry-tag-badge"); + tags_box.append(&tag_label); + } + + text_box.append(&tags_box); + } + // Provider badge let badge = Label::builder() .label(&item.provider.to_string())