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:
@@ -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'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user