feat: add tags, configurable tabs, and tag-based filtering
- Add `tags` field to LaunchItem for categorization - Extract .desktop Categories as tags for applications - Add semantic tags to providers (systemd, ssh, script, etc.) - Display tag badges in result rows (max 3 tags) - Add `tabs` config option for customizable header tabs - Dynamic Ctrl+1-9 shortcuts based on tab config - Add `:tag:XXX` prefix for tag-based filtering - Include tags in fuzzy search with lower weight - Update config.example.toml with tabs documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -22,6 +22,18 @@ pub struct GeneralConfig {
|
||||
/// If None or empty, launches directly via sh -c
|
||||
#[serde(default)]
|
||||
pub launch_wrapper: Option<String>,
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
fn default_tabs() -> Vec<String> {
|
||||
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,
|
||||
|
||||
@@ -17,6 +17,7 @@ pub struct ProviderFilter {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedQuery {
|
||||
pub prefix: Option<ProviderType>,
|
||||
pub tag_filter: Option<String>,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -135,6 +135,12 @@ impl Provider for ApplicationProvider {
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Extract categories as tags (lowercase for consistency)
|
||||
let tags: Vec<String> = 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);
|
||||
|
||||
@@ -157,6 +157,7 @@ impl BookmarksProvider {
|
||||
provider: ProviderType::Bookmarks,
|
||||
command: format!("xdg-open '{}'", url.replace('\'', "'\\''")),
|
||||
terminal: false,
|
||||
tags: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -99,6 +99,7 @@ impl ClipboardProvider {
|
||||
provider: ProviderType::Clipboard,
|
||||
command,
|
||||
terminal: false,
|
||||
tags: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ impl Provider for CommandProvider {
|
||||
provider: ProviderType::Command,
|
||||
command: name,
|
||||
terminal: false,
|
||||
tags: Vec::new(),
|
||||
};
|
||||
|
||||
self.items.push(item);
|
||||
|
||||
@@ -101,6 +101,7 @@ impl Provider for DmenuProvider {
|
||||
provider: ProviderType::Dmenu,
|
||||
command: line.to_string(),
|
||||
terminal: false,
|
||||
tags: Vec::new(),
|
||||
};
|
||||
|
||||
self.items.push(item);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -191,6 +191,7 @@ impl FileSearchProvider {
|
||||
provider: ProviderType::Files,
|
||||
command,
|
||||
terminal: false,
|
||||
tags: Vec::new(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
#[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| {
|
||||
|
||||
@@ -87,6 +87,7 @@ impl ScriptsProvider {
|
||||
provider: ProviderType::Scripts,
|
||||
command: path.to_string_lossy().to_string(),
|
||||
terminal: false,
|
||||
tags: vec!["script".to_string()],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ impl SshProvider {
|
||||
provider: ProviderType::Ssh,
|
||||
command,
|
||||
terminal: false, // We're already wrapping in terminal
|
||||
tags: vec!["ssh".to_string()],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ impl SystemProvider {
|
||||
provider: ProviderType::System,
|
||||
command: command.to_string(),
|
||||
terminal: false,
|
||||
tags: vec!["power".to_string(), "system".to_string()],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ impl WebSearchProvider {
|
||||
provider: ProviderType::WebSearch,
|
||||
command,
|
||||
terminal: false,
|
||||
tags: vec!["web".to_string(), "search".to_string()],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -48,6 +48,8 @@ pub struct MainWindow {
|
||||
hints_label: Label,
|
||||
filter_buttons: Rc<RefCell<HashMap<ProviderType, ToggleButton>>>,
|
||||
submenu_state: Rc<RefCell<SubmenuState>>,
|
||||
/// Parsed tab config (ProviderTypes for cycling)
|
||||
tab_order: Rc<Vec<ProviderType>>,
|
||||
}
|
||||
|
||||
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<ProviderType> = 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<RefCell<ProviderFilter>>,
|
||||
tabs: &[String],
|
||||
) -> HashMap<ProviderType, ToggleButton> {
|
||||
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<LaunchItem> = 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<RefCell<HashMap<ProviderType, ToggleButton>>>,
|
||||
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<LaunchItem> = 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()
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user