1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
//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()}
}
|