diff --git a/README.md b/README.md index 2e7cfec..d830fda 100644 --- a/README.md +++ b/README.md @@ -208,8 +208,8 @@ cp /usr/share/doc/owlry/config.example.toml ~/.config/owlry/config.toml show_icons = true max_results = 10 tabs = ["app", "cmd", "uuctl"] -# terminal_command = "kitty" # Auto-detected -# launch_wrapper = "uwsm app --" # Auto-detected +# terminal_command = "kitty" # Auto-detected +# use_uwsm = false # Enable for systemd session integration [appearance] width = 850 diff --git a/crates/owlry/src/config/mod.rs b/crates/owlry/src/config/mod.rs index 83375b0..dc6a57f 100644 --- a/crates/owlry/src/config/mod.rs +++ b/crates/owlry/src/config/mod.rs @@ -27,11 +27,12 @@ pub struct GeneralConfig { /// Terminal command (auto-detected if not specified) #[serde(default)] pub terminal_command: Option, - /// Launch wrapper command for app execution. - /// Examples: "uwsm app --", "hyprctl dispatch exec --", "systemd-run --user --" - /// If None or empty, launches directly via sh -c + /// Enable uwsm (Universal Wayland Session Manager) for launching apps. + /// When enabled, desktop files are launched via `uwsm app -- ` + /// which starts apps in a proper systemd user session. + /// When disabled (default), apps are launched via `gio launch`. #[serde(default)] - pub launch_wrapper: Option, + pub use_uwsm: bool, /// Provider tabs shown in the header bar. /// Valid values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web #[serde(default = "default_tabs")] @@ -44,7 +45,7 @@ impl Default for GeneralConfig { show_icons: true, max_results: 100, terminal_command: None, - launch_wrapper: None, + use_uwsm: false, tabs: default_tabs(), } } @@ -396,28 +397,6 @@ fn default_pomodoro_break() -> u32 { 5 } -/// Detect the best launch wrapper for the current session -/// Checks for uwsm (Universal Wayland Session Manager) and hyprland -fn detect_launch_wrapper() -> Option { - // 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()) - && 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() - && 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: @@ -578,11 +557,6 @@ impl Config { } } - // Auto-detect launch wrapper if not configured - if config.general.launch_wrapper.is_none() { - config.general.launch_wrapper = detect_launch_wrapper(); - } - Ok(config) } diff --git a/crates/owlry/src/ui/main_window.rs b/crates/owlry/src/ui/main_window.rs index 7d3aba1..e41380a 100644 --- a/crates/owlry/src/ui/main_window.rs +++ b/crates/owlry/src/ui/main_window.rs @@ -1341,14 +1341,13 @@ impl MainWindow { } } - /// Launch a .desktop file using gio (GLib's desktop entry launcher). + /// Launch a .desktop file. /// - /// gio is always available as it's part of glib2, which is a hard dependency - /// of GTK4. It handles D-Bus activation, field codes, Terminal flag, etc. - /// per the freedesktop Desktop Entry specification. + /// When `use_uwsm` is enabled in config, launches via `uwsm app -- ` + /// which starts the app in a proper systemd user session. /// - /// Optionally wraps with a session manager (uwsm, hyprctl) for proper - /// process tracking in Wayland compositors. + /// Otherwise, uses `gio launch` which is always available (part of glib2/GTK4) + /// and handles D-Bus activation, field codes, Terminal flag, etc. fn launch_desktop_file(desktop_path: &str, config: &Config) -> std::io::Result { use std::path::Path; @@ -1360,25 +1359,32 @@ impl MainWindow { return Err(std::io::Error::new(std::io::ErrorKind::NotFound, msg)); } - match &config.general.launch_wrapper { - // With wrapper: wrapper manages the process, gio handles the .desktop file - Some(wrapper) if !wrapper.is_empty() => { - info!("Launching via {} + gio launch: {}", wrapper, desktop_path); - let mut parts: Vec<&str> = wrapper.split_whitespace().collect(); - let cmd = parts.remove(0); - Command::new(cmd) - .args(&parts) - .args(["gio", "launch"]) - .arg(desktop_path) - .spawn() - } - // No wrapper: use gio directly - _ => { - info!("Launching via gio launch: {}", desktop_path); - Command::new("gio") - .args(["launch", desktop_path]) - .spawn() + if config.general.use_uwsm { + // Check if uwsm is available + let uwsm_available = Command::new("which") + .arg("uwsm") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false); + + if !uwsm_available { + let msg = "uwsm is enabled in config but not installed"; + log::error!("{}", msg); + crate::notify::notify("Launch failed", msg); + return Err(std::io::Error::new(std::io::ErrorKind::NotFound, msg)); } + + info!("Launching via uwsm: {}", desktop_path); + Command::new("uwsm") + .args(["app", "--", desktop_path]) + .spawn() + } else { + info!("Launching via gio: {}", desktop_path); + Command::new("gio") + .args(["launch", desktop_path]) + .spawn() } } @@ -1391,8 +1397,9 @@ impl MainWindow { command.to_string() }; - // Detect shell commands that shouldn't use a wrapper - let is_shell_command = cmd.starts_with("playerctl ") + // Shell/system commands run directly without uwsm wrapper + // (they're typically short-lived or system utilities) + let is_system_command = cmd.starts_with("playerctl ") || cmd.starts_with("dbus-send ") || cmd.starts_with("systemctl ") || cmd.starts_with("journalctl ") @@ -1402,19 +1409,14 @@ impl MainWindow { || cmd.contains(" > ") || cmd.contains(" < "); - match &config.general.launch_wrapper { - Some(wrapper) if !wrapper.is_empty() && !is_shell_command => { - info!("Launching command via {}: {}", wrapper, cmd); - let mut parts: Vec<&str> = wrapper.split_whitespace().collect(); - let wrapper_cmd = parts.remove(0); - Command::new(wrapper_cmd) - .args(&parts) - .args(["sh", "-c", &cmd]) - .spawn() - } - _ => { - Command::new("sh").arg("-c").arg(&cmd).spawn() - } + // Use uwsm for regular commands if enabled (and not a system command) + if config.general.use_uwsm && !is_system_command { + info!("Launching command via uwsm: {}", cmd); + Command::new("uwsm") + .args(["app", "--", "sh", "-c", &cmd]) + .spawn() + } else { + Command::new("sh").arg("-c").arg(&cmd).spawn() } } } diff --git a/data/config.example.toml b/data/config.example.toml index 224edb1..8a119ad 100644 --- a/data/config.example.toml +++ b/data/config.example.toml @@ -30,9 +30,11 @@ max_results = 10 # Uncomment to override: # terminal_command = "kitty" -# Launch wrapper for app execution (auto-detected for uwsm/Hyprland) -# Examples: "uwsm app --", "hyprctl dispatch exec --", "" -# launch_wrapper = "uwsm app --" +# Enable uwsm (Universal Wayland Session Manager) for launching apps. +# When enabled, apps are launched via "uwsm app --" which starts them +# in a proper systemd user session for better process management. +# Requires: uwsm to be installed +# use_uwsm = true # Header tabs - providers shown as toggle buttons (Ctrl+1, Ctrl+2, etc.) # Values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web