1package wayland
2
3import (
4 "context"
5 "time"
6
7 "github.com/bnema/go-wayland-bindings/server/extidlenotify"
8 "github.com/bnema/go-wayland-bindings/server/wayland"
9 "github.com/bnema/go-wayland-bindings/server/wlroutputpowermanagement"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/bnema/purego-libwayland/server"
12)
13
14// Idle and output power. ext_idle_notifier_v1: each notification has its
15// own timeout, restarted by user activity (ports.UserActivity from core,
16// or Channels.IdleActivity from D-Bus clients); idle inhibitors, surfaces
17// or D-Bus clients (Channels.IdleInhibited), hold every notification but
18// input-idle ones.
19// zwlr_output_power_v1: a client such as wlopm turns a display off or on;
20// core decides and the scene carries it.
21
22// idleNotification is one ext_idle_notification_v1.
23type idleNotification struct {
24 res *extidlenotify.ExtIdleNotificationV1
25 timeout time.Duration
26 input bool // get_input_idle_notification: ignores idle inhibitors
27 idle bool // idled sent, resumed not yet
28 timer ports.Timer
29 gen uint64 // bumped on stop: a firing timer of an older gen is stale
30}
31
32// outputPower is one zwlr_output_power_v1.
33type outputPower struct {
34 res *wlroutputpowermanagement.ZwlrOutputPowerV1
35 output string
36}
37
38func registerIdle(d *server.Display, s *Server) error {
39 if err := extidlenotify.NewExtIdleNotifierV1Global(d, 2, func(c server.Client, v, id uint32) {
40 _, _ = extidlenotify.NewExtIdleNotifierV1(c, int32(v), id, idleNotifier{s})
41 }); err != nil {
42 return err
43 }
44 return wlroutputpowermanagement.NewZwlrOutputPowerManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
45 _, _ = wlroutputpowermanagement.NewZwlrOutputPowerManagerV1(c, int32(v), id, powerManager{s})
46 })
47}
48
49type idleNotifier struct{ server *Server }
50
51func (idleNotifier) Destroy(*extidlenotify.ExtIdleNotifierV1) {}
52
53func (n idleNotifier) GetIdleNotification(r *extidlenotify.ExtIdleNotifierV1, id, timeout uint32, _ *wayland.Seat) {
54 n.server.addIdle(r, id, timeout, false)
55}
56
57func (n idleNotifier) GetInputIdleNotification(r *extidlenotify.ExtIdleNotifierV1, id, timeout uint32, _ *wayland.Seat) {
58 n.server.addIdle(r, id, timeout, true)
59}
60
61type idleNotificationHandler struct{}
62
63func (idleNotificationHandler) Destroy(*extidlenotify.ExtIdleNotificationV1) {}
64
65func (s *Server) addIdle(r *extidlenotify.ExtIdleNotifierV1, id, timeout uint32, input bool) {
66 res, err := extidlenotify.NewExtIdleNotificationV1(r.Client(), r.Version(), id, idleNotificationHandler{})
67 if err != nil {
68 return
69 }
70 n := &idleNotification{res: res, timeout: time.Duration(timeout) * time.Millisecond, input: input}
71 s.idleNotes = append(s.idleNotes, n)
72 res.OnDestroy = func() {
73 n.stop()
74 for i, x := range s.idleNotes {
75 if x == n {
76 s.idleNotes = append(s.idleNotes[:i], s.idleNotes[i+1:]...)
77 break
78 }
79 }
80 }
81 s.armIdle(n)
82}
83
84// idleHeld reports whether an idle inhibitor keeps n from firing.
85func (s *Server) idleHeld(n *idleNotification) bool {
86 return !n.input && s.inhibited()
87}
88
89// inhibited reports whether any idle inhibitor is in effect.
90func (s *Server) inhibited() bool {
91 return len(s.idleWindows) > 0 || s.busInhibited
92}
93
94// forwardBusIdle applies D-Bus idle inhibition and simulated activity on
95// the display goroutine. A closed channel stops only its own forwarding.
96func (s *Server) forwardBusIdle(ctx context.Context) {
97 inhibited, activity := s.channels.IdleInhibited, s.channels.IdleActivity
98 for inhibited != nil || activity != nil {
99 var apply func()
100 select {
101 case <-ctx.Done():
102 return
103 case <-s.display.Stopped():
104 return
105 case on, ok := <-inhibited:
106 if !ok {
107 inhibited = nil
108 continue
109 }
110 apply = func() {
111 wasHeld := s.inhibited()
112 s.busInhibited = on
113 s.syncIdle(wasHeld)
114 }
115 case _, ok := <-activity:
116 if !ok {
117 activity = nil
118 continue
119 }
120 apply = s.userActivity
121 }
122 if !s.display.Do(apply) {
123 return
124 }
125 }
126}
127
128// armIdle restarts n's timer, unless an inhibitor holds it. The timer
129// fires on the display goroutine.
130func (s *Server) armIdle(n *idleNotification) {
131 n.stop()
132 if s.idleHeld(n) {
133 return
134 }
135 gen := n.gen
136 // Core reports activity at most once per ActivityInterval: a shorter
137 // timeout would fire while the user is still typing.
138 n.timer = s.clock.AfterFunc(max(n.timeout, ports.ActivityInterval), func() {
139 s.display.Do(func() {
140 // A timer stopped or replaced while it waited for Do is stale.
141 if n.gen != gen || !n.res.Resource.Alive() || n.idle || s.idleHeld(n) {
142 return
143 }
144 n.idle = true
145 n.res.SendIdled()
146 })
147 })
148}
149
150func (n *idleNotification) stop() {
151 n.gen++
152 if n.timer != nil {
153 n.timer.Stop()
154 n.timer = nil
155 }
156}
157
158// userActivity resumes idle notifications and restarts their timers.
159func (s *Server) userActivity() {
160 for _, n := range s.idleNotes {
161 if n.idle {
162 n.idle = false
163 n.res.SendResumed()
164 }
165 s.armIdle(n)
166 }
167}
168
169// syncIdle follows idle inhibitors: while one exists, inhibitable timers
170// stop; once the last one goes, they start again from zero.
171func (s *Server) syncIdle(wasHeld bool) {
172 if held := s.inhibited(); held != wasHeld {
173 for _, n := range s.idleNotes {
174 if !n.input {
175 s.armIdle(n)
176 }
177 }
178 }
179}
180
181type powerManager struct{ server *Server }
182
183func (powerManager) Destroy(*wlroutputpowermanagement.ZwlrOutputPowerManagerV1) {}
184
185func (m powerManager) GetOutputPower(r *wlroutputpowermanagement.ZwlrOutputPowerManagerV1, id uint32, wl *wayland.Output) {
186 s := m.server
187 o := s.outputOf(wl)
188 p := &outputPower{}
189 res, err := wlroutputpowermanagement.NewZwlrOutputPowerV1(r.Client(), r.Version(), id, powerHandler{s, p})
190 if err != nil {
191 return
192 }
193 p.res = res
194 if o == nil {
195 // Unplugged or unknown: the object is inert.
196 res.SendFailed()
197 return
198 }
199 p.output = o.name()
200 s.powers = append(s.powers, p)
201 res.OnDestroy = func() {
202 for i, x := range s.powers {
203 if x == p {
204 s.powers = append(s.powers[:i], s.powers[i+1:]...)
205 break
206 }
207 }
208 }
209 res.SendMode(powerMode(!s.outputsOff[p.output]))
210}
211
212type powerHandler struct {
213 server *Server
214 p *outputPower
215}
216
217func (powerHandler) Destroy(*wlroutputpowermanagement.ZwlrOutputPowerV1) {}
218
219func (h powerHandler) SetMode(r *wlroutputpowermanagement.ZwlrOutputPowerV1, mode uint32) {
220 if mode != uint32(wlroutputpowermanagement.ZwlrOutputPowerV1ModeOff) && mode != uint32(wlroutputpowermanagement.ZwlrOutputPowerV1ModeOn) {
221 r.PostError(uint32(wlroutputpowermanagement.ZwlrOutputPowerV1ErrorInvalidMode), "invalid mode")
222 return
223 }
224 if h.p.output == "" {
225 return
226 }
227 h.server.emit(ports.OutputPower{Output: h.p.output, On: mode == uint32(wlroutputpowermanagement.ZwlrOutputPowerV1ModeOn)})
228}
229
230func powerMode(on bool) uint32 {
231 if on {
232 return uint32(wlroutputpowermanagement.ZwlrOutputPowerV1ModeOn)
233 }
234 return uint32(wlroutputpowermanagement.ZwlrOutputPowerV1ModeOff)
235}
236
237// setOutputsOff applies core's power state: each object of an output
238// whose mode changed hears it; objects of gone outputs fail.
239func (s *Server) setOutputsOff(off map[string]bool) {
240 names := map[string]bool{}
241 for _, o := range s.outputs {
242 names[o.name()] = true
243 }
244 kept := s.powers[:0]
245 for _, p := range s.powers {
246 if !names[p.output] {
247 // Failed objects are inert, even if an output of that name
248 // comes back.
249 p.res.SendFailed()
250 p.res.OnDestroy = nil
251 p.output = ""
252 continue
253 }
254 if off[p.output] != s.outputsOff[p.output] {
255 p.res.SendMode(powerMode(!off[p.output]))
256 }
257 kept = append(kept, p)
258 }
259 s.powers = kept
260 s.outputsOff = off
261}