//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)
}