//go:build darwin package autotrack import ( "context" "os/exec" "strings" "time" ) // osTimeout bounds every OS shell-out so a wedged osascript can never stall // the poll loop (parity with the Python daemon's timeout=4). const osTimeout = 4 * time.Second // browsers maps app names to the osascript that reads their active-tab URL. var browsers = map[string]string{ "Safari": `tell application "Safari" to get URL of front document`, "Google Chrome": `tell application "Google Chrome" to get URL of active tab of front window`, "Brave Browser": `tell application "Brave Browser" to get URL of active tab of front window`, "Microsoft Edge": `tell application "Microsoft Edge" to get URL of active tab of front window`, "Arc": `tell application "Arc" to get URL of active tab of front window`, "Dia": `tell application "Dia" to get URL of active tab of front window`, } // runCmd executes a command with the standard timeout, returning stdout // ("" on any failure — an OS hiccup must never panic the loop). func runCmd(name string, args ...string) string { ctx, cancel := context.WithTimeout(context.Background(), osTimeout) defer cancel() out, err := exec.CommandContext(ctx, name, args...).Output() if err != nil { return "" } return string(out) } func osa(script string) string { return strings.TrimSpace(runCmd("osascript", "-e", script)) } func frontApp() string { return osa(`tell application "System Events" to get name of first application process whose frontmost is true`) } func frontTitle() string { return osa(`tell application "System Events" to tell (first application process whose frontmost is true) to get name of front window`) } func browserURL(app string) string { script, ok := browsers[app] if !ok { return "" } return osa(script) } func idleSecs() int { return parseHIDIdle(runCmd("ioreg", "-c", "IOHIDSystem")) } func screenLocked() bool { return parseLocked(runCmd("ioreg", "-n", "Root", "-d1", "-r", "-k", "CGSSessionScreenIsLocked")) } // winctxApp reads the frontmost {app, title} via the bundled AX helper // (richer titles than System Events). Empties when the helper is missing, // denied, or errors — the caller falls back to osascript. func winctxApp() (string, string) { bin := HelperPath("winctx") if bin == "" { return "", "" } out := strings.TrimSpace(runCmd(bin)) if out == "" { return "", "" } return parseWinctx([]byte(out)) } // readContext samples the current foreground context; nil when no app. // Prefers the AX helper's app+title, falling back to System Events per field. func readContext() *Ctx { app, title := winctxApp() if app == "" { app = frontApp() } if app == "" { return nil } if title == "" { title = frontTitle() } u := browserURL(app) return &Ctx{App: app, Title: title, URL: u, Domain: Domain(u)} } // ReadSample gathers one full OS observation for the poll loop. func ReadSample() Sample { return Sample{Idle: idleSecs(), Raw: readContext(), Locked: screenLocked()} }