detached f46a2b0

feat(wayland): track surface buffers and complete frame callbacks

commit
f46a2b0dc219dd4abbc9a609baf7ff41f8f552fb
author
brice · · 2026-09-23 12:14 UTC
parent
4fb1824
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +278 -23
M internal/adapters/wayland/globals.go +10 -20
@@ -6,10 +6,11 @@ import (
66 "golang.org/x/sys/unix"
77 )
88
9-func registerGlobals(d *server.Display, o Options) error {
9+func registerGlobals(d *server.Display, o Options, s *Server) error {
1010 for _, register := range []func() error{
11+ func() error { return registerXDG(d, s) },
1112 func() error {
12- return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{}) })
13+ return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })
1314 },
1415 func() error {
1516 return wayland.NewShmGlobal(d, 1, func(c server.Client, v, id uint32) {
@@ -62,31 +63,20 @@ func registerGlobals(d *server.Display, o Options) error {
6263 return nil
6364 }
6465
65-type compositor struct{}
66+type compositor struct{ server *Server }
6667
67-func (compositor) CreateSurface(r *wayland.Compositor, id uint32) {
68- wayland.NewSurface(r.Client(), r.Version(), id, surface{})
68+func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) {
69+ state := &surface{server: c.server}
70+ if w, err := wayland.NewSurface(r.Client(), r.Version(), id, state); err == nil {
71+ c.server.surfaces[w.Resource] = state
72+ w.OnDestroy = func() { delete(c.server.surfaces, w.Resource); state.Destroy(w) }
73+ }
6974 }
7075 func (compositor) CreateRegion(r *wayland.Compositor, id uint32) {
7176 wayland.NewRegion(r.Client(), r.Version(), id, region{})
7277 }
7378 func (compositor) Release(*wayland.Compositor) {}
7479
75-type surface struct{}
76-
77-func (surface) Destroy(*wayland.Surface) {}
78-func (surface) Attach(*wayland.Surface, *wayland.Buffer, int32, int32) {}
79-func (surface) Damage(*wayland.Surface, int32, int32, int32, int32) {}
80-func (surface) Frame(*wayland.Surface, uint32) {}
81-func (surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {}
82-func (surface) SetInputRegion(*wayland.Surface, *wayland.Region) {}
83-func (surface) Commit(*wayland.Surface) {}
84-func (surface) SetBufferTransform(*wayland.Surface, int32) {}
85-func (surface) SetBufferScale(*wayland.Surface, int32) {}
86-func (surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
87-func (surface) Offset(*wayland.Surface, int32, int32) {}
88-func (surface) GetRelease(*wayland.Surface, uint32) {}
89-
9080 type region struct{}
9181
9282 func (region) Destroy(*wayland.Region) {}
@@ -6,10 +6,11 @@ import (
6 "golang.org/x/sys/unix"6 "golang.org/x/sys/unix"
7 )7 )
8 8
9-func registerGlobals(d *server.Display, o Options) error {9+func registerGlobals(d *server.Display, o Options, s *Server) error {
10 for _, register := range []func() error{10 for _, register := range []func() error{
11+ func() error { return registerXDG(d, s) },
11 func() error {12 func() error {
12- return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{}) })13+ return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })
13 },14 },
14 func() error {15 func() error {
15 return wayland.NewShmGlobal(d, 1, func(c server.Client, v, id uint32) {16 return wayland.NewShmGlobal(d, 1, func(c server.Client, v, id uint32) {
@@ -62,31 +63,20 @@ func registerGlobals(d *server.Display, o Options) error {
62 return nil63 return nil
63 }64 }
64 65
65-type compositor struct{}66+type compositor struct{ server *Server }
66 67
67-func (compositor) CreateSurface(r *wayland.Compositor, id uint32) {68+func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) {
68- wayland.NewSurface(r.Client(), r.Version(), id, surface{})69+ state := &surface{server: c.server}
70+ if w, err := wayland.NewSurface(r.Client(), r.Version(), id, state); err == nil {
71+ c.server.surfaces[w.Resource] = state
72+ w.OnDestroy = func() { delete(c.server.surfaces, w.Resource); state.Destroy(w) }
73+ }
69 }74 }
70 func (compositor) CreateRegion(r *wayland.Compositor, id uint32) {75 func (compositor) CreateRegion(r *wayland.Compositor, id uint32) {
71 wayland.NewRegion(r.Client(), r.Version(), id, region{})76 wayland.NewRegion(r.Client(), r.Version(), id, region{})
72 }77 }
73 func (compositor) Release(*wayland.Compositor) {}78 func (compositor) Release(*wayland.Compositor) {}
74 79
75-type surface struct{}
76-
77-func (surface) Destroy(*wayland.Surface) {}
78-func (surface) Attach(*wayland.Surface, *wayland.Buffer, int32, int32) {}
79-func (surface) Damage(*wayland.Surface, int32, int32, int32, int32) {}
80-func (surface) Frame(*wayland.Surface, uint32) {}
81-func (surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {}
82-func (surface) SetInputRegion(*wayland.Surface, *wayland.Region) {}
83-func (surface) Commit(*wayland.Surface) {}
84-func (surface) SetBufferTransform(*wayland.Surface, int32) {}
85-func (surface) SetBufferScale(*wayland.Surface, int32) {}
86-func (surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
87-func (surface) Offset(*wayland.Surface, int32, int32) {}
88-func (surface) GetRelease(*wayland.Surface, uint32) {}
89-
90 type region struct{}80 type region struct{}
91 81
92 func (region) Destroy(*wayland.Region) {}82 func (region) Destroy(*wayland.Region) {}
M internal/adapters/wayland/server.go +47 -3
@@ -3,7 +3,10 @@ package wayland
33 import (
44 "context"
55 "fmt"
6+ "github.com/bnema/purego-libwayland/protocol/wayland"
67 "os"
8+ "time"
9+ "sync"
710
811 "github.com/bnema/nefertty/internal/ports"
912 "github.com/bnema/purego-libwayland/server"
@@ -25,6 +28,11 @@ type Server struct {
2528 cleanup func()
2629 log zerowrap.Logger
2730 channels Channels
31+ awaiting []*wayland.Callback
32+ frames uint64
33+ started time.Time
34+ surfaces map[*server.Resource]*surface
35+ serial uint32
2836 }
2937
3038 func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
@@ -47,15 +55,51 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
4755 cleanup()
4856 return nil, err
4957 }
50- if err = registerGlobals(d, opts); err != nil {
58+ s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface)}
59+ if err = registerGlobals(d, opts, s); err != nil {
5160 cleanup()
5261 return nil, err
5362 }
54- return &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch}, nil
63+ return s, nil
5564 }
5665 func (s *Server) SocketName() string { return s.name }
5766 func (s *Server) Run(ctx context.Context) error {
5867 defer s.cleanup()
5968 s.log.Info().Str("socket", s.name).Msg("starting wayland")
60- return s.display.Run(ctx)
69+ s.started = time.Now()
70+ var wg sync.WaitGroup
71+ wg.Add(1)
72+ go func() {
73+ defer wg.Done()
74+ ticker := time.NewTicker(16 * time.Millisecond)
75+ defer ticker.Stop()
76+ for {
77+ select {
78+ case <-ctx.Done():
79+ return
80+ case <-ticker.C:
81+ if ctx.Err() != nil {
82+ return
83+ }
84+ s.display.Do(func() {
85+ if ctx.Err() != nil {
86+ return
87+ }
88+ callbacks := s.awaiting
89+ s.awaiting = nil
90+ for _, cb := range callbacks {
91+ if !cb.Resource.Alive() {
92+ continue
93+ }
94+ cb.SendDone(uint32(time.Since(s.started).Milliseconds()))
95+ cb.Destroy()
96+ s.frames++
97+ }
98+ })
99+ }
100+ }
101+ }()
102+ err := s.display.Run(ctx)
103+ wg.Wait()
104+ return err
61105 }
@@ -3,7 +3,10 @@ package wayland
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6+ "github.com/bnema/purego-libwayland/protocol/wayland"
6 "os"7 "os"
8+ "time"
9+ "sync"
7 10
8 "github.com/bnema/nefertty/internal/ports"11 "github.com/bnema/nefertty/internal/ports"
9 "github.com/bnema/purego-libwayland/server"12 "github.com/bnema/purego-libwayland/server"
@@ -25,6 +28,11 @@ type Server struct {
25 cleanup func()28 cleanup func()
26 log zerowrap.Logger29 log zerowrap.Logger
27 channels Channels30 channels Channels
31+ awaiting []*wayland.Callback
32+ frames uint64
33+ started time.Time
34+ surfaces map[*server.Resource]*surface
35+ serial uint32
28 }36 }
29 37
30 func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {38 func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
@@ -47,15 +55,51 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
47 cleanup()55 cleanup()
48 return nil, err56 return nil, err
49 }57 }
50- if err = registerGlobals(d, opts); err != nil {58+ s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface)}
59+ if err = registerGlobals(d, opts, s); err != nil {
51 cleanup()60 cleanup()
52 return nil, err61 return nil, err
53 }62 }
54- return &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch}, nil63+ return s, nil
55 }64 }
56 func (s *Server) SocketName() string { return s.name }65 func (s *Server) SocketName() string { return s.name }
57 func (s *Server) Run(ctx context.Context) error {66 func (s *Server) Run(ctx context.Context) error {
58 defer s.cleanup()67 defer s.cleanup()
59 s.log.Info().Str("socket", s.name).Msg("starting wayland")68 s.log.Info().Str("socket", s.name).Msg("starting wayland")
60- return s.display.Run(ctx)69+ s.started = time.Now()
70+ var wg sync.WaitGroup
71+ wg.Add(1)
72+ go func() {
73+ defer wg.Done()
74+ ticker := time.NewTicker(16 * time.Millisecond)
75+ defer ticker.Stop()
76+ for {
77+ select {
78+ case <-ctx.Done():
79+ return
80+ case <-ticker.C:
81+ if ctx.Err() != nil {
82+ return
83+ }
84+ s.display.Do(func() {
85+ if ctx.Err() != nil {
86+ return
87+ }
88+ callbacks := s.awaiting
89+ s.awaiting = nil
90+ for _, cb := range callbacks {
91+ if !cb.Resource.Alive() {
92+ continue
93+ }
94+ cb.SendDone(uint32(time.Since(s.started).Milliseconds()))
95+ cb.Destroy()
96+ s.frames++
97+ }
98+ })
99+ }
100+ }
101+ }()
102+ err := s.display.Run(ctx)
103+ wg.Wait()
104+ return err
61 }105 }
M internal/adapters/wayland/server_test.go +62 -0
@@ -53,3 +53,65 @@ func TestWaylandInfo(t *testing.T) {
5353 }
5454 }
5555 }
56+
57+func TestWestonFrames(t *testing.T) {
58+ tool, err := exec.LookPath("weston-simple-shm")
59+ if err != nil {
60+ t.Skip("weston-simple-shm not installed")
61+ }
62+ dir := t.TempDir()
63+ s, err := New(Options{RuntimeDir: dir, OutputWidth: 1920, OutputHeight: 1080}, Channels{}, logging.For(context.Background(), "wayland"))
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+ ctx, cancel := context.WithCancel(context.Background())
68+ done := make(chan error, 1)
69+ go func() { done <- s.Run(ctx) }()
70+ t.Cleanup(func() {
71+ cancel()
72+ select {
73+ case err := <-done:
74+ if err != nil {
75+ t.Errorf("Run: %v", err)
76+ }
77+ case <-time.After(2 * time.Second):
78+ t.Error("Run hung")
79+ }
80+ })
81+ clientCtx, stop := context.WithCancel(context.Background())
82+ cmd := exec.CommandContext(clientCtx, tool)
83+ cmd.Env = append(os.Environ(), "XDG_RUNTIME_DIR="+dir, "WAYLAND_DISPLAY="+s.SocketName())
84+ var stderr strings.Builder
85+ cmd.Stderr = &stderr
86+ if err := cmd.Start(); err != nil {
87+ stop()
88+ t.Fatal(err)
89+ }
90+ exited := make(chan error, 1)
91+ go func() { exited <- cmd.Wait() }()
92+ t.Cleanup(func() {
93+ stop()
94+ select {
95+ case <-exited:
96+ case <-time.After(2 * time.Second):
97+ t.Error("client did not exit")
98+ }
99+ })
100+ deadline := time.After(2 * time.Second)
101+ tick := time.NewTicker(25 * time.Millisecond)
102+ defer tick.Stop()
103+ for {
104+ select {
105+ case err := <-exited:
106+ t.Fatalf("client exited: %v: %s", err, stderr.String())
107+ case <-deadline:
108+ t.Fatalf("only %d frames; stderr: %s", s.frames, stderr.String())
109+ case <-tick.C:
110+ var n uint64
111+ s.display.Do(func() { n = s.frames })
112+ if n >= 10 {
113+ return
114+ }
115+ }
116+ }
117+}
@@ -53,3 +53,65 @@ func TestWaylandInfo(t *testing.T) {
53 }53 }
54 }54 }
55 }55 }
56+
57+func TestWestonFrames(t *testing.T) {
58+ tool, err := exec.LookPath("weston-simple-shm")
59+ if err != nil {
60+ t.Skip("weston-simple-shm not installed")
61+ }
62+ dir := t.TempDir()
63+ s, err := New(Options{RuntimeDir: dir, OutputWidth: 1920, OutputHeight: 1080}, Channels{}, logging.For(context.Background(), "wayland"))
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+ ctx, cancel := context.WithCancel(context.Background())
68+ done := make(chan error, 1)
69+ go func() { done <- s.Run(ctx) }()
70+ t.Cleanup(func() {
71+ cancel()
72+ select {
73+ case err := <-done:
74+ if err != nil {
75+ t.Errorf("Run: %v", err)
76+ }
77+ case <-time.After(2 * time.Second):
78+ t.Error("Run hung")
79+ }
80+ })
81+ clientCtx, stop := context.WithCancel(context.Background())
82+ cmd := exec.CommandContext(clientCtx, tool)
83+ cmd.Env = append(os.Environ(), "XDG_RUNTIME_DIR="+dir, "WAYLAND_DISPLAY="+s.SocketName())
84+ var stderr strings.Builder
85+ cmd.Stderr = &stderr
86+ if err := cmd.Start(); err != nil {
87+ stop()
88+ t.Fatal(err)
89+ }
90+ exited := make(chan error, 1)
91+ go func() { exited <- cmd.Wait() }()
92+ t.Cleanup(func() {
93+ stop()
94+ select {
95+ case <-exited:
96+ case <-time.After(2 * time.Second):
97+ t.Error("client did not exit")
98+ }
99+ })
100+ deadline := time.After(2 * time.Second)
101+ tick := time.NewTicker(25 * time.Millisecond)
102+ defer tick.Stop()
103+ for {
104+ select {
105+ case err := <-exited:
106+ t.Fatalf("client exited: %v: %s", err, stderr.String())
107+ case <-deadline:
108+ t.Fatalf("only %d frames; stderr: %s", s.frames, stderr.String())
109+ case <-tick.C:
110+ var n uint64
111+ s.display.Do(func() { n = s.frames })
112+ if n >= 10 {
113+ return
114+ }
115+ }
116+ }
117+}
A internal/adapters/wayland/surface.go +53 -0
@@ -0,0 +1,53 @@
1+package wayland
2+
3+import "github.com/bnema/purego-libwayland/protocol/wayland"
4+
5+type surface struct {
6+ server *Server
7+ current, pending *wayland.Buffer
8+ attached bool
9+ callbacks []*wayland.Callback
10+ role func(bool)
11+ destroyed bool
12+}
13+
14+func (s *surface) Destroy(*wayland.Surface) {
15+ s.destroyed = true
16+ s.callbacks = nil
17+ if s.role != nil {
18+ s.role(false)
19+ }
20+}
21+func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) {
22+ s.pending = b
23+ s.attached = true
24+}
25+func (s *surface) Frame(r *wayland.Surface, id uint32) {
26+ cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{})
27+ if err == nil {
28+ s.callbacks = append(s.callbacks, cb)
29+ }
30+}
31+func (s *surface) Commit(*wayland.Surface) {
32+ if s.attached {
33+ if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {
34+ s.current.SendRelease()
35+ }
36+ s.current = s.pending
37+ s.pending = nil
38+ s.attached = false
39+ }
40+ s.server.awaiting = append(s.server.awaiting, s.callbacks...)
41+ s.callbacks = nil
42+ if s.role != nil {
43+ s.role(s.current != nil)
44+ }
45+}
46+func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {}
47+func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
48+func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {}
49+func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {}
50+func (*surface) SetBufferTransform(*wayland.Surface, int32) {}
51+func (*surface) SetBufferScale(*wayland.Surface, int32) {}
52+func (*surface) Offset(*wayland.Surface, int32, int32) {}
53+func (*surface) GetRelease(*wayland.Surface, uint32) {}
@@ -0,0 +1,53 @@
1+package wayland
2+
3+import "github.com/bnema/purego-libwayland/protocol/wayland"
4+
5+type surface struct {
6+ server *Server
7+ current, pending *wayland.Buffer
8+ attached bool
9+ callbacks []*wayland.Callback
10+ role func(bool)
11+ destroyed bool
12+}
13+
14+func (s *surface) Destroy(*wayland.Surface) {
15+ s.destroyed = true
16+ s.callbacks = nil
17+ if s.role != nil {
18+ s.role(false)
19+ }
20+}
21+func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) {
22+ s.pending = b
23+ s.attached = true
24+}
25+func (s *surface) Frame(r *wayland.Surface, id uint32) {
26+ cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{})
27+ if err == nil {
28+ s.callbacks = append(s.callbacks, cb)
29+ }
30+}
31+func (s *surface) Commit(*wayland.Surface) {
32+ if s.attached {
33+ if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {
34+ s.current.SendRelease()
35+ }
36+ s.current = s.pending
37+ s.pending = nil
38+ s.attached = false
39+ }
40+ s.server.awaiting = append(s.server.awaiting, s.callbacks...)
41+ s.callbacks = nil
42+ if s.role != nil {
43+ s.role(s.current != nil)
44+ }
45+}
46+func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {}
47+func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
48+func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {}
49+func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {}
50+func (*surface) SetBufferTransform(*wayland.Surface, int32) {}
51+func (*surface) SetBufferScale(*wayland.Surface, int32) {}
52+func (*surface) Offset(*wayland.Surface, int32, int32) {}
53+func (*surface) GetRelease(*wayland.Surface, uint32) {}
A internal/adapters/wayland/xdg.go +106 -0
@@ -0,0 +1,106 @@
1+package wayland
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/wayland"
5+ "github.com/bnema/purego-libwayland/protocol/xdgshell"
6+ "github.com/bnema/purego-libwayland/server"
7+)
8+
9+func registerXDG(d *server.Display, s *Server) error {
10+ return xdgshell.NewWmBaseGlobal(d, 6, func(c server.Client, v, id uint32) { xdgshell.NewWmBase(c, int32(v), id, wm{s}) })
11+}
12+
13+type wm struct{ server *Server }
14+
15+func (wm) Destroy(*xdgshell.WmBase) {}
16+func (wm) Pong(*xdgshell.WmBase, uint32) {}
17+func (wm) CreatePositioner(r *xdgshell.WmBase, id uint32) {
18+ xdgshell.NewPositioner(r.Client(), r.Version(), id, positioner{})
19+}
20+func (m wm) GetXdgSurface(r *xdgshell.WmBase, id uint32, w *wayland.Surface) {
21+ if w == nil {
22+ return
23+ }
24+ state := m.server.surfaces[w.Resource]
25+ if state == nil {
26+ return
27+ }
28+ if state.role != nil {
29+ r.PostError(0, "surface already has role")
30+ return
31+ }
32+ x := &xdgSurface{server: m.server, surface: state}
33+ resource, err := xdgshell.NewSurface(r.Client(), r.Version(), id, x)
34+ if err == nil {
35+ x.resource = resource
36+ }
37+
38+}
39+
40+type positioner struct{}
41+
42+func (positioner) Destroy(*xdgshell.Positioner) {}
43+func (positioner) SetSize(*xdgshell.Positioner, int32, int32) {}
44+func (positioner) SetAnchorRect(*xdgshell.Positioner, int32, int32, int32, int32) {}
45+func (positioner) SetAnchor(*xdgshell.Positioner, uint32) {}
46+func (positioner) SetGravity(*xdgshell.Positioner, uint32) {}
47+func (positioner) SetConstraintAdjustment(*xdgshell.Positioner, uint32) {}
48+func (positioner) SetOffset(*xdgshell.Positioner, int32, int32) {}
49+func (positioner) SetReactive(*xdgshell.Positioner) {}
50+func (positioner) SetParentSize(*xdgshell.Positioner, int32, int32) {}
51+func (positioner) SetParentConfigure(*xdgshell.Positioner, uint32) {}
52+
53+type xdgSurface struct {
54+ resource *xdgshell.Surface
55+ server *Server
56+ surface *surface
57+ configured bool
58+ serial uint32
59+}
60+
61+func (x *xdgSurface) Destroy(*xdgshell.Surface) {}
62+func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
63+ t, err := xdgshell.NewToplevel(r.Client(), r.Version(), id, &top{x})
64+ if err != nil {
65+ return
66+ }
67+ x.surface.role = func(buffer bool) {
68+ if !buffer && !x.configured {
69+ x.configured = true
70+ x.server.serial++
71+ t.SendConfigure(0, 0, nil)
72+ r.SendConfigure(x.server.serial)
73+ }
74+ }
75+ t.OnDestroy = func() { x.surface.role = nil }
76+}
77+func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) {
78+ if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil {
79+ p.SendPopupDone()
80+ }
81+}
82+func (x *xdgSurface) SetWindowGeometry(*xdgshell.Surface, int32, int32, int32, int32) {}
83+func (x *xdgSurface) AckConfigure(_ *xdgshell.Surface, serial uint32) { x.serial = serial }
84+
85+type top struct{ x *xdgSurface }
86+
87+func (top) Destroy(*xdgshell.Toplevel) {}
88+func (top) SetParent(*xdgshell.Toplevel, *xdgshell.Toplevel) {}
89+func (top) SetTitle(*xdgshell.Toplevel, string) {}
90+func (top) SetAppId(*xdgshell.Toplevel, string) {}
91+func (top) ShowWindowMenu(*xdgshell.Toplevel, *wayland.Seat, uint32, int32, int32) {}
92+func (top) Move(*xdgshell.Toplevel, *wayland.Seat, uint32) {}
93+func (top) Resize(*xdgshell.Toplevel, *wayland.Seat, uint32, uint32) {}
94+func (top) SetMaxSize(*xdgshell.Toplevel, int32, int32) {}
95+func (top) SetMinSize(*xdgshell.Toplevel, int32, int32) {}
96+func (top) SetMaximized(*xdgshell.Toplevel) {}
97+func (top) UnsetMaximized(*xdgshell.Toplevel) {}
98+func (top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) {}
99+func (top) UnsetFullscreen(*xdgshell.Toplevel) {}
100+func (top) SetMinimized(*xdgshell.Toplevel) {}
101+
102+type popup struct{}
103+
104+func (popup) Destroy(*xdgshell.Popup) {}
105+func (popup) Grab(*xdgshell.Popup, *wayland.Seat, uint32) {}
106+func (popup) Reposition(*xdgshell.Popup, *xdgshell.Positioner, uint32) {}
@@ -0,0 +1,106 @@
1+package wayland
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/wayland"
5+ "github.com/bnema/purego-libwayland/protocol/xdgshell"
6+ "github.com/bnema/purego-libwayland/server"
7+)
8+
9+func registerXDG(d *server.Display, s *Server) error {
10+ return xdgshell.NewWmBaseGlobal(d, 6, func(c server.Client, v, id uint32) { xdgshell.NewWmBase(c, int32(v), id, wm{s}) })
11+}
12+
13+type wm struct{ server *Server }
14+
15+func (wm) Destroy(*xdgshell.WmBase) {}
16+func (wm) Pong(*xdgshell.WmBase, uint32) {}
17+func (wm) CreatePositioner(r *xdgshell.WmBase, id uint32) {
18+ xdgshell.NewPositioner(r.Client(), r.Version(), id, positioner{})
19+}
20+func (m wm) GetXdgSurface(r *xdgshell.WmBase, id uint32, w *wayland.Surface) {
21+ if w == nil {
22+ return
23+ }
24+ state := m.server.surfaces[w.Resource]
25+ if state == nil {
26+ return
27+ }
28+ if state.role != nil {
29+ r.PostError(0, "surface already has role")
30+ return
31+ }
32+ x := &xdgSurface{server: m.server, surface: state}
33+ resource, err := xdgshell.NewSurface(r.Client(), r.Version(), id, x)
34+ if err == nil {
35+ x.resource = resource
36+ }
37+
38+}
39+
40+type positioner struct{}
41+
42+func (positioner) Destroy(*xdgshell.Positioner) {}
43+func (positioner) SetSize(*xdgshell.Positioner, int32, int32) {}
44+func (positioner) SetAnchorRect(*xdgshell.Positioner, int32, int32, int32, int32) {}
45+func (positioner) SetAnchor(*xdgshell.Positioner, uint32) {}
46+func (positioner) SetGravity(*xdgshell.Positioner, uint32) {}
47+func (positioner) SetConstraintAdjustment(*xdgshell.Positioner, uint32) {}
48+func (positioner) SetOffset(*xdgshell.Positioner, int32, int32) {}
49+func (positioner) SetReactive(*xdgshell.Positioner) {}
50+func (positioner) SetParentSize(*xdgshell.Positioner, int32, int32) {}
51+func (positioner) SetParentConfigure(*xdgshell.Positioner, uint32) {}
52+
53+type xdgSurface struct {
54+ resource *xdgshell.Surface
55+ server *Server
56+ surface *surface
57+ configured bool
58+ serial uint32
59+}
60+
61+func (x *xdgSurface) Destroy(*xdgshell.Surface) {}
62+func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
63+ t, err := xdgshell.NewToplevel(r.Client(), r.Version(), id, &top{x})
64+ if err != nil {
65+ return
66+ }
67+ x.surface.role = func(buffer bool) {
68+ if !buffer && !x.configured {
69+ x.configured = true
70+ x.server.serial++
71+ t.SendConfigure(0, 0, nil)
72+ r.SendConfigure(x.server.serial)
73+ }
74+ }
75+ t.OnDestroy = func() { x.surface.role = nil }
76+}
77+func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) {
78+ if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil {
79+ p.SendPopupDone()
80+ }
81+}
82+func (x *xdgSurface) SetWindowGeometry(*xdgshell.Surface, int32, int32, int32, int32) {}
83+func (x *xdgSurface) AckConfigure(_ *xdgshell.Surface, serial uint32) { x.serial = serial }
84+
85+type top struct{ x *xdgSurface }
86+
87+func (top) Destroy(*xdgshell.Toplevel) {}
88+func (top) SetParent(*xdgshell.Toplevel, *xdgshell.Toplevel) {}
89+func (top) SetTitle(*xdgshell.Toplevel, string) {}
90+func (top) SetAppId(*xdgshell.Toplevel, string) {}
91+func (top) ShowWindowMenu(*xdgshell.Toplevel, *wayland.Seat, uint32, int32, int32) {}
92+func (top) Move(*xdgshell.Toplevel, *wayland.Seat, uint32) {}
93+func (top) Resize(*xdgshell.Toplevel, *wayland.Seat, uint32, uint32) {}
94+func (top) SetMaxSize(*xdgshell.Toplevel, int32, int32) {}
95+func (top) SetMinSize(*xdgshell.Toplevel, int32, int32) {}
96+func (top) SetMaximized(*xdgshell.Toplevel) {}
97+func (top) UnsetMaximized(*xdgshell.Toplevel) {}
98+func (top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) {}
99+func (top) UnsetFullscreen(*xdgshell.Toplevel) {}
100+func (top) SetMinimized(*xdgshell.Toplevel) {}
101+
102+type popup struct{}
103+
104+func (popup) Destroy(*xdgshell.Popup) {}
105+func (popup) Grab(*xdgshell.Popup, *wayland.Seat, uint32) {}
106+func (popup) Reposition(*xdgshell.Popup, *xdgshell.Positioner, uint32) {}

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 23

Tags 10

↑↓ moveenter openesc close