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:
2025-12-29 16:46:14 +01:00
parent 3f7a8950eb
commit 0eccdc5883
16 changed files with 396 additions and 115 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target
CLAUDE.md

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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)?;

View File

@@ -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)?;

View File

@@ -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)

View File

@@ -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
View 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"
);
}
}
}

View File

@@ -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()
}
}

View File

@@ -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);
}
}
}

View File

@@ -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([

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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 {