detached 4fb1824

feat(wayland): add headless socket and core wayland globals

commit
4fb182498d56dba9e9bc744ccb6ba6d541b116fc
author
brice · · 2026-09-23 12:09 UTC
parent
cd738d1
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +387 -2
M README.md +2 -0
@@ -20,3 +20,5 @@ gaps = 8
2020 ```
2121
2222 See `examples/config.toml` for all defaults.
23+
24+Building requires a sibling checkout of purego-libwayland until it is published.
@@ -20,3 +20,5 @@ gaps = 8
20 ```20 ```
21 21
22 See `examples/config.toml` for all defaults.22 See `examples/config.toml` for all defaults.
23+
24+Building requires a sibling checkout of purego-libwayland until it is published.
M go.mod +6 -2
@@ -3,15 +3,19 @@ module github.com/bnema/nefertty
33 go 1.27
44
55 require (
6+ github.com/BurntSushi/toml v1.5.0
7+ github.com/bnema/purego-libwayland v0.0.0
68 github.com/bnema/zerowrap v1.4.1
79 github.com/rs/zerolog v1.35.1
10+ golang.org/x/sys v0.48.0
811 golang.org/x/term v0.46.0
912 )
1013
1114 require (
12- github.com/BurntSushi/toml v1.5.0
15+ github.com/bnema/purego v0.11.0-bnema.4 // indirect
1316 github.com/mattn/go-colorable v0.1.15 // indirect
1417 github.com/mattn/go-isatty v0.0.22 // indirect
15- golang.org/x/sys v0.48.0 // indirect
1618 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
1719 )
20+
21+replace github.com/bnema/purego-libwayland => ../purego-libwayland
@@ -3,15 +3,19 @@ module github.com/bnema/nefertty
3 go 1.273 go 1.27
4 4
5 require (5 require (
6+ github.com/BurntSushi/toml v1.5.0
7+ github.com/bnema/purego-libwayland v0.0.0
6 github.com/bnema/zerowrap v1.4.18 github.com/bnema/zerowrap v1.4.1
7 github.com/rs/zerolog v1.35.19 github.com/rs/zerolog v1.35.1
10+ golang.org/x/sys v0.48.0
8 golang.org/x/term v0.46.011 golang.org/x/term v0.46.0
9 )12 )
10 13
11 require (14 require (
12- github.com/BurntSushi/toml v1.5.015+ github.com/bnema/purego v0.11.0-bnema.4 // indirect
13 github.com/mattn/go-colorable v0.1.15 // indirect16 github.com/mattn/go-colorable v0.1.15 // indirect
14 github.com/mattn/go-isatty v0.0.22 // indirect17 github.com/mattn/go-isatty v0.0.22 // indirect
15- golang.org/x/sys v0.48.0 // indirect
16 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect18 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
17 )19 )
20+
21+replace github.com/bnema/purego-libwayland => ../purego-libwayland
M go.sum +2 -0 generated file: collapsed
A internal/adapters/wayland/globals.go +174 -0
@@ -0,0 +1,174 @@
1+package wayland
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/wayland"
5+ "github.com/bnema/purego-libwayland/server"
6+ "golang.org/x/sys/unix"
7+)
8+
9+func registerGlobals(d *server.Display, o Options) error {
10+ for _, register := range []func() error{
11+ func() error {
12+ return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{}) })
13+ },
14+ func() error {
15+ return wayland.NewShmGlobal(d, 1, func(c server.Client, v, id uint32) {
16+ r, e := wayland.NewShm(c, int32(v), id, shm{})
17+ if e == nil {
18+ r.SendFormat(0)
19+ r.SendFormat(1)
20+ }
21+ })
22+ },
23+ func() error {
24+ return wayland.NewOutputGlobal(d, 4, func(c server.Client, v, id uint32) {
25+ r, e := wayland.NewOutput(c, int32(v), id, output{})
26+ if e != nil {
27+ return
28+ }
29+ r.SendGeometry(0, 0, 0, 0, 0, "nefertty", "headless", 0)
30+ r.SendMode(3, int32(o.OutputWidth), int32(o.OutputHeight), 60000)
31+ if v >= 2 {
32+ r.SendScale(1)
33+ }
34+ if v >= 4 {
35+ r.SendName("HEADLESS-1")
36+ r.SendDescription("NeferTTY headless output")
37+ }
38+ if v >= 2 {
39+ r.SendDone()
40+ }
41+ })
42+ },
43+ func() error {
44+ return wayland.NewSeatGlobal(d, 7, func(c server.Client, v, id uint32) {
45+ r, e := wayland.NewSeat(c, int32(v), id, seat{})
46+ if e == nil {
47+ r.SendCapabilities(0)
48+ if v >= 2 {
49+ r.SendName("seat0")
50+ }
51+ }
52+ })
53+ },
54+ func() error {
55+ return wayland.NewDataDeviceManagerGlobal(d, 3, func(c server.Client, v, id uint32) { wayland.NewDataDeviceManager(c, int32(v), id, dataManager{}) })
56+ },
57+ } {
58+ if err := register(); err != nil {
59+ return err
60+ }
61+ }
62+ return nil
63+}
64+
65+type compositor struct{}
66+
67+func (compositor) CreateSurface(r *wayland.Compositor, id uint32) {
68+ wayland.NewSurface(r.Client(), r.Version(), id, surface{})
69+}
70+func (compositor) CreateRegion(r *wayland.Compositor, id uint32) {
71+ wayland.NewRegion(r.Client(), r.Version(), id, region{})
72+}
73+func (compositor) Release(*wayland.Compositor) {}
74+
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{}
91+
92+func (region) Destroy(*wayland.Region) {}
93+func (region) Add(*wayland.Region, int32, int32, int32, int32) {}
94+func (region) Subtract(*wayland.Region, int32, int32, int32, int32) {}
95+
96+type shm struct{}
97+
98+func (shm) CreatePool(r *wayland.Shm, id uint32, fd int, size int32) {
99+ p, e := wayland.NewShmPool(r.Client(), 1, id, &pool{fd: fd, size: size})
100+ if e != nil {
101+ unix.Close(fd)
102+ } else {
103+ p.OnDestroy = func() { unix.Close(fd) }
104+ }
105+}
106+func (shm) Release(*wayland.Shm) {}
107+
108+type pool struct {
109+ fd int
110+ size int32
111+}
112+
113+func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, _ int32, _ int32, _ int32, _ int32, _ uint32) {
114+ wayland.NewBuffer(r.Client(), 1, id, buffer{})
115+}
116+func (p *pool) Destroy(*wayland.ShmPool) {}
117+func (p *pool) Resize(_ *wayland.ShmPool, size int32) { p.size = size }
118+
119+type buffer struct{}
120+
121+func (buffer) Destroy(*wayland.Buffer) {}
122+
123+type output struct{}
124+
125+func (output) Release(*wayland.Output) {}
126+
127+type seat struct{}
128+
129+func (seat) GetPointer(r *wayland.Seat, id uint32) {
130+ wayland.NewPointer(r.Client(), r.Version(), id, pointer{})
131+}
132+func (seat) GetKeyboard(r *wayland.Seat, id uint32) {
133+ wayland.NewKeyboard(r.Client(), r.Version(), id, keyboard{})
134+}
135+func (seat) GetTouch(r *wayland.Seat, id uint32) {
136+ wayland.NewTouch(r.Client(), r.Version(), id, touch{})
137+}
138+func (seat) Release(*wayland.Seat) {}
139+
140+type pointer struct{}
141+
142+func (pointer) SetCursor(*wayland.Pointer, uint32, *wayland.Surface, int32, int32) {}
143+func (pointer) Release(*wayland.Pointer) {}
144+
145+type keyboard struct{}
146+
147+func (keyboard) Release(*wayland.Keyboard) {}
148+
149+type touch struct{}
150+
151+func (touch) Release(*wayland.Touch) {}
152+
153+type dataManager struct{}
154+
155+func (dataManager) CreateDataSource(r *wayland.DataDeviceManager, id uint32) {
156+ wayland.NewDataSource(r.Client(), r.Version(), id, dataSource{})
157+}
158+func (dataManager) GetDataDevice(r *wayland.DataDeviceManager, id uint32, _ *wayland.Seat) {
159+ wayland.NewDataDevice(r.Client(), r.Version(), id, dataDevice{})
160+}
161+func (dataManager) Release(*wayland.DataDeviceManager) {}
162+
163+type dataSource struct{}
164+
165+func (dataSource) Offer(*wayland.DataSource, string) {}
166+func (dataSource) Destroy(*wayland.DataSource) {}
167+func (dataSource) SetActions(*wayland.DataSource, uint32) {}
168+
169+type dataDevice struct{}
170+
171+func (dataDevice) StartDrag(*wayland.DataDevice, *wayland.DataSource, *wayland.Surface, *wayland.Surface, uint32) {
172+}
173+func (dataDevice) SetSelection(*wayland.DataDevice, *wayland.DataSource, uint32) {}
174+func (dataDevice) Release(*wayland.DataDevice) {}
@@ -0,0 +1,174 @@
1+package wayland
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/wayland"
5+ "github.com/bnema/purego-libwayland/server"
6+ "golang.org/x/sys/unix"
7+)
8+
9+func registerGlobals(d *server.Display, o Options) error {
10+ for _, register := range []func() error{
11+ func() error {
12+ return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{}) })
13+ },
14+ func() error {
15+ return wayland.NewShmGlobal(d, 1, func(c server.Client, v, id uint32) {
16+ r, e := wayland.NewShm(c, int32(v), id, shm{})
17+ if e == nil {
18+ r.SendFormat(0)
19+ r.SendFormat(1)
20+ }
21+ })
22+ },
23+ func() error {
24+ return wayland.NewOutputGlobal(d, 4, func(c server.Client, v, id uint32) {
25+ r, e := wayland.NewOutput(c, int32(v), id, output{})
26+ if e != nil {
27+ return
28+ }
29+ r.SendGeometry(0, 0, 0, 0, 0, "nefertty", "headless", 0)
30+ r.SendMode(3, int32(o.OutputWidth), int32(o.OutputHeight), 60000)
31+ if v >= 2 {
32+ r.SendScale(1)
33+ }
34+ if v >= 4 {
35+ r.SendName("HEADLESS-1")
36+ r.SendDescription("NeferTTY headless output")
37+ }
38+ if v >= 2 {
39+ r.SendDone()
40+ }
41+ })
42+ },
43+ func() error {
44+ return wayland.NewSeatGlobal(d, 7, func(c server.Client, v, id uint32) {
45+ r, e := wayland.NewSeat(c, int32(v), id, seat{})
46+ if e == nil {
47+ r.SendCapabilities(0)
48+ if v >= 2 {
49+ r.SendName("seat0")
50+ }
51+ }
52+ })
53+ },
54+ func() error {
55+ return wayland.NewDataDeviceManagerGlobal(d, 3, func(c server.Client, v, id uint32) { wayland.NewDataDeviceManager(c, int32(v), id, dataManager{}) })
56+ },
57+ } {
58+ if err := register(); err != nil {
59+ return err
60+ }
61+ }
62+ return nil
63+}
64+
65+type compositor struct{}
66+
67+func (compositor) CreateSurface(r *wayland.Compositor, id uint32) {
68+ wayland.NewSurface(r.Client(), r.Version(), id, surface{})
69+}
70+func (compositor) CreateRegion(r *wayland.Compositor, id uint32) {
71+ wayland.NewRegion(r.Client(), r.Version(), id, region{})
72+}
73+func (compositor) Release(*wayland.Compositor) {}
74+
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{}
91+
92+func (region) Destroy(*wayland.Region) {}
93+func (region) Add(*wayland.Region, int32, int32, int32, int32) {}
94+func (region) Subtract(*wayland.Region, int32, int32, int32, int32) {}
95+
96+type shm struct{}
97+
98+func (shm) CreatePool(r *wayland.Shm, id uint32, fd int, size int32) {
99+ p, e := wayland.NewShmPool(r.Client(), 1, id, &pool{fd: fd, size: size})
100+ if e != nil {
101+ unix.Close(fd)
102+ } else {
103+ p.OnDestroy = func() { unix.Close(fd) }
104+ }
105+}
106+func (shm) Release(*wayland.Shm) {}
107+
108+type pool struct {
109+ fd int
110+ size int32
111+}
112+
113+func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, _ int32, _ int32, _ int32, _ int32, _ uint32) {
114+ wayland.NewBuffer(r.Client(), 1, id, buffer{})
115+}
116+func (p *pool) Destroy(*wayland.ShmPool) {}
117+func (p *pool) Resize(_ *wayland.ShmPool, size int32) { p.size = size }
118+
119+type buffer struct{}
120+
121+func (buffer) Destroy(*wayland.Buffer) {}
122+
123+type output struct{}
124+
125+func (output) Release(*wayland.Output) {}
126+
127+type seat struct{}
128+
129+func (seat) GetPointer(r *wayland.Seat, id uint32) {
130+ wayland.NewPointer(r.Client(), r.Version(), id, pointer{})
131+}
132+func (seat) GetKeyboard(r *wayland.Seat, id uint32) {
133+ wayland.NewKeyboard(r.Client(), r.Version(), id, keyboard{})
134+}
135+func (seat) GetTouch(r *wayland.Seat, id uint32) {
136+ wayland.NewTouch(r.Client(), r.Version(), id, touch{})
137+}
138+func (seat) Release(*wayland.Seat) {}
139+
140+type pointer struct{}
141+
142+func (pointer) SetCursor(*wayland.Pointer, uint32, *wayland.Surface, int32, int32) {}
143+func (pointer) Release(*wayland.Pointer) {}
144+
145+type keyboard struct{}
146+
147+func (keyboard) Release(*wayland.Keyboard) {}
148+
149+type touch struct{}
150+
151+func (touch) Release(*wayland.Touch) {}
152+
153+type dataManager struct{}
154+
155+func (dataManager) CreateDataSource(r *wayland.DataDeviceManager, id uint32) {
156+ wayland.NewDataSource(r.Client(), r.Version(), id, dataSource{})
157+}
158+func (dataManager) GetDataDevice(r *wayland.DataDeviceManager, id uint32, _ *wayland.Seat) {
159+ wayland.NewDataDevice(r.Client(), r.Version(), id, dataDevice{})
160+}
161+func (dataManager) Release(*wayland.DataDeviceManager) {}
162+
163+type dataSource struct{}
164+
165+func (dataSource) Offer(*wayland.DataSource, string) {}
166+func (dataSource) Destroy(*wayland.DataSource) {}
167+func (dataSource) SetActions(*wayland.DataSource, uint32) {}
168+
169+type dataDevice struct{}
170+
171+func (dataDevice) StartDrag(*wayland.DataDevice, *wayland.DataSource, *wayland.Surface, *wayland.Surface, uint32) {
172+}
173+func (dataDevice) SetSelection(*wayland.DataDevice, *wayland.DataSource, uint32) {}
174+func (dataDevice) Release(*wayland.DataDevice) {}
A internal/adapters/wayland/server.go +61 -0
@@ -0,0 +1,61 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "fmt"
6+ "os"
7+
8+ "github.com/bnema/nefertty/internal/ports"
9+ "github.com/bnema/purego-libwayland/server"
10+ "github.com/bnema/zerowrap"
11+ "golang.org/x/sys/unix"
12+)
13+
14+type Options struct {
15+ RuntimeDir string
16+ OutputWidth, OutputHeight int
17+}
18+type Channels struct {
19+ Events chan<- ports.ClientEvent
20+ Commands <-chan ports.ClientCommand
21+}
22+type Server struct {
23+ display *server.Display
24+ name string
25+ cleanup func()
26+ log zerowrap.Logger
27+ channels Channels
28+}
29+
30+func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
31+ if opts.RuntimeDir == "" {
32+ opts.RuntimeDir = os.Getenv("XDG_RUNTIME_DIR")
33+ }
34+ if opts.RuntimeDir == "" {
35+ return nil, fmt.Errorf("XDG_RUNTIME_DIR is empty")
36+ }
37+ d, err := server.NewDisplay()
38+ if err != nil {
39+ return nil, err
40+ }
41+ name, fd, cleanup, err := listen(opts.RuntimeDir)
42+ if err != nil {
43+ return nil, err
44+ }
45+ if err = d.AddSocketFD(fd); err != nil {
46+ unix.Close(fd)
47+ cleanup()
48+ return nil, err
49+ }
50+ if err = registerGlobals(d, opts); err != nil {
51+ cleanup()
52+ return nil, err
53+ }
54+ return &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch}, nil
55+}
56+func (s *Server) SocketName() string { return s.name }
57+func (s *Server) Run(ctx context.Context) error {
58+ defer s.cleanup()
59+ s.log.Info().Str("socket", s.name).Msg("starting wayland")
60+ return s.display.Run(ctx)
61+}
@@ -0,0 +1,61 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "fmt"
6+ "os"
7+
8+ "github.com/bnema/nefertty/internal/ports"
9+ "github.com/bnema/purego-libwayland/server"
10+ "github.com/bnema/zerowrap"
11+ "golang.org/x/sys/unix"
12+)
13+
14+type Options struct {
15+ RuntimeDir string
16+ OutputWidth, OutputHeight int
17+}
18+type Channels struct {
19+ Events chan<- ports.ClientEvent
20+ Commands <-chan ports.ClientCommand
21+}
22+type Server struct {
23+ display *server.Display
24+ name string
25+ cleanup func()
26+ log zerowrap.Logger
27+ channels Channels
28+}
29+
30+func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
31+ if opts.RuntimeDir == "" {
32+ opts.RuntimeDir = os.Getenv("XDG_RUNTIME_DIR")
33+ }
34+ if opts.RuntimeDir == "" {
35+ return nil, fmt.Errorf("XDG_RUNTIME_DIR is empty")
36+ }
37+ d, err := server.NewDisplay()
38+ if err != nil {
39+ return nil, err
40+ }
41+ name, fd, cleanup, err := listen(opts.RuntimeDir)
42+ if err != nil {
43+ return nil, err
44+ }
45+ if err = d.AddSocketFD(fd); err != nil {
46+ unix.Close(fd)
47+ cleanup()
48+ return nil, err
49+ }
50+ if err = registerGlobals(d, opts); err != nil {
51+ cleanup()
52+ return nil, err
53+ }
54+ return &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch}, nil
55+}
56+func (s *Server) SocketName() string { return s.name }
57+func (s *Server) Run(ctx context.Context) error {
58+ defer s.cleanup()
59+ s.log.Info().Str("socket", s.name).Msg("starting wayland")
60+ return s.display.Run(ctx)
61+}
A internal/adapters/wayland/server_test.go +55 -0
@@ -0,0 +1,55 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "os"
6+ "os/exec"
7+ "path/filepath"
8+ "strings"
9+ "testing"
10+ "time"
11+
12+ "github.com/bnema/nefertty/internal/logging"
13+)
14+
15+func TestWaylandInfo(t *testing.T) {
16+ tool, err := exec.LookPath("wayland-info")
17+ if err != nil {
18+ t.Skip("wayland-info not installed")
19+ }
20+ dir := t.TempDir()
21+ s, err := New(Options{RuntimeDir: dir, OutputWidth: 1920, OutputHeight: 1080}, Channels{}, logging.For(context.Background(), "wayland"))
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ ctx, cancel := context.WithCancel(context.Background())
26+ done := make(chan error, 1)
27+ go func() { done <- s.Run(ctx) }()
28+ defer func() {
29+ cancel()
30+ select {
31+ case err := <-done:
32+ if err != nil {
33+ t.Errorf("Run: %v", err)
34+ }
35+ case <-time.After(2 * time.Second):
36+ t.Error("Run did not stop")
37+ }
38+ if _, err := os.Stat(filepath.Join(dir, s.SocketName())); !os.IsNotExist(err) {
39+ t.Errorf("socket remains: %v", err)
40+ }
41+ }()
42+ cmdCtx, stop := context.WithTimeout(context.Background(), 5*time.Second)
43+ defer stop()
44+ cmd := exec.CommandContext(cmdCtx, tool)
45+ cmd.Env = append(os.Environ(), "XDG_RUNTIME_DIR="+dir, "WAYLAND_DISPLAY="+s.SocketName())
46+ out, err := cmd.CombinedOutput()
47+ if err != nil {
48+ t.Fatalf("wayland-info: %v\n%s", err, out)
49+ }
50+ for _, want := range []string{"wl_compositor", "wl_shm", "wl_output", "wl_seat", "wl_data_device_manager", "HEADLESS-1"} {
51+ if !strings.Contains(string(out), want) {
52+ t.Errorf("missing %q in:\n%s", want, out)
53+ }
54+ }
55+}
@@ -0,0 +1,55 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "os"
6+ "os/exec"
7+ "path/filepath"
8+ "strings"
9+ "testing"
10+ "time"
11+
12+ "github.com/bnema/nefertty/internal/logging"
13+)
14+
15+func TestWaylandInfo(t *testing.T) {
16+ tool, err := exec.LookPath("wayland-info")
17+ if err != nil {
18+ t.Skip("wayland-info not installed")
19+ }
20+ dir := t.TempDir()
21+ s, err := New(Options{RuntimeDir: dir, OutputWidth: 1920, OutputHeight: 1080}, Channels{}, logging.For(context.Background(), "wayland"))
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ ctx, cancel := context.WithCancel(context.Background())
26+ done := make(chan error, 1)
27+ go func() { done <- s.Run(ctx) }()
28+ defer func() {
29+ cancel()
30+ select {
31+ case err := <-done:
32+ if err != nil {
33+ t.Errorf("Run: %v", err)
34+ }
35+ case <-time.After(2 * time.Second):
36+ t.Error("Run did not stop")
37+ }
38+ if _, err := os.Stat(filepath.Join(dir, s.SocketName())); !os.IsNotExist(err) {
39+ t.Errorf("socket remains: %v", err)
40+ }
41+ }()
42+ cmdCtx, stop := context.WithTimeout(context.Background(), 5*time.Second)
43+ defer stop()
44+ cmd := exec.CommandContext(cmdCtx, tool)
45+ cmd.Env = append(os.Environ(), "XDG_RUNTIME_DIR="+dir, "WAYLAND_DISPLAY="+s.SocketName())
46+ out, err := cmd.CombinedOutput()
47+ if err != nil {
48+ t.Fatalf("wayland-info: %v\n%s", err, out)
49+ }
50+ for _, want := range []string{"wl_compositor", "wl_shm", "wl_output", "wl_seat", "wl_data_device_manager", "HEADLESS-1"} {
51+ if !strings.Contains(string(out), want) {
52+ t.Errorf("missing %q in:\n%s", want, out)
53+ }
54+ }
55+}
A internal/adapters/wayland/socket.go +49 -0
@@ -0,0 +1,49 @@
1+package wayland
2+
3+import (
4+ "fmt"
5+ "os"
6+ "path/filepath"
7+
8+ "golang.org/x/sys/unix"
9+)
10+
11+func listen(runtimeDir string) (name string, fd int, cleanup func(), err error) {
12+ for n := 1; n <= 32; n++ {
13+ name = fmt.Sprintf("wayland-%d", n)
14+ path := filepath.Join(runtimeDir, name)
15+ lock := path + ".lock"
16+ l, e := unix.Open(lock, unix.O_CREAT|unix.O_RDWR|unix.O_CLOEXEC, 0660)
17+ if e != nil {
18+ return "", -1, nil, e
19+ }
20+ if e = unix.Flock(l, unix.LOCK_EX|unix.LOCK_NB); e != nil {
21+ unix.Close(l)
22+ if e == unix.EWOULDBLOCK {
23+ continue
24+ }
25+ return "", -1, nil, e
26+ }
27+ cleanup = func() { os.Remove(path); os.Remove(lock); unix.Close(l) }
28+ if e = os.Remove(path); e != nil && !os.IsNotExist(e) {
29+ cleanup()
30+ return "", -1, nil, e
31+ }
32+ fd, e = unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
33+ if e == nil {
34+ e = unix.Bind(fd, &unix.SockaddrUnix{Name: path})
35+ }
36+ if e == nil {
37+ e = unix.Listen(fd, 128)
38+ }
39+ if e != nil {
40+ if fd >= 0 {
41+ unix.Close(fd)
42+ }
43+ cleanup()
44+ return "", -1, nil, e
45+ }
46+ return name, fd, cleanup, nil
47+ }
48+ return "", -1, nil, fmt.Errorf("no free wayland socket in %s", runtimeDir)
49+}
@@ -0,0 +1,49 @@
1+package wayland
2+
3+import (
4+ "fmt"
5+ "os"
6+ "path/filepath"
7+
8+ "golang.org/x/sys/unix"
9+)
10+
11+func listen(runtimeDir string) (name string, fd int, cleanup func(), err error) {
12+ for n := 1; n <= 32; n++ {
13+ name = fmt.Sprintf("wayland-%d", n)
14+ path := filepath.Join(runtimeDir, name)
15+ lock := path + ".lock"
16+ l, e := unix.Open(lock, unix.O_CREAT|unix.O_RDWR|unix.O_CLOEXEC, 0660)
17+ if e != nil {
18+ return "", -1, nil, e
19+ }
20+ if e = unix.Flock(l, unix.LOCK_EX|unix.LOCK_NB); e != nil {
21+ unix.Close(l)
22+ if e == unix.EWOULDBLOCK {
23+ continue
24+ }
25+ return "", -1, nil, e
26+ }
27+ cleanup = func() { os.Remove(path); os.Remove(lock); unix.Close(l) }
28+ if e = os.Remove(path); e != nil && !os.IsNotExist(e) {
29+ cleanup()
30+ return "", -1, nil, e
31+ }
32+ fd, e = unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
33+ if e == nil {
34+ e = unix.Bind(fd, &unix.SockaddrUnix{Name: path})
35+ }
36+ if e == nil {
37+ e = unix.Listen(fd, 128)
38+ }
39+ if e != nil {
40+ if fd >= 0 {
41+ unix.Close(fd)
42+ }
43+ cleanup()
44+ return "", -1, nil, e
45+ }
46+ return name, fd, cleanup, nil
47+ }
48+ return "", -1, nil, fmt.Errorf("no free wayland socket in %s", runtimeDir)
49+}
A internal/adapters/wayland/socket_test.go +38 -0
@@ -0,0 +1,38 @@
1+package wayland
2+
3+import (
4+ "golang.org/x/sys/unix"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+)
9+
10+func TestListen(t *testing.T) {
11+ dir := t.TempDir()
12+ n1, fd1, close1, err := listen(dir)
13+ if err != nil {
14+ t.Fatal(err)
15+ }
16+ defer close1()
17+ defer unix.Close(fd1)
18+ n2, fd2, close2, err := listen(dir)
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ defer unix.Close(fd2)
23+ if n1 != "wayland-1" || n2 != "wayland-2" {
24+ t.Fatalf("names: %s, %s", n1, n2)
25+ }
26+ close2()
27+ for _, suffix := range []string{"", ".lock"} {
28+ if _, err := os.Stat(filepath.Join(dir, n2+suffix)); !os.IsNotExist(err) {
29+ t.Fatalf("%s still exists: %v", n2+suffix, err)
30+ }
31+ }
32+ close1()
33+ for _, suffix := range []string{"", ".lock"} {
34+ if _, err := os.Stat(filepath.Join(dir, n1+suffix)); !os.IsNotExist(err) {
35+ t.Fatalf("%s still exists: %v", n1+suffix, err)
36+ }
37+ }
38+}
@@ -0,0 +1,38 @@
1+package wayland
2+
3+import (
4+ "golang.org/x/sys/unix"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+)
9+
10+func TestListen(t *testing.T) {
11+ dir := t.TempDir()
12+ n1, fd1, close1, err := listen(dir)
13+ if err != nil {
14+ t.Fatal(err)
15+ }
16+ defer close1()
17+ defer unix.Close(fd1)
18+ n2, fd2, close2, err := listen(dir)
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ defer unix.Close(fd2)
23+ if n1 != "wayland-1" || n2 != "wayland-2" {
24+ t.Fatalf("names: %s, %s", n1, n2)
25+ }
26+ close2()
27+ for _, suffix := range []string{"", ".lock"} {
28+ if _, err := os.Stat(filepath.Join(dir, n2+suffix)); !os.IsNotExist(err) {
29+ t.Fatalf("%s still exists: %v", n2+suffix, err)
30+ }
31+ }
32+ close1()
33+ for _, suffix := range []string{"", ".lock"} {
34+ if _, err := os.Stat(filepath.Join(dir, n1+suffix)); !os.IsNotExist(err) {
35+ t.Fatalf("%s still exists: %v", n1+suffix, err)
36+ }
37+ }
38+}

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