use std::path::PathBuf;
use std::process::Command;
use crate::platform::autostart::{xml_escape, AutostartError, AutostartProgram};
pub fn render_registration(program: &AutostartProgram<'_>) -> String {
let binary = xml_escape(&program.program.to_string_lossy());
let label = xml_escape(program.label);
let start = xml_escape(program.start_argument);
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{label}</string>
<key>ProgramArguments</key>
<array>
<string>{binary}</string>
<string>{start}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
"#,
)
}
pub fn register(program: &AutostartProgram<'_>) -> Result<PathBuf, AutostartError> {
let path = plist_path(program)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, render_registration(program))?;
let domain = format!("gui/{}", current_uid()?);
match Command::new("launchctl")
.args(["bootstrap", &domain, &path.to_string_lossy()])
.status()
{
Ok(status) if status.success() => {}
_ => {
let _ = Command::new("launchctl")
.args(["load", "-w", &path.to_string_lossy()])
.status();
}
}
Ok(path)
}
pub fn unregister(program: &AutostartProgram<'_>) -> Result<(), AutostartError> {
let path = plist_path(program)?;
let target = format!("gui/{}/{}", current_uid()?, program.label);
let modern = Command::new("launchctl").args(["bootout", &target]).status();
if !matches!(modern, Ok(status) if status.success()) {
let _ = Command::new("launchctl")
.args(["unload", "-w", &path.to_string_lossy()])
.status();
}
if path.exists() {
std::fs::remove_file(&path)?;
}
Ok(())
}
fn plist_path(program: &AutostartProgram<'_>) -> Result<PathBuf, AutostartError> {
let home = std::env::var_os("HOME")
.ok_or_else(|| AutostartError::Resolve("HOME is not set".into()))?;
Ok(PathBuf::from(home)
.join("Library")
.join("LaunchAgents")
.join(format!("{}.plist", program.label)))
}
fn current_uid() -> Result<u32, AutostartError> {
let output = Command::new("id")
.arg("-u")
.output()
.map_err(|error| AutostartError::InitSystem(format!("id -u failed: {error}")))?;
if !output.status.success() {
return Err(AutostartError::InitSystem(format!(
"id -u exited non-zero: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
text.parse::<u32>()
.map_err(|error| AutostartError::InitSystem(format!("could not parse uid {text:?}: {error}")))
}