detached e3eccfd

fix(drm,wayland): search every card for the configured output and reconfigure on late decoration requests

commit
e3eccfdbb04a05e879bfab4497a6aa72738f6822
author
brice · · 2026-09-24 09:38 UTC
parent
493b4a9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +83 -48
M internal/adapters/drm/kms.go +3 -1
@@ -120,6 +120,8 @@ type Want struct {
120120 W, H int
121121 Hz float64 // 0: highest refresh at W x H
122122 Disabled map[string]bool
123+ // Strict accepts only the connector named Name (used to search every card first).
124+ Strict bool
123125 }
124126
125127 // refreshMilli is the exact refresh in mHz, computed from the timings like wlroots.
@@ -153,7 +155,7 @@ func pickConnector(conns []connector, want Want) (connector, modeInfo, error) {
153155 chosen = c
154156 break
155157 }
156- if chosen == nil {
158+ if chosen == nil && !want.Strict {
157159 chosen = c
158160 }
159161 }
@@ -120,6 +120,8 @@ type Want struct {
120 W, H int120 W, H int
121 Hz float64 // 0: highest refresh at W x H121 Hz float64 // 0: highest refresh at W x H
122 Disabled map[string]bool122 Disabled map[string]bool
123+ // Strict accepts only the connector named Name (used to search every card first).
124+ Strict bool
123 }125 }
124 126
125 // refreshMilli is the exact refresh in mHz, computed from the timings like wlroots.127 // refreshMilli is the exact refresh in mHz, computed from the timings like wlroots.
@@ -153,7 +155,7 @@ func pickConnector(conns []connector, want Want) (connector, modeInfo, error) {
153 chosen = c155 chosen = c
154 break156 break
155 }157 }
156- if chosen == nil {158+ if chosen == nil && !want.Strict {
157 chosen = c159 chosen = c
158 }160 }
159 }161 }
M internal/adapters/drm/kms_test.go +2 -0
@@ -58,6 +58,8 @@ func TestPickConnector(t *testing.T) {
5858 {"unknown mode falls back to preferred", []connector{lg}, Want{Name: "DP-2", W: 800, H: 600}, "DP-2", "5120x2160@165.000", false},
5959 {"missing name falls back", []connector{tv}, Want{Name: "DP-2"}, "HDMI-A-1", "3840x2160@60.000", false},
6060 {"disabled skipped", []connector{tv, lg}, Want{Disabled: map[string]bool{"HDMI-A-1": true}}, "DP-2", "5120x2160@165.000", false},
61+ {"strict finds named", []connector{tv, lg}, Want{Name: "DP-2", Strict: true}, "DP-2", "5120x2160@165.000", false},
62+ {"strict without named", []connector{tv}, Want{Name: "DP-2", Strict: true}, "", "", true},
6163 {"all disabled", []connector{tv}, Want{Disabled: map[string]bool{"HDMI-A-1": true}}, "", "", true},
6264 } {
6365 c, m, err := pickConnector(tc.conns, tc.want)
@@ -58,6 +58,8 @@ func TestPickConnector(t *testing.T) {
58 {"unknown mode falls back to preferred", []connector{lg}, Want{Name: "DP-2", W: 800, H: 600}, "DP-2", "5120x2160@165.000", false},58 {"unknown mode falls back to preferred", []connector{lg}, Want{Name: "DP-2", W: 800, H: 600}, "DP-2", "5120x2160@165.000", false},
59 {"missing name falls back", []connector{tv}, Want{Name: "DP-2"}, "HDMI-A-1", "3840x2160@60.000", false},59 {"missing name falls back", []connector{tv}, Want{Name: "DP-2"}, "HDMI-A-1", "3840x2160@60.000", false},
60 {"disabled skipped", []connector{tv, lg}, Want{Disabled: map[string]bool{"HDMI-A-1": true}}, "DP-2", "5120x2160@165.000", false},60 {"disabled skipped", []connector{tv, lg}, Want{Disabled: map[string]bool{"HDMI-A-1": true}}, "DP-2", "5120x2160@165.000", false},
61+ {"strict finds named", []connector{tv, lg}, Want{Name: "DP-2", Strict: true}, "DP-2", "5120x2160@165.000", false},
62+ {"strict without named", []connector{tv}, Want{Name: "DP-2", Strict: true}, "", "", true},
61 {"all disabled", []connector{tv}, Want{Disabled: map[string]bool{"HDMI-A-1": true}}, "", "", true},63 {"all disabled", []connector{tv}, Want{Disabled: map[string]bool{"HDMI-A-1": true}}, "", "", true},
62 } {64 } {
63 c, m, err := pickConnector(tc.conns, tc.want)65 c, m, err := pickConnector(tc.conns, tc.want)
M internal/adapters/wayland/decoration.go +23 -13
@@ -8,34 +8,44 @@ import (
88
99 // Server-side decorations: nefertty draws the border, so clients must not draw
1010 // their own title bar or shadow (which would shrink the visible content).
11-type decorationManager struct{}
11+type decorationManager struct{ s *Server }
1212
13-func registerDecoration(d *server.Display) error {
13+func registerDecoration(d *server.Display, s *Server) error {
1414 return xdgdecoration.NewZxdgDecorationManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
15- _, _ = xdgdecoration.NewZxdgDecorationManagerV1(c, int32(v), id, decorationManager{})
15+ _, _ = xdgdecoration.NewZxdgDecorationManagerV1(c, int32(v), id, decorationManager{s})
1616 })
1717 }
1818
1919 func (decorationManager) Destroy(*xdgdecoration.ZxdgDecorationManagerV1) {}
2020
21-func (decorationManager) GetToplevelDecoration(r *xdgdecoration.ZxdgDecorationManagerV1, id uint32, _ *xdgshell.Toplevel) {
22- d, err := xdgdecoration.NewZxdgToplevelDecorationV1(r.Client(), r.Version(), id, decoration{})
21+func (m decorationManager) GetToplevelDecoration(r *xdgdecoration.ZxdgDecorationManagerV1, id uint32, t *xdgshell.Toplevel) {
22+ d := &decoration{s: m.s, toplevel: t}
23+ res, err := xdgdecoration.NewZxdgToplevelDecorationV1(r.Client(), r.Version(), id, d)
2324 if err != nil {
2425 return
2526 }
26- // Sent before the toplevel's configure, which the client waits for.
27- d.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide))
27+ d.send(res)
2828 }
2929
30-type decoration struct{}
30+type decoration struct {
31+ s *Server
32+ toplevel *xdgshell.Toplevel
33+}
3134
32-func (decoration) Destroy(*xdgdecoration.ZxdgToplevelDecorationV1) {}
35+func (*decoration) Destroy(*xdgdecoration.ZxdgToplevelDecorationV1) {}
3336
3437 // Any requested mode is answered with server-side.
35-func (decoration) SetMode(r *xdgdecoration.ZxdgToplevelDecorationV1, _ uint32) {
36- r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide))
37-}
38+func (d *decoration) SetMode(r *xdgdecoration.ZxdgToplevelDecorationV1, _ uint32) { d.send(r) }
39+func (d *decoration) UnsetMode(r *xdgdecoration.ZxdgToplevelDecorationV1) { d.send(r) }
3840
39-func (decoration) UnsetMode(r *xdgdecoration.ZxdgToplevelDecorationV1) {
41+// send answers server-side. The mode applies on the next xdg_surface.configure,
42+// so a window already configured is configured again with its last size.
43+func (d *decoration) send(r *xdgdecoration.ZxdgToplevelDecorationV1) {
4044 r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide))
45+ for _, w := range d.s.windows {
46+ if w.toplevel == d.toplevel && w.hasLast && w.toplevel.Resource.Alive() && w.xdg.resource.Resource.Alive() {
47+ w.sendConfigure()
48+ return
49+ }
50+ }
4151 }
@@ -8,34 +8,44 @@ import (
8 8
9 // Server-side decorations: nefertty draws the border, so clients must not draw9 // Server-side decorations: nefertty draws the border, so clients must not draw
10 // their own title bar or shadow (which would shrink the visible content).10 // their own title bar or shadow (which would shrink the visible content).
11-type decorationManager struct{}11+type decorationManager struct{ s *Server }
12 12
13-func registerDecoration(d *server.Display) error {13+func registerDecoration(d *server.Display, s *Server) error {
14 return xdgdecoration.NewZxdgDecorationManagerV1Global(d, 1, func(c server.Client, v, id uint32) {14 return xdgdecoration.NewZxdgDecorationManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
15- _, _ = xdgdecoration.NewZxdgDecorationManagerV1(c, int32(v), id, decorationManager{})15+ _, _ = xdgdecoration.NewZxdgDecorationManagerV1(c, int32(v), id, decorationManager{s})
16 })16 })
17 }17 }
18 18
19 func (decorationManager) Destroy(*xdgdecoration.ZxdgDecorationManagerV1) {}19 func (decorationManager) Destroy(*xdgdecoration.ZxdgDecorationManagerV1) {}
20 20
21-func (decorationManager) GetToplevelDecoration(r *xdgdecoration.ZxdgDecorationManagerV1, id uint32, _ *xdgshell.Toplevel) {21+func (m decorationManager) GetToplevelDecoration(r *xdgdecoration.ZxdgDecorationManagerV1, id uint32, t *xdgshell.Toplevel) {
22- d, err := xdgdecoration.NewZxdgToplevelDecorationV1(r.Client(), r.Version(), id, decoration{})22+ d := &decoration{s: m.s, toplevel: t}
23+ res, err := xdgdecoration.NewZxdgToplevelDecorationV1(r.Client(), r.Version(), id, d)
23 if err != nil {24 if err != nil {
24 return25 return
25 }26 }
26- // Sent before the toplevel's configure, which the client waits for.27+ d.send(res)
27- d.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide))
28 }28 }
29 29
30-type decoration struct{}30+type decoration struct {
31+ s *Server
32+ toplevel *xdgshell.Toplevel
33+}
31 34
32-func (decoration) Destroy(*xdgdecoration.ZxdgToplevelDecorationV1) {}35+func (*decoration) Destroy(*xdgdecoration.ZxdgToplevelDecorationV1) {}
33 36
34 // Any requested mode is answered with server-side.37 // Any requested mode is answered with server-side.
35-func (decoration) SetMode(r *xdgdecoration.ZxdgToplevelDecorationV1, _ uint32) {38+func (d *decoration) SetMode(r *xdgdecoration.ZxdgToplevelDecorationV1, _ uint32) { d.send(r) }
36- r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide))39+func (d *decoration) UnsetMode(r *xdgdecoration.ZxdgToplevelDecorationV1) { d.send(r) }
37-}
38 40
39-func (decoration) UnsetMode(r *xdgdecoration.ZxdgToplevelDecorationV1) {41+// send answers server-side. The mode applies on the next xdg_surface.configure,
42+// so a window already configured is configured again with its last size.
43+func (d *decoration) send(r *xdgdecoration.ZxdgToplevelDecorationV1) {
40 r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide))44 r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide))
45+ for _, w := range d.s.windows {
46+ if w.toplevel == d.toplevel && w.hasLast && w.toplevel.Resource.Alive() && w.xdg.resource.Resource.Alive() {
47+ w.sendConfigure()
48+ return
49+ }
50+ }
41 }51 }
M internal/adapters/wayland/globals.go +1 -1
@@ -13,7 +13,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
1313 func() error { return registerXDG(d, s) },
1414 func() error { return registerLayer(d, o, s) },
1515 func() error { return registerXDGOutput(d, o) },
16- func() error { return registerDecoration(d) },
16+ func() error { return registerDecoration(d, s) },
1717 func() error {
1818 return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })
1919 },
@@ -13,7 +13,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
13 func() error { return registerXDG(d, s) },13 func() error { return registerXDG(d, s) },
14 func() error { return registerLayer(d, o, s) },14 func() error { return registerLayer(d, o, s) },
15 func() error { return registerXDGOutput(d, o) },15 func() error { return registerXDGOutput(d, o) },
16- func() error { return registerDecoration(d) },16+ func() error { return registerDecoration(d, s) },
17 func() error {17 func() error {
18 return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })18 return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })
19 },19 },
M internal/adapters/wayland/server.go +2 -20
@@ -10,7 +10,6 @@ import (
1010
1111 "github.com/bnema/nefertty/internal/ports"
1212 "github.com/bnema/purego-libwayland/protocol/wayland"
13- "github.com/bnema/purego-libwayland/protocol/xdgshell"
1413 "github.com/bnema/purego-libwayland/server"
1514 "github.com/bnema/zerowrap"
1615 "golang.org/x/sys/unix"
@@ -381,26 +380,9 @@ func (s *Server) apply(cmd ports.ClientCommand) {
381380 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window")
382381 return
383382 }
384- var states []byte
385- if c.Fullscreen {
386- states = binary.LittleEndian.AppendUint32(states, uint32(xdgshell.ToplevelStateFullscreen))
387- }
388- if c.Activated {
389- states = binary.LittleEndian.AppendUint32(states, uint32(xdgshell.ToplevelStateActivated))
390- }
391- if !c.Fullscreen {
392- // Tiled: the client must use exactly this size (no CSD shadows or rounding).
393- for _, st := range []xdgshell.ToplevelState{xdgshell.ToplevelStateTiledLeft, xdgshell.ToplevelStateTiledRight, xdgshell.ToplevelStateTiledTop, xdgshell.ToplevelStateTiledBottom} {
394- if w.toplevel.Version() >= 2 {
395- states = binary.LittleEndian.AppendUint32(states, uint32(st))
396- }
397- }
398- }
399383 s.log.Info().Uint64("id", uint64(c.ID)).Int("w", c.Width).Int("h", c.Height).Bool("fullscreen", c.Fullscreen).Bool("activated", c.Activated).Msg("configure")
400- w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), states)
401- s.serial++
402- w.xdg.resource.SendConfigure(s.serial)
403- w.xdg.serials = append(w.xdg.serials, s.serial)
384+ w.last, w.hasLast = c, true
385+ w.sendConfigure()
404386 case ports.CloseWindow:
405387 w := s.windows[c.ID]
406388 if w == nil || !w.toplevel.Resource.Alive() {
@@ -10,7 +10,6 @@ import (
10 10
11 "github.com/bnema/nefertty/internal/ports"11 "github.com/bnema/nefertty/internal/ports"
12 "github.com/bnema/purego-libwayland/protocol/wayland"12 "github.com/bnema/purego-libwayland/protocol/wayland"
13- "github.com/bnema/purego-libwayland/protocol/xdgshell"
14 "github.com/bnema/purego-libwayland/server"13 "github.com/bnema/purego-libwayland/server"
15 "github.com/bnema/zerowrap"14 "github.com/bnema/zerowrap"
16 "golang.org/x/sys/unix"15 "golang.org/x/sys/unix"
@@ -381,26 +380,9 @@ func (s *Server) apply(cmd ports.ClientCommand) {
381 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window")380 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window")
382 return381 return
383 }382 }
384- var states []byte
385- if c.Fullscreen {
386- states = binary.LittleEndian.AppendUint32(states, uint32(xdgshell.ToplevelStateFullscreen))
387- }
388- if c.Activated {
389- states = binary.LittleEndian.AppendUint32(states, uint32(xdgshell.ToplevelStateActivated))
390- }
391- if !c.Fullscreen {
392- // Tiled: the client must use exactly this size (no CSD shadows or rounding).
393- for _, st := range []xdgshell.ToplevelState{xdgshell.ToplevelStateTiledLeft, xdgshell.ToplevelStateTiledRight, xdgshell.ToplevelStateTiledTop, xdgshell.ToplevelStateTiledBottom} {
394- if w.toplevel.Version() >= 2 {
395- states = binary.LittleEndian.AppendUint32(states, uint32(st))
396- }
397- }
398- }
399 s.log.Info().Uint64("id", uint64(c.ID)).Int("w", c.Width).Int("h", c.Height).Bool("fullscreen", c.Fullscreen).Bool("activated", c.Activated).Msg("configure")383 s.log.Info().Uint64("id", uint64(c.ID)).Int("w", c.Width).Int("h", c.Height).Bool("fullscreen", c.Fullscreen).Bool("activated", c.Activated).Msg("configure")
400- w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), states)384+ w.last, w.hasLast = c, true
401- s.serial++385+ w.sendConfigure()
402- w.xdg.resource.SendConfigure(s.serial)
403- w.xdg.serials = append(w.xdg.serials, s.serial)
404 case ports.CloseWindow:386 case ports.CloseWindow:
405 w := s.windows[c.ID]387 w := s.windows[c.ID]
406 if w == nil || !w.toplevel.Resource.Alive() {388 if w == nil || !w.toplevel.Resource.Alive() {
M internal/adapters/wayland/xdg.go +27 -0
@@ -1,6 +1,7 @@
11 package wayland
22
33 import (
4+ "encoding/binary"
45 "github.com/bnema/nefertty/internal/ports"
56 "github.com/bnema/purego-libwayland/protocol/wayland"
67 "github.com/bnema/purego-libwayland/protocol/xdgshell"
@@ -77,6 +78,32 @@ type window struct {
7778 xdg *xdgSurface
7879 appID, title string
7980 mapped bool
81+ // last is the most recent core configure, resent when decoration mode changes.
82+ last ports.ConfigureWindow
83+ hasLast bool
84+}
85+
86+// sendConfigure sends w.last as xdg_toplevel + xdg_surface configure.
87+func (w *window) sendConfigure() {
88+ c := w.last
89+ var states []byte
90+ if c.Fullscreen {
91+ states = binary.LittleEndian.AppendUint32(states, uint32(xdgshell.ToplevelStateFullscreen))
92+ }
93+ if c.Activated {
94+ states = binary.LittleEndian.AppendUint32(states, uint32(xdgshell.ToplevelStateActivated))
95+ }
96+ if !c.Fullscreen && w.toplevel.Version() >= 2 {
97+ // Tiled: the client must use exactly this size (no CSD shadows or rounding).
98+ for _, st := range []xdgshell.ToplevelState{xdgshell.ToplevelStateTiledLeft, xdgshell.ToplevelStateTiledRight, xdgshell.ToplevelStateTiledTop, xdgshell.ToplevelStateTiledBottom} {
99+ states = binary.LittleEndian.AppendUint32(states, uint32(st))
100+ }
101+ }
102+ w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), states)
103+ s := w.xdg.server
104+ s.serial++
105+ w.xdg.resource.SendConfigure(s.serial)
106+ w.xdg.serials = append(w.xdg.serials, s.serial)
80107 }
81108
82109 func (w *window) unmap() {
@@ -1,6 +1,7 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "encoding/binary"
4 "github.com/bnema/nefertty/internal/ports"5 "github.com/bnema/nefertty/internal/ports"
5 "github.com/bnema/purego-libwayland/protocol/wayland"6 "github.com/bnema/purego-libwayland/protocol/wayland"
6 "github.com/bnema/purego-libwayland/protocol/xdgshell"7 "github.com/bnema/purego-libwayland/protocol/xdgshell"
@@ -77,6 +78,32 @@ type window struct {
77 xdg *xdgSurface78 xdg *xdgSurface
78 appID, title string79 appID, title string
79 mapped bool80 mapped bool
81+ // last is the most recent core configure, resent when decoration mode changes.
82+ last ports.ConfigureWindow
83+ hasLast bool
84+}
85+
86+// sendConfigure sends w.last as xdg_toplevel + xdg_surface configure.
87+func (w *window) sendConfigure() {
88+ c := w.last
89+ var states []byte
90+ if c.Fullscreen {
91+ states = binary.LittleEndian.AppendUint32(states, uint32(xdgshell.ToplevelStateFullscreen))
92+ }
93+ if c.Activated {
94+ states = binary.LittleEndian.AppendUint32(states, uint32(xdgshell.ToplevelStateActivated))
95+ }
96+ if !c.Fullscreen && w.toplevel.Version() >= 2 {
97+ // Tiled: the client must use exactly this size (no CSD shadows or rounding).
98+ for _, st := range []xdgshell.ToplevelState{xdgshell.ToplevelStateTiledLeft, xdgshell.ToplevelStateTiledRight, xdgshell.ToplevelStateTiledTop, xdgshell.ToplevelStateTiledBottom} {
99+ states = binary.LittleEndian.AppendUint32(states, uint32(st))
100+ }
101+ }
102+ w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), states)
103+ s := w.xdg.server
104+ s.serial++
105+ w.xdg.resource.SendConfigure(s.serial)
106+ w.xdg.serials = append(w.xdg.serials, s.serial)
80 }107 }
81 108
82 func (w *window) unmap() {109 func (w *window) unmap() {
M internal/app/drm.go +24 -13
@@ -49,20 +49,31 @@ func openDRM(ctx context.Context, outputs []ports.OutputConfig) (*drmBackend, er
4949 cards, _ := filepath.Glob("/dev/dri/card[0-9]*")
5050 sort.Strings(cards)
5151 var errs []error
52- for _, card := range cards {
53- fd, err := s.OpenDevice(card)
54- if err != nil {
55- errs = append(errs, err)
56- continue
57- }
58- out, err := drm.Open(fd, card, want, log)
59- if err != nil {
60- log.Warn().Err(err).Str("card", card).Msg("card unusable")
61- errs = append(errs, err)
62- s.CloseDevice(fd)
63- continue
52+ // First pass: the configured output on any card; second: any usable output.
53+ passes := []drm.Want{want}
54+ if want.Name != "" {
55+ strict := want
56+ strict.Strict = true
57+ passes = []drm.Want{strict, want}
58+ }
59+ for _, w := range passes {
60+ for _, card := range cards {
61+ fd, err := s.OpenDevice(card)
62+ if err != nil {
63+ errs = append(errs, err)
64+ continue
65+ }
66+ out, err := drm.Open(fd, card, w, log)
67+ if err != nil {
68+ if !w.Strict {
69+ log.Warn().Err(err).Str("card", card).Msg("card unusable")
70+ errs = append(errs, err)
71+ }
72+ s.CloseDevice(fd)
73+ continue
74+ }
75+ return &drmBackend{seat: s, out: out, fd: fd}, nil
6476 }
65- return &drmBackend{seat: s, out: out, fd: fd}, nil
6677 }
6778 s.Close()
6879 err = fmt.Errorf("no usable DRM card in %v: %w", cards, errors.Join(errs...))
@@ -49,20 +49,31 @@ func openDRM(ctx context.Context, outputs []ports.OutputConfig) (*drmBackend, er
49 cards, _ := filepath.Glob("/dev/dri/card[0-9]*")49 cards, _ := filepath.Glob("/dev/dri/card[0-9]*")
50 sort.Strings(cards)50 sort.Strings(cards)
51 var errs []error51 var errs []error
52- for _, card := range cards {52+ // First pass: the configured output on any card; second: any usable output.
53- fd, err := s.OpenDevice(card)53+ passes := []drm.Want{want}
54- if err != nil {54+ if want.Name != "" {
55- errs = append(errs, err)55+ strict := want
56- continue56+ strict.Strict = true
57- }57+ passes = []drm.Want{strict, want}
58- out, err := drm.Open(fd, card, want, log)58+ }
59- if err != nil {59+ for _, w := range passes {
60- log.Warn().Err(err).Str("card", card).Msg("card unusable")60+ for _, card := range cards {
61- errs = append(errs, err)61+ fd, err := s.OpenDevice(card)
62- s.CloseDevice(fd)62+ if err != nil {
63- continue63+ errs = append(errs, err)
64+ continue
65+ }
66+ out, err := drm.Open(fd, card, w, log)
67+ if err != nil {
68+ if !w.Strict {
69+ log.Warn().Err(err).Str("card", card).Msg("card unusable")
70+ errs = append(errs, err)
71+ }
72+ s.CloseDevice(fd)
73+ continue
74+ }
75+ return &drmBackend{seat: s, out: out, fd: fd}, nil
64 }76 }
65- return &drmBackend{seat: s, out: out, fd: fd}, nil
66 }77 }
67 s.Close()78 s.Close()
68 err = fmt.Errorf("no usable DRM card in %v: %w", cards, errors.Join(errs...))79 err = fmt.Errorf("no usable DRM card in %v: %w", cards, errors.Join(errs...))
M internal/core/core.go +1 -0
@@ -145,6 +145,7 @@ func (c *Core) command(ctx context.Context, v ports.ClientCommand) error {
145145 return nil
146146 }
147147 }
148+
148149 // clientRect is the placement minus the border drawn around the client.
149150 // The renderer applies the same inset (ports.Border).
150151 func (c *Core) clientRect(p Placement) Rect {
@@ -145,6 +145,7 @@ func (c *Core) command(ctx context.Context, v ports.ClientCommand) error {
145 return nil145 return nil
146 }146 }
147 }147 }
148+
148 // clientRect is the placement minus the border drawn around the client.149 // clientRect is the placement minus the border drawn around the client.
149 // The renderer applies the same inset (ports.Border).150 // The renderer applies the same inset (ports.Border).
150 func (c *Core) clientRect(p Placement) Rect {151 func (c *Core) clientRect(p Placement) Rect {

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 25

Tags 10

↑↓ moveenter openesc close