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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 120 | 120 | W, H int | |
| 121 | 121 | Hz float64 // 0: highest refresh at W x H | |
| 122 | 122 | 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 | 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 | 155 | chosen = c | |
| 154 | 156 | break | |
| 155 | 157 | } | |
| 156 | - | if chosen == nil { | |
| 158 | + | if chosen == nil && !want.Strict { | |
| 157 | 159 | chosen = c | |
| 158 | 160 | } | |
| 159 | 161 | } | |
| @@ -120,6 +120,8 @@ type Want struct { | |||||
| 120 | W, H int | 120 | W, H int | ||
| 121 | Hz float64 // 0: highest refresh at W x H | 121 | Hz float64 // 0: highest refresh at W x H | ||
| 122 | Disabled map[string]bool | 122 | 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 = c | 155 | chosen = c | ||
| 154 | break | 156 | break | ||
| 155 | } | 157 | } | ||
| 156 | - | if chosen == nil { | 158 | + | if chosen == nil && !want.Strict { |
| 157 | chosen = c | 159 | chosen = c | ||
| 158 | } | 160 | } | ||
| 159 | } | 161 | } | ||
M internal/adapters/drm/kms_test.go +2 -0
| @@ -58,6 +58,8 @@ func TestPickConnector(t *testing.T) { | |||
| 58 | 58 | {"unknown mode falls back to preferred", []connector{lg}, Want{Name: "DP-2", W: 800, H: 600}, "DP-2", "5120x2160@165.000", false}, | |
| 59 | 59 | {"missing name falls back", []connector{tv}, Want{Name: "DP-2"}, "HDMI-A-1", "3840x2160@60.000", false}, | |
| 60 | 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 | 63 | {"all disabled", []connector{tv}, Want{Disabled: map[string]bool{"HDMI-A-1": true}}, "", "", true}, | |
| 62 | 64 | } { | |
| 63 | 65 | 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 ( | |||
| 8 | 8 | ||
| 9 | 9 | // Server-side decorations: nefertty draws the border, so clients must not draw | |
| 10 | 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 | 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 | 19 | func (decorationManager) Destroy(*xdgdecoration.ZxdgDecorationManagerV1) {} | |
| 20 | 20 | ||
| 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) | |
| 23 | 24 | if err != nil { | |
| 24 | 25 | return | |
| 25 | 26 | } | |
| 26 | - | // Sent before the toplevel's configure, which the client waits for. | |
| 27 | - | d.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide)) | |
| 27 | + | d.send(res) | |
| 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 | 37 | // 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) } | |
| 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 | 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 | } | |
| @@ -8,34 +8,44 @@ import ( | |||||
| 8 | 8 | ||||
| 9 | // Server-side decorations: nefertty draws the border, so clients must not draw | 9 | // 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 | return | 25 | 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 { | |||
| 13 | 13 | func() error { return registerXDG(d, s) }, | |
| 14 | 14 | func() error { return registerLayer(d, o, s) }, | |
| 15 | 15 | func() error { return registerXDGOutput(d, o) }, | |
| 16 | - | func() error { return registerDecoration(d) }, | |
| 16 | + | func() error { return registerDecoration(d, s) }, | |
| 17 | 17 | func() error { | |
| 18 | 18 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | |
| 19 | 19 | }, | |
| @@ -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 ( | |||
| 10 | 10 | ||
| 11 | 11 | "github.com/bnema/nefertty/internal/ports" | |
| 12 | 12 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 13 | - | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 14 | 13 | "github.com/bnema/purego-libwayland/server" | |
| 15 | 14 | "github.com/bnema/zerowrap" | |
| 16 | 15 | "golang.org/x/sys/unix" | |
| @@ -381,26 +380,9 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 381 | 380 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window") | |
| 382 | 381 | 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 | 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) | |
| 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() | |
| 404 | 386 | case ports.CloseWindow: | |
| 405 | 387 | w := s.windows[c.ID] | |
| 406 | 388 | 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 | return | 381 | 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 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "encoding/binary" | |
| 4 | 5 | "github.com/bnema/nefertty/internal/ports" | |
| 5 | 6 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 6 | 7 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| @@ -77,6 +78,32 @@ type window struct { | |||
| 77 | 78 | xdg *xdgSurface | |
| 78 | 79 | appID, title string | |
| 79 | 80 | 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 | 109 | func (w *window) unmap() { | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package wayland | 1 | 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 *xdgSurface | 78 | xdg *xdgSurface | ||
| 78 | appID, title string | 79 | appID, title string | ||
| 79 | mapped bool | 80 | 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 | |||
| 49 | 49 | cards, _ := filepath.Glob("/dev/dri/card[0-9]*") | |
| 50 | 50 | sort.Strings(cards) | |
| 51 | 51 | 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 | |
| 64 | 76 | } | |
| 65 | - | return &drmBackend{seat: s, out: out, fd: fd}, nil | |
| 66 | 77 | } | |
| 67 | 78 | s.Close() | |
| 68 | 79 | 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 []error | 51 | 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 | - | continue | 56 | + | 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 | - | continue | 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 | |||
| 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 { | |||
| 145 | 145 | return nil | |
| 146 | 146 | } | |
| 147 | 147 | } | |
| 148 | + | ||
| 148 | 149 | // clientRect is the placement minus the border drawn around the client. | |
| 149 | 150 | // The renderer applies the same inset (ports.Border). | |
| 150 | 151 | func (c *Core) clientRect(p Placement) Rect { | |
| @@ -145,6 +145,7 @@ func (c *Core) command(ctx context.Context, v ports.ClientCommand) error { | |||||
| 145 | return nil | 145 | 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 { | ||