detached 31f302d

fix(wayland): turn off outputs that connect while the screens are off (#12)

* fix(wayland): turn off outputs that connect while the screens are off

* test(wayland): share the client bootstrap and document the reconnect claim
commit
31f302d0cbb32e276e625bf28eeee932c67a01eb
author
bnema · · 2026-10-03 12:51 UTC
committer
GitHub · · 2026-10-03 12:51 UTC
parent
131f96f
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.5.1
3 files · +126 -28
M README.md +1 -1
@@ -79,7 +79,7 @@ auth.mode = password
7979 | `auth.pin-env` | environment variable name (with `env`) | none |
8080 | `auth.pin-entry` | `pass` entry (with `pass`) | none |
8181
82-Durations use Go syntax (`30s`, `5m`, `1h`), up to 24h. The order of the actions comes from their delays: for example, `lock.after = 20s` with `screens.off-after = 50s` turns the screens off 30 seconds after the lock.
82+Durations use Go syntax (`30s`, `5m`, `1h`), up to 24h. The order of the actions comes from their delays: for example, `lock.after = 20s` with `screens.off-after = 50s` turns the screens off 30 seconds after the lock. A screen that connects while the screens are off, such as a display that reconnects from deep sleep, is turned off too.
8383
8484 The lock prompt shows on every output. Set `lock.output` to show it only on that output while it is connected; the other outputs then show black. A change applies from the next lock. `wlr-randr` lists the output names.
8585
@@ -79,7 +79,7 @@ auth.mode = password
79 | `auth.pin-env` | environment variable name (with `env`) | none |79 | `auth.pin-env` | environment variable name (with `env`) | none |
80 | `auth.pin-entry` | `pass` entry (with `pass`) | none |80 | `auth.pin-entry` | `pass` entry (with `pass`) | none |
81 81
82-Durations use Go syntax (`30s`, `5m`, `1h`), up to 24h. The order of the actions comes from their delays: for example, `lock.after = 20s` with `screens.off-after = 50s` turns the screens off 30 seconds after the lock.82+Durations use Go syntax (`30s`, `5m`, `1h`), up to 24h. The order of the actions comes from their delays: for example, `lock.after = 20s` with `screens.off-after = 50s` turns the screens off 30 seconds after the lock. A screen that connects while the screens are off, such as a display that reconnects from deep sleep, is turned off too.
83 83
84 The lock prompt shows on every output. Set `lock.output` to show it only on that output while it is connected; the other outputs then show black. A change applies from the next lock. `wlr-randr` lists the output names.84 The lock prompt shows on every output. Set `lock.output` to show it only on that output while it is connected; the other outputs then show black. A change applies from the next lock. `wlr-randr` lists the output names.
85 85
M internal/adapters/wayland/client.go +20 -0
@@ -72,6 +72,10 @@ type Client struct {
7272 deadlines map[time.Duration]bool
7373 generation, next uint64
7474 dirty bool
75+ // screensOff is the latest requested power state (OutputPower command
76+ // or input wake): an output that appears while it is set (a display
77+ // reconnecting from deep sleep) is turned off too.
78+ screensOff bool
7579 }
7680
7781 // New takes ownership of conn. Run is called exactly once; no other reader or
@@ -291,10 +295,24 @@ func (c *Client) handle(ctx context.Context, out chan<- ports.WaylandEvent, n no
291295 if c.outputs[o.global] != o || o.failed {
292296 return nil
293297 }
298+ first := !o.modeKnown
294299 o.mode, o.modeKnown = n.mode, true
295300 if n.mode == outputpower.MODE_ON {
296301 o.off = false
297302 }
303+ // The first mode is the initial state of a new power control. An
304+ // output that appears while the screens are off is turned off and
305+ // claimed, even if it is already off (a compositor may keep a
306+ // reconnected output off), so that activity turns it on. This is
307+ // the one exception to never claiming an output already off.
308+ if first && c.screensOff {
309+ if n.mode == outputpower.MODE_ON {
310+ if err := o.power.SetMode(outputpower.MODE_OFF); err != nil {
311+ return err
312+ }
313+ }
314+ o.off = true
315+ }
298316 case 5:
299317 o := n.output
300318 if c.outputs[o.global] != o || o.failed {
@@ -379,6 +397,7 @@ func (c *Client) reconcile() error {
379397 }
380398 o.off = false
381399 o.failed = false
400+ o.modeKnown = false // a new control reports its initial mode
382401 }
383402 if c.power != nil {
384403 if err := c.power.Destroy(); err != nil {
@@ -514,6 +533,7 @@ func (c *Client) destroyNotification(p *notification) error {
514533 return p.proxy.Destroy()
515534 }
516535 func (c *Client) setPower(on bool) error {
536+ c.screensOff = !on
517537 for _, o := range c.outputs {
518538 if o.power == nil || o.failed {
519539 continue
@@ -72,6 +72,10 @@ type Client struct {
72 deadlines map[time.Duration]bool72 deadlines map[time.Duration]bool
73 generation, next uint6473 generation, next uint64
74 dirty bool74 dirty bool
75+ // screensOff is the latest requested power state (OutputPower command
76+ // or input wake): an output that appears while it is set (a display
77+ // reconnecting from deep sleep) is turned off too.
78+ screensOff bool
75 }79 }
76 80
77 // New takes ownership of conn. Run is called exactly once; no other reader or81 // New takes ownership of conn. Run is called exactly once; no other reader or
@@ -291,10 +295,24 @@ func (c *Client) handle(ctx context.Context, out chan<- ports.WaylandEvent, n no
291 if c.outputs[o.global] != o || o.failed {295 if c.outputs[o.global] != o || o.failed {
292 return nil296 return nil
293 }297 }
298+ first := !o.modeKnown
294 o.mode, o.modeKnown = n.mode, true299 o.mode, o.modeKnown = n.mode, true
295 if n.mode == outputpower.MODE_ON {300 if n.mode == outputpower.MODE_ON {
296 o.off = false301 o.off = false
297 }302 }
303+ // The first mode is the initial state of a new power control. An
304+ // output that appears while the screens are off is turned off and
305+ // claimed, even if it is already off (a compositor may keep a
306+ // reconnected output off), so that activity turns it on. This is
307+ // the one exception to never claiming an output already off.
308+ if first && c.screensOff {
309+ if n.mode == outputpower.MODE_ON {
310+ if err := o.power.SetMode(outputpower.MODE_OFF); err != nil {
311+ return err
312+ }
313+ }
314+ o.off = true
315+ }
298 case 5:316 case 5:
299 o := n.output317 o := n.output
300 if c.outputs[o.global] != o || o.failed {318 if c.outputs[o.global] != o || o.failed {
@@ -379,6 +397,7 @@ func (c *Client) reconcile() error {
379 }397 }
380 o.off = false398 o.off = false
381 o.failed = false399 o.failed = false
400+ o.modeKnown = false // a new control reports its initial mode
382 }401 }
383 if c.power != nil {402 if c.power != nil {
384 if err := c.power.Destroy(); err != nil {403 if err := c.power.Destroy(); err != nil {
@@ -514,6 +533,7 @@ func (c *Client) destroyNotification(p *notification) error {
514 return p.proxy.Destroy()533 return p.proxy.Destroy()
515 }534 }
516 func (c *Client) setPower(on bool) error {535 func (c *Client) setPower(on bool) error {
536+ c.screensOff = !on
517 for _, o := range c.outputs {537 for _, o := range c.outputs {
518 if o.power == nil || o.failed {538 if o.power == nil || o.failed {
519 continue539 continue
M internal/adapters/wayland/client_test.go +105 -27
@@ -109,59 +109,83 @@ func nextEvent(t *testing.T, out <-chan ports.WaylandEvent) ports.WaylandEvent {
109109 }
110110 }
111111
112-func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) {
112+// started is a client running against a fake compositor that announced
113+// its globals and answered the initial bindings (one output, HEADLESS-1).
114+type started struct {
115+ peer *net.UnixConn
116+ registry, wake, power uint32
117+ ids map[string]uint32 // bound ID by interface
118+ notifications map[uint32]uint32 // idle notification by timeout (ms)
119+ commands chan ports.WaylandCommand
120+ out chan ports.WaylandEvent
121+ done chan error
122+}
123+
124+// extraGlobal is one more global announced at start.
125+type extraGlobal struct {
126+ name uint32
127+ iface string
128+ version uint32
129+}
130+
131+// start runs a client and the initial discovery; extra globals follow
132+// wl_seat, wl_output, ext_idle_notifier_v1 and zwlr_output_power_manager_v1.
133+func start(t *testing.T, ctx context.Context, options Options, extra ...extraGlobal) *started {
134+ t.Helper()
113135 conn, peer := sockets(t)
114- c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true, Lock: true, Visual: true}, Generation: 1, Deadlines: []time.Duration{time.Second, time.Second, 2 * time.Second}}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
136+ c, err := New(conn, options, zerowrap.New(zerowrap.Config{Output: io.Discard}))
115137 if err != nil {
116138 t.Fatal(err)
117139 }
118140 _, _, body := readWire(t, peer)
119- registry := binary.NativeEndian.Uint32(body)
120- ctx, cancel := context.WithCancel(context.Background())
121- defer cancel()
122- commands := make(chan ports.WaylandCommand, 8)
123- out := make(chan ports.WaylandEvent, 64)
124- done := make(chan error, 1)
125- go func() { done <- c.Run(ctx, commands, out) }()
141+ s := &started{peer: peer, registry: binary.NativeEndian.Uint32(body), ids: map[string]uint32{}, notifications: map[uint32]uint32{}, commands: make(chan ports.WaylandCommand, 8), out: make(chan ports.WaylandEvent, 64), done: make(chan error, 1)}
142+ go func() { s.done <- c.Run(ctx, s.commands, s.out) }()
126143 _, _, body = readWire(t, peer)
127- global(t, peer, registry, 10, "wl_seat", 5)
128- global(t, peer, registry, 11, "wl_output", 4)
129- global(t, peer, registry, 12, "ext_idle_notifier_v1", 2)
130- global(t, peer, registry, 13, "zwlr_output_power_manager_v1", 1)
131- global(t, peer, registry, 14, "ext_session_lock_manager_v1", 1)
144+ global(t, peer, s.registry, 10, "wl_seat", 5)
145+ global(t, peer, s.registry, 11, "wl_output", 4)
146+ global(t, peer, s.registry, 12, "ext_idle_notifier_v1", 2)
147+ global(t, peer, s.registry, 13, "zwlr_output_power_manager_v1", 1)
148+ for _, g := range extra {
149+ global(t, peer, s.registry, g.name, g.iface, g.version)
150+ }
132151 synchronize(t, peer, body)
133- ids := map[string]uint32{}
134- notifications := map[uint32]uint32{}
135- var wake, power uint32
136152 for {
137153 id, op, b := readWire(t, peer)
138154 if id == 1 {
139155 synchronize(t, peer, b)
140156 break
141157 }
142- if id == registry {
158+ if id == s.registry {
143159 n := int(binary.NativeEndian.Uint32(b[4:]))
144160 iface := string(b[8 : 8+n-1])
145- ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:])
161+ s.ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:])
146162 if iface == "wl_output" {
147- sendWire(t, peer, ids[iface], 4, wireString("HEADLESS-1"))
163+ sendWire(t, peer, s.ids[iface], 4, wireString("HEADLESS-1"))
148164 }
149165 continue
150166 }
151- if id == ids["ext_idle_notifier_v1"] {
167+ if id == s.ids["ext_idle_notifier_v1"] {
152168 child := binary.NativeEndian.Uint32(b)
153- ms := binary.NativeEndian.Uint32(b[4:])
154169 if op == 2 {
155- wake = child
170+ s.wake = child
156171 } else {
157- notifications[ms] = child
172+ s.notifications[binary.NativeEndian.Uint32(b[4:])] = child
158173 }
159174 }
160- if id == ids["zwlr_output_power_manager_v1"] {
161- power = binary.NativeEndian.Uint32(b)
162- sendWire(t, peer, power, 0, words(1))
175+ if id == s.ids["zwlr_output_power_manager_v1"] {
176+ s.power = binary.NativeEndian.Uint32(b)
177+ sendWire(t, peer, s.power, 0, words(1))
163178 }
164179 }
180+ return s
181+}
182+
183+func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) {
184+ ctx, cancel := context.WithCancel(context.Background())
185+ defer cancel()
186+ s := start(t, ctx, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true, Lock: true, Visual: true}, Generation: 1, Deadlines: []time.Duration{time.Second, time.Second, 2 * time.Second}}, extraGlobal{14, "ext_session_lock_manager_v1", 1})
187+ peer, registry, ids, notifications, wake, power := s.peer, s.registry, s.ids, s.notifications, s.wake, s.power
188+ commands, out, done := s.commands, s.out, s.done
165189 caps := capability(t, out)
166190 if len(notifications) != 2 || wake == 0 || power == 0 || len(caps.Outputs) != 1 || caps.Outputs[0].Name != "HEADLESS-1" || !caps.Outputs[0].PowerSupported {
167191 t.Fatalf("initial: notifications=%v wake=%d power=%d caps=%+v", notifications, wake, power, caps)
@@ -263,6 +287,60 @@ func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) {
263287 }
264288 }
265289
290+func TestOutputAddedWhileOffIsTurnedOffAndWakesWithTheOthers(t *testing.T) {
291+ ctx, cancel := context.WithCancel(context.Background())
292+ defer cancel()
293+ s := start(t, ctx, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true}, Generation: 1, Deadlines: []time.Duration{time.Second}})
294+ peer, registry, wake, power := s.peer, s.registry, s.wake, s.power
295+ commands, out, done := s.commands, s.out, s.done
296+ manager := s.ids["zwlr_output_power_manager_v1"]
297+ capability(t, out)
298+ if manager == 0 || wake == 0 || power == 0 {
299+ t.Fatal(manager, wake, power)
300+ }
301+ commands <- ports.OutputPower{On: false}
302+ if id, op, b := readWire(t, peer); id != power || op != 0 || binary.NativeEndian.Uint32(b) != 0 {
303+ t.Fatalf("off %d/%d %x", id, op, b)
304+ }
305+ // addOutput announces an output and answers its power control's initial
306+ // mode; it returns that control.
307+ addOutput := func(name, mode uint32) uint32 {
308+ global(t, peer, registry, name, "wl_output", 4)
309+ var p uint32
310+ for i := 0; i < 2; i++ {
311+ if id, _, b := readWire(t, peer); id == manager {
312+ p = binary.NativeEndian.Uint32(b)
313+ }
314+ }
315+ sendWire(t, peer, p, 0, words(mode))
316+ return p
317+ }
318+ // A display reconnecting from deep sleep comes back on: turned off.
319+ on := addOutput(14, 1)
320+ if id, op, b := readWire(t, peer); id != on || op != 0 || binary.NativeEndian.Uint32(b) != 0 {
321+ t.Fatalf("new output off %d/%d %x", id, op, b)
322+ }
323+ // One that comes back off needs no request, but is woken with the rest.
324+ off := addOutput(15, 0)
325+ sendWire(t, peer, wake, 0, nil)
326+ sendWire(t, peer, wake, 1, nil)
327+ woken := map[uint32]bool{}
328+ for range 3 {
329+ id, op, b := readWire(t, peer)
330+ if op != 0 || binary.NativeEndian.Uint32(b) != 1 {
331+ t.Fatalf("wake %d/%d %x", id, op, b)
332+ }
333+ woken[id] = true
334+ }
335+ if !woken[power] || !woken[on] || !woken[off] {
336+ t.Fatal(woken)
337+ }
338+ cancel()
339+ if err := <-done; err != nil {
340+ t.Fatal(err)
341+ }
342+}
343+
266344 func TestMissingAndDisabledCapabilities(t *testing.T) {
267345 conn, peer := sockets(t)
268346 c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Lock: true, Visual: true}}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
@@ -109,59 +109,83 @@ func nextEvent(t *testing.T, out <-chan ports.WaylandEvent) ports.WaylandEvent {
109 }109 }
110 }110 }
111 111
112-func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) {112+// started is a client running against a fake compositor that announced
113+// its globals and answered the initial bindings (one output, HEADLESS-1).
114+type started struct {
115+ peer *net.UnixConn
116+ registry, wake, power uint32
117+ ids map[string]uint32 // bound ID by interface
118+ notifications map[uint32]uint32 // idle notification by timeout (ms)
119+ commands chan ports.WaylandCommand
120+ out chan ports.WaylandEvent
121+ done chan error
122+}
123+
124+// extraGlobal is one more global announced at start.
125+type extraGlobal struct {
126+ name uint32
127+ iface string
128+ version uint32
129+}
130+
131+// start runs a client and the initial discovery; extra globals follow
132+// wl_seat, wl_output, ext_idle_notifier_v1 and zwlr_output_power_manager_v1.
133+func start(t *testing.T, ctx context.Context, options Options, extra ...extraGlobal) *started {
134+ t.Helper()
113 conn, peer := sockets(t)135 conn, peer := sockets(t)
114- c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true, Lock: true, Visual: true}, Generation: 1, Deadlines: []time.Duration{time.Second, time.Second, 2 * time.Second}}, zerowrap.New(zerowrap.Config{Output: io.Discard}))136+ c, err := New(conn, options, zerowrap.New(zerowrap.Config{Output: io.Discard}))
115 if err != nil {137 if err != nil {
116 t.Fatal(err)138 t.Fatal(err)
117 }139 }
118 _, _, body := readWire(t, peer)140 _, _, body := readWire(t, peer)
119- registry := binary.NativeEndian.Uint32(body)141+ s := &started{peer: peer, registry: binary.NativeEndian.Uint32(body), ids: map[string]uint32{}, notifications: map[uint32]uint32{}, commands: make(chan ports.WaylandCommand, 8), out: make(chan ports.WaylandEvent, 64), done: make(chan error, 1)}
120- ctx, cancel := context.WithCancel(context.Background())142+ go func() { s.done <- c.Run(ctx, s.commands, s.out) }()
121- defer cancel()
122- commands := make(chan ports.WaylandCommand, 8)
123- out := make(chan ports.WaylandEvent, 64)
124- done := make(chan error, 1)
125- go func() { done <- c.Run(ctx, commands, out) }()
126 _, _, body = readWire(t, peer)143 _, _, body = readWire(t, peer)
127- global(t, peer, registry, 10, "wl_seat", 5)144+ global(t, peer, s.registry, 10, "wl_seat", 5)
128- global(t, peer, registry, 11, "wl_output", 4)145+ global(t, peer, s.registry, 11, "wl_output", 4)
129- global(t, peer, registry, 12, "ext_idle_notifier_v1", 2)146+ global(t, peer, s.registry, 12, "ext_idle_notifier_v1", 2)
130- global(t, peer, registry, 13, "zwlr_output_power_manager_v1", 1)147+ global(t, peer, s.registry, 13, "zwlr_output_power_manager_v1", 1)
131- global(t, peer, registry, 14, "ext_session_lock_manager_v1", 1)148+ for _, g := range extra {
149+ global(t, peer, s.registry, g.name, g.iface, g.version)
150+ }
132 synchronize(t, peer, body)151 synchronize(t, peer, body)
133- ids := map[string]uint32{}
134- notifications := map[uint32]uint32{}
135- var wake, power uint32
136 for {152 for {
137 id, op, b := readWire(t, peer)153 id, op, b := readWire(t, peer)
138 if id == 1 {154 if id == 1 {
139 synchronize(t, peer, b)155 synchronize(t, peer, b)
140 break156 break
141 }157 }
142- if id == registry {158+ if id == s.registry {
143 n := int(binary.NativeEndian.Uint32(b[4:]))159 n := int(binary.NativeEndian.Uint32(b[4:]))
144 iface := string(b[8 : 8+n-1])160 iface := string(b[8 : 8+n-1])
145- ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:])161+ s.ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:])
146 if iface == "wl_output" {162 if iface == "wl_output" {
147- sendWire(t, peer, ids[iface], 4, wireString("HEADLESS-1"))163+ sendWire(t, peer, s.ids[iface], 4, wireString("HEADLESS-1"))
148 }164 }
149 continue165 continue
150 }166 }
151- if id == ids["ext_idle_notifier_v1"] {167+ if id == s.ids["ext_idle_notifier_v1"] {
152 child := binary.NativeEndian.Uint32(b)168 child := binary.NativeEndian.Uint32(b)
153- ms := binary.NativeEndian.Uint32(b[4:])
154 if op == 2 {169 if op == 2 {
155- wake = child170+ s.wake = child
156 } else {171 } else {
157- notifications[ms] = child172+ s.notifications[binary.NativeEndian.Uint32(b[4:])] = child
158 }173 }
159 }174 }
160- if id == ids["zwlr_output_power_manager_v1"] {175+ if id == s.ids["zwlr_output_power_manager_v1"] {
161- power = binary.NativeEndian.Uint32(b)176+ s.power = binary.NativeEndian.Uint32(b)
162- sendWire(t, peer, power, 0, words(1))177+ sendWire(t, peer, s.power, 0, words(1))
163 }178 }
164 }179 }
180+ return s
181+}
182+
183+func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) {
184+ ctx, cancel := context.WithCancel(context.Background())
185+ defer cancel()
186+ s := start(t, ctx, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true, Lock: true, Visual: true}, Generation: 1, Deadlines: []time.Duration{time.Second, time.Second, 2 * time.Second}}, extraGlobal{14, "ext_session_lock_manager_v1", 1})
187+ peer, registry, ids, notifications, wake, power := s.peer, s.registry, s.ids, s.notifications, s.wake, s.power
188+ commands, out, done := s.commands, s.out, s.done
165 caps := capability(t, out)189 caps := capability(t, out)
166 if len(notifications) != 2 || wake == 0 || power == 0 || len(caps.Outputs) != 1 || caps.Outputs[0].Name != "HEADLESS-1" || !caps.Outputs[0].PowerSupported {190 if len(notifications) != 2 || wake == 0 || power == 0 || len(caps.Outputs) != 1 || caps.Outputs[0].Name != "HEADLESS-1" || !caps.Outputs[0].PowerSupported {
167 t.Fatalf("initial: notifications=%v wake=%d power=%d caps=%+v", notifications, wake, power, caps)191 t.Fatalf("initial: notifications=%v wake=%d power=%d caps=%+v", notifications, wake, power, caps)
@@ -263,6 +287,60 @@ func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) {
263 }287 }
264 }288 }
265 289
290+func TestOutputAddedWhileOffIsTurnedOffAndWakesWithTheOthers(t *testing.T) {
291+ ctx, cancel := context.WithCancel(context.Background())
292+ defer cancel()
293+ s := start(t, ctx, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true}, Generation: 1, Deadlines: []time.Duration{time.Second}})
294+ peer, registry, wake, power := s.peer, s.registry, s.wake, s.power
295+ commands, out, done := s.commands, s.out, s.done
296+ manager := s.ids["zwlr_output_power_manager_v1"]
297+ capability(t, out)
298+ if manager == 0 || wake == 0 || power == 0 {
299+ t.Fatal(manager, wake, power)
300+ }
301+ commands <- ports.OutputPower{On: false}
302+ if id, op, b := readWire(t, peer); id != power || op != 0 || binary.NativeEndian.Uint32(b) != 0 {
303+ t.Fatalf("off %d/%d %x", id, op, b)
304+ }
305+ // addOutput announces an output and answers its power control's initial
306+ // mode; it returns that control.
307+ addOutput := func(name, mode uint32) uint32 {
308+ global(t, peer, registry, name, "wl_output", 4)
309+ var p uint32
310+ for i := 0; i < 2; i++ {
311+ if id, _, b := readWire(t, peer); id == manager {
312+ p = binary.NativeEndian.Uint32(b)
313+ }
314+ }
315+ sendWire(t, peer, p, 0, words(mode))
316+ return p
317+ }
318+ // A display reconnecting from deep sleep comes back on: turned off.
319+ on := addOutput(14, 1)
320+ if id, op, b := readWire(t, peer); id != on || op != 0 || binary.NativeEndian.Uint32(b) != 0 {
321+ t.Fatalf("new output off %d/%d %x", id, op, b)
322+ }
323+ // One that comes back off needs no request, but is woken with the rest.
324+ off := addOutput(15, 0)
325+ sendWire(t, peer, wake, 0, nil)
326+ sendWire(t, peer, wake, 1, nil)
327+ woken := map[uint32]bool{}
328+ for range 3 {
329+ id, op, b := readWire(t, peer)
330+ if op != 0 || binary.NativeEndian.Uint32(b) != 1 {
331+ t.Fatalf("wake %d/%d %x", id, op, b)
332+ }
333+ woken[id] = true
334+ }
335+ if !woken[power] || !woken[on] || !woken[off] {
336+ t.Fatal(woken)
337+ }
338+ cancel()
339+ if err := <-done; err != nil {
340+ t.Fatal(err)
341+ }
342+}
343+
266 func TestMissingAndDisabledCapabilities(t *testing.T) {344 func TestMissingAndDisabledCapabilities(t *testing.T) {
267 conn, peer := sockets(t)345 conn, peer := sockets(t)
268 c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Lock: true, Visual: true}}, zerowrap.New(zerowrap.Config{Output: io.Discard}))346 c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Lock: true, Visual: true}}, zerowrap.New(zerowrap.Config{Output: io.Discard}))

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 9

↑↓ moveenter openesc close