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>
This commit is contained in:
2025-12-28 15:35:29 +01:00
parent 884f871d7f
commit 254af3f0b2
5 changed files with 179 additions and 9 deletions

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