feat(wayland): add headless socket and core wayland globals
- commit
- 4fb182498d56dba9e9bc744ccb6ba6d541b116fc
- author
- brice · · 2026-09-23 12:09 UTC
- parent
- cd738d1
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +387 -2
M README.md +2 -0
| @@ -20,3 +20,5 @@ gaps = 8 | |||
| 20 | 20 | ``` | |
| 21 | 21 | ||
| 22 | 22 | 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 | |||
| 3 | 3 | go 1.27 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | + | github.com/BurntSushi/toml v1.5.0 | |
| 7 | + | github.com/bnema/purego-libwayland v0.0.0 | |
| 6 | 8 | github.com/bnema/zerowrap v1.4.1 | |
| 7 | 9 | github.com/rs/zerolog v1.35.1 | |
| 10 | + | golang.org/x/sys v0.48.0 | |
| 8 | 11 | golang.org/x/term v0.46.0 | |
| 9 | 12 | ) | |
| 10 | 13 | ||
| 11 | 14 | require ( | |
| 12 | - | github.com/BurntSushi/toml v1.5.0 | |
| 15 | + | github.com/bnema/purego v0.11.0-bnema.4 // indirect | |
| 13 | 16 | github.com/mattn/go-colorable v0.1.15 // indirect | |
| 14 | 17 | github.com/mattn/go-isatty v0.0.22 // indirect | |
| 15 | - | golang.org/x/sys v0.48.0 // indirect | |
| 16 | 18 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect | |
| 17 | 19 | ) | |
| 20 | + | ||
| 21 | + | replace github.com/bnema/purego-libwayland => ../purego-libwayland | |
| @@ -3,15 +3,19 @@ module github.com/bnema/nefertty | |||||
| 3 | go 1.27 | 3 | 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.1 | 8 | github.com/bnema/zerowrap v1.4.1 | ||
| 7 | github.com/rs/zerolog v1.35.1 | 9 | github.com/rs/zerolog v1.35.1 | ||
| 10 | + | golang.org/x/sys v0.48.0 | |||
| 8 | golang.org/x/term v0.46.0 | 11 | golang.org/x/term v0.46.0 | ||
| 9 | ) | 12 | ) | ||
| 10 | 13 | ||||
| 11 | require ( | 14 | require ( | ||
| 12 | - | github.com/BurntSushi/toml v1.5.0 | 15 | + | github.com/bnema/purego v0.11.0-bnema.4 // indirect |
| 13 | github.com/mattn/go-colorable v0.1.15 // indirect | 16 | github.com/mattn/go-colorable v0.1.15 // indirect | ||
| 14 | github.com/mattn/go-isatty v0.0.22 // indirect | 17 | 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 // indirect | 18 | 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 | + | } | |||