refactor: centralize path handling with XDG Base Directory compliance
- Add src/paths.rs module for all XDG path lookups - Move scripts from ~/.config to ~/.local/share (XDG data) - Use $XDG_CONFIG_HOME for browser bookmark paths - Add dev-logging feature flag for verbose debug output - Add dev-install profile for testable release builds - Remove CLAUDE.md from version control BREAKING: Scripts directory moved from ~/.config/owlry/scripts/ to ~/.local/share/owlry/scripts/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
32
CLAUDE.md
32
CLAUDE.md
@@ -1,32 +0,0 @@
|
|||||||
# Owlry - Claude Code Instructions
|
|
||||||
|
|
||||||
## Release Workflow
|
|
||||||
|
|
||||||
Always use `just` for releases and AUR deployment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Bump version (updates Cargo.toml + Cargo.lock, commits)
|
|
||||||
just bump 0.x.y
|
|
||||||
|
|
||||||
# Push and create tag
|
|
||||||
git push && just tag
|
|
||||||
|
|
||||||
# Update AUR package
|
|
||||||
just aur-update
|
|
||||||
|
|
||||||
# Review changes, then publish
|
|
||||||
just aur-publish
|
|
||||||
```
|
|
||||||
|
|
||||||
Do NOT manually edit Cargo.toml for version bumps - use `just bump`.
|
|
||||||
|
|
||||||
## Available just recipes
|
|
||||||
|
|
||||||
- `just build` / `just release` - Build debug/release
|
|
||||||
- `just check` - Run cargo check + clippy
|
|
||||||
- `just test` - Run tests
|
|
||||||
- `just bump <version>` - Bump version
|
|
||||||
- `just tag` - Create and push git tag
|
|
||||||
- `just aur-update` - Update PKGBUILD checksums
|
|
||||||
- `just aur-publish` - Commit and push to AUR
|
|
||||||
- `just aur-test` - Test PKGBUILD locally
|
|
||||||
11
Cargo.toml
11
Cargo.toml
@@ -55,6 +55,11 @@ serde_json = "1"
|
|||||||
# Date/time for frecency calculations
|
# Date/time for frecency calculations
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
# Enable verbose debug logging (for development/testing builds)
|
||||||
|
dev-logging = []
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
@@ -65,3 +70,9 @@ opt-level = "z" # Optimize for size
|
|||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 0
|
opt-level = 0
|
||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
|
# For installing a testable build: cargo install --path . --profile dev-install --features dev-logging
|
||||||
|
[profile.dev-install]
|
||||||
|
inherits = "release"
|
||||||
|
strip = false
|
||||||
|
debug = 1 # Basic debug info for stack traces
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::cli::CliArgs;
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::data::FrecencyStore;
|
use crate::data::FrecencyStore;
|
||||||
use crate::filter::ProviderFilter;
|
use crate::filter::ProviderFilter;
|
||||||
|
use crate::paths;
|
||||||
use crate::providers::ProviderManager;
|
use crate::providers::ProviderManager;
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::ui::MainWindow;
|
use crate::ui::MainWindow;
|
||||||
@@ -98,10 +99,8 @@ impl OwlryApp {
|
|||||||
debug!("Loaded built-in owl theme");
|
debug!("Loaded built-in owl theme");
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Check for custom theme in ~/.config/owlry/themes/{name}.css
|
// Check for custom theme in $XDG_CONFIG_HOME/owlry/themes/{name}.css
|
||||||
if let Some(theme_path) = dirs::config_dir()
|
if let Some(theme_path) = paths::theme_file(theme_name) {
|
||||||
.map(|p| p.join("owlry").join("themes").join(format!("{}.css", theme_name)))
|
|
||||||
{
|
|
||||||
if theme_path.exists() {
|
if theme_path.exists() {
|
||||||
theme_provider.load_from_path(&theme_path);
|
theme_provider.load_from_path(&theme_path);
|
||||||
debug!("Loaded custom theme from {:?}", theme_path);
|
debug!("Loaded custom theme from {:?}", theme_path);
|
||||||
@@ -119,7 +118,7 @@ impl OwlryApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Load user's custom stylesheet if exists
|
// 3. Load user's custom stylesheet if exists
|
||||||
if let Some(custom_path) = dirs::config_dir().map(|p| p.join("owlry").join("style.css")) {
|
if let Some(custom_path) = paths::custom_style_file() {
|
||||||
if custom_path.exists() {
|
if custom_path.exists() {
|
||||||
let custom_provider = CssProvider::new();
|
let custom_provider = CssProvider::new();
|
||||||
custom_provider.load_from_path(&custom_path);
|
custom_provider.load_from_path(&custom_path);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
use log::{debug, info, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use log::{info, warn, debug};
|
|
||||||
|
use crate::paths;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -250,7 +252,7 @@ impl Default for Config {
|
|||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn config_path() -> Option<PathBuf> {
|
pub fn config_path() -> Option<PathBuf> {
|
||||||
dirs::config_dir().map(|p| p.join("owlry").join("config.toml"))
|
paths::config_file()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_or_default() -> Self {
|
pub fn load_or_default() -> Self {
|
||||||
@@ -289,9 +291,7 @@ impl Config {
|
|||||||
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let path = Self::config_path().ok_or("Could not determine config path")?;
|
let path = Self::config_path().ok_or("Could not determine config path")?;
|
||||||
|
|
||||||
if let Some(parent) = path.parent() {
|
paths::ensure_parent_dir(&path)?;
|
||||||
std::fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = toml::to_string_pretty(self)?;
|
let content = toml::to_string_pretty(self)?;
|
||||||
std::fs::write(&path, content)?;
|
std::fs::write(&path, content)?;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::paths;
|
||||||
|
|
||||||
/// A single frecency entry tracking launch count and recency
|
/// A single frecency entry tracking launch count and recency
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct FrecencyEntry {
|
pub struct FrecencyEntry {
|
||||||
@@ -56,10 +58,7 @@ impl FrecencyStore {
|
|||||||
|
|
||||||
/// Get the path to the frecency data file
|
/// Get the path to the frecency data file
|
||||||
fn data_path() -> PathBuf {
|
fn data_path() -> PathBuf {
|
||||||
dirs::data_dir()
|
paths::frecency_file().unwrap_or_else(|| PathBuf::from("frecency.json"))
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
|
||||||
.join("owlry")
|
|
||||||
.join("frecency.json")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load frecency data from a file
|
/// Load frecency data from a file
|
||||||
@@ -85,10 +84,7 @@ impl FrecencyStore {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure directory exists
|
paths::ensure_parent_dir(&self.path)?;
|
||||||
if let Some(parent) = self.path.parent() {
|
|
||||||
std::fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = serde_json::to_string_pretty(&self.data)?;
|
let content = serde_json::to_string_pretty(&self.data)?;
|
||||||
std::fs::write(&self.path, content)?;
|
std::fs::write(&self.path, content)?;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
use crate::config::ProvidersConfig;
|
use crate::config::ProvidersConfig;
|
||||||
use crate::providers::ProviderType;
|
use crate::providers::ProviderType;
|
||||||
|
|
||||||
@@ -69,10 +72,15 @@ impl ProviderFilter {
|
|||||||
set
|
set
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
let filter = Self {
|
||||||
enabled,
|
enabled,
|
||||||
active_prefix: None,
|
active_prefix: None,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Filter] Created with enabled providers: {:?}", filter.enabled);
|
||||||
|
|
||||||
|
filter
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default filter: apps only
|
/// Default filter: apps only
|
||||||
@@ -92,8 +100,12 @@ impl ProviderFilter {
|
|||||||
if self.enabled.is_empty() {
|
if self.enabled.is_empty() {
|
||||||
self.enabled.insert(ProviderType::Application);
|
self.enabled.insert(ProviderType::Application);
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Filter] Toggled OFF {:?}, enabled: {:?}", provider, self.enabled);
|
||||||
} else {
|
} else {
|
||||||
self.enabled.insert(provider);
|
self.enabled.insert(provider);
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Filter] Toggled ON {:?}, enabled: {:?}", provider, self.enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +130,10 @@ impl ProviderFilter {
|
|||||||
|
|
||||||
/// Set prefix mode (from :app, :cmd, etc.)
|
/// Set prefix mode (from :app, :cmd, etc.)
|
||||||
pub fn set_prefix(&mut self, prefix: Option<ProviderType>) {
|
pub fn set_prefix(&mut self, prefix: Option<ProviderType>) {
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
if self.active_prefix != prefix {
|
||||||
|
debug!("[Filter] Prefix changed: {:?} -> {:?}", self.active_prefix, prefix);
|
||||||
|
}
|
||||||
self.active_prefix = prefix;
|
self.active_prefix = prefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +192,8 @@ impl ProviderFilter {
|
|||||||
|
|
||||||
for (prefix_str, provider) in prefixes {
|
for (prefix_str, provider) in prefixes {
|
||||||
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
|
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest);
|
||||||
return ParsedQuery {
|
return ParsedQuery {
|
||||||
prefix: Some(provider),
|
prefix: Some(provider),
|
||||||
query: rest.to_string(),
|
query: rest.to_string(),
|
||||||
@@ -214,6 +232,8 @@ impl ProviderFilter {
|
|||||||
|
|
||||||
for (prefix_str, provider) in partial_prefixes {
|
for (prefix_str, provider) in partial_prefixes {
|
||||||
if trimmed == prefix_str {
|
if trimmed == prefix_str {
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider);
|
||||||
return ParsedQuery {
|
return ParsedQuery {
|
||||||
prefix: Some(provider),
|
prefix: Some(provider),
|
||||||
query: String::new(),
|
query: String::new(),
|
||||||
@@ -221,10 +241,15 @@ impl ProviderFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ParsedQuery {
|
let result = ParsedQuery {
|
||||||
prefix: None,
|
prefix: None,
|
||||||
query: query.to_string(),
|
query: query.to_string(),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, result.prefix, result.query);
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get enabled providers for UI display (sorted)
|
/// Get enabled providers for UI display (sorted)
|
||||||
|
|||||||
18
src/main.rs
18
src/main.rs
@@ -3,6 +3,7 @@ mod cli;
|
|||||||
mod config;
|
mod config;
|
||||||
mod data;
|
mod data;
|
||||||
mod filter;
|
mod filter;
|
||||||
|
mod paths;
|
||||||
mod providers;
|
mod providers;
|
||||||
mod theme;
|
mod theme;
|
||||||
mod ui;
|
mod ui;
|
||||||
@@ -11,11 +12,26 @@ use app::OwlryApp;
|
|||||||
use cli::CliArgs;
|
use cli::CliArgs;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
let default_level = if cfg!(feature = "dev-logging") { "debug" } else { "info" };
|
||||||
|
|
||||||
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default_level))
|
||||||
|
.format_timestamp_millis()
|
||||||
|
.init();
|
||||||
|
|
||||||
let args = CliArgs::parse_args();
|
let args = CliArgs::parse_args();
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
{
|
||||||
|
debug!("┌─────────────────────────────────────────┐");
|
||||||
|
debug!("│ DEV-LOGGING: Verbose output enabled │");
|
||||||
|
debug!("└─────────────────────────────────────────┘");
|
||||||
|
debug!("CLI args: {:?}", args);
|
||||||
|
}
|
||||||
|
|
||||||
info!("Starting Owlry launcher");
|
info!("Starting Owlry launcher");
|
||||||
|
|
||||||
// Diagnostic: log critical environment variables
|
// Diagnostic: log critical environment variables
|
||||||
|
|||||||
214
src/paths.rs
Normal file
214
src/paths.rs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
//! Centralized path handling following XDG Base Directory Specification.
|
||||||
|
//!
|
||||||
|
//! XDG directories used:
|
||||||
|
//! - `$XDG_CONFIG_HOME/owlry/` - User configuration (config.toml, themes/, style.css)
|
||||||
|
//! - `$XDG_DATA_HOME/owlry/` - User data (scripts/, frecency.json)
|
||||||
|
//! - `$XDG_CACHE_HOME/owlry/` - Cache files (future use)
|
||||||
|
//!
|
||||||
|
//! See: https://specifications.freedesktop.org/basedir-spec/latest/
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Application name used in XDG paths
|
||||||
|
const APP_NAME: &str = "owlry";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// XDG Base Directories
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Get XDG config home: `$XDG_CONFIG_HOME` or `~/.config`
|
||||||
|
pub fn config_home() -> Option<PathBuf> {
|
||||||
|
dirs::config_dir()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get XDG data home: `$XDG_DATA_HOME` or `~/.local/share`
|
||||||
|
pub fn data_home() -> Option<PathBuf> {
|
||||||
|
dirs::data_dir()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get XDG cache home: `$XDG_CACHE_HOME` or `~/.cache`
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn cache_home() -> Option<PathBuf> {
|
||||||
|
dirs::cache_dir()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get user home directory
|
||||||
|
pub fn home() -> Option<PathBuf> {
|
||||||
|
dirs::home_dir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Owlry-specific directories
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Owlry config directory: `$XDG_CONFIG_HOME/owlry/`
|
||||||
|
pub fn owlry_config_dir() -> Option<PathBuf> {
|
||||||
|
config_home().map(|p| p.join(APP_NAME))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Owlry data directory: `$XDG_DATA_HOME/owlry/`
|
||||||
|
pub fn owlry_data_dir() -> Option<PathBuf> {
|
||||||
|
data_home().map(|p| p.join(APP_NAME))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Owlry cache directory: `$XDG_CACHE_HOME/owlry/`
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn owlry_cache_dir() -> Option<PathBuf> {
|
||||||
|
cache_home().map(|p| p.join(APP_NAME))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Config files
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Main config file: `$XDG_CONFIG_HOME/owlry/config.toml`
|
||||||
|
pub fn config_file() -> Option<PathBuf> {
|
||||||
|
owlry_config_dir().map(|p| p.join("config.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Custom user stylesheet: `$XDG_CONFIG_HOME/owlry/style.css`
|
||||||
|
pub fn custom_style_file() -> Option<PathBuf> {
|
||||||
|
owlry_config_dir().map(|p| p.join("style.css"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User themes directory: `$XDG_CONFIG_HOME/owlry/themes/`
|
||||||
|
pub fn themes_dir() -> Option<PathBuf> {
|
||||||
|
owlry_config_dir().map(|p| p.join("themes"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get path for a specific theme: `$XDG_CONFIG_HOME/owlry/themes/{name}.css`
|
||||||
|
pub fn theme_file(name: &str) -> Option<PathBuf> {
|
||||||
|
themes_dir().map(|p| p.join(format!("{}.css", name)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Data files
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// User scripts directory: `$XDG_DATA_HOME/owlry/scripts/`
|
||||||
|
pub fn scripts_dir() -> Option<PathBuf> {
|
||||||
|
owlry_data_dir().map(|p| p.join("scripts"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frecency data file: `$XDG_DATA_HOME/owlry/frecency.json`
|
||||||
|
pub fn frecency_file() -> Option<PathBuf> {
|
||||||
|
owlry_data_dir().map(|p| p.join("frecency.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// System directories
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// System data directories for applications (XDG_DATA_DIRS)
|
||||||
|
pub fn system_data_dirs() -> Vec<PathBuf> {
|
||||||
|
let mut dirs = Vec::new();
|
||||||
|
|
||||||
|
// User data directory first
|
||||||
|
if let Some(data) = data_home() {
|
||||||
|
dirs.push(data.join("applications"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// System directories
|
||||||
|
dirs.push(PathBuf::from("/usr/share/applications"));
|
||||||
|
dirs.push(PathBuf::from("/usr/local/share/applications"));
|
||||||
|
|
||||||
|
// Flatpak directories
|
||||||
|
if let Some(data) = data_home() {
|
||||||
|
dirs.push(data.join("flatpak/exports/share/applications"));
|
||||||
|
}
|
||||||
|
dirs.push(PathBuf::from("/var/lib/flatpak/exports/share/applications"));
|
||||||
|
|
||||||
|
dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// External application paths
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// SSH config file: `~/.ssh/config`
|
||||||
|
pub fn ssh_config() -> Option<PathBuf> {
|
||||||
|
home().map(|p| p.join(".ssh").join("config"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Firefox profile directory: `~/.mozilla/firefox/`
|
||||||
|
pub fn firefox_dir() -> Option<PathBuf> {
|
||||||
|
home().map(|p| p.join(".mozilla").join("firefox"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chromium-based browser bookmark paths (using XDG config where browsers support it)
|
||||||
|
pub fn chromium_bookmark_paths() -> Vec<PathBuf> {
|
||||||
|
let config = match config_home() {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![
|
||||||
|
// Google Chrome
|
||||||
|
config.join("google-chrome/Default/Bookmarks"),
|
||||||
|
// Chromium
|
||||||
|
config.join("chromium/Default/Bookmarks"),
|
||||||
|
// Brave
|
||||||
|
config.join("BraveSoftware/Brave-Browser/Default/Bookmarks"),
|
||||||
|
// Microsoft Edge
|
||||||
|
config.join("microsoft-edge/Default/Bookmarks"),
|
||||||
|
// Vivaldi
|
||||||
|
config.join("vivaldi/Default/Bookmarks"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Helper functions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Ensure a directory exists, creating it if necessary
|
||||||
|
pub fn ensure_dir(path: &PathBuf) -> std::io::Result<()> {
|
||||||
|
if !path.exists() {
|
||||||
|
std::fs::create_dir_all(path)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure parent directory of a file exists
|
||||||
|
pub fn ensure_parent_dir(path: &PathBuf) -> std::io::Result<()> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
if !parent.exists() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_paths_are_consistent() {
|
||||||
|
// All owlry paths should be under XDG directories
|
||||||
|
if let (Some(config), Some(data)) = (owlry_config_dir(), owlry_data_dir()) {
|
||||||
|
assert!(config.ends_with("owlry"));
|
||||||
|
assert!(data.ends_with("owlry"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_file_path() {
|
||||||
|
if let Some(path) = config_file() {
|
||||||
|
assert!(path.ends_with("config.toml"));
|
||||||
|
assert!(path.to_string_lossy().contains("owlry"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_frecency_in_data_dir() {
|
||||||
|
if let Some(path) = frecency_file() {
|
||||||
|
assert!(path.ends_with("frecency.json"));
|
||||||
|
// Should be in data dir, not config dir
|
||||||
|
let path_str = path.to_string_lossy();
|
||||||
|
assert!(
|
||||||
|
path_str.contains(".local/share") || path_str.contains("XDG_DATA_HOME"),
|
||||||
|
"frecency should be in data directory"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::{LaunchItem, Provider, ProviderType};
|
use super::{LaunchItem, Provider, ProviderType};
|
||||||
|
use crate::paths;
|
||||||
use freedesktop_desktop_entry::{DesktopEntry, Iter};
|
use freedesktop_desktop_entry::{DesktopEntry, Iter};
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// Clean desktop file field codes from command string.
|
/// Clean desktop file field codes from command string.
|
||||||
/// Removes %f, %F, %u, %U, %d, %D, %n, %N, %i, %c, %k, %v, %m field codes
|
/// Removes %f, %F, %u, %U, %d, %D, %n, %N, %i, %c, %k, %v, %m field codes
|
||||||
@@ -75,25 +75,8 @@ impl ApplicationProvider {
|
|||||||
Self { items: Vec::new() }
|
Self { items: Vec::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_application_dirs() -> Vec<PathBuf> {
|
fn get_application_dirs() -> Vec<std::path::PathBuf> {
|
||||||
let mut dirs = Vec::new();
|
paths::system_data_dirs()
|
||||||
|
|
||||||
// User applications
|
|
||||||
if let Some(data_home) = dirs::data_dir() {
|
|
||||||
dirs.push(data_home.join("applications"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// System applications
|
|
||||||
dirs.push(PathBuf::from("/usr/share/applications"));
|
|
||||||
dirs.push(PathBuf::from("/usr/local/share/applications"));
|
|
||||||
|
|
||||||
// Flatpak applications
|
|
||||||
if let Some(data_home) = dirs::data_dir() {
|
|
||||||
dirs.push(data_home.join("flatpak/exports/share/applications"));
|
|
||||||
}
|
|
||||||
dirs.push(PathBuf::from("/var/lib/flatpak/exports/share/applications"));
|
|
||||||
|
|
||||||
dirs
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::paths;
|
||||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -27,8 +28,8 @@ impl BookmarksProvider {
|
|||||||
fn load_firefox_bookmarks(&mut self) {
|
fn load_firefox_bookmarks(&mut self) {
|
||||||
// Firefox stores bookmarks in places.sqlite
|
// Firefox stores bookmarks in places.sqlite
|
||||||
// The file is locked when Firefox is running, so we read from backup
|
// The file is locked when Firefox is running, so we read from backup
|
||||||
let firefox_dir = match dirs::home_dir() {
|
let firefox_dir = match paths::firefox_dir() {
|
||||||
Some(h) => h.join(".mozilla").join("firefox"),
|
Some(d) => d,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,29 +100,10 @@ impl BookmarksProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_chrome_bookmarks(&mut self) {
|
fn load_chrome_bookmarks(&mut self) {
|
||||||
// Chrome/Chromium bookmarks are in JSON format
|
// Chrome/Chromium bookmarks are in JSON format (XDG config paths)
|
||||||
let home = match dirs::home_dir() {
|
for path in paths::chromium_bookmark_paths() {
|
||||||
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() {
|
if path.exists() {
|
||||||
self.read_chrome_bookmarks(path);
|
self.read_chrome_bookmarks(&path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::paths;
|
||||||
use crate::providers::{LaunchItem, ProviderType};
|
use crate::providers::{LaunchItem, ProviderType};
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -106,7 +107,7 @@ impl FileSearchProvider {
|
|||||||
|
|
||||||
fn search_with_fd(&self, pattern: &str) -> Vec<LaunchItem> {
|
fn search_with_fd(&self, pattern: &str) -> Vec<LaunchItem> {
|
||||||
// fd searches from home directory by default
|
// fd searches from home directory by default
|
||||||
let home = dirs::home_dir().unwrap_or_default();
|
let home = paths::home().unwrap_or_default();
|
||||||
|
|
||||||
let output = match Command::new("fd")
|
let output = match Command::new("fd")
|
||||||
.args([
|
.args([
|
||||||
@@ -132,7 +133,7 @@ impl FileSearchProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn search_with_locate(&self, pattern: &str) -> Vec<LaunchItem> {
|
fn search_with_locate(&self, pattern: &str) -> Vec<LaunchItem> {
|
||||||
let home = dirs::home_dir().unwrap_or_default();
|
let home = paths::home().unwrap_or_default();
|
||||||
|
|
||||||
let output = match Command::new("locate")
|
let output = match Command::new("locate")
|
||||||
.args([
|
.args([
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ use fuzzy_matcher::FuzzyMatcher;
|
|||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
use crate::data::FrecencyStore;
|
use crate::data::FrecencyStore;
|
||||||
|
|
||||||
/// Represents a single searchable/launchable item
|
/// Represents a single searchable/launchable item
|
||||||
@@ -288,12 +291,16 @@ impl ProviderManager {
|
|||||||
frecency: &FrecencyStore,
|
frecency: &FrecencyStore,
|
||||||
frecency_weight: f64,
|
frecency_weight: f64,
|
||||||
) -> Vec<(LaunchItem, i64)> {
|
) -> Vec<(LaunchItem, i64)> {
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Search] query={:?}, max={}, frecency_weight={}", query, max_results, frecency_weight);
|
||||||
|
|
||||||
let mut results: Vec<(LaunchItem, i64)> = Vec::new();
|
let mut results: Vec<(LaunchItem, i64)> = Vec::new();
|
||||||
|
|
||||||
// Check for calculator query (= or calc prefix)
|
// Check for calculator query (= or calc prefix)
|
||||||
if CalculatorProvider::is_calculator_query(query) {
|
if CalculatorProvider::is_calculator_query(query) {
|
||||||
if let Some(calc_result) = self.calculator.evaluate(query) {
|
if let Some(calc_result) = self.calculator.evaluate(query) {
|
||||||
// Calculator results get a high score to appear first
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Search] Calculator result: {}", calc_result.name);
|
||||||
results.push((calc_result, 10000));
|
results.push((calc_result, 10000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,6 +330,8 @@ impl ProviderManager {
|
|||||||
// Check for file search query
|
// Check for file search query
|
||||||
if FileSearchProvider::is_file_query(query) {
|
if FileSearchProvider::is_file_query(query) {
|
||||||
let file_results = self.filesearch.evaluate(query);
|
let file_results = self.filesearch.evaluate(query);
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Search] File search returned {} results", file_results.len());
|
||||||
for (idx, item) in file_results.into_iter().enumerate() {
|
for (idx, item) in file_results.into_iter().enumerate() {
|
||||||
// Score decreases for each result to maintain order
|
// Score decreases for each result to maintain order
|
||||||
results.push((item, 8000 - idx as i64));
|
results.push((item, 8000 - idx as i64));
|
||||||
@@ -387,6 +396,18 @@ impl ProviderManager {
|
|||||||
results.extend(search_results);
|
results.extend(search_results);
|
||||||
results.sort_by(|a, b| b.1.cmp(&a.1));
|
results.sort_by(|a, b| b.1.cmp(&a.1));
|
||||||
results.truncate(max_results);
|
results.truncate(max_results);
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
{
|
||||||
|
debug!("[Search] Returning {} results", results.len());
|
||||||
|
for (i, (item, score)) in results.iter().take(5).enumerate() {
|
||||||
|
debug!("[Search] #{}: {} (score={}, provider={:?})", i + 1, item.name, score, item.provider);
|
||||||
|
}
|
||||||
|
if results.len() > 5 {
|
||||||
|
debug!("[Search] ... and {} more", results.len() - 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
use crate::paths;
|
||||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Custom scripts provider - runs user scripts from ~/.config/owlry/scripts/
|
/// Custom scripts provider - runs user scripts from `$XDG_DATA_HOME/owlry/scripts/`
|
||||||
pub struct ScriptsProvider {
|
pub struct ScriptsProvider {
|
||||||
items: Vec<LaunchItem>,
|
items: Vec<LaunchItem>,
|
||||||
}
|
}
|
||||||
@@ -14,14 +15,10 @@ impl ScriptsProvider {
|
|||||||
Self { items: Vec::new() }
|
Self { items: Vec::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scripts_dir() -> Option<PathBuf> {
|
|
||||||
dirs::config_dir().map(|p| p.join("owlry").join("scripts"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_scripts(&mut self) {
|
fn load_scripts(&mut self) {
|
||||||
self.items.clear();
|
self.items.clear();
|
||||||
|
|
||||||
let scripts_dir = match Self::scripts_dir() {
|
let scripts_dir = match paths::scripts_dir() {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => {
|
None => {
|
||||||
debug!("Could not determine scripts directory");
|
debug!("Could not determine scripts directory");
|
||||||
@@ -32,7 +29,7 @@ impl ScriptsProvider {
|
|||||||
if !scripts_dir.exists() {
|
if !scripts_dir.exists() {
|
||||||
debug!("Scripts directory not found at {:?}", scripts_dir);
|
debug!("Scripts directory not found at {:?}", scripts_dir);
|
||||||
// Create the directory for the user
|
// Create the directory for the user
|
||||||
if let Err(e) = fs::create_dir_all(&scripts_dir) {
|
if let Err(e) = paths::ensure_dir(&scripts_dir) {
|
||||||
warn!("Failed to create scripts directory: {}", e);
|
warn!("Failed to create scripts directory: {}", e);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
use crate::paths;
|
||||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// SSH connections provider - parses ~/.ssh/config
|
/// SSH connections provider - parses ~/.ssh/config
|
||||||
pub struct SshProvider {
|
pub struct SshProvider {
|
||||||
@@ -27,8 +27,8 @@ impl SshProvider {
|
|||||||
self.terminal_command = terminal.to_string();
|
self.terminal_command = terminal.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssh_config_path() -> Option<PathBuf> {
|
fn ssh_config_path() -> Option<std::path::PathBuf> {
|
||||||
dirs::home_dir().map(|p| p.join(".ssh").join("config"))
|
paths::ssh_config()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ssh_config(&mut self) {
|
fn parse_ssh_config(&mut self) {
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ use gtk4::{
|
|||||||
ListBoxRow, Orientation, ScrolledWindow, SelectionMode, ToggleButton,
|
ListBoxRow, Orientation, ScrolledWindow, SelectionMode, ToggleButton,
|
||||||
};
|
};
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -143,7 +147,7 @@ impl MainWindow {
|
|||||||
hints_box.add_css_class("owlry-hints");
|
hints_box.add_css_class("owlry-hints");
|
||||||
|
|
||||||
let hints_label = Label::builder()
|
let hints_label = Label::builder()
|
||||||
.label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close = calc ? web :app :cmd")
|
.label(&Self::build_hints(&cfg.providers))
|
||||||
.halign(gtk4::Align::Center)
|
.halign(gtk4::Align::Center)
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.build();
|
.build();
|
||||||
@@ -252,6 +256,52 @@ impl MainWindow {
|
|||||||
format!("Search {}...", active.join(", "))
|
format!("Search {}...", active.join(", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build dynamic hints based on enabled providers
|
||||||
|
fn build_hints(config: &crate::config::ProvidersConfig) -> String {
|
||||||
|
let mut parts: Vec<String> = vec![
|
||||||
|
"Tab: cycle".to_string(),
|
||||||
|
"↑↓: nav".to_string(),
|
||||||
|
"Enter: launch".to_string(),
|
||||||
|
"Esc: close".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add trigger hints for enabled dynamic providers
|
||||||
|
if config.calculator {
|
||||||
|
parts.push("= calc".to_string());
|
||||||
|
}
|
||||||
|
if config.websearch {
|
||||||
|
parts.push("? web".to_string());
|
||||||
|
}
|
||||||
|
if config.files {
|
||||||
|
parts.push("/ files".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add prefix hints for static providers
|
||||||
|
let mut prefixes = Vec::new();
|
||||||
|
if config.system {
|
||||||
|
prefixes.push(":sys");
|
||||||
|
}
|
||||||
|
if config.emoji {
|
||||||
|
prefixes.push(":emoji");
|
||||||
|
}
|
||||||
|
if config.ssh {
|
||||||
|
prefixes.push(":ssh");
|
||||||
|
}
|
||||||
|
if config.clipboard {
|
||||||
|
prefixes.push(":clip");
|
||||||
|
}
|
||||||
|
if config.bookmarks {
|
||||||
|
prefixes.push(":bm");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show first few prefixes to avoid overflow
|
||||||
|
if !prefixes.is_empty() {
|
||||||
|
parts.push(prefixes[..prefixes.len().min(4)].join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
/// Scroll the given row into view within the scrolled window
|
/// Scroll the given row into view within the scrolled window
|
||||||
fn scroll_to_row(scrolled: &ScrolledWindow, results_list: &ListBox, row: &ListBoxRow) {
|
fn scroll_to_row(scrolled: &ScrolledWindow, results_list: &ListBox, row: &ListBoxRow) {
|
||||||
let vadj = scrolled.vadjustment();
|
let vadj = scrolled.vadjustment();
|
||||||
@@ -298,6 +348,9 @@ impl MainWindow {
|
|||||||
display_name: &str,
|
display_name: &str,
|
||||||
is_active: bool,
|
is_active: bool,
|
||||||
) {
|
) {
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[UI] Entering submenu for service: {} (active={})", unit_name, is_active);
|
||||||
|
|
||||||
let actions = UuctlProvider::actions_for_service(unit_name, display_name, is_active);
|
let actions = UuctlProvider::actions_for_service(unit_name, display_name, is_active);
|
||||||
|
|
||||||
// Save current state
|
// Save current state
|
||||||
@@ -340,7 +393,11 @@ impl MainWindow {
|
|||||||
hints_label: &Label,
|
hints_label: &Label,
|
||||||
search_entry: &Entry,
|
search_entry: &Entry,
|
||||||
filter: &Rc<RefCell<ProviderFilter>>,
|
filter: &Rc<RefCell<ProviderFilter>>,
|
||||||
|
config: &Rc<RefCell<Config>>,
|
||||||
) {
|
) {
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[UI] Exiting submenu");
|
||||||
|
|
||||||
let saved_search = {
|
let saved_search = {
|
||||||
let mut state = submenu_state.borrow_mut();
|
let mut state = submenu_state.borrow_mut();
|
||||||
state.active = false;
|
state.active = false;
|
||||||
@@ -350,7 +407,7 @@ impl MainWindow {
|
|||||||
|
|
||||||
// Restore UI
|
// Restore UI
|
||||||
mode_label.set_label(filter.borrow().mode_display_name());
|
mode_label.set_label(filter.borrow().mode_display_name());
|
||||||
hints_label.set_label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close = calc ? web :app :cmd");
|
hints_label.set_label(&Self::build_hints(&config.borrow().providers));
|
||||||
search_entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow())));
|
search_entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow())));
|
||||||
search_entry.set_text(&saved_search);
|
search_entry.set_text(&saved_search);
|
||||||
|
|
||||||
@@ -553,7 +610,7 @@ impl MainWindow {
|
|||||||
let scrolled = self.scrolled.clone();
|
let scrolled = self.scrolled.clone();
|
||||||
let search_entry = self.search_entry.clone();
|
let search_entry = self.search_entry.clone();
|
||||||
let _current_results = self.current_results.clone();
|
let _current_results = self.current_results.clone();
|
||||||
let _config = self.config.clone();
|
let config = self.config.clone();
|
||||||
let filter = self.filter.clone();
|
let filter = self.filter.clone();
|
||||||
let filter_buttons = self.filter_buttons.clone();
|
let filter_buttons = self.filter_buttons.clone();
|
||||||
let mode_label = self.mode_label.clone();
|
let mode_label = self.mode_label.clone();
|
||||||
@@ -564,6 +621,9 @@ impl MainWindow {
|
|||||||
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
|
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
|
||||||
let shift = modifiers.contains(gtk4::gdk::ModifierType::SHIFT_MASK);
|
let shift = modifiers.contains(gtk4::gdk::ModifierType::SHIFT_MASK);
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[UI] Key pressed: {:?} (ctrl={}, shift={})", key, ctrl, shift);
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
Key::Escape => {
|
Key::Escape => {
|
||||||
// If in submenu, exit submenu first
|
// If in submenu, exit submenu first
|
||||||
@@ -574,6 +634,7 @@ impl MainWindow {
|
|||||||
&hints_label,
|
&hints_label,
|
||||||
&search_entry,
|
&search_entry,
|
||||||
&filter,
|
&filter,
|
||||||
|
&config,
|
||||||
);
|
);
|
||||||
gtk4::glib::Propagation::Stop
|
gtk4::glib::Propagation::Stop
|
||||||
} else {
|
} else {
|
||||||
@@ -590,6 +651,7 @@ impl MainWindow {
|
|||||||
&hints_label,
|
&hints_label,
|
||||||
&search_entry,
|
&search_entry,
|
||||||
&filter,
|
&filter,
|
||||||
|
&config,
|
||||||
);
|
);
|
||||||
gtk4::glib::Propagation::Stop
|
gtk4::glib::Propagation::Stop
|
||||||
} else {
|
} else {
|
||||||
@@ -833,10 +895,15 @@ impl MainWindow {
|
|||||||
// Record this launch for frecency tracking
|
// Record this launch for frecency tracking
|
||||||
if config.providers.frecency {
|
if config.providers.frecency {
|
||||||
frecency.borrow_mut().record_launch(&item.id);
|
frecency.borrow_mut().record_launch(&item.id);
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[UI] Recorded frecency launch for: {}", item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Launching: {} ({})", item.name, item.command);
|
info!("Launching: {} ({})", item.name, item.command);
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[UI] Launch details: terminal={}, provider={:?}", item.terminal, item.provider);
|
||||||
|
|
||||||
let cmd = if item.terminal {
|
let cmd = if item.terminal {
|
||||||
format!("{} -e {}", config.general.terminal_command, item.command)
|
format!("{} -e {}", config.general.terminal_command, item.command)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user