//go:build darwin package platform import ( "errors" "fmt" "os" "path/filepath" "strings" ) // launchAgentLabel is the reverse-DNS label + plist filename for the per-user // LaunchAgent that starts cdrop at login. 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 } return os.WriteFile(p, []byte(buildLaunchAgentPlist(launchAgentLabel, args)), 0o644) } // 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) }