//go:build linux package evidence import ( "context" "github.com/jezek/xgb/xproto" "github.com/jezek/xgbutil" "github.com/jezek/xgbutil/ewmh" "github.com/jezek/xgbutil/icccm" "github.com/jezek/xgbutil/xevent" "github.com/jezek/xgbutil/xprop" "github.com/jezek/xgbutil/xwindow" ) // NewSource returns the real X11 active-window sensor. func NewSource() Source { return &x11Source{} } type x11Source struct{} // Watch opens one long-lived X connection, subscribes to _NET_ACTIVE_WINDOW // changes on the root window, and emits a snapshot immediately plus on every // change. Any failure degrades to an Unavailable snapshot; it never panics the // daemon. func (s *x11Source) Watch(ctx context.Context, onChange func(WindowSnapshot)) { X, err := xgbutil.NewConn() if err != nil { onChange(unavailable("cannot connect to X server: " + err.Error())) <-ctx.Done() return } defer X.Conn().Close() root := X.RootWin() activeAtom, err := xprop.Atm(X, "_NET_ACTIVE_WINDOW") if err != nil { onChange(unavailable("no _NET_ACTIVE_WINDOW atom: " + err.Error())) <-ctx.Done() return } // Listen for property changes on the root window. if err := xwindow.New(X, root).Listen(xproto.EventMaskPropertyChange); err != nil { onChange(unavailable("cannot listen on root window: " + err.Error())) <-ctx.Done() return } emit := func() { onChange(snapshot(X)) } emit() // immediate current window xevent.PropertyNotifyFun(func(_ *xgbutil.XUtil, ev xevent.PropertyNotifyEvent) { if ev.Atom == activeAtom { emit() } }).Connect(X, root) // Run the event loop until ctx is cancelled. go func() { <-ctx.Done() xevent.Quit(X) }() xevent.Main(X) } func snapshot(X *xgbutil.XUtil) WindowSnapshot { active, err := ewmh.ActiveWindowGet(X) if err != nil || active == 0 { return unavailable("no active window") } title, err := ewmh.WmNameGet(X, active) if err != nil || title == "" { // Fall back to ICCCM WM_NAME. if t, e := icccm.WmNameGet(X, active); e == nil { title = t } } var class string if wmClass, err := icccm.WmClassGet(X, active); err == nil && wmClass != nil { class = wmClass.Class } return WindowSnapshot{ Title: title, Class: class, Health: EvidenceHealth{Available: true}, } }