open_sesame/core/
launcher.rs

1//! Application launch command handling
2//!
3//! Represents commands to launch applications with environment configuration.
4
5use crate::util::load_env_files;
6use std::collections::HashMap;
7use std::process::Command;
8
9/// A command to launch an application
10///
11/// Represents a command to execute with environment configuration support.
12/// Created from [`crate::config::LaunchConfig`] for execution.
13///
14/// # Examples
15///
16/// ```no_run
17/// use open_sesame::core::LaunchCommand;
18///
19/// // Simple command
20/// let cmd = LaunchCommand::simple("firefox");
21/// cmd.execute(&[])?;
22///
23/// // Advanced command with args and env
24/// let mut env = std::collections::HashMap::new();
25/// env.insert("EDITOR".to_string(), "vim".to_string());
26///
27/// let cmd = LaunchCommand::advanced(
28///     "ghostty",
29///     vec!["--config".to_string(), "custom.toml".to_string()],
30///     vec!["~/.config/ghostty/.env".to_string()],
31///     env,
32/// );
33/// cmd.execute(&[])?;
34/// # Ok::<(), std::io::Error>(())
35/// ```
36#[derive(Debug, Clone)]
37pub struct LaunchCommand {
38    /// The command/binary to execute
39    pub command: String,
40    /// Arguments to pass
41    pub args: Vec<String>,
42    /// Environment files to load (paths)
43    pub env_files: Vec<String>,
44    /// Explicit environment variables
45    pub env: HashMap<String, String>,
46}
47
48impl LaunchCommand {
49    /// Create a simple launch command with just a command name
50    pub fn simple(command: impl Into<String>) -> Self {
51        Self {
52            command: command.into(),
53            args: Vec::new(),
54            env_files: Vec::new(),
55            env: HashMap::new(),
56        }
57    }
58
59    /// Create an advanced launch command with all options
60    pub fn advanced(
61        command: impl Into<String>,
62        args: Vec<String>,
63        env_files: Vec<String>,
64        env: HashMap<String, String>,
65    ) -> Self {
66        Self {
67            command: command.into(),
68            args,
69            env_files,
70            env,
71        }
72    }
73
74    /// Executes the launch command.
75    ///
76    /// Environment variable layering (later overrides earlier):
77    /// 1. Inherited from current process (WAYLAND_DISPLAY, XDG_*, PATH, etc.)
78    /// 2. Global env_files from settings
79    /// 3. Per-app env_files
80    /// 4. Explicit env vars
81    pub fn execute(&self, global_env_files: &[String]) -> Result<u32, std::io::Error> {
82        tracing::info!("Launching: {} {}", self.command, self.args.join(" "));
83
84        let mut cmd = Command::new(&self.command);
85        cmd.args(&self.args);
86
87        // Applies environment variable layering: inherited -> global files -> app files -> explicit
88        let global_env = load_env_files(global_env_files);
89        let app_env = load_env_files(&self.env_files);
90
91        cmd.envs(&global_env).envs(&app_env).envs(&self.env);
92
93        let total = global_env.len() + app_env.len() + self.env.len();
94        if total > 0 {
95            tracing::debug!("Set {} environment variables", total);
96        }
97
98        let child = cmd.spawn()?;
99        let pid = child.id();
100        tracing::debug!("Launched PID: {}", pid);
101
102        Ok(pid)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_simple_command() {
112        let cmd = LaunchCommand::simple("firefox");
113        assert_eq!(cmd.command, "firefox");
114        assert!(cmd.args.is_empty());
115        assert!(cmd.env_files.is_empty());
116        assert!(cmd.env.is_empty());
117    }
118
119    #[test]
120    fn test_advanced_command() {
121        let mut env = HashMap::new();
122        env.insert("MY_VAR".to_string(), "value".to_string());
123
124        let cmd = LaunchCommand::advanced(
125            "firefox",
126            vec!["--private-window".to_string()],
127            vec!["~/.env".to_string()],
128            env,
129        );
130
131        assert_eq!(cmd.command, "firefox");
132        assert_eq!(cmd.args, vec!["--private-window"]);
133        assert_eq!(cmd.env_files, vec!["~/.env"]);
134        assert_eq!(cmd.env.get("MY_VAR"), Some(&"value".to_string()));
135    }
136}