//go:build darwin package platform import ( "errors" "fmt" "os" "path/filepath" "strings" ) // launchAgentLabel is a stable internal identifier. The product name shown to // users comes from the application metadata, not this LaunchAgent label. const launchAgentLabel = "net.commilitia.cdrop" func launchAgentPath() (string, error) { home, err := os.UserHomeDir() if err != nil { return "", err } return filepath.Join(home, "Library", "LaunchAgents", launchAgentLabel+".plist"), nil } // launchTarget returns the program the LaunchAgent should run. For a packaged // .app it launches the bundle via `open` (proper LaunchServices activation); // in a bare dev binary it runs the executable directly. Either way it appends // --hidden so a login launch comes up to the menu bar with no window (see // launchedHidden in main); `open --args` forwards the rest as the app's argv. func launchTarget() ([]string, error) { exe, err := os.Executable() if err != nil { return nil, err } if resolved, err := filepath.EvalSymlinks(exe); err == nil { exe = resolved } if bundle := appBundlePath(exe); bundle != "" { return []string{"/usr/bin/open", bundle, "--args", "--hidden"}, nil } return []string{exe, "--hidden"}, nil } // appBundlePath returns the enclosing .app path if exe lives inside one // (…/Foo.app/Contents/MacOS/binary), else "". func appBundlePath(exe string) string { const marker = ".app/Contents/MacOS/" if i := strings.LastIndex(exe, marker); i != -1 { return exe[:i+len(".app")] } return "" } // SetLaunchAtLogin installs or removes the LaunchAgent plist. Removing a missing // agent is a no-op success. func SetLaunchAtLogin(enabled bool) error { p, err := launchAgentPath() if err != nil { return err } if !enabled { if err := os.Remove(p); err != nil && !errors.Is(err, os.ErrNotExist) { return err } return nil } args, err := launchTarget() if err != nil { return err } if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { return err } if err := writeLaunchAgentAtomically( p, []byte(buildLaunchAgentPlist(launchAgentLabel, args)), ); err != nil { return err } return nil } func writeLaunchAgentAtomically(path string, contents []byte) error { dir := filepath.Dir(path) f, err := os.CreateTemp(dir, "."+filepath.Base(path)+".tmp-*") if err != nil { return err } tempPath := f.Name() defer os.Remove(tempPath) if err := f.Chmod(0o644); err != nil { f.Close() return err } if _, err := f.Write(contents); err != nil { f.Close() return err } if err := f.Sync(); err != nil { f.Close() return err } if err := f.Close(); err != nil { return err } return os.Rename(tempPath, path) } // IsLaunchAtLoginEnabled reports whether the LaunchAgent plist is present. func IsLaunchAtLoginEnabled() bool { p, err := launchAgentPath() if err != nil { return false } _, err = os.Stat(p) return err == nil } func buildLaunchAgentPlist(label string, programArgs []string) string { var args strings.Builder for _, a := range programArgs { fmt.Fprintf(&args, "\n %s", xmlEscape(a)) } return fmt.Sprintf(` Label %s ProgramArguments %s RunAtLoad `, xmlEscape(label), args.String()) } func xmlEscape(s string) string { r := strings.NewReplacer( "&", "&", "<", "<", ">", ">", `"`, """, "'", "'", ) return r.Replace(s) }