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
|
||||
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
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Enable verbose debug logging (for development/testing builds)
|
||||
dev-logging = []
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
@@ -65,3 +70,9 @@ opt-level = "z" # Optimize for size
|
||||
[profile.dev]
|
||||
opt-level = 0
|
||||
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::data::FrecencyStore;
|
||||
use crate::filter::ProviderFilter;
|
||||
use crate::paths;
|
||||
use crate::providers::ProviderManager;
|
||||
use crate::theme;
|
||||
use crate::ui::MainWindow;
|
||||
@@ -98,10 +99,8 @@ impl OwlryApp {
|
||||
debug!("Loaded built-in owl theme");
|
||||
}
|
||||
_ => {
|
||||
// Check for custom theme in ~/.config/owlry/themes/{name}.css
|
||||
if let Some(theme_path) = dirs::config_dir()
|
||||
.map(|p| p.join("owlry").join("themes").join(format!("{}.css", theme_name)))
|
||||
{
|
||||
// Check for custom theme in $XDG_CONFIG_HOME/owlry/themes/{name}.css
|
||||
if let Some(theme_path) = paths::theme_file(theme_name) {
|
||||
if theme_path.exists() {
|
||||
theme_provider.load_from_path(&theme_path);
|
||||
debug!("Loaded custom theme from {:?}", theme_path);
|
||||
@@ -119,7 +118,7 @@ impl OwlryApp {
|
||||
}
|
||||
|
||||
// 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() {
|
||||
let custom_provider = CssProvider::new();
|
||||
custom_provider.load_from_path(&custom_path);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use log::{debug, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use log::{info, warn, debug};
|
||||
|
||||
use crate::paths;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
@@ -250,7 +252,7 @@ impl Default for Config {
|
||||
|
||||
impl Config {
|
||||
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 {
|
||||
@@ -289,9 +291,7 @@ impl Config {
|
||||
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = Self::config_path().ok_or("Could not determine config path")?;
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
paths::ensure_parent_dir(&path)?;
|
||||
|
||||
let content = toml::to_string_pretty(self)?;
|
||||
std::fs::write(&path, content)?;
|
||||
|
||||
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::paths;
|
||||
|
||||
/// A single frecency entry tracking launch count and recency
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FrecencyEntry {
|
||||
@@ -56,10 +58,7 @@ impl FrecencyStore {
|
||||
|
||||
/// 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")
|
||||
paths::frecency_file().unwrap_or_else(|| PathBuf::from("frecency.json"))
|
||||
}
|
||||
|
||||
/// Load frecency data from a file
|
||||
@@ -85,10 +84,7 @@ impl FrecencyStore {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if let Some(parent) = self.path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
paths::ensure_parent_dir(&self.path)?;
|
||||
|
||||
let content = serde_json::to_string_pretty(&self.data)?;
|
||||
std::fs::write(&self.path, content)?;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
use log::debug;
|
||||
|
||||
use crate::config::ProvidersConfig;
|
||||
use crate::providers::ProviderType;
|
||||
|
||||
@@ -69,10 +72,15 @@ impl ProviderFilter {
|
||||
set
|
||||
};
|
||||
|
||||
Self {
|
||||
let filter = Self {
|
||||
enabled,
|
||||
active_prefix: None,
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] Created with enabled providers: {:?}", filter.enabled);
|
||||
|
||||
filter
|
||||
}
|
||||
|
||||
/// Default filter: apps only
|
||||
@@ -92,8 +100,12 @@ impl ProviderFilter {
|
||||
if self.enabled.is_empty() {
|
||||
self.enabled.insert(ProviderType::Application);
|
||||
}
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] Toggled OFF {:?}, enabled: {:?}", provider, self.enabled);
|
||||
} else {
|
||||
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.)
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -176,6 +192,8 @@ impl ProviderFilter {
|
||||
|
||||
for (prefix_str, provider) in prefixes {
|
||||
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest);
|
||||
return ParsedQuery {
|
||||
prefix: Some(provider),
|
||||
query: rest.to_string(),
|
||||
@@ -214,6 +232,8 @@ impl ProviderFilter {
|
||||
|
||||
for (prefix_str, provider) in partial_prefixes {
|
||||
if trimmed == prefix_str {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider);
|
||||
return ParsedQuery {
|
||||
prefix: Some(provider),
|
||||
query: String::new(),
|
||||
@@ -221,10 +241,15 @@ impl ProviderFilter {
|
||||
}
|
||||
}
|
||||
|
||||
ParsedQuery {
|
||||
let result = ParsedQuery {
|
||||
prefix: None,
|
||||
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)
|
||||
|
||||
18
src/main.rs
18
src/main.rs
@@ -3,6 +3,7 @@ mod cli;
|
||||
mod config;
|
||||
mod data;
|
||||
mod filter;
|
||||
mod paths;
|
||||
mod providers;
|
||||
mod theme;
|
||||
mod ui;
|
||||
@@ -11,11 +12,26 @@ use app::OwlryApp;
|
||||
use cli::CliArgs;
|
||||
use log::{info, warn};
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
use log::debug;
|
||||
|
||||
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();
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
{
|
||||
debug!("┌─────────────────────────────────────────┐");
|
||||
debug!("│ DEV-LOGGING: Verbose output enabled │");
|
||||
debug!("└─────────────────────────────────────────┘");
|
||||
debug!("CLI args: {:?}", args);
|
||||
}
|
||||
|
||||
info!("Starting Owlry launcher");
|
||||
|
||||
// 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 crate::paths;
|
||||
use freedesktop_desktop_entry::{DesktopEntry, Iter};
|
||||
use log::{debug, warn};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Clean desktop file field codes from command string.
|
||||
/// 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() }
|
||||
}
|
||||
|
||||
fn get_application_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = Vec::new();
|
||||
|
||||
// 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
|
||||
fn get_application_dirs() -> Vec<std::path::PathBuf> {
|
||||
paths::system_data_dirs()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::paths;
|
||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||
use log::{debug, warn};
|
||||
use serde::Deserialize;
|
||||
@@ -27,8 +28,8 @@ impl BookmarksProvider {
|
||||
fn load_firefox_bookmarks(&mut self) {
|
||||
// Firefox stores bookmarks in places.sqlite
|
||||
// The file is locked when Firefox is running, so we read from backup
|
||||
let firefox_dir = match dirs::home_dir() {
|
||||
Some(h) => h.join(".mozilla").join("firefox"),
|
||||
let firefox_dir = match paths::firefox_dir() {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
|
||||
@@ -99,29 +100,10 @@ impl BookmarksProvider {
|
||||
}
|
||||
|
||||
fn load_chrome_bookmarks(&mut self) {
|
||||
// Chrome/Chromium bookmarks are in JSON format
|
||||
let home = match dirs::home_dir() {
|
||||
Some(h) => h,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Try multiple browser paths
|
||||
let bookmark_paths = [
|
||||
// Chrome
|
||||
home.join(".config/google-chrome/Default/Bookmarks"),
|
||||
// Chromium
|
||||
home.join(".config/chromium/Default/Bookmarks"),
|
||||
// Brave
|
||||
home.join(".config/BraveSoftware/Brave-Browser/Default/Bookmarks"),
|
||||
// Edge
|
||||
home.join(".config/microsoft-edge/Default/Bookmarks"),
|
||||
// Vivaldi
|
||||
home.join(".config/vivaldi/Default/Bookmarks"),
|
||||
];
|
||||
|
||||
for path in &bookmark_paths {
|
||||
// Chrome/Chromium bookmarks are in JSON format (XDG config paths)
|
||||
for path in paths::chromium_bookmark_paths() {
|
||||
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 log::{debug, warn};
|
||||
use std::process::Command;
|
||||
@@ -106,7 +107,7 @@ impl FileSearchProvider {
|
||||
|
||||
fn search_with_fd(&self, pattern: &str) -> Vec<LaunchItem> {
|
||||
// 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")
|
||||
.args([
|
||||
@@ -132,7 +133,7 @@ impl FileSearchProvider {
|
||||
}
|
||||
|
||||
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")
|
||||
.args([
|
||||
|
||||
@@ -30,6 +30,9 @@ use fuzzy_matcher::FuzzyMatcher;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
use log::debug;
|
||||
|
||||
use crate::data::FrecencyStore;
|
||||
|
||||
/// Represents a single searchable/launchable item
|
||||
@@ -288,12 +291,16 @@ impl ProviderManager {
|
||||
frecency: &FrecencyStore,
|
||||
frecency_weight: f64,
|
||||
) -> 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();
|
||||
|
||||
// Check for calculator query (= or calc prefix)
|
||||
if CalculatorProvider::is_calculator_query(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));
|
||||
}
|
||||
}
|
||||
@@ -323,6 +330,8 @@ impl ProviderManager {
|
||||
// Check for file search query
|
||||
if FileSearchProvider::is_file_query(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() {
|
||||
// Score decreases for each result to maintain order
|
||||
results.push((item, 8000 - idx as i64));
|
||||
@@ -387,6 +396,18 @@ impl ProviderManager {
|
||||
results.extend(search_results);
|
||||
results.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::paths;
|
||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||
use log::{debug, warn};
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Custom scripts provider - runs user scripts from ~/.config/owlry/scripts/
|
||||
/// Custom scripts provider - runs user scripts from `$XDG_DATA_HOME/owlry/scripts/`
|
||||
pub struct ScriptsProvider {
|
||||
items: Vec<LaunchItem>,
|
||||
}
|
||||
@@ -14,14 +15,10 @@ impl ScriptsProvider {
|
||||
Self { items: Vec::new() }
|
||||
}
|
||||
|
||||
fn scripts_dir() -> Option<PathBuf> {
|
||||
dirs::config_dir().map(|p| p.join("owlry").join("scripts"))
|
||||
}
|
||||
|
||||
fn load_scripts(&mut self) {
|
||||
self.items.clear();
|
||||
|
||||
let scripts_dir = match Self::scripts_dir() {
|
||||
let scripts_dir = match paths::scripts_dir() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
debug!("Could not determine scripts directory");
|
||||
@@ -32,7 +29,7 @@ impl ScriptsProvider {
|
||||
if !scripts_dir.exists() {
|
||||
debug!("Scripts directory not found at {:?}", scripts_dir);
|
||||
// Create the directory for the user
|
||||
if let Err(e) = fs::create_dir_all(&scripts_dir) {
|
||||
if let Err(e) = paths::ensure_dir(&scripts_dir) {
|
||||
warn!("Failed to create scripts directory: {}", e);
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::paths;
|
||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||
use log::{debug, warn};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// SSH connections provider - parses ~/.ssh/config
|
||||
pub struct SshProvider {
|
||||
@@ -27,8 +27,8 @@ impl SshProvider {
|
||||
self.terminal_command = terminal.to_string();
|
||||
}
|
||||
|
||||
fn ssh_config_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|p| p.join(".ssh").join("config"))
|
||||
fn ssh_config_path() -> Option<std::path::PathBuf> {
|
||||
paths::ssh_config()
|
||||
}
|
||||
|
||||
fn parse_ssh_config(&mut self) {
|
||||
|
||||
@@ -10,6 +10,10 @@ use gtk4::{
|
||||
ListBoxRow, Orientation, ScrolledWindow, SelectionMode, ToggleButton,
|
||||
};
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
use log::debug;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
@@ -143,7 +147,7 @@ impl MainWindow {
|
||||
hints_box.add_css_class("owlry-hints");
|
||||
|
||||
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)
|
||||
.hexpand(true)
|
||||
.build();
|
||||
@@ -252,6 +256,52 @@ impl MainWindow {
|
||||
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
|
||||
fn scroll_to_row(scrolled: &ScrolledWindow, results_list: &ListBox, row: &ListBoxRow) {
|
||||
let vadj = scrolled.vadjustment();
|
||||
@@ -298,6 +348,9 @@ impl MainWindow {
|
||||
display_name: &str,
|
||||
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);
|
||||
|
||||
// Save current state
|
||||
@@ -340,7 +393,11 @@ impl MainWindow {
|
||||
hints_label: &Label,
|
||||
search_entry: &Entry,
|
||||
filter: &Rc<RefCell<ProviderFilter>>,
|
||||
config: &Rc<RefCell<Config>>,
|
||||
) {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[UI] Exiting submenu");
|
||||
|
||||
let saved_search = {
|
||||
let mut state = submenu_state.borrow_mut();
|
||||
state.active = false;
|
||||
@@ -350,7 +407,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 = 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_text(&saved_search);
|
||||
|
||||
@@ -553,7 +610,7 @@ impl MainWindow {
|
||||
let scrolled = self.scrolled.clone();
|
||||
let search_entry = self.search_entry.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_buttons = self.filter_buttons.clone();
|
||||
let mode_label = self.mode_label.clone();
|
||||
@@ -564,6 +621,9 @@ impl MainWindow {
|
||||
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_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 {
|
||||
Key::Escape => {
|
||||
// If in submenu, exit submenu first
|
||||
@@ -574,6 +634,7 @@ impl MainWindow {
|
||||
&hints_label,
|
||||
&search_entry,
|
||||
&filter,
|
||||
&config,
|
||||
);
|
||||
gtk4::glib::Propagation::Stop
|
||||
} else {
|
||||
@@ -590,6 +651,7 @@ impl MainWindow {
|
||||
&hints_label,
|
||||
&search_entry,
|
||||
&filter,
|
||||
&config,
|
||||
);
|
||||
gtk4::glib::Propagation::Stop
|
||||
} else {
|
||||
@@ -833,10 +895,15 @@ impl MainWindow {
|
||||
// Record this launch for frecency tracking
|
||||
if config.providers.frecency {
|
||||
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);
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[UI] Launch details: terminal={}, provider={:?}", item.terminal, item.provider);
|
||||
|
||||
let cmd = if item.terminal {
|
||||
format!("{} -e {}", config.general.terminal_command, item.command)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user