-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathscreen.go
248 lines (220 loc) · 8.57 KB
/
screen.go
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
//nolint:goerr113 // Dynamic errors in main are OK
package main
import (
"fmt"
"sync/atomic"
"github.com/anoopengineer/edidparser/edid"
"github.com/jezek/xgb"
"github.com/jezek/xgb/randr"
"github.com/jezek/xgb/screensaver"
"github.com/jezek/xgb/xproto"
)
// Screen is a connection to an X Windows server for the purposes of watching
// for screen saver events and for the presence of a particular monitor. The
// monitor is identified by a manufacturer ID and a product code, both fields
// from the monitor's [EDID] block. Screen saver events are only monitored
// while a monitor matching that manufacturer ID / product code pair is plugged
// into the X server.
//
// [EDID]: https://en.wikipedia.org/wiki/Extended_Display_Identification_Data
type Screen struct {
xconn *xgb.Conn
rootWin xproto.Window
manufacturerID string
productCode uint16
ssOn atomic.Bool
present atomic.Bool
}
// ScreenWatcher is a callback interface that is called by [Watch] when the
// state of the screen saver changes - i.e. when the screen saver turns on or
// off. It is not called if the TV/monitor is not plugged in.
type ScreenWatcher interface {
SSChange(ssOn bool) error
}
// ScreenWatcherFunc is a function adaptor for the ScreenWatcher interface.
type ScreenWatcherFunc func(ssOn bool) error
// SSChange calls the function adaptor with the value of ssOn.
func (swf ScreenWatcherFunc) SSChange(ssOn bool) error {
return swf(ssOn)
}
// NewScreen returns a new Screen with a connection to the X server for the
// given display, with the RANDR and SCREENSAVER extensions initialised (i.e.
// verified that the X server has these extensions). The manufacturerID and
// productCode are used for monitor presence detection.
//
// An error is returned if the connection to the X server could not be
// established, the extensions are not present on the server or the current
// screen saver state or monitor presence could not be queried.
func NewScreen(display, manufacturerID string, productCode uint16) (*Screen, error) {
c, err := xgb.NewConnDisplay(display)
if err != nil {
return nil, fmt.Errorf("could not open display %s: %w", display, err)
}
// Intitialise the RANDR and SCREENSAVER extensions. These will fail if the
// X11 server does not support these extensions.
if err := randr.Init(c); err != nil {
return nil, fmt.Errorf("could not initialise RANDR extension: %w", err)
}
if err := screensaver.Init(c); err != nil {
return nil, fmt.Errorf("could not initialise SCREENSAVER extension: %w", err)
}
s := &Screen{
xconn: c,
rootWin: xproto.Setup(c).DefaultScreen(c).Root,
manufacturerID: manufacturerID,
productCode: productCode,
}
// Set the initial state of the screen saver and monitor presence.
ssOn, err := s.queryScreenSaver()
if err != nil {
return nil, fmt.Errorf("could not query screen saver state: %w", err)
}
s.ssOn.Store(ssOn)
present, err := s.queryPresence()
if err != nil {
return nil, fmt.Errorf("could not query TV presence: %w", err)
}
s.present.Store(present)
return s, nil
}
// Close closes the screen's connection to the X server. This will cause
// [Screen.Watch] to return.
func (s *Screen) Close() {
s.xconn.Close()
}
// IsScreenSaverOn returns the current state of the screen saver.
func (s *Screen) IsScreenSaverOn() bool {
return s.ssOn.Load()
}
// IsPresent returns whether the screen's monitor is present or not.
func (s *Screen) IsPresent() bool {
return s.present.Load()
}
// Blank forces the screen saver to an active/enabled state.
func (s *Screen) Blank() error {
return xproto.ForceScreenSaverChecked(s.xconn, xproto.ScreenSaverActive).Check()
}
// Watch loops while the connection to the X server is open (see
// [Screen.Close]) calling the given watcher when the state of the screen saver
// changes, but only if the screen's monitor is present. If the screen's
// monitor becomes present the state of the screen saver at that time is passed
// to the watcher.
func (s *Screen) Watch(watcher ScreenWatcher) error {
// Listen for randr events (monitor plug/unplug)
err := randr.SelectInputChecked(s.xconn, s.rootWin, randr.NotifyMaskOutputChange).Check()
if err != nil {
return fmt.Errorf("could not watch RANDR events: %w", err)
}
// Listen for screensaver events (screensaver on/off)
// For some reason, screensaver wants the root window as a "Drawable"
drawableRoot := xproto.Drawable(s.rootWin)
err = screensaver.SelectInputChecked(s.xconn, drawableRoot, screensaver.EventNotifyMask).Check()
if err != nil {
return fmt.Errorf("could not watch SCREENSAVER events: %w", err)
}
for {
ev, err := s.xconn.WaitForEvent()
if err != nil {
return fmt.Errorf("could not wait for events: %w", err)
}
if ev == nil { // X11 connection closed
return nil
}
switch event := ev.(type) {
case screensaver.NotifyEvent:
isOn := event.State == screensaver.StateOn || event.State == screensaver.StateCycle
wasOn := s.ssOn.Swap(isOn)
// Send the screensaver state if it changes and the monitor is present
if isOn != wasOn && s.IsPresent() {
if err := watcher.SSChange(isOn); err != nil {
return err
}
}
case randr.NotifyEvent:
// It is too hard to determine from the randr event whether it is for
// the display being connected/disconnected, so for every randr event,
// just check the presence by checking the randr properties.
present, err := s.queryPresence()
if err != nil {
return fmt.Errorf("could not query TV presence: %w", err)
}
wasPresent := s.present.Swap(present)
// If the monitor has just appeared, send the screensaver state
if present && !wasPresent {
if err := watcher.SSChange(s.IsScreenSaverOn()); err != nil {
return err
}
}
}
}
}
// queryScreenSaver queries the X server for the state of the screen saver.
func (s *Screen) queryScreenSaver() (bool, error) {
info, err := screensaver.QueryInfo(s.xconn, xproto.Drawable(s.rootWin)).Reply()
if err != nil {
return false, fmt.Errorf("QueryInfo failed: %w", err)
}
return info.State == screensaver.StateOn, nil
}
// queryPresence queries the X server for the presence of the screen's monitor.
func (s *Screen) queryPresence() (bool, error) {
var present bool
err := RangeEDID(s.xconn, s.rootWin, func(_ randr.Output, e *edid.Edid) (bool, error) {
if e.ManufacturerId == s.manufacturerID && e.ProductCode == s.productCode {
present = true
return false /* stop ranging */, nil
}
return true /* keep ranging */, nil
})
return present, err
}
// RangeEDIDFunc is called by [RangeEDID] for each X11 xrandr output that has
// EDID data. The function returns a bool that tells [RangeEDID] whether to
// continue ranging over subsequent outputs or not, and an error that if not
// nil will be returned to the caller of [RangeEDID]. If the RangeEDIDFunc
// returns false or an error, [RangeEDID] terminates and returns to the caller.
type RangeEDIDFunc func(output randr.Output, edidData *edid.Edid) (cont bool, err error)
// RangeEDID calls fn for each X11 xrandr output that has an EDID property.
// If fn returns false or an error, iteration will terminate. The error is
// returned.
//
// If root is zero (not a valid window ID) then RangeEDID will get it from
// the provided xgb.Conn. This needs to unpack a bunch of serialised data,
// so it can be more efficient to provide the root window ID if you have it.
func RangeEDID(c *xgb.Conn, root xproto.Window, fn RangeEDIDFunc) error {
if root == xproto.Window(0) {
root = xproto.Setup(c).DefaultScreen(c).Root
}
r, err := randr.GetScreenResourcesCurrent(c, root).Reply()
if err != nil {
return fmt.Errorf("could not get screens: %w", err)
}
edidAtom, err := xproto.InternAtom(c, false /* OnlyIfExists */, 4, "EDID").Reply()
if err != nil {
return fmt.Errorf("could not intern X11 atom: %w", err)
}
for _, output := range r.Outputs {
// the length of 64 gives a maximum EDID data size of 256 bytes (4 * 64).
// EDID maxes out at 256 bytes long, so should be fine.
const offset, length, del, pending = 0, 64, false, false
// https://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt#n872
opr, err := randr.GetOutputProperty(c, output, edidAtom.Atom, xproto.AtomAny, offset, length, del, pending).Reply()
if err != nil {
return fmt.Errorf("could not get output properties: %w", err)
}
if opr.BytesAfter != 0 {
return fmt.Errorf("EDID data too large. Max is 256 bytes, got %d bytes", 256+opr.BytesAfter)
}
if len(opr.Data) == 0 {
continue
}
ed, err := edid.NewEdid(opr.Data)
if err != nil {
return fmt.Errorf("could not parse EDID data: %w", err)
}
if cont, err := fn(output, ed); !cont || err != nil {
return err
}
}
return nil
}