feat: add calculator provider and frecency tracking

Calculator:
- Type "= expression" or "calc expression" for instant math evaluation
- Supports standard math functions (sqrt, sin, cos, etc.) and constants (pi, e)
- Copies result to clipboard on Enter via wl-copy

Frecency:
- Firefox-style algorithm: score = launch_count * recency_weight
- Boosts frequently and recently launched items in search results
- Persists to ~/.local/share/owlry/frecency.json
- Configurable via providers.frecency and providers.frecency_weight

New config options:
- providers.calculator = true
- providers.frecency = true
- providers.frecency_weight = 0.3

UI updates:
- Added :calc prefix support
- Calculator badge with yellow styling
- Updated hints to show "= calc" syntax

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-28 17:16:01 +01:00
parent a1351f05e9
commit 738fecc6da
15 changed files with 872 additions and 27 deletions

View File

@@ -1,5 +1,6 @@
use crate::cli::CliArgs;
use crate::config::Config;
use crate::data::FrecencyStore;
use crate::filter::ProviderFilter;
use crate::providers::ProviderManager;
use crate::theme;
@@ -40,6 +41,7 @@ impl OwlryApp {
let config = Rc::new(RefCell::new(Config::load_or_default()));
let providers = Rc::new(RefCell::new(ProviderManager::new()));
let frecency = Rc::new(RefCell::new(FrecencyStore::load_or_default()));
// Create filter from CLI args and config
let filter = ProviderFilter::new(
@@ -49,7 +51,7 @@ impl OwlryApp {
);
let filter = Rc::new(RefCell::new(filter));
let window = MainWindow::new(app, config.clone(), providers.clone(), filter.clone());
let window = MainWindow::new(app, config.clone(), providers.clone(), frecency.clone(), filter.clone());
// Set up layer shell for Wayland overlay behavior
window.init_layer_shell();

View File

@@ -35,6 +35,7 @@ pub struct ThemeColors {
pub accent_bright: Option<String>,
// Provider badge colors
pub badge_app: Option<String>,
pub badge_calc: Option<String>,
pub badge_cmd: Option<String>,
pub badge_dmenu: Option<String>,
pub badge_uuctl: Option<String>,
@@ -59,6 +60,23 @@ pub struct ProvidersConfig {
pub applications: bool,
pub commands: bool,
pub uuctl: bool,
/// Enable calculator provider (= expression or calc expression)
#[serde(default = "default_true")]
pub calculator: bool,
/// Enable frecency-based result ranking
#[serde(default = "default_true")]
pub frecency: bool,
/// Weight for frecency boost (0.0 = disabled, 1.0 = strong boost)
#[serde(default = "default_frecency_weight")]
pub frecency_weight: f64,
}
fn default_true() -> bool {
true
}
fn default_frecency_weight() -> f64 {
0.3
}
/// Detect the best launch wrapper for the current session
@@ -172,6 +190,9 @@ impl Default for Config {
applications: true,
commands: true,
uuctl: true,
calculator: true,
frecency: true,
frecency_weight: 0.3,
},
}
}

223
src/data/frecency.rs Normal file
View File

@@ -0,0 +1,223 @@
use chrono::{DateTime, Utc};
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
/// A single frecency entry tracking launch count and recency
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrecencyEntry {
pub launch_count: u32,
pub last_launch: DateTime<Utc>,
}
/// Persistent frecency data store
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrecencyData {
pub version: u32,
pub entries: HashMap<String, FrecencyEntry>,
}
impl Default for FrecencyData {
fn default() -> Self {
Self {
version: 1,
entries: HashMap::new(),
}
}
}
/// Frecency store for tracking and boosting recently/frequently used items
pub struct FrecencyStore {
data: FrecencyData,
path: PathBuf,
dirty: bool,
}
impl FrecencyStore {
/// Create a new frecency store, loading existing data if available
pub fn new() -> Self {
let path = Self::data_path();
let data = Self::load_from_path(&path).unwrap_or_default();
info!("Frecency store loaded with {} entries", data.entries.len());
Self {
data,
path,
dirty: false,
}
}
/// Alias for new() - loads from disk or creates default
pub fn load_or_default() -> Self {
Self::new()
}
/// Get the path to the frecency data file
fn data_path() -> PathBuf {
dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("owlry")
.join("frecency.json")
}
/// Load frecency data from a file
fn load_from_path(path: &PathBuf) -> Option<FrecencyData> {
if !path.exists() {
debug!("Frecency file not found at {:?}", path);
return None;
}
let content = std::fs::read_to_string(path).ok()?;
match serde_json::from_str(&content) {
Ok(data) => Some(data),
Err(e) => {
warn!("Failed to parse frecency data: {}", e);
None
}
}
}
/// Save frecency data to disk
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
if !self.dirty {
return Ok(());
}
// Ensure directory exists
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = serde_json::to_string_pretty(&self.data)?;
std::fs::write(&self.path, content)?;
self.dirty = false;
debug!("Frecency data saved to {:?}", self.path);
Ok(())
}
/// Record a launch event for an item
pub fn record_launch(&mut self, item_id: &str) {
let now = Utc::now();
let entry = self
.data
.entries
.entry(item_id.to_string())
.or_insert(FrecencyEntry {
launch_count: 0,
last_launch: now,
});
entry.launch_count += 1;
entry.last_launch = now;
self.dirty = true;
debug!(
"Recorded launch for '{}': count={}, last={}",
item_id, entry.launch_count, entry.last_launch
);
// Auto-save after recording
if let Err(e) = self.save() {
warn!("Failed to save frecency data: {}", e);
}
}
/// Calculate frecency score for an item
/// Uses Firefox-style algorithm: score = launch_count * recency_weight
pub fn get_score(&self, item_id: &str) -> f64 {
match self.data.entries.get(item_id) {
Some(entry) => Self::calculate_frecency(entry.launch_count, entry.last_launch),
None => 0.0,
}
}
/// Calculate frecency using Firefox-style algorithm
fn calculate_frecency(launch_count: u32, last_launch: DateTime<Utc>) -> f64 {
let now = Utc::now();
let age = now.signed_duration_since(last_launch);
let age_days = age.num_hours() as f64 / 24.0;
// Recency weight based on how recently the item was used
let recency_weight = if age_days < 1.0 {
100.0 // Today
} else if age_days < 7.0 {
70.0 // This week
} else if age_days < 30.0 {
50.0 // This month
} else if age_days < 90.0 {
30.0 // This quarter
} else {
10.0 // Older
};
launch_count as f64 * recency_weight
}
/// Get all entries (for debugging/display)
#[allow(dead_code)]
pub fn entries(&self) -> &HashMap<String, FrecencyEntry> {
&self.data.entries
}
/// Clear all frecency data
#[allow(dead_code)]
pub fn clear(&mut self) {
self.data.entries.clear();
self.dirty = true;
}
}
impl Default for FrecencyStore {
fn default() -> Self {
Self::new()
}
}
impl Drop for FrecencyStore {
fn drop(&mut self) {
// Attempt to save on drop
if let Err(e) = self.save() {
warn!("Failed to save frecency data on drop: {}", e);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frecency_calculation() {
let now = Utc::now();
// Recent launch should have high score
let score_today = FrecencyStore::calculate_frecency(10, now);
assert!(score_today > 900.0); // 10 * 100
// Older launch should have lower score
let week_ago = now - chrono::Duration::days(5);
let score_week = FrecencyStore::calculate_frecency(10, week_ago);
assert!(score_week < score_today);
assert!(score_week > 600.0); // 10 * 70
// Much older launch
let month_ago = now - chrono::Duration::days(45);
let score_month = FrecencyStore::calculate_frecency(10, month_ago);
assert!(score_month < score_week);
}
#[test]
fn test_launch_count_matters() {
let now = Utc::now();
let score_few = FrecencyStore::calculate_frecency(2, now);
let score_many = FrecencyStore::calculate_frecency(20, now);
assert!(score_many > score_few);
assert!((score_many / score_few - 10.0).abs() < 0.1); // Should be ~10x
}
}

3
src/data/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
mod frecency;
pub use frecency::FrecencyStore;

View File

@@ -129,6 +129,8 @@ impl ProviderFilter {
let prefixes = [
(":app ", ProviderType::Application),
(":apps ", ProviderType::Application),
(":calc ", ProviderType::Calculator),
(":calculator ", ProviderType::Calculator),
(":cmd ", ProviderType::Command),
(":command ", ProviderType::Command),
(":uuctl ", ProviderType::Uuctl),
@@ -147,6 +149,8 @@ impl ProviderFilter {
let partial_prefixes = [
(":app", ProviderType::Application),
(":apps", ProviderType::Application),
(":calc", ProviderType::Calculator),
(":calculator", ProviderType::Calculator),
(":cmd", ProviderType::Command),
(":command", ProviderType::Command),
(":uuctl", ProviderType::Uuctl),
@@ -172,9 +176,10 @@ impl ProviderFilter {
let mut providers: Vec<_> = self.enabled.iter().copied().collect();
providers.sort_by_key(|p| match p {
ProviderType::Application => 0,
ProviderType::Command => 1,
ProviderType::Uuctl => 2,
ProviderType::Dmenu => 3,
ProviderType::Calculator => 1,
ProviderType::Command => 2,
ProviderType::Uuctl => 3,
ProviderType::Dmenu => 4,
});
providers
}
@@ -184,6 +189,7 @@ impl ProviderFilter {
if let Some(prefix) = self.active_prefix {
return match prefix {
ProviderType::Application => "Apps",
ProviderType::Calculator => "Calc",
ProviderType::Command => "Commands",
ProviderType::Uuctl => "uuctl",
ProviderType::Dmenu => "dmenu",
@@ -194,6 +200,7 @@ impl ProviderFilter {
if enabled.len() == 1 {
match enabled[0] {
ProviderType::Application => "Apps",
ProviderType::Calculator => "Calc",
ProviderType::Command => "Commands",
ProviderType::Uuctl => "uuctl",
ProviderType::Dmenu => "dmenu",

View File

@@ -1,6 +1,7 @@
mod app;
mod cli;
mod config;
mod data;
mod filter;
mod providers;
mod theme;

191
src/providers/calculator.rs Normal file
View File

@@ -0,0 +1,191 @@
use super::{LaunchItem, Provider, ProviderType};
use log::debug;
/// Calculator provider for evaluating math expressions
/// Syntax: `= expression` or `calc expression`
pub struct CalculatorProvider {
/// Cached result from last evaluation
cached_result: Option<LaunchItem>,
}
impl CalculatorProvider {
pub fn new() -> Self {
Self {
cached_result: None,
}
}
/// Check if a query is a calculator expression
pub fn is_calculator_query(query: &str) -> bool {
let trimmed = query.trim();
trimmed.starts_with("= ") || trimmed.starts_with("calc ")
}
/// Extract the expression from a calculator query
fn extract_expression(query: &str) -> Option<&str> {
let trimmed = query.trim();
if let Some(expr) = trimmed.strip_prefix("= ") {
Some(expr.trim())
} else if let Some(expr) = trimmed.strip_prefix("calc ") {
Some(expr.trim())
} else {
None
}
}
/// Evaluate an expression and return a LaunchItem result
pub fn evaluate(&mut self, query: &str) -> Option<LaunchItem> {
let expr = Self::extract_expression(query)?;
if expr.is_empty() {
return None;
}
debug!("Evaluating expression: {}", expr);
match meval::eval_str(expr) {
Ok(result) => {
// Format result nicely
let result_str = if result.fract() == 0.0 && result.abs() < 1e15 {
// Integer result
format!("{}", result as i64)
} else {
// Float result with reasonable precision
let formatted = format!("{:.10}", result);
// Trim trailing zeros
formatted.trim_end_matches('0').trim_end_matches('.').to_string()
};
let item = LaunchItem {
id: format!("calc:{}", expr),
name: result_str.clone(),
description: Some(format!("= {}", expr)),
icon: Some("accessories-calculator".to_string()),
provider: ProviderType::Calculator,
// Copy result to clipboard using wl-copy
command: format!("sh -c 'echo -n \"{}\" | wl-copy'", result_str),
terminal: false,
};
debug!("Calculator result: {} = {}", expr, result_str);
self.cached_result = Some(item.clone());
Some(item)
}
Err(e) => {
debug!("Calculator error for '{}': {}", expr, e);
None
}
}
}
}
impl Provider for CalculatorProvider {
fn name(&self) -> &str {
"Calculator"
}
fn provider_type(&self) -> ProviderType {
ProviderType::Calculator
}
fn refresh(&mut self) {
// Calculator doesn't need refresh - it evaluates on-demand
self.cached_result = None;
}
fn items(&self) -> &[LaunchItem] {
// Calculator is a dynamic provider - items are generated from query
// Return cached result if available (for UI display)
match &self.cached_result {
Some(item) => std::slice::from_ref(item),
None => &[],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_calculator_query() {
assert!(CalculatorProvider::is_calculator_query("= 5+3"));
assert!(CalculatorProvider::is_calculator_query("calc 5+3"));
assert!(CalculatorProvider::is_calculator_query(" = 5+3"));
assert!(!CalculatorProvider::is_calculator_query("5+3"));
assert!(!CalculatorProvider::is_calculator_query("firefox"));
}
#[test]
fn test_extract_expression() {
assert_eq!(
CalculatorProvider::extract_expression("= 5+3"),
Some("5+3")
);
assert_eq!(
CalculatorProvider::extract_expression("calc 5+3"),
Some("5+3")
);
assert_eq!(
CalculatorProvider::extract_expression("= 5 + 3 "),
Some("5 + 3")
);
assert_eq!(CalculatorProvider::extract_expression("5+3"), None);
}
#[test]
fn test_evaluate_basic() {
let mut calc = CalculatorProvider::new();
let result = calc.evaluate("= 5+3").unwrap();
assert_eq!(result.name, "8");
let result = calc.evaluate("= 10 * 2").unwrap();
assert_eq!(result.name, "20");
let result = calc.evaluate("= 15 / 3").unwrap();
assert_eq!(result.name, "5");
}
#[test]
fn test_evaluate_float() {
let mut calc = CalculatorProvider::new();
let result = calc.evaluate("= 5/2").unwrap();
assert_eq!(result.name, "2.5");
let result = calc.evaluate("= 1/3").unwrap();
assert!(result.name.starts_with("0.333"));
}
#[test]
fn test_evaluate_functions() {
let mut calc = CalculatorProvider::new();
let result = calc.evaluate("= sqrt(16)").unwrap();
assert_eq!(result.name, "4");
let result = calc.evaluate("= abs(-5)").unwrap();
assert_eq!(result.name, "5");
}
#[test]
fn test_evaluate_constants() {
let mut calc = CalculatorProvider::new();
let result = calc.evaluate("= pi").unwrap();
assert!(result.name.starts_with("3.14159"));
let result = calc.evaluate("= e").unwrap();
assert!(result.name.starts_with("2.718"));
}
#[test]
fn test_evaluate_invalid() {
let mut calc = CalculatorProvider::new();
assert!(calc.evaluate("= ").is_none());
assert!(calc.evaluate("= invalid").is_none());
assert!(calc.evaluate("= 5 +").is_none());
}
}

View File

@@ -1,9 +1,11 @@
mod application;
mod calculator;
mod command;
mod dmenu;
mod uuctl;
pub use application::ApplicationProvider;
pub use calculator::CalculatorProvider;
pub use command::CommandProvider;
pub use dmenu::DmenuProvider;
pub use uuctl::UuctlProvider;
@@ -12,6 +14,8 @@ use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use log::info;
use crate::data::FrecencyStore;
/// Represents a single searchable/launchable item
#[derive(Debug, Clone)]
pub struct LaunchItem {
@@ -28,6 +32,7 @@ pub struct LaunchItem {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProviderType {
Application,
Calculator,
Command,
Dmenu,
Uuctl,
@@ -39,10 +44,14 @@ impl std::str::FromStr for ProviderType {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"app" | "apps" | "application" | "applications" => Ok(ProviderType::Application),
"calc" | "calculator" => Ok(ProviderType::Calculator),
"cmd" | "command" | "commands" => Ok(ProviderType::Command),
"uuctl" => Ok(ProviderType::Uuctl),
"dmenu" => Ok(ProviderType::Dmenu),
_ => Err(format!("Unknown provider: '{}'. Valid: app, cmd, uuctl", s)),
_ => Err(format!(
"Unknown provider: '{}'. Valid: app, calc, cmd, uuctl",
s
)),
}
}
}
@@ -51,6 +60,7 @@ 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::Calculator => write!(f, "calc"),
ProviderType::Command => write!(f, "cmd"),
ProviderType::Dmenu => write!(f, "dmenu"),
ProviderType::Uuctl => write!(f, "uuctl"),
@@ -70,6 +80,7 @@ pub trait Provider: Send {
/// Manages all providers and handles searching
pub struct ProviderManager {
providers: Vec<Box<dyn Provider>>,
calculator: CalculatorProvider,
matcher: SkimMatcherV2,
}
@@ -77,6 +88,7 @@ impl ProviderManager {
pub fn new() -> Self {
let mut manager = Self {
providers: Vec::new(),
calculator: CalculatorProvider::new(),
matcher: SkimMatcherV2::default(),
};
@@ -206,6 +218,79 @@ impl ProviderManager {
results
}
/// Search with frecency boosting and calculator support
pub fn search_with_frecency(
&mut self,
query: &str,
max_results: usize,
filter: &crate::filter::ProviderFilter,
frecency: &FrecencyStore,
frecency_weight: f64,
) -> Vec<(LaunchItem, i64)> {
let mut results: Vec<(LaunchItem, i64)> = Vec::new();
// Check for calculator query first
if CalculatorProvider::is_calculator_query(query) {
if let Some(calc_result) = self.calculator.evaluate(query) {
// Calculator results get a high score to appear first
results.push((calc_result, 10000));
}
}
// Empty query (after checking calculator) - return frecency-sorted items
if query.is_empty() {
let mut items: Vec<(LaunchItem, i64)> = self
.providers
.iter()
.filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned())
.map(|item| {
let frecency_score = frecency.get_score(&item.id);
let boosted = (frecency_score * frecency_weight * 100.0) as i64;
(item, boosted)
})
.collect();
items.sort_by(|a, b| b.1.cmp(&a.1));
items.truncate(max_results);
return items;
}
// Regular search with frecency boost
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| {
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,
};
base_score.map(|s| {
let frecency_score = frecency.get_score(&item.id);
let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64;
(item.clone(), s + frecency_boost)
})
})
})
.collect();
results.extend(search_results);
results.sort_by(|a, b| b.1.cmp(&a.1));
results.truncate(max_results);
results
}
/// Get all available provider types (for UI tabs)
#[allow(dead_code)]
pub fn available_providers(&self) -> Vec<ProviderType> {

View File

@@ -35,6 +35,9 @@ 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_calc) = config.colors.badge_calc {
css.push_str(&format!(" --owlry-badge-calc: {};\n", badge_calc));
}
if let Some(ref badge_cmd) = config.colors.badge_cmd {
css.push_str(&format!(" --owlry-badge-cmd: {};\n", badge_cmd));
}

View File

@@ -1,4 +1,5 @@
use crate::config::Config;
use crate::data::FrecencyStore;
use crate::filter::ProviderFilter;
use crate::providers::{LaunchItem, ProviderManager, ProviderType, UuctlProvider};
use crate::ui::ResultRow;
@@ -36,6 +37,7 @@ pub struct MainWindow {
scrolled: ScrolledWindow,
config: Rc<RefCell<Config>>,
providers: Rc<RefCell<ProviderManager>>,
frecency: Rc<RefCell<FrecencyStore>>,
current_results: Rc<RefCell<Vec<LaunchItem>>>,
filter: Rc<RefCell<ProviderFilter>>,
mode_label: Label,
@@ -49,6 +51,7 @@ impl MainWindow {
app: &Application,
config: Rc<RefCell<Config>>,
providers: Rc<RefCell<ProviderManager>>,
frecency: Rc<RefCell<FrecencyStore>>,
filter: Rc<RefCell<ProviderFilter>>,
) -> Self {
let cfg = config.borrow();
@@ -140,7 +143,7 @@ impl MainWindow {
hints_box.add_css_class("owlry-hints");
let hints_label = Label::builder()
.label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close :app :cmd :uuctl")
.label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close = calc :app :cmd :uuctl")
.halign(gtk4::Align::Center)
.hexpand(true)
.build();
@@ -163,6 +166,7 @@ impl MainWindow {
scrolled,
config,
providers,
frecency,
current_results: Rc::new(RefCell::new(Vec::new())),
filter,
mode_label,
@@ -202,6 +206,7 @@ impl MainWindow {
button.add_css_class("owlry-filter-button");
let css_class = match provider_type {
ProviderType::Application => "owlry-filter-app",
ProviderType::Calculator => "owlry-filter-calc",
ProviderType::Command => "owlry-filter-cmd",
ProviderType::Uuctl => "owlry-filter-uuctl",
ProviderType::Dmenu => "owlry-filter-dmenu",
@@ -221,6 +226,7 @@ impl MainWindow {
.iter()
.map(|p| match p {
ProviderType::Application => "applications",
ProviderType::Calculator => "calculator",
ProviderType::Command => "commands",
ProviderType::Uuctl => "uuctl units",
ProviderType::Dmenu => "options",
@@ -328,7 +334,7 @@ impl MainWindow {
// Restore UI
mode_label.set_label(filter.borrow().mode_display_name());
hints_label.set_label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close :app :cmd :uuctl");
hints_label.set_label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close = calc :app :cmd :uuctl");
search_entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow())));
search_entry.set_text(&saved_search);
@@ -341,6 +347,7 @@ impl MainWindow {
let providers = self.providers.clone();
let results_list = self.results_list.clone();
let config = self.config.clone();
let frecency = self.frecency.clone();
let current_results = self.current_results.clone();
let filter = self.filter.clone();
let mode_label = self.mode_label.clone();
@@ -401,6 +408,7 @@ impl MainWindow {
if parsed.prefix.is_some() {
let prefix_name = match parsed.prefix.unwrap() {
ProviderType::Application => "applications",
ProviderType::Calculator => "calculator",
ProviderType::Command => "commands",
ProviderType::Uuctl => "uuctl units",
ProviderType::Dmenu => "options",
@@ -409,13 +417,27 @@ impl MainWindow {
.set_placeholder_text(Some(&format!("Search {}...", prefix_name)));
}
let max_results = config.borrow().general.max_results;
let results: Vec<LaunchItem> = providers
.borrow()
.search_filtered(&parsed.query, max_results, &filter.borrow())
.into_iter()
.map(|(item, _)| item)
.collect();
let cfg = config.borrow();
let max_results = cfg.general.max_results;
let frecency_weight = cfg.providers.frecency_weight;
let use_frecency = cfg.providers.frecency;
drop(cfg);
let results: Vec<LaunchItem> = if use_frecency {
providers
.borrow_mut()
.search_with_frecency(&parsed.query, max_results, &filter.borrow(), &frecency.borrow(), frecency_weight)
.into_iter()
.map(|(item, _)| item)
.collect()
} else {
providers
.borrow()
.search_filtered(&parsed.query, max_results, &filter.borrow())
.into_iter()
.map(|(item, _)| item)
.collect()
};
while let Some(child) = results_list.first_child() {
results_list.remove(&child);
@@ -437,6 +459,7 @@ impl MainWindow {
let results_list_for_activate = self.results_list.clone();
let current_results_for_activate = self.current_results.clone();
let config_for_activate = self.config.clone();
let frecency_for_activate = self.frecency.clone();
let window_for_activate = self.window.clone();
let submenu_state_for_activate = self.submenu_state.clone();
let mode_label_for_activate = self.mode_label.clone();
@@ -470,7 +493,7 @@ impl MainWindow {
);
} else {
// Execute the command
Self::launch_item(item, &config_for_activate.borrow());
Self::launch_item(item, &config_for_activate.borrow(), &frecency_for_activate);
window_for_activate.close();
}
}
@@ -647,6 +670,7 @@ impl MainWindow {
// Double-click to launch
let current_results = self.current_results.clone();
let config = self.config.clone();
let frecency = self.frecency.clone();
let window = self.window.clone();
let submenu_state = self.submenu_state.clone();
let results_list_for_click = self.results_list.clone();
@@ -675,7 +699,7 @@ impl MainWindow {
is_active,
);
} else {
Self::launch_item(item, &config.borrow());
Self::launch_item(item, &config.borrow(), &frecency);
window.close();
}
}
@@ -743,14 +767,27 @@ impl MainWindow {
}
fn update_results(&self, query: &str) {
let max_results = self.config.borrow().general.max_results;
let results: Vec<LaunchItem> = self
.providers
.borrow()
.search_filtered(query, max_results, &self.filter.borrow())
.into_iter()
.map(|(item, _)| item)
.collect();
let cfg = self.config.borrow();
let max_results = cfg.general.max_results;
let frecency_weight = cfg.providers.frecency_weight;
let use_frecency = cfg.providers.frecency;
drop(cfg);
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)
.into_iter()
.map(|(item, _)| item)
.collect()
} else {
self.providers
.borrow()
.search_filtered(query, max_results, &self.filter.borrow())
.into_iter()
.map(|(item, _)| item)
.collect()
};
while let Some(child) = self.results_list.first_child() {
self.results_list.remove(&child);
@@ -768,7 +805,12 @@ impl MainWindow {
*self.current_results.borrow_mut() = results;
}
fn launch_item(item: &LaunchItem, config: &Config) {
fn launch_item(item: &LaunchItem, config: &Config, frecency: &Rc<RefCell<FrecencyStore>>) {
// Record this launch for frecency tracking
if config.providers.frecency {
frecency.borrow_mut().record_launch(&item.id);
}
info!("Launching: {} ({})", item.name, item.command);
let cmd = if item.terminal {

View File

@@ -32,6 +32,7 @@ impl ResultRow {
// Default icon based on provider type
let default_icon = match item.provider {
crate::providers::ProviderType::Application => "application-x-executable",
crate::providers::ProviderType::Calculator => "accessories-calculator",
crate::providers::ProviderType::Command => "utilities-terminal",
crate::providers::ProviderType::Dmenu => "view-list-symbolic",
crate::providers::ProviderType::Uuctl => "system-run",