4 Commits

Author SHA1 Message Date
34145d5fbe feat: add startup diagnostics for environment issues
- Log HOME, PATH, XDG_DATA_HOME at startup
- Warn when critical env vars are missing
- Log item count per provider after refresh

This helps diagnose why items may not load when launched
from window manager keybinds vs terminal.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 15:52:08 +01:00
e94eb2050c chore: bump version to 0.1.2 2025-12-28 15:35:35 +01:00
254af3f0b2 feat: add uwsm/hyprland launch wrapper and fix CLI args
- Add launch_wrapper config option with auto-detection for uwsm and
  hyprland sessions, ensuring apps launch with proper session management
- Fix CLI argument parsing by preventing GTK from intercepting
  clap-parsed args (--mode, --providers)
- Improve desktop file Exec field parsing to properly handle quoted
  arguments and FreeDesktop field codes (%u, %F, etc.)
- Add unit tests for Exec field parsing

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 15:35:29 +01:00
884f871d7f docs: add AUR installation instructions
- Add AUR badge to shields
- Add Arch Linux (AUR) as recommended install method
- Reorganize build-from-source section

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 15:05:30 +01:00
9 changed files with 215 additions and 14 deletions

2
Cargo.lock generated
View File

@@ -859,7 +859,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "owlry"
version = "0.1.1"
version = "0.1.3"
dependencies = [
"clap",
"dirs",

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry"
version = "0.1.1"
version = "0.1.3"
edition = "2024"
rust-version = "1.90"
description = "A lightweight, owl-themed application launcher for Wayland"

View File

@@ -1,5 +1,6 @@
# Owlry
[![AUR](https://img.shields.io/aur/version/owlry?logo=archlinux&label=AUR)](https://aur.archlinux.org/packages/owlry)
[![Rust](https://img.shields.io/badge/rust-1.90%2B-orange.svg)](https://www.rust-lang.org/)
[![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg)](LICENSE)
[![GTK4](https://img.shields.io/badge/GTK-4.12-green.svg)](https://gtk.org/)
@@ -19,7 +20,19 @@ A lightweight, owl-themed application launcher for Wayland, built with GTK4 and
## Installation
### Dependencies
### Arch Linux (AUR)
```bash
# Using yay
yay -S owlry
# Using paru
paru -S owlry
```
### Build from source
#### Dependencies
```bash
# Arch Linux
@@ -32,7 +45,7 @@ sudo apt install libgtk-4-dev libgtk4-layer-shell-dev
sudo dnf install gtk4-devel gtk4-layer-shell-devel
```
### Build from source
#### Build
Requires Rust 1.90 or later.
@@ -92,6 +105,7 @@ Configuration file: `~/.config/owlry/config.toml`
show_icons = true
max_results = 10
# terminal_command = "kitty" # Auto-detected if not set
# launch_wrapper = "uwsm app --" # Auto-detected for uwsm/hyprland sessions
[appearance]
width = 600
@@ -118,12 +132,25 @@ uuctl = true
| `show_icons` | `true` |
| `max_results` | `10` |
| `terminal_command` | Auto-detected ($TERMINAL → xdg-terminal-exec → kitty/alacritty/etc) |
| `launch_wrapper` | Auto-detected (uwsm → hyprctl → none) |
| `width` | `600` |
| `height` | `400` |
| `font_size` | `14` |
| `border_radius` | `12` |
| `theme` | None (GTK default) |
### Launch Wrapper
When running in uwsm-managed or Hyprland sessions, owlry auto-detects and uses the appropriate launch wrapper for proper session integration:
| Session | Wrapper | Purpose |
|---------|---------|---------|
| uwsm | `uwsm app --` | Proper systemd scope and session management |
| Hyprland | `hyprctl dispatch exec --` | Native Hyprland window management |
| Other | None (direct `sh -c`) | Standard shell execution |
You can override this with `launch_wrapper` in config, or set to empty string `""` to disable.
## Theming
### GTK Theme (Default)

View File

@@ -30,7 +30,9 @@ impl OwlryApp {
}
pub fn run(&self) -> i32 {
self.app.run().into()
// Use empty args since clap already parsed our CLI arguments.
// This prevents GTK from trying to parse --mode, --providers, etc.
self.app.run_with_args(&[] as &[&str]).into()
}
fn on_activate(app: &Application, args: &CliArgs) {

View File

@@ -15,6 +15,11 @@ pub struct GeneralConfig {
pub show_icons: bool,
pub max_results: usize,
pub terminal_command: String,
/// Launch wrapper command for app execution.
/// Examples: "uwsm app --", "hyprctl dispatch exec --", "systemd-run --user --"
/// If None or empty, launches directly via sh -c
#[serde(default)]
pub launch_wrapper: Option<String>,
}
/// User-customizable theme colors
@@ -56,6 +61,32 @@ pub struct ProvidersConfig {
pub uuctl: bool,
}
/// Detect the best launch wrapper for the current session
/// Checks for uwsm (Universal Wayland Session Manager) and hyprland
fn detect_launch_wrapper() -> Option<String> {
// Check if running under uwsm (has UWSM_FINALIZE_VARNAMES or similar uwsm env vars)
if std::env::var("UWSM_FINALIZE_VARNAMES").is_ok()
|| std::env::var("__UWSM_SELECT_TAG").is_ok()
{
if command_exists("uwsm") {
debug!("Detected uwsm session, using 'uwsm app --' wrapper");
return Some("uwsm app --".to_string());
}
}
// Check if running under Hyprland
if std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() {
if command_exists("hyprctl") {
debug!("Detected Hyprland session, using 'hyprctl dispatch exec --' wrapper");
return Some("hyprctl dispatch exec --".to_string());
}
}
// No wrapper needed for other environments
debug!("No launch wrapper detected, using direct execution");
None
}
/// Detect the best available terminal emulator
/// Fallback chain:
/// 1. $TERMINAL env var (user's explicit preference)
@@ -127,6 +158,7 @@ impl Default for Config {
show_icons: true,
max_results: 10,
terminal_command: terminal,
launch_wrapper: detect_launch_wrapper(),
},
appearance: AppearanceConfig {
width: 600,

View File

@@ -8,7 +8,7 @@ mod ui;
use app::OwlryApp;
use cli::CliArgs;
use log::info;
use log::{info, warn};
fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
@@ -17,6 +17,18 @@ fn main() {
info!("Starting Owlry launcher");
// Diagnostic: log critical environment variables
let home = std::env::var("HOME").unwrap_or_else(|_| "<not set>".to_string());
let path = std::env::var("PATH").unwrap_or_else(|_| "<not set>".to_string());
let xdg_data = std::env::var("XDG_DATA_HOME").unwrap_or_else(|_| "<not set>".to_string());
info!("HOME={}", home);
info!("PATH={}", path);
info!("XDG_DATA_HOME={}", xdg_data);
if home == "<not set>" || path == "<not set>" {
warn!("Critical environment variables missing! Items may not load correctly.");
}
let app = OwlryApp::new(args);
std::process::exit(app.run());
}

View File

@@ -3,6 +3,69 @@ 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
/// while preserving quoted arguments and %% (literal percent).
/// See: https://specifications.freedesktop.org/desktop-entry-spec/latest/exec-variables.html
fn clean_desktop_exec_field(cmd: &str) -> String {
let mut result = String::with_capacity(cmd.len());
let mut chars = cmd.chars().peekable();
let mut in_single_quote = false;
let mut in_double_quote = false;
while let Some(c) = chars.next() {
match c {
'\'' if !in_double_quote => {
in_single_quote = !in_single_quote;
result.push(c);
}
'"' if !in_single_quote => {
in_double_quote = !in_double_quote;
result.push(c);
}
'%' if !in_single_quote => {
// Check the next character for field code
if let Some(&next) = chars.peek() {
match next {
// Standard field codes to remove (with following space if present)
'f' | 'F' | 'u' | 'U' | 'd' | 'D' | 'n' | 'N' | 'i' | 'c' | 'k' | 'v'
| 'm' => {
chars.next(); // consume the field code letter
// Skip trailing whitespace after the field code
while chars.peek() == Some(&' ') {
chars.next();
}
}
// %% is escaped percent, output single %
'%' => {
chars.next();
result.push('%');
}
// Unknown % sequence, keep as-is
_ => {
result.push('%');
}
}
} else {
// % at end of string, keep it
result.push('%');
}
}
_ => {
result.push(c);
}
}
}
// Clean up any double spaces that may have resulted from removing field codes
let mut cleaned = result.trim().to_string();
while cleaned.contains(" ") {
cleaned = cleaned.replace(" ", " ");
}
cleaned
}
pub struct ApplicationProvider {
items: Vec<LaunchItem>,
}
@@ -85,13 +148,7 @@ impl Provider for ApplicationProvider {
};
let run_cmd = match desktop_entry.exec() {
Some(e) => {
// Clean up run command (remove %u, %U, %f, %F, etc.)
e.split_whitespace()
.filter(|s| !s.starts_with('%'))
.collect::<Vec<_>>()
.join(" ")
}
Some(e) => clean_desktop_exec_field(e),
None => continue,
};
@@ -118,3 +175,49 @@ impl Provider for ApplicationProvider {
&self.items
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clean_desktop_exec_simple() {
assert_eq!(clean_desktop_exec_field("firefox"), "firefox");
assert_eq!(clean_desktop_exec_field("firefox %u"), "firefox");
assert_eq!(clean_desktop_exec_field("code %F"), "code");
}
#[test]
fn test_clean_desktop_exec_multiple_placeholders() {
assert_eq!(clean_desktop_exec_field("app %f %u %U"), "app");
assert_eq!(clean_desktop_exec_field("app --flag %u --other"), "app --flag --other");
}
#[test]
fn test_clean_desktop_exec_preserves_quotes() {
// Double quotes preserve spacing but field codes are still processed
assert_eq!(
clean_desktop_exec_field(r#"bash -c "echo hello""#),
r#"bash -c "echo hello""#
);
// Field codes in double quotes are stripped (per FreeDesktop spec: undefined behavior,
// but practical implementations strip them)
assert_eq!(
clean_desktop_exec_field(r#"bash -c "test %u value""#),
r#"bash -c "test value""#
);
}
#[test]
fn test_clean_desktop_exec_escaped_percent() {
assert_eq!(clean_desktop_exec_field("echo 100%%"), "echo 100%");
}
#[test]
fn test_clean_desktop_exec_single_quotes() {
assert_eq!(
clean_desktop_exec_field("bash -c 'echo %u'"),
"bash -c 'echo %u'"
);
}
}

View File

@@ -10,6 +10,7 @@ pub use uuctl::UuctlProvider;
use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use log::info;
/// Represents a single searchable/launchable item
#[derive(Debug, Clone)]
@@ -110,6 +111,11 @@ impl ProviderManager {
pub fn refresh_all(&mut self) {
for provider in &mut self.providers {
provider.refresh();
info!(
"Provider '{}' loaded {} items",
provider.name(),
provider.items().len()
);
}
}

View File

@@ -777,7 +777,26 @@ impl MainWindow {
item.command.clone()
};
if let Err(e) = Command::new("sh").arg("-c").arg(&cmd).spawn() {
// Use launch wrapper if configured (uwsm, hyprctl, etc.)
let result = match &config.general.launch_wrapper {
Some(wrapper) if !wrapper.is_empty() => {
info!("Using launch wrapper: {}", wrapper);
// Split wrapper into command and args (e.g., "uwsm app --" -> ["uwsm", "app", "--"])
let mut wrapper_parts: Vec<&str> = wrapper.split_whitespace().collect();
if wrapper_parts.is_empty() {
Command::new("sh").arg("-c").arg(&cmd).spawn()
} else {
let wrapper_cmd = wrapper_parts.remove(0);
Command::new(wrapper_cmd)
.args(&wrapper_parts)
.arg(&cmd)
.spawn()
}
}
_ => Command::new("sh").arg("-c").arg(&cmd).spawn(),
};
if let Err(e) = result {
log::error!("Failed to launch '{}': {}", item.name, e);
}
}