#21 WIP: feat(xwayland): add built-in Xwayland launcher
- state
- draft
- branch
- feat/xwayland-builtin → main
- opened
- bnema ·
- updated
M .mockery.yml +30 -0
M Makefile +9 -2
M README.md +1 -1
M docs/config.md +9 -3
M docs/desktop.md +12 -1
M docs/dev/headless.md +25 -0
M go.mod +4 -3
M go.sum +8 -6
M internal/adapters/config/config.go +18 -4
M internal/adapters/config/config_test.go +36 -4
M internal/adapters/drm/colorop_match.go +11 -0
A internal/adapters/drm/sdrplane_test.go +30 -0
M internal/adapters/vulkan/capture_test.go +3 -0
A internal/adapters/vulkan/norace_test.go +5 -0
A internal/adapters/vulkan/race_test.go +8 -0
M internal/adapters/vulkan/scene_perf_test.go +6 -0
M internal/adapters/wayland/activation.go +3 -3
M internal/adapters/wayland/alloc_hotpath_test.go +9 -0
M internal/adapters/wayland/clipboard.go +5 -0
A internal/adapters/wayland/clipboard_xwm.go +93 -0
A internal/adapters/wayland/clipboard_xwm_test.go +147 -0
M internal/adapters/wayland/constraints.go +14 -13
M internal/adapters/wayland/decoration.go +1 -1
M internal/adapters/wayland/dialog.go +1 -1
M internal/adapters/wayland/dmabuf.go +3 -2
M internal/adapters/wayland/extforeigntoplevel.go +1 -1
M internal/adapters/wayland/foreigntoplevel.go +6 -6
M internal/adapters/wayland/frames.go +10 -6
M internal/adapters/wayland/globals.go +11 -1
M internal/adapters/wayland/imagecopy.go +1 -1
M internal/adapters/wayland/inhibit.go +5 -0
M internal/adapters/wayland/outputs.go +72 -1
M internal/adapters/wayland/popup.go +2 -2
M internal/adapters/wayland/scale.go +5 -3
M internal/adapters/wayland/screencast.go +1 -1
M internal/adapters/wayland/seat.go +11 -9
M internal/adapters/wayland/server.go +51 -16
M internal/adapters/wayland/surface.go +15 -2
A internal/adapters/wayland/window.go +112 -0
M internal/adapters/wayland/wire_client_test.go +6 -0
A internal/adapters/wayland/x11override_test.go +107 -0
A internal/adapters/wayland/x11peer_test.go +332 -0
A internal/adapters/wayland/x11window.go +458 -0
A internal/adapters/wayland/x11window_test.go +627 -0
M internal/adapters/wayland/xdg.go +32 -21
A internal/adapters/wayland/xdgoutputx_test.go +143 -0
A internal/adapters/wayland/xwayland.go +75 -0
A internal/adapters/wayland/xwayland_test.go +84 -0
A internal/adapters/wayland/xwaylandgrab.go +90 -0
A internal/adapters/wayland/xwm/atoms.go +118 -0
A internal/adapters/wayland/xwm/configure.go +317 -0
| @@ -0,0 +1,317 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "math" | |
| 5 | + | ||
| 6 | + | "bnema.dev/xturbo" | |
| 7 | + | "bnema.dev/xturbo/xproto" | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // maxClientSize bounds the size a floating window may ask for. | |
| 12 | + | const maxClientSize = 16384 | |
| 13 | + | ||
| 14 | + | // _NET_WM_STATE bits tracked in record.stateSent. | |
| 15 | + | const ( | |
| 16 | + | stateFullscreen uint8 = 1 << iota | |
| 17 | + | stateFocused | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | // size is the size to send: the requested one, else the last known. | |
| 21 | + | func (r *record) size() (w, h uint32) { | |
| 22 | + | w, h = r.pend.w, r.pend.h | |
| 23 | + | if w == 0 { | |
| 24 | + | w = r.w | |
| 25 | + | } | |
| 26 | + | if h == 0 { | |
| 27 | + | h = r.h | |
| 28 | + | } | |
| 29 | + | return w, h | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // differs reports whether the pending configure is not what the X server has. | |
| 33 | + | func (r *record) differs() bool { | |
| 34 | + | w, h := r.size() | |
| 35 | + | p, s := &r.pend, &r.sent | |
| 36 | + | return p.x != s.x || p.y != s.y || w != s.w || h != s.h || | |
| 37 | + | p.fullscreen != s.fullscreen || p.activated != s.activated | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // markDirty queues s for the next flush when its pending configure differs. | |
| 41 | + | func (x *XWM) markDirty(s Slot, r *record) { | |
| 42 | + | if r.pend.dirty || !r.differs() { | |
| 43 | + | return | |
| 44 | + | } | |
| 45 | + | if len(x.dirty) == cap(x.dirty) { | |
| 46 | + | // Stale entries of freed and reused slots filled the queue: send | |
| 47 | + | // what is queued rather than grow it. r stays valid, nothing here | |
| 48 | + | // grows the table. | |
| 49 | + | x.flushConfigures() | |
| 50 | + | } | |
| 51 | + | r.pend.dirty = true | |
| 52 | + | x.dirty = append(x.dirty, s) | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // managedRec returns the record of a live managed window. | |
| 56 | + | func (x *XWM) managedRec(s Slot) *record { | |
| 57 | + | if x.closed { | |
| 58 | + | return nil | |
| 59 | + | } | |
| 60 | + | if r := x.tab.rec(s); r != nil && r.kind == kindManaged { | |
| 61 | + | return r | |
| 62 | + | } | |
| 63 | + | return nil | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // Configure records the size and state core wants for s; sent at flush. A | |
| 67 | + | // zero size keeps the current one. | |
| 68 | + | func (x *XWM) Configure(s Slot, c ports.ConfigureWindow) { | |
| 69 | + | r := x.managedRec(s) | |
| 70 | + | if r == nil { | |
| 71 | + | return | |
| 72 | + | } | |
| 73 | + | if c.Width > 0 { | |
| 74 | + | r.pend.w = uint32(min(c.Width, maxClientSize)) | |
| 75 | + | } | |
| 76 | + | if c.Height > 0 { | |
| 77 | + | r.pend.h = uint32(min(c.Height, maxClientSize)) | |
| 78 | + | } | |
| 79 | + | r.pend.fullscreen, r.pend.activated = c.Fullscreen, c.Activated | |
| 80 | + | x.markDirty(s, r) | |
| 81 | + | x.wake() | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // Place records the layout position of the client rectangle of s. | |
| 85 | + | func (x *XWM) Place(s Slot, px, py int) { | |
| 86 | + | r := x.managedRec(s) | |
| 87 | + | if r == nil { | |
| 88 | + | return | |
| 89 | + | } | |
| 90 | + | r.lx, r.ly, r.placed = int32(px), int32(py), true | |
| 91 | + | r.pend.x, r.pend.y = x.toX(r) | |
| 92 | + | x.markDirty(s, r) | |
| 93 | + | x.wake() | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // toX converts the layout position of r to X coordinates. | |
| 97 | + | func (x *XWM) toX(r *record) (px, py int32) { | |
| 98 | + | ix, iy := x.space.ToX(int(r.lx), int(r.ly)) | |
| 99 | + | return int32(ix), int32(iy) | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // SetLayout installs the output layout that X coordinates derive from. When it | |
| 103 | + | // changes, placed managed windows are repositioned and mapped override windows | |
| 104 | + | // are re-reported in layout coordinates. Display goroutine only. | |
| 105 | + | func (x *XWM) SetLayout(l ports.Layout) { | |
| 106 | + | if x.closed { | |
| 107 | + | return | |
| 108 | + | } | |
| 109 | + | x.LayoutChanged(x.space.Set(l)) | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | // UseSpace makes sp the coordinate mapping, owned by the host, which calls | |
| 113 | + | // Set on it and then LayoutChanged. Call before Attach. | |
| 114 | + | func (x *XWM) UseSpace(sp *Space) { x.space = sp } | |
| 115 | + | ||
| 116 | + | // LayoutChanged repositions windows after the shared Space was set; changed | |
| 117 | + | // is Set's result. Display goroutine only. | |
| 118 | + | func (x *XWM) LayoutChanged(changed bool) { | |
| 119 | + | if x.closed { | |
| 120 | + | return | |
| 121 | + | } | |
| 122 | + | if n := x.space.Dropped(); n > 0 && !x.warnedSpace { | |
| 123 | + | x.warnedSpace = true | |
| 124 | + | x.log.Warn().Int("dropped", n).Int("max", maxRegions).Msg("more outputs than the X11 layout holds, extra outputs ignored") | |
| 125 | + | } | |
| 126 | + | if !changed { | |
| 127 | + | return | |
| 128 | + | } | |
| 129 | + | for i := range x.tab.recs { | |
| 130 | + | r, s := &x.tab.recs[i], Slot(i+1) | |
| 131 | + | switch { | |
| 132 | + | case r.kind == kindManaged && r.placed: | |
| 133 | + | r.pend.x, r.pend.y = x.toX(r) | |
| 134 | + | x.markDirty(s, r) | |
| 135 | + | case r.kind == kindUnmanaged && r.ovPaired: | |
| 136 | + | x.queueOverride(s, r) | |
| 137 | + | } | |
| 138 | + | } | |
| 139 | + | x.wake() | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | // flushConfigures sends the pending configures, once per display iteration, | |
| 143 | + | // then what focus left to do. | |
| 144 | + | func (x *XWM) flushConfigures() { | |
| 145 | + | for _, s := range x.dirty { | |
| 146 | + | if r := x.tab.rec(s); r != nil && r.kind == kindManaged && r.pend.dirty { | |
| 147 | + | x.sendPending(s, r) | |
| 148 | + | } | |
| 149 | + | } | |
| 150 | + | x.dirty = x.dirty[:0] | |
| 151 | + | x.flushOverrides() | |
| 152 | + | x.flushFocus() | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | // sendPending writes the geometry (with the synthetic ConfigureNotify ICCCM | |
| 156 | + | // 4.1.5 asks for, since Xwayland may not move the window) and the | |
| 157 | + | // _NET_WM_STATE bits that changed. | |
| 158 | + | func (x *XWM) sendPending(s Slot, r *record) { | |
| 159 | + | r.pend.dirty = false | |
| 160 | + | p := &r.pend | |
| 161 | + | w, h := r.size() | |
| 162 | + | if w > 0 && h > 0 && (p.x != r.sent.x || p.y != r.sent.y || w != r.sent.w || h != r.sent.h) { | |
| 163 | + | var v xproto.ConfigureWindowValues | |
| 164 | + | v.SetX(p.x).SetY(p.y).SetWidth(w).SetHeight(h).SetBorderWidth(0) | |
| 165 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v), kindConfigure, x.tab.tag(s), 0) | |
| 166 | + | r.x, r.y, r.w, r.h = p.x, p.y, w, h | |
| 167 | + | x.sendConfigureNotify(r) | |
| 168 | + | r.sent.x, r.sent.y, r.sent.w, r.sent.h = p.x, p.y, w, h | |
| 169 | + | } | |
| 170 | + | var bits uint8 | |
| 171 | + | if p.fullscreen { | |
| 172 | + | bits |= stateFullscreen | |
| 173 | + | } | |
| 174 | + | if p.activated { | |
| 175 | + | bits |= stateFocused | |
| 176 | + | } | |
| 177 | + | r.sent.fullscreen, r.sent.activated = p.fullscreen, p.activated | |
| 178 | + | r.fullscreen = p.fullscreen // ours now: the property is not read back | |
| 179 | + | if bits != r.stateSent { | |
| 180 | + | r.stateSent = bits | |
| 181 | + | x.writeState(r) | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | // writeState rewrites _NET_WM_STATE of r from its bits. | |
| 186 | + | func (x *XWM) writeState(r *record) { | |
| 187 | + | var list [3]uint32 | |
| 188 | + | n := 0 | |
| 189 | + | if r.modal { | |
| 190 | + | list[n] = x.atoms[atomNetWMStateModal] | |
| 191 | + | n++ | |
| 192 | + | } | |
| 193 | + | if r.stateSent&stateFullscreen != 0 { | |
| 194 | + | list[n] = x.atoms[atomNetWMStateFullscreen] | |
| 195 | + | n++ | |
| 196 | + | } | |
| 197 | + | if r.stateSent&stateFocused != 0 { | |
| 198 | + | list[n] = x.atoms[atomNetWMStateFocused] | |
| 199 | + | n++ | |
| 200 | + | } | |
| 201 | + | x.setProp32(r.xid, atomNetWMState, xproto.AtomAtom, list[:n]) | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | // sendConfigureNotify tells the client its geometry as the XWM sees it. | |
| 205 | + | func (x *XWM) sendConfigureNotify(r *record) { | |
| 206 | + | var raw [32]byte | |
| 207 | + | d := xproto.ConfigureNotifyEventData{ | |
| 208 | + | Event: xproto.Window(r.xid), Window: xproto.Window(r.xid), | |
| 209 | + | X: int16(min(max(r.x, math.MinInt16), math.MaxInt16)), Y: int16(min(max(r.y, math.MinInt16), math.MaxInt16)), | |
| 210 | + | Width: uint16(min(r.w, math.MaxUint16)), Height: uint16(min(r.h, math.MaxUint16)), | |
| 211 | + | } | |
| 212 | + | d.Put(&raw) | |
| 213 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(r.xid), xproto.EventMaskStructureNotify, &raw) | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | // onConfigureRequest answers a client resizing itself. Tiled windows are told | |
| 217 | + | // their current geometry; floating ones get the size they ask for; unmanaged | |
| 218 | + | // and unknown windows are configured as requested. | |
| 219 | + | func (x *XWM) onConfigureRequest(ev *xturbo.Event) { | |
| 220 | + | e := xproto.ConfigureRequestOf(ev) | |
| 221 | + | xid := uint32(e.Window()) | |
| 222 | + | mask := e.ValueMask() | |
| 223 | + | s := x.tab.lookup(xid) | |
| 224 | + | r := x.managedRec(s) | |
| 225 | + | if r == nil { | |
| 226 | + | var v xproto.ConfigureWindowValues | |
| 227 | + | if mask&xproto.ConfigWindowX != 0 { | |
| 228 | + | v.SetX(int32(e.X())) | |
| 229 | + | } | |
| 230 | + | if mask&xproto.ConfigWindowY != 0 { | |
| 231 | + | v.SetY(int32(e.Y())) | |
| 232 | + | } | |
| 233 | + | if mask&xproto.ConfigWindowWidth != 0 { | |
| 234 | + | v.SetWidth(uint32(e.Width())) | |
| 235 | + | } | |
| 236 | + | if mask&xproto.ConfigWindowHeight != 0 { | |
| 237 | + | v.SetHeight(uint32(e.Height())) | |
| 238 | + | } | |
| 239 | + | if mask&xproto.ConfigWindowBorderWidth != 0 { | |
| 240 | + | v.SetBorderWidth(uint32(e.BorderWidth())) | |
| 241 | + | } | |
| 242 | + | if mask&xproto.ConfigWindowSibling != 0 { | |
| 243 | + | v.SetSibling(e.Sibling()) | |
| 244 | + | } | |
| 245 | + | if mask&xproto.ConfigWindowStackMode != 0 { | |
| 246 | + | v.SetStackMode(e.StackMode()) | |
| 247 | + | } | |
| 248 | + | xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v) | |
| 249 | + | return | |
| 250 | + | } | |
| 251 | + | if !r.ready { | |
| 252 | + | x.configureBeforeReady(s, r, e) | |
| 253 | + | return | |
| 254 | + | } | |
| 255 | + | if r.floating && mask&(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight) != 0 { | |
| 256 | + | w, h := r.w, r.h | |
| 257 | + | if mask&xproto.ConfigWindowWidth != 0 { | |
| 258 | + | w = uint32(min(max(int(e.Width()), 1), maxClientSize)) | |
| 259 | + | } | |
| 260 | + | if mask&xproto.ConfigWindowHeight != 0 { | |
| 261 | + | h = uint32(min(max(int(e.Height()), 1), maxClientSize)) | |
| 262 | + | } | |
| 263 | + | if w > 0 && h > 0 { | |
| 264 | + | var v xproto.ConfigureWindowValues | |
| 265 | + | v.SetWidth(w).SetHeight(h) | |
| 266 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v), kindConfigure, x.tab.tag(s), 0) | |
| 267 | + | r.w, r.h = w, h | |
| 268 | + | // The size is the client's own: core learns it from the commit | |
| 269 | + | // and Configure only resends it when core wants another. | |
| 270 | + | r.pend.w, r.pend.h = w, h | |
| 271 | + | r.sent.w, r.sent.h = w, h | |
| 272 | + | } | |
| 273 | + | } | |
| 274 | + | x.sendConfigureNotify(r) | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | // configureBeforeReady honours the geometry a window asks for before it is | |
| 278 | + | // ready (XMoveResize before XMapWindow), floating or not: core only sees the | |
| 279 | + | // window at Ready and the client must find the geometry it set. | |
| 280 | + | func (x *XWM) configureBeforeReady(s Slot, r *record, e xproto.ConfigureRequestEvent) { | |
| 281 | + | mask := e.ValueMask() | |
| 282 | + | var v xproto.ConfigureWindowValues | |
| 283 | + | if mask&xproto.ConfigWindowX != 0 { | |
| 284 | + | r.x = int32(e.X()) | |
| 285 | + | v.SetX(r.x) | |
| 286 | + | } | |
| 287 | + | if mask&xproto.ConfigWindowY != 0 { | |
| 288 | + | r.y = int32(e.Y()) | |
| 289 | + | v.SetY(r.y) | |
| 290 | + | } | |
| 291 | + | if mask&xproto.ConfigWindowWidth != 0 { | |
| 292 | + | r.w, r.sized = uint32(min(max(int(e.Width()), 1), maxClientSize)), true | |
| 293 | + | v.SetWidth(r.w) | |
| 294 | + | } | |
| 295 | + | if mask&xproto.ConfigWindowHeight != 0 { | |
| 296 | + | r.h, r.sized = uint32(min(max(int(e.Height()), 1), maxClientSize)), true | |
| 297 | + | v.SetHeight(r.h) | |
| 298 | + | } | |
| 299 | + | if mask&(xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth|xproto.ConfigWindowHeight) != 0 { | |
| 300 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v), kindConfigure, x.tab.tag(s), 0) | |
| 301 | + | } | |
| 302 | + | x.sendConfigureNotify(r) | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | // onConfigureNotify keeps the known geometry of every top-level window. | |
| 306 | + | func (x *XWM) onConfigureNotify(ev *xturbo.Event) { | |
| 307 | + | e := xproto.ConfigureNotifyOf(ev) | |
| 308 | + | if e.Event() != xproto.Window(x.root) { | |
| 309 | + | return | |
| 310 | + | } | |
| 311 | + | if r := x.tab.rec(x.tab.lookup(uint32(e.Window()))); r != nil && r.kind != kindFree { | |
| 312 | + | r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height()) | |
| 313 | + | if r.kind == kindUnmanaged && r.ovPaired { | |
| 314 | + | x.queueOverride(x.tab.lookup(r.xid), r) | |
| 315 | + | } | |
| 316 | + | } | |
| 317 | + | } | |
A internal/adapters/wayland/xwm/configure_test.go +591 -0
| @@ -0,0 +1,591 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "runtime" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "bnema.dev/xturbo/xproto" | |
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |
| 11 | + | "github.com/stretchr/testify/mock" | |
| 12 | + | "github.com/stretchr/testify/require" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // newWin makes a ready managed window known to the XWM and the mocked host. | |
| 16 | + | func newWin(t *testing.T, x *XWM, p *xpeer, h *mockhost, xid uint32) Slot { | |
| 17 | + | t.Helper() | |
| 18 | + | h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(xid)).Once() | |
| 19 | + | p.createNotify(xid, false) | |
| 20 | + | readUntil(t, x, func() bool { return x.tab.lookup(xid) != 0 }) | |
| 21 | + | s := x.tab.lookup(xid) | |
| 22 | + | r := x.tab.rec(s) | |
| 23 | + | r.ready, r.w, r.h = true, 300, 200 | |
| 24 | + | return s | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // settle flushes the XWM and waits until the peer has seen everything sent so | |
| 28 | + | // far; it returns the number of requests received. | |
| 29 | + | func settle(t *testing.T, x *XWM, p *xpeer) int { | |
| 30 | + | t.Helper() | |
| 31 | + | x.flush() | |
| 32 | + | n := len(p.requests()) | |
| 33 | + | xproto.GetInputFocus(x.conn.Buf()) | |
| 34 | + | require.NoError(t, x.conn.Flush()) | |
| 35 | + | _, n = p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus }) | |
| 36 | + | return n | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // since returns the requests of the given major opcode received after skip. | |
| 40 | + | func since(p *xpeer, skip int, major uint8) []xpeerReq { | |
| 41 | + | var out []xpeerReq | |
| 42 | + | for _, r := range p.requests()[skip:] { | |
| 43 | + | if r.major == major { | |
| 44 | + | out = append(out, r) | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | return out | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // sentEvents returns the 32-byte events of the SendEvent requests to dest. | |
| 51 | + | func sentEvents(p *xpeer, skip int, dest uint32) [][]byte { | |
| 52 | + | var out [][]byte | |
| 53 | + | for _, r := range since(p, skip, opSendEvent) { | |
| 54 | + | if r.u32(4) == dest { | |
| 55 | + | out = append(out, r.raw[12:44]) | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | return out | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | func propsTo(p *xpeer, skip int, win uint32, atom string) []xpeerReq { | |
| 62 | + | var out []xpeerReq | |
| 63 | + | a := p.atom(atom) | |
| 64 | + | for _, r := range since(p, skip, opChangeProperty) { | |
| 65 | + | if r.u32(4) == win && r.u32(8) == a { | |
| 66 | + | out = append(out, r) | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | return out | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | func (x *XWM) feed(t *testing.T, p *xpeer, send func()) { | |
| 73 | + | t.Helper() | |
| 74 | + | ev := x.stats.Events | |
| 75 | + | send() | |
| 76 | + | readUntil(t, x, func() bool { return x.stats.Events > ev }) | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | func TestConfigureCoalescesIntoOneRequest(t *testing.T) { | |
| 80 | + | x, p, h, _, runs := attached(t) | |
| 81 | + | const win = 0x500001 | |
| 82 | + | s := newWin(t, x, p, h, win) | |
| 83 | + | n := settle(t, x, p) | |
| 84 | + | ||
| 85 | + | x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480}) | |
| 86 | + | x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true}) | |
| 87 | + | x.Place(s, 10, 20) | |
| 88 | + | require.Len(t, *runs, 1, "one flush posted") | |
| 89 | + | n2 := settle(t, x, p) | |
| 90 | + | ||
| 91 | + | cw := since(p, n, opConfigureWindow) | |
| 92 | + | require.Len(t, cw, 1) | |
| 93 | + | require.Equal(t, uint32(win), cw[0].u32(4)) | |
| 94 | + | require.EqualValues(t, 0x1f, binary.LittleEndian.Uint16(cw[0].raw[8:]), "x y width height border") | |
| 95 | + | require.Equal(t, []uint32{10, 20, 800, 600, 0}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20), cw[0].u32(24), cw[0].u32(28)}) | |
| 96 | + | ||
| 97 | + | evs := sentEvents(p, n, win) | |
| 98 | + | require.Len(t, evs, 1, "one synthetic ConfigureNotify") | |
| 99 | + | require.EqualValues(t, 22, evs[0][0]) | |
| 100 | + | require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][4:])) | |
| 101 | + | require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][8:])) | |
| 102 | + | require.EqualValues(t, 10, binary.LittleEndian.Uint16(evs[0][16:])) | |
| 103 | + | require.EqualValues(t, 20, binary.LittleEndian.Uint16(evs[0][18:])) | |
| 104 | + | require.EqualValues(t, 800, binary.LittleEndian.Uint16(evs[0][20:])) | |
| 105 | + | require.EqualValues(t, 600, binary.LittleEndian.Uint16(evs[0][22:])) | |
| 106 | + | require.Equal(t, xproto.EventMaskStructureNotify, xproto.EventMask(since(p, n, opSendEvent)[0].u32(8))) | |
| 107 | + | ||
| 108 | + | // Unchanged: nothing is sent. | |
| 109 | + | x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true}) | |
| 110 | + | x.Place(s, 10, 20) | |
| 111 | + | settle(t, x, p) | |
| 112 | + | require.Len(t, since(p, n2, opConfigureWindow), 0) | |
| 113 | + | require.Len(t, sentEvents(p, n2, win), 0) | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | func TestConfigureWritesNetWMState(t *testing.T) { | |
| 117 | + | x, p, h, _, _ := attached(t) | |
| 118 | + | const win = 0x500001 | |
| 119 | + | s := newWin(t, x, p, h, win) | |
| 120 | + | x.tab.rec(s).modal = true | |
| 121 | + | n := settle(t, x, p) | |
| 122 | + | ||
| 123 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100}) | |
| 124 | + | settle(t, x, p) | |
| 125 | + | require.Empty(t, propsTo(p, n, win, "_NET_WM_STATE"), "no state bit changed") | |
| 126 | + | ||
| 127 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Fullscreen: true, Activated: true}) | |
| 128 | + | settle(t, x, p) | |
| 129 | + | st, ok := p.prop(win, "_NET_WM_STATE") | |
| 130 | + | require.True(t, ok) | |
| 131 | + | require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FULLSCREEN"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |
| 132 | + | ||
| 133 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Activated: true}) | |
| 134 | + | settle(t, x, p) | |
| 135 | + | st, _ = p.prop(win, "_NET_WM_STATE") | |
| 136 | + | require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | func TestConfigureBadWindowIsAttributedAndIgnored(t *testing.T) { | |
| 140 | + | x, p, h, _, _ := attached(t) | |
| 141 | + | const win = 0x500001 | |
| 142 | + | s := newWin(t, x, p, h, win) | |
| 143 | + | n := settle(t, x, p) | |
| 144 | + | x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480}) | |
| 145 | + | x.flush() | |
| 146 | + | cw := []xpeerReq{} | |
| 147 | + | r, _ := p.waitReq(n, func(r xpeerReq) bool { return r.major == opConfigureWindow }) | |
| 148 | + | cw = append(cw, r) | |
| 149 | + | ||
| 150 | + | p.mu.Lock() | |
| 151 | + | e := make([]byte, 32) | |
| 152 | + | e[1] = xproto.BadWindow | |
| 153 | + | binary.LittleEndian.PutUint16(e[2:], cw[0].seq) | |
| 154 | + | binary.LittleEndian.PutUint32(e[4:], win) | |
| 155 | + | e[10] = opConfigureWindow | |
| 156 | + | p.write(e) | |
| 157 | + | p.mu.Unlock() | |
| 158 | + | readUntil(t, x, func() bool { return x.stats.Errors == 1 }) | |
| 159 | + | require.Zero(t, x.stats.Unattributed) | |
| 160 | + | require.False(t, x.closed) | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | func sendConfigureRequest(p *xpeer, win uint32, mask uint16, x, y int16, w, h uint16) { | |
| 164 | + | p.event(23, func(b *[32]byte) { | |
| 165 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |
| 166 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 167 | + | binary.LittleEndian.PutUint16(b[16:], uint16(x)) | |
| 168 | + | binary.LittleEndian.PutUint16(b[18:], uint16(y)) | |
| 169 | + | binary.LittleEndian.PutUint16(b[20:], w) | |
| 170 | + | binary.LittleEndian.PutUint16(b[22:], h) | |
| 171 | + | binary.LittleEndian.PutUint16(b[26:], mask) | |
| 172 | + | }) | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | func TestConfigureRequestTiledOnlyGetsNotify(t *testing.T) { | |
| 176 | + | x, p, h, _, _ := attached(t) | |
| 177 | + | const win = 0x500001 | |
| 178 | + | newWin(t, x, p, h, win) | |
| 179 | + | n := settle(t, x, p) | |
| 180 | + | ||
| 181 | + | x.feed(t, p, func() { sendConfigureRequest(p, win, 0xf, 5, 6, 1000, 900) }) | |
| 182 | + | settle(t, x, p) | |
| 183 | + | require.Empty(t, since(p, n, opConfigureWindow), "a tiled window cannot resize itself") | |
| 184 | + | evs := sentEvents(p, n, win) | |
| 185 | + | require.Len(t, evs, 1) | |
| 186 | + | require.EqualValues(t, 22, evs[0][0]) | |
| 187 | + | require.EqualValues(t, 300, binary.LittleEndian.Uint16(evs[0][20:]), "current geometry") | |
| 188 | + | require.EqualValues(t, 200, binary.LittleEndian.Uint16(evs[0][22:])) | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | func TestConfigureRequestFloatingHonoursSize(t *testing.T) { | |
| 192 | + | x, p, h, _, _ := attached(t) | |
| 193 | + | const win = 0x500001 | |
| 194 | + | s := newWin(t, x, p, h, win) | |
| 195 | + | x.tab.rec(s).floating = true | |
| 196 | + | n := settle(t, x, p) | |
| 197 | + | ||
| 198 | + | x.feed(t, p, func() { | |
| 199 | + | sendConfigureRequest(p, win, uint16(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight), 0, 0, 70000&0xffff, 20000) | |
| 200 | + | }) | |
| 201 | + | settle(t, x, p) | |
| 202 | + | cw := since(p, n, opConfigureWindow) | |
| 203 | + | require.Len(t, cw, 1) | |
| 204 | + | require.EqualValues(t, xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, binary.LittleEndian.Uint16(cw[0].raw[8:])) | |
| 205 | + | require.EqualValues(t, 4464, cw[0].u32(12)) | |
| 206 | + | require.EqualValues(t, maxClientSize, cw[0].u32(16), "clamped") | |
| 207 | + | require.Len(t, sentEvents(p, n, win), 1) | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | func TestConfigureRequestUnmanagedHonouredVerbatim(t *testing.T) { | |
| 211 | + | x, p, _, _, _ := attached(t) | |
| 212 | + | const win = 0x500009 | |
| 213 | + | p.createNotify(win, true) | |
| 214 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) != 0 }) | |
| 215 | + | n := settle(t, x, p) | |
| 216 | + | ||
| 217 | + | x.feed(t, p, func() { | |
| 218 | + | sendConfigureRequest(p, win, uint16(xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth), 30, 40, 50, 60) | |
| 219 | + | }) | |
| 220 | + | settle(t, x, p) | |
| 221 | + | cw := since(p, n, opConfigureWindow) | |
| 222 | + | require.Len(t, cw, 1) | |
| 223 | + | require.EqualValues(t, 7, binary.LittleEndian.Uint16(cw[0].raw[8:])) | |
| 224 | + | require.Equal(t, []uint32{30, 40, 50}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20)}) | |
| 225 | + | require.Empty(t, sentEvents(p, n, win)) | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | func TestConfigureStaleAndDuplicateDirtySlots(t *testing.T) { | |
| 229 | + | x, p, h, _, _ := attached(t) | |
| 230 | + | const a, b = 0x500001, 0x500002 | |
| 231 | + | s := newWin(t, x, p, h, a) | |
| 232 | + | for i := 1; i <= 50; i++ { | |
| 233 | + | x.Configure(s, ports.ConfigureWindow{Width: 100 + i, Height: 100}) | |
| 234 | + | } | |
| 235 | + | require.Len(t, x.dirty, 1, "queued once per false→true transition") | |
| 236 | + | ||
| 237 | + | // The window dies with its entry queued; the slot is reused. | |
| 238 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 239 | + | p.destroyNotify(a) | |
| 240 | + | readUntil(t, x, func() bool { return x.tab.lookup(a) == 0 }) | |
| 241 | + | s2 := newWin(t, x, p, h, b) | |
| 242 | + | require.Equal(t, s, s2, "LIFO slot reuse") | |
| 243 | + | n := len(p.requests()) | |
| 244 | + | x.Configure(s2, ports.ConfigureWindow{Width: 640, Height: 480}) | |
| 245 | + | require.Len(t, x.dirty, 1) | |
| 246 | + | x.dirty = append(x.dirty, s, s) // stale and duplicate entries must be skipped | |
| 247 | + | settle(t, x, p) | |
| 248 | + | cw := since(p, n, opConfigureWindow) | |
| 249 | + | require.Len(t, cw, 1, "sent once despite duplicates") | |
| 250 | + | require.Equal(t, uint32(b), cw[0].u32(4)) | |
| 251 | + | require.Empty(t, x.dirty) | |
| 252 | + | require.LessOrEqual(t, cap(x.dirty), len(x.tab.recs)) | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | func TestDirtyQueueNeverGrows(t *testing.T) { | |
| 256 | + | x, p, h, _, _ := attached(t) | |
| 257 | + | c0 := cap(x.dirty) | |
| 258 | + | const base = 0x600000 | |
| 259 | + | for i := 0; i < 3*c0; i++ { // each round leaves a stale entry behind | |
| 260 | + | xid := uint32(base + i) | |
| 261 | + | h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(1)).Once() | |
| 262 | + | p.createNotify(xid, false) | |
| 263 | + | readUntil(t, x, func() bool { return x.tab.lookup(xid) != 0 }) | |
| 264 | + | s := x.tab.lookup(xid) | |
| 265 | + | x.tab.rec(s).ready = true | |
| 266 | + | x.Configure(s, ports.ConfigureWindow{Width: 10, Height: 10}) | |
| 267 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 268 | + | p.destroyNotify(xid) | |
| 269 | + | readUntil(t, x, func() bool { return x.tab.lookup(xid) == 0 }) | |
| 270 | + | } | |
| 271 | + | require.Equal(t, c0, cap(x.dirty)) | |
| 272 | + | require.LessOrEqual(t, len(x.dirty), c0) | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | func TestPublicCommandsIgnoredWhenClosed(t *testing.T) { | |
| 276 | + | x, p, h, src, _ := attached(t) | |
| 277 | + | const win = 0x500001 | |
| 278 | + | s := newWin(t, x, p, h, win) | |
| 279 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 280 | + | src.EXPECT().Remove().Once() | |
| 281 | + | x.Close() | |
| 282 | + | require.NotPanics(t, func() { | |
| 283 | + | x.Configure(s, ports.ConfigureWindow{Width: 1, Height: 1}) | |
| 284 | + | x.Place(s, 1, 2) | |
| 285 | + | x.Focus(s) | |
| 286 | + | x.CloseWindow(s) | |
| 287 | + | }) | |
| 288 | + | require.Empty(t, x.dirty) | |
| 289 | + | require.Zero(t, x.active) | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | // Focus. | |
| 293 | + | ||
| 294 | + | type focusModel struct { | |
| 295 | + | name string | |
| 296 | + | input, take bool | |
| 297 | + | setFocus bool // SetInputFocus on the window | |
| 298 | + | takeFocus bool // WM_TAKE_FOCUS sent | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | func TestFocusInputModels(t *testing.T) { | |
| 302 | + | for _, m := range []focusModel{ | |
| 303 | + | {"passive", true, false, true, false}, | |
| 304 | + | {"locally active", true, true, true, true}, | |
| 305 | + | {"globally active", false, true, false, true}, | |
| 306 | + | {"no input", false, false, false, false}, | |
| 307 | + | } { | |
| 308 | + | t.Run(m.name, func(t *testing.T) { | |
| 309 | + | x, p, h, _, _ := attached(t) | |
| 310 | + | const win = 0x500001 | |
| 311 | + | s := newWin(t, x, p, h, win) | |
| 312 | + | r := x.tab.rec(s) | |
| 313 | + | r.input, r.takeFocus = m.input, m.take | |
| 314 | + | n := settle(t, x, p) | |
| 315 | + | ||
| 316 | + | x.Focus(s) | |
| 317 | + | settle(t, x, p) | |
| 318 | + | var focuses []xpeerReq | |
| 319 | + | for _, r := range since(p, n, opSetInputFocus) { | |
| 320 | + | if r.u32(4) == win { | |
| 321 | + | focuses = append(focuses, r) | |
| 322 | + | } | |
| 323 | + | } | |
| 324 | + | if m.setFocus { | |
| 325 | + | require.Len(t, focuses, 1) | |
| 326 | + | require.EqualValues(t, xproto.InputFocusNone, focuses[0].raw[1], "never PointerRoot") | |
| 327 | + | require.Zero(t, focuses[0].u32(8), "CurrentTime") | |
| 328 | + | } else { | |
| 329 | + | require.Empty(t, focuses) | |
| 330 | + | } | |
| 331 | + | var takes int | |
| 332 | + | for _, ev := range sentEvents(p, n, win) { | |
| 333 | + | if ev[0] == 33 && binary.LittleEndian.Uint32(ev[8:]) == p.atom("WM_PROTOCOLS") { | |
| 334 | + | require.EqualValues(t, 32, ev[1]) | |
| 335 | + | require.Equal(t, p.atom("WM_TAKE_FOCUS"), binary.LittleEndian.Uint32(ev[12:])) | |
| 336 | + | require.Zero(t, binary.LittleEndian.Uint32(ev[16:]), "CurrentTime") | |
| 337 | + | takes++ | |
| 338 | + | } | |
| 339 | + | } | |
| 340 | + | require.Equal(t, map[bool]int{true: 1, false: 0}[m.takeFocus], takes) | |
| 341 | + | ||
| 342 | + | act, ok := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 343 | + | require.True(t, ok) | |
| 344 | + | require.Equal(t, []uint32{win}, atomsOf(act), "named even for No-input windows") | |
| 345 | + | st, ok := p.prop(win, "_NET_WM_STATE") | |
| 346 | + | require.True(t, ok) | |
| 347 | + | require.Contains(t, atomsOf(st), p.atom("_NET_WM_STATE_FOCUSED")) | |
| 348 | + | }) | |
| 349 | + | } | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | func TestFocusActiveWindowWrittenOnlyOnChange(t *testing.T) { | |
| 353 | + | x, p, h, _, _ := attached(t) | |
| 354 | + | const a, b = 0x500001, 0x500002 | |
| 355 | + | sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b) | |
| 356 | + | n := settle(t, x, p) | |
| 357 | + | ||
| 358 | + | x.Focus(sa) | |
| 359 | + | x.Focus(sa) | |
| 360 | + | settle(t, x, p) | |
| 361 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_ACTIVE_WINDOW"), 1) | |
| 362 | + | ||
| 363 | + | x.Focus(sb) | |
| 364 | + | settle(t, x, p) | |
| 365 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 366 | + | require.Equal(t, []uint32{b}, atomsOf(act)) | |
| 367 | + | sta, _ := p.prop(a, "_NET_WM_STATE") | |
| 368 | + | require.NotContains(t, atomsOf(sta), p.atom("_NET_WM_STATE_FOCUSED"), "old window loses FOCUSED") | |
| 369 | + | stb, _ := p.prop(b, "_NET_WM_STATE") | |
| 370 | + | require.Contains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED")) | |
| 371 | + | ||
| 372 | + | n = settle(t, x, p) | |
| 373 | + | x.Focus(0) | |
| 374 | + | settle(t, x, p) | |
| 375 | + | none := since(p, n, opSetInputFocus) | |
| 376 | + | require.Len(t, none, 1) | |
| 377 | + | require.Zero(t, none[0].u32(4), "focus None") | |
| 378 | + | require.EqualValues(t, xproto.InputFocusNone, none[0].raw[1]) | |
| 379 | + | act, _ = p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 380 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |
| 381 | + | stb, _ = p.prop(b, "_NET_WM_STATE") | |
| 382 | + | require.NotContains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED")) | |
| 383 | + | require.Zero(t, x.active) | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | func TestFocusStackingLists(t *testing.T) { | |
| 387 | + | x, p, h, _, _ := attached(t) | |
| 388 | + | const a, b, c = 0x500001, 0x500002, 0x500003 | |
| 389 | + | sa := newWin(t, x, p, h, a) | |
| 390 | + | newWin(t, x, p, h, b) | |
| 391 | + | newWin(t, x, p, h, c) | |
| 392 | + | for _, w := range []uint32{a, b, c} { | |
| 393 | + | x.addClient(w) | |
| 394 | + | } | |
| 395 | + | settle(t, x, p) | |
| 396 | + | list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 397 | + | require.Equal(t, []uint32{a, b, c}, atomsOf(list)) | |
| 398 | + | n := settle(t, x, p) | |
| 399 | + | ||
| 400 | + | x.Focus(sa) | |
| 401 | + | settle(t, x, p) | |
| 402 | + | stack, _ := p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING") | |
| 403 | + | require.Equal(t, []uint32{b, c, a}, atomsOf(stack), "bottom to top") | |
| 404 | + | list, _ = p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 405 | + | require.Equal(t, []uint32{a, b, c}, atomsOf(list), "map order") | |
| 406 | + | require.Empty(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), "clients list untouched by focus") | |
| 407 | + | var raised bool | |
| 408 | + | for _, r := range since(p, n, opConfigureWindow) { | |
| 409 | + | if r.u32(4) == a && binary.LittleEndian.Uint16(r.raw[8:]) == uint16(xproto.ConfigWindowStackMode) { | |
| 410 | + | require.EqualValues(t, xproto.StackModeAbove, r.u32(12)) | |
| 411 | + | raised = true | |
| 412 | + | } | |
| 413 | + | } | |
| 414 | + | require.True(t, raised) | |
| 415 | + | ||
| 416 | + | // A burst costs one write per list. | |
| 417 | + | n = settle(t, x, p) | |
| 418 | + | x.removeClient(b) | |
| 419 | + | x.removeClient(c) | |
| 420 | + | settle(t, x, p) | |
| 421 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), 1) | |
| 422 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST_STACKING"), 1) | |
| 423 | + | stack, _ = p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING") | |
| 424 | + | require.Equal(t, []uint32{a}, atomsOf(stack)) | |
| 425 | + | } | |
| 426 | + | ||
| 427 | + | func TestDestroyedActiveWindowClearsActive(t *testing.T) { | |
| 428 | + | x, p, h, _, _ := attached(t) | |
| 429 | + | const win = 0x500001 | |
| 430 | + | s := newWin(t, x, p, h, win) | |
| 431 | + | x.addClient(win) | |
| 432 | + | x.Focus(s) | |
| 433 | + | settle(t, x, p) | |
| 434 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 435 | + | p.destroyNotify(win) | |
| 436 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 }) | |
| 437 | + | settle(t, x, p) | |
| 438 | + | require.Zero(t, x.active) | |
| 439 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 440 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |
| 441 | + | list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 442 | + | require.Empty(t, list.data) | |
| 443 | + | } | |
| 444 | + | ||
| 445 | + | func TestFocusInDenied(t *testing.T) { | |
| 446 | + | x, p, h, _, _ := attached(t) | |
| 447 | + | const a, b = 0x500001, 0x500002 | |
| 448 | + | sa := newWin(t, x, p, h, a) | |
| 449 | + | newWin(t, x, p, h, b) | |
| 450 | + | x.Focus(sa) | |
| 451 | + | n := settle(t, x, p) | |
| 452 | + | ||
| 453 | + | focusIn := func(win uint32, detail byte) { | |
| 454 | + | p.event(9, func(e *[32]byte) { | |
| 455 | + | e[1] = detail | |
| 456 | + | binary.LittleEndian.PutUint32(e[4:], win) | |
| 457 | + | }) | |
| 458 | + | } | |
| 459 | + | focusCount := func(skip int) int { | |
| 460 | + | c := 0 | |
| 461 | + | for _, r := range since(p, skip, opSetInputFocus) { | |
| 462 | + | if r.u32(4) == a { | |
| 463 | + | c++ | |
| 464 | + | } | |
| 465 | + | } | |
| 466 | + | return c | |
| 467 | + | } | |
| 468 | + | x.feed(t, p, func() { focusIn(a, 0) }) // the active window itself: fine | |
| 469 | + | x.feed(t, p, func() { focusIn(b, byte(xproto.NotifyDetailPointer)) }) | |
| 470 | + | settle(t, x, p) | |
| 471 | + | require.Zero(t, focusCount(n)) | |
| 472 | + | ||
| 473 | + | x.feed(t, p, func() { focusIn(b, 0); focusIn(b, 0) }) | |
| 474 | + | readUntil(t, x, func() bool { return x.stats.Events >= 5 }) | |
| 475 | + | settle(t, x, p) | |
| 476 | + | require.Equal(t, 1, focusCount(n), "denied once per iteration") | |
| 477 | + | n = settle(t, x, p) | |
| 478 | + | settle(t, x, p) | |
| 479 | + | require.Zero(t, focusCount(n)) | |
| 480 | + | } | |
| 481 | + | ||
| 482 | + | func TestCloseWindow(t *testing.T) { | |
| 483 | + | x, p, h, _, _ := attached(t) | |
| 484 | + | const a, b = 0x500001, 0x500002 | |
| 485 | + | sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b) | |
| 486 | + | x.tab.rec(sa).deleteWindow = true | |
| 487 | + | n := settle(t, x, p) | |
| 488 | + | ||
| 489 | + | x.CloseWindow(sa) | |
| 490 | + | x.CloseWindow(sb) | |
| 491 | + | settle(t, x, p) | |
| 492 | + | evs := sentEvents(p, n, a) | |
| 493 | + | require.Len(t, evs, 1) | |
| 494 | + | require.EqualValues(t, 33, evs[0][0]) | |
| 495 | + | require.EqualValues(t, 32, evs[0][1]) | |
| 496 | + | require.Equal(t, p.atom("WM_PROTOCOLS"), binary.LittleEndian.Uint32(evs[0][8:])) | |
| 497 | + | require.Equal(t, p.atom("WM_DELETE_WINDOW"), binary.LittleEndian.Uint32(evs[0][12:])) | |
| 498 | + | require.Zero(t, binary.LittleEndian.Uint32(evs[0][16:])) | |
| 499 | + | require.Empty(t, sentEvents(p, n, b)) | |
| 500 | + | ||
| 501 | + | kills := since(p, n, opKillClient) | |
| 502 | + | require.Len(t, kills, 1) | |
| 503 | + | require.Equal(t, uint32(b), kills[0].u32(4)) | |
| 504 | + | } | |
| 505 | + | ||
| 506 | + | // Allocation guards. The peer sinks requests so the connection never blocks; | |
| 507 | + | // its goroutine is idle in the sink loop before measuring. | |
| 508 | + | ||
| 509 | + | func sinkPeer(t *testing.T, x *XWM, p *xpeer) { | |
| 510 | + | t.Helper() | |
| 511 | + | // Everything written after sink.Store(true) is discarded, so one marker | |
| 512 | + | // request proves the peer loop is in sink mode and idle again. | |
| 513 | + | // A round trip first: the peer has consumed every earlier request, so the | |
| 514 | + | // marker is the only one it can see in sink mode. | |
| 515 | + | require.NoError(t, x.conn.Sync(&x.h)) | |
| 516 | + | before := p.discarded.Load() | |
| 517 | + | p.sink.Store(true) | |
| 518 | + | xproto.GetInputFocus(x.conn.Buf()) | |
| 519 | + | require.NoError(t, x.conn.Flush()) | |
| 520 | + | deadline := time.Now().Add(30 * time.Second) // failure guard only: the wait is event driven | |
| 521 | + | for p.discarded.Load() == before { | |
| 522 | + | if time.Now().After(deadline) { | |
| 523 | + | t.Fatal("xpeer: sink marker never discarded") | |
| 524 | + | } | |
| 525 | + | runtime.Gosched() | |
| 526 | + | } | |
| 527 | + | x.src = nil // no source: write backpressure would not allocate either | |
| 528 | + | x.post = func(func()) {} | |
| 529 | + | x.warnedRing = true // the sink never answers: Expect slots stay used | |
| 530 | + | } | |
| 531 | + | ||
| 532 | + | func TestConfigureFlushAllocations(t *testing.T) { | |
| 533 | + | x, p, h, _, _ := attached(t) | |
| 534 | + | s := newWin(t, x, p, h, 0x500001) | |
| 535 | + | sinkPeer(t, x, p) | |
| 536 | + | i := 0 | |
| 537 | + | allocs := testing.AllocsPerRun(300, func() { | |
| 538 | + | i++ | |
| 539 | + | x.Configure(s, ports.ConfigureWindow{Width: 100 + i%2, Height: 100, Fullscreen: i%3 == 0, Activated: i%2 == 0}) | |
| 540 | + | x.Place(s, i%5, i%7) | |
| 541 | + | x.posted = false | |
| 542 | + | x.flush() | |
| 543 | + | }) | |
| 544 | + | require.Zero(t, allocs) | |
| 545 | + | require.False(t, x.closed) | |
| 546 | + | } | |
| 547 | + | ||
| 548 | + | func TestFocusAllocations(t *testing.T) { | |
| 549 | + | x, p, h, _, _ := attached(t) | |
| 550 | + | a, b := newWin(t, x, p, h, 0x500001), newWin(t, x, p, h, 0x500002) | |
| 551 | + | x.addClient(x.tab.rec(a).xid) | |
| 552 | + | x.addClient(x.tab.rec(b).xid) | |
| 553 | + | x.tab.rec(b).takeFocus = true | |
| 554 | + | x.tab.rec(a).input, x.tab.rec(b).input = true, true | |
| 555 | + | sinkPeer(t, x, p) | |
| 556 | + | i := 0 | |
| 557 | + | allocs := testing.AllocsPerRun(150, func() { | |
| 558 | + | i++ | |
| 559 | + | switch i % 3 { | |
| 560 | + | case 0: | |
| 561 | + | x.Focus(a) | |
| 562 | + | case 1: | |
| 563 | + | x.Focus(b) | |
| 564 | + | default: | |
| 565 | + | x.Focus(0) | |
| 566 | + | } | |
| 567 | + | x.refocus = i%5 == 0 | |
| 568 | + | x.posted = false | |
| 569 | + | x.flush() | |
| 570 | + | }) | |
| 571 | + | require.Zero(t, allocs) | |
| 572 | + | require.False(t, x.closed) | |
| 573 | + | } | |
| 574 | + | ||
| 575 | + | // Repeated identical fullscreen configures cost nothing. | |
| 576 | + | func TestFullscreenConfigureAllocations(t *testing.T) { | |
| 577 | + | x, p, h, _, _ := attached(t) | |
| 578 | + | s := newWin(t, x, p, h, 0x500001) | |
| 579 | + | sinkPeer(t, x, p) | |
| 580 | + | cfg := ports.ConfigureWindow{Width: 1920, Height: 1080, Fullscreen: true, Activated: true} | |
| 581 | + | x.Configure(s, cfg) | |
| 582 | + | x.posted = false | |
| 583 | + | x.flush() | |
| 584 | + | allocs := testing.AllocsPerRun(300, func() { | |
| 585 | + | x.Configure(s, cfg) | |
| 586 | + | x.Place(s, 0, 0) | |
| 587 | + | x.posted = false | |
| 588 | + | x.flush() | |
| 589 | + | }) | |
| 590 | + | require.Zero(t, allocs) | |
| 591 | + | } | |
A internal/adapters/wayland/xwm/cursor.go +109 -0
| @@ -0,0 +1,109 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bnema.dev/xturbo" | |
| 5 | + | "bnema.dev/xturbo/render" | |
| 6 | + | "bnema.dev/xturbo/xproto" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // Root cursor: a themed arrow on the root window so the X background does not | |
| 10 | + | // show Xwayland's cross. One-shot at boot; never fatal. | |
| 11 | + | ||
| 12 | + | const maxCursorBytes = 1 << 18 // one PutImage request, below the core limit | |
| 13 | + | ||
| 14 | + | // bootRootCursor sets the cursor of the root window. | |
| 15 | + | func (x *XWM) bootRootCursor() error { | |
| 16 | + | if !x.cfg.RootCursor { | |
| 17 | + | return nil | |
| 18 | + | } | |
| 19 | + | img := x.cfg.Cursor | |
| 20 | + | usable := img.W > 0 && img.H > 0 && img.W <= 0xffff && img.H <= 0xffff && | |
| 21 | + | len(img.Pixels) == img.W*img.H*4 && len(img.Pixels) <= maxCursorBytes | |
| 22 | + | if usable && x.conn.Ext().Info(xturbo.ExtRender).Present { | |
| 23 | + | b := x.conn.Buf() | |
| 24 | + | x.expect(render.QueryPictFormats(b), kindPictFormats, 0, 0) | |
| 25 | + | if err := x.conn.Sync(&x.h); err != nil { | |
| 26 | + | return err | |
| 27 | + | } | |
| 28 | + | if x.bootErr != nil { | |
| 29 | + | return x.bootErr | |
| 30 | + | } | |
| 31 | + | if x.xs.argb != 0 && x.argbCursor(img.W, img.H, img.HotX, img.HotY, img.Pixels) { | |
| 32 | + | return nil | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | x.glyphCursor() | |
| 36 | + | return nil | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // onPictFormatsReply remembers the ARGB32 picture format (depth 32, direct, | |
| 40 | + | // alpha mask 0xff at shift 24). | |
| 41 | + | func (x *XWM) onPictFormatsReply(r *xturbo.Reply) { | |
| 42 | + | v, ok := render.ParseQueryPictFormatsReply(r.Raw) | |
| 43 | + | if !ok { | |
| 44 | + | return | |
| 45 | + | } | |
| 46 | + | l := v.Formats() | |
| 47 | + | for i := 0; i < l.Len(); i++ { | |
| 48 | + | f := l.At(i) | |
| 49 | + | if f.Type == render.PictTypeDirect && f.Depth == 32 && f.Direct.AlphaMask == 0xff && f.Direct.AlphaShift == 24 && | |
| 50 | + | f.Direct.RedMask == 0xff && f.Direct.RedShift == 16 { | |
| 51 | + | x.xs.argb = uint32(f.Id) | |
| 52 | + | return | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | func (x *XWM) newIDs(ids []uint32) bool { | |
| 58 | + | for i := range ids { | |
| 59 | + | id, err := x.conn.NewXID() | |
| 60 | + | if err != nil { | |
| 61 | + | return false | |
| 62 | + | } | |
| 63 | + | ids[i] = id | |
| 64 | + | } | |
| 65 | + | return true | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // argbCursor uploads the image and installs it on the root. | |
| 69 | + | func (x *XWM) argbCursor(w, h, hotX, hotY int, px []byte) bool { | |
| 70 | + | var id [4]uint32 // pixmap, gc, picture, cursor | |
| 71 | + | if !x.newIDs(id[:]) { | |
| 72 | + | return false | |
| 73 | + | } | |
| 74 | + | b := x.conn.Buf() | |
| 75 | + | pm, gc, pic, cur := xproto.Pixmap(id[0]), xproto.Gcontext(id[1]), render.Picture(id[2]), xproto.Cursor(id[3]) | |
| 76 | + | xproto.CreatePixmap(b, 32, pm, xproto.Drawable(x.root), uint16(w), uint16(h)) | |
| 77 | + | var gv xproto.CreateGCValues | |
| 78 | + | xproto.CreateGC(b, gc, xproto.Drawable(pm), &gv) | |
| 79 | + | xproto.PutImage(b, xproto.ImageFormatZPixmap, xproto.Drawable(pm), gc, uint16(w), uint16(h), 0, 0, 0, 32, px) | |
| 80 | + | var pv render.CreatePictureValues | |
| 81 | + | render.CreatePicture(b, pic, xproto.Drawable(pm), render.Pictformat(x.xs.argb), &pv) | |
| 82 | + | render.CreateCursor(b, cur, pic, uint16(min(max(hotX, 0), w-1)), uint16(min(max(hotY, 0), h-1))) | |
| 83 | + | var wv xproto.ChangeWindowAttributesValues | |
| 84 | + | wv.SetCursor(cur) | |
| 85 | + | xproto.ChangeWindowAttributes(b, xproto.Window(x.root), &wv) | |
| 86 | + | render.FreePicture(b, pic) | |
| 87 | + | xproto.FreeGC(b, gc) | |
| 88 | + | xproto.FreePixmap(b, pm) | |
| 89 | + | xproto.FreeCursor(b, cur) | |
| 90 | + | return true | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // glyphCursor installs the "left_ptr" glyph of the X cursor font (glyph 68, | |
| 94 | + | // mask 69). | |
| 95 | + | func (x *XWM) glyphCursor() { | |
| 96 | + | var id [2]uint32 // font, cursor | |
| 97 | + | if !x.newIDs(id[:]) { | |
| 98 | + | return | |
| 99 | + | } | |
| 100 | + | b := x.conn.Buf() | |
| 101 | + | font, cur := xproto.Font(id[0]), xproto.Cursor(id[1]) | |
| 102 | + | xproto.OpenFont(b, font, "cursor") | |
| 103 | + | xproto.CreateGlyphCursor(b, cur, font, font, 68, 69, 0, 0, 0, 0xffff, 0xffff, 0xffff) | |
| 104 | + | var wv xproto.ChangeWindowAttributesValues | |
| 105 | + | wv.SetCursor(cur) | |
| 106 | + | xproto.ChangeWindowAttributes(b, xproto.Window(x.root), &wv) | |
| 107 | + | xproto.CloseFont(b, font) | |
| 108 | + | xproto.FreeCursor(b, cur) | |
| 109 | + | } | |
A internal/adapters/wayland/xwm/cursor_test.go +86 -0
| @@ -0,0 +1,86 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func rootCursorExts() map[string]xpeerExt { | |
| 11 | + | e := fullPeerExts() | |
| 12 | + | e["RENDER"] = xpeerExt{139, 0, 0} | |
| 13 | + | return e | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | func bootCursor(t *testing.T, exts map[string]xpeerExt, mutate func(*xpeer), cfg ports.XwaylandSettings) (*XWM, *xpeer) { | |
| 17 | + | t.Helper() | |
| 18 | + | fd, p := startXPeer(t, exts) | |
| 19 | + | if mutate != nil { | |
| 20 | + | p.set(mutate) | |
| 21 | + | } | |
| 22 | + | x, err := BootSettings(t.Context(), fd, testLog(), cfg) | |
| 23 | + | require.NoError(t, err) | |
| 24 | + | t.Cleanup(x.CloseConn) | |
| 25 | + | return x, p | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func cursorCfg() ports.XwaylandSettings { | |
| 29 | + | return ports.XwaylandSettings{RootCursor: true, Cursor: ports.CursorImage{W: 2, H: 2, HotX: 1, HotY: 0, Pixels: make([]byte, 16)}} | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func TestRootCursorRequests(t *testing.T) { | |
| 33 | + | _, p := bootCursor(t, rootCursorExts(), nil, cursorCfg()) | |
| 34 | + | // QueryPictFormats (render minor 1) comes first; then the core requests. | |
| 35 | + | var got []string | |
| 36 | + | for _, r := range p.requests() { | |
| 37 | + | switch { | |
| 38 | + | case r.major == 139 && r.minor == 1: | |
| 39 | + | got = append(got, "QueryPictFormats") | |
| 40 | + | case r.major == 53: | |
| 41 | + | got = append(got, "CreatePixmap") | |
| 42 | + | case r.major == 72: | |
| 43 | + | got = append(got, "PutImage") | |
| 44 | + | case r.major == 139 && r.minor == 4: | |
| 45 | + | got = append(got, "CreatePicture") | |
| 46 | + | case r.major == 139 && r.minor == 27: | |
| 47 | + | got = append(got, "CreateCursor") | |
| 48 | + | case r.major == opChangeWindowAttributes && r.u32(4) == peerRoot && r.u32(8)&(1<<14) != 0: | |
| 49 | + | got = append(got, "ChangeWindowAttributes(root)") | |
| 50 | + | case r.major == 54: | |
| 51 | + | got = append(got, "FreePixmap") | |
| 52 | + | } | |
| 53 | + | } | |
| 54 | + | require.Equal(t, []string{"QueryPictFormats", "CreatePixmap", "PutImage", "CreatePicture", "CreateCursor", | |
| 55 | + | "ChangeWindowAttributes(root)", "FreePixmap"}, got) | |
| 56 | + | for _, r := range p.requests() { | |
| 57 | + | if r.major == 72 { // PutImage: ZPixmap, depth 32 | |
| 58 | + | require.EqualValues(t, 2, r.raw[1]) | |
| 59 | + | require.EqualValues(t, 32, r.raw[21]) | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | func TestRootCursorFallbackWithoutRender(t *testing.T) { | |
| 65 | + | _, p := bootCursor(t, fullPeerExts(), nil, cursorCfg()) | |
| 66 | + | var open, glyph, set bool | |
| 67 | + | for _, r := range p.requests() { | |
| 68 | + | open = open || r.major == 45 | |
| 69 | + | glyph = glyph || r.major == 94 | |
| 70 | + | set = set || r.major == opChangeWindowAttributes && r.u32(4) == peerRoot && r.u32(8)&(1<<14) != 0 | |
| 71 | + | } | |
| 72 | + | require.True(t, open && glyph && set, "OpenFont, CreateGlyphCursor, root cursor") | |
| 73 | + | require.Zero(t, p.count(53, nil), "no pixmap") | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | func TestRootCursorFallbackWithoutARGB(t *testing.T) { | |
| 77 | + | _, p := bootCursor(t, rootCursorExts(), func(p *xpeer) { p.noARGB = true }, cursorCfg()) | |
| 78 | + | require.NotZero(t, p.count(94, nil)) | |
| 79 | + | require.Zero(t, p.count(72, nil)) | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | func TestRootCursorOff(t *testing.T) { | |
| 83 | + | _, p := bootCursor(t, rootCursorExts(), nil, ports.XwaylandSettings{}) | |
| 84 | + | require.Zero(t, p.count(94, nil)) | |
| 85 | + | require.Zero(t, p.count(53, nil)) | |
| 86 | + | } | |
A internal/adapters/wayland/xwm/events.go +303 -0
| @@ -0,0 +1,303 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bnema.dev/xturbo" | |
| 5 | + | "bnema.dev/xturbo/composite" | |
| 6 | + | "bnema.dev/xturbo/res" | |
| 7 | + | "bnema.dev/xturbo/xfixes" | |
| 8 | + | "bnema.dev/xturbo/xproto" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Reply kinds registered with Expect. The first argument is the window tag | |
| 12 | + | // (slot | generation<<16, see table.tag), the second a property index. | |
| 13 | + | const ( | |
| 14 | + | kindAttr uint16 = 16 + iota | |
| 15 | + | kindProp | |
| 16 | + | kindGeom | |
| 17 | + | kindClientIds | |
| 18 | + | kindConfigure | |
| 19 | + | kindFocus | |
| 20 | + | kindRootMask | |
| 21 | + | kindVersion | |
| 22 | + | kindTree | |
| 23 | + | kindOverrideType | |
| 24 | + | kindSelTargets | |
| 25 | + | kindSelData | |
| 26 | + | kindSelOut | |
| 27 | + | kindSelOwner | |
| 28 | + | kindPictFormats | |
| 29 | + | ) | |
| 30 | + | ||
| 31 | + | // onEvent dispatches an X event. Before Attach there is no host: only the | |
| 32 | + | // table is maintained, and Attach replays the result. | |
| 33 | + | func (x *XWM) onEvent(ev *xturbo.Event) { | |
| 34 | + | x.stats.Events++ | |
| 35 | + | if x.closed { | |
| 36 | + | return | |
| 37 | + | } | |
| 38 | + | switch ev.Kind { | |
| 39 | + | case xproto.KindPropertyNotify: | |
| 40 | + | x.noteTime(uint32(xproto.PropertyNotifyOf(ev).Time())) | |
| 41 | + | case xproto.KindSelectionRequest: | |
| 42 | + | x.noteTime(uint32(xproto.SelectionRequestOf(ev).Time())) | |
| 43 | + | } | |
| 44 | + | switch ev.Kind { | |
| 45 | + | case xproto.KindCreateNotify: | |
| 46 | + | x.onCreateNotify(ev) | |
| 47 | + | case xproto.KindDestroyNotify: | |
| 48 | + | x.onDestroyNotify(ev) | |
| 49 | + | case xproto.KindReparentNotify: | |
| 50 | + | x.onReparentNotify(ev) | |
| 51 | + | } | |
| 52 | + | if ev.Kind == xproto.KindSelectionClear { | |
| 53 | + | x.onXSettingsClear(ev) | |
| 54 | + | } | |
| 55 | + | if x.host == nil { | |
| 56 | + | x.onEventDetached(ev) | |
| 57 | + | return | |
| 58 | + | } | |
| 59 | + | switch ev.Kind { | |
| 60 | + | case xproto.KindMapRequest: | |
| 61 | + | x.onMapRequest(ev) | |
| 62 | + | case xproto.KindMapNotify: | |
| 63 | + | x.onMapNotify(ev) | |
| 64 | + | case xproto.KindUnmapNotify: | |
| 65 | + | x.onUnmapNotify(ev) | |
| 66 | + | case xproto.KindConfigureRequest: | |
| 67 | + | x.onConfigureRequest(ev) | |
| 68 | + | case xproto.KindConfigureNotify: | |
| 69 | + | x.onConfigureNotify(ev) | |
| 70 | + | case xproto.KindPropertyNotify: | |
| 71 | + | x.onPropertyNotify(ev) | |
| 72 | + | case xproto.KindClientMessage: | |
| 73 | + | x.onClientMessage(ev) | |
| 74 | + | case xproto.KindFocusIn: | |
| 75 | + | x.onFocusIn(ev) | |
| 76 | + | default: | |
| 77 | + | x.onSelectionEvent(ev) | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // onEventDetached keeps what Attach replays: windows asked to map, and those | |
| 82 | + | // withdrawn again, so Attach skips them; geometry requests are answered. | |
| 83 | + | func (x *XWM) onEventDetached(ev *xturbo.Event) { | |
| 84 | + | switch ev.Kind { | |
| 85 | + | case xproto.KindMapRequest: | |
| 86 | + | if s := x.tab.lookup(uint32(xproto.MapRequestOf(ev).Window())); s != 0 { | |
| 87 | + | x.tab.rec(s).mapping = true | |
| 88 | + | } | |
| 89 | + | case xproto.KindUnmapNotify: | |
| 90 | + | if ev.SendEvent { | |
| 91 | + | return | |
| 92 | + | } | |
| 93 | + | if s := x.tab.lookup(uint32(xproto.UnmapNotifyOf(ev).Window())); s != 0 { | |
| 94 | + | r := x.tab.rec(s) | |
| 95 | + | r.mapped, r.mapping = false, false | |
| 96 | + | } | |
| 97 | + | case xproto.KindConfigureRequest: | |
| 98 | + | x.onConfigureRequest(ev) | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func (x *XWM) onReply(r *xturbo.Reply) { | |
| 103 | + | x.stats.Replies++ | |
| 104 | + | if x.closed { | |
| 105 | + | return | |
| 106 | + | } | |
| 107 | + | switch r.Kind { | |
| 108 | + | case kindVersion: | |
| 109 | + | x.onVersionReply(r) | |
| 110 | + | return | |
| 111 | + | case kindTree: | |
| 112 | + | x.onTreeReply(r) | |
| 113 | + | return | |
| 114 | + | case kindSelTargets: | |
| 115 | + | x.onTargetsReply(r) | |
| 116 | + | return | |
| 117 | + | case kindSelData: | |
| 118 | + | x.onSelDataReply(r) | |
| 119 | + | return | |
| 120 | + | case kindSelOwner: | |
| 121 | + | x.onSelOwnerReply(r) | |
| 122 | + | return | |
| 123 | + | case kindPictFormats: | |
| 124 | + | x.onPictFormatsReply(r) | |
| 125 | + | return | |
| 126 | + | } | |
| 127 | + | s, rec := x.tab.live(r.Args[0]) | |
| 128 | + | if rec == nil { | |
| 129 | + | x.stats.Stale++ | |
| 130 | + | return | |
| 131 | + | } | |
| 132 | + | switch r.Kind { | |
| 133 | + | case kindAttr: | |
| 134 | + | x.onAttrReply(rec, r) | |
| 135 | + | case kindProp: | |
| 136 | + | x.onPropReply(s, rec, r) | |
| 137 | + | case kindGeom: | |
| 138 | + | x.onGeomReply(s, rec, r) | |
| 139 | + | case kindClientIds: | |
| 140 | + | x.onClientIdsReply(s, rec, r) | |
| 141 | + | case kindOverrideType: | |
| 142 | + | x.onOverrideType(s, rec, r) | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | // onError never tears the XWM down: an error names a request, not the | |
| 147 | + | // connection. Sticky Buf errors surface through Flush. | |
| 148 | + | func (x *XWM) onError(e *xturbo.XError) { | |
| 149 | + | x.stats.Errors++ | |
| 150 | + | if e.Kind == 0 { | |
| 151 | + | x.stats.Unattributed++ | |
| 152 | + | x.log.Debug().Uint8("code", e.Code).Uint8("major", e.Major).Uint16("minor", e.Minor). | |
| 153 | + | Uint64("seq", uint64(e.Seq)).Msg("unattributed X error") | |
| 154 | + | return | |
| 155 | + | } | |
| 156 | + | switch e.Kind { | |
| 157 | + | case kindSelTargets, kindSelData, kindSelOut: | |
| 158 | + | x.onSelError(e) | |
| 159 | + | case kindRootMask: | |
| 160 | + | if e.Code == xproto.BadAccess { | |
| 161 | + | x.bootErr = ErrWMExists | |
| 162 | + | } | |
| 163 | + | case kindProp, kindGeom, kindClientIds, kindConfigure, kindFocus, kindOverrideType: | |
| 164 | + | // Usually BadWindow: the window died between request and reply and | |
| 165 | + | // DestroyNotify follows. Whatever the code, the awaited reply will | |
| 166 | + | // never come, so the map must not wait for it. | |
| 167 | + | s, rec := x.tab.live(e.Args[0]) | |
| 168 | + | if rec == nil { | |
| 169 | + | x.stats.Stale++ | |
| 170 | + | return | |
| 171 | + | } | |
| 172 | + | switch e.Kind { | |
| 173 | + | case kindProp: | |
| 174 | + | rec.pending &^= 1 << e.Args[1] | |
| 175 | + | case kindGeom: | |
| 176 | + | rec.pending &^= propGeom | |
| 177 | + | case kindClientIds: | |
| 178 | + | rec.pending &^= propClientIDs | |
| 179 | + | case kindOverrideType: | |
| 180 | + | if e.Code != xproto.BadWindow { // typed unknown: the window still shows | |
| 181 | + | x.overrideReady(s) | |
| 182 | + | } | |
| 183 | + | return | |
| 184 | + | default: | |
| 185 | + | return | |
| 186 | + | } | |
| 187 | + | if e.Code != xproto.BadWindow && e.Code != xproto.BadDrawable { // a dead window is never announced | |
| 188 | + | x.checkReady(s) | |
| 189 | + | } | |
| 190 | + | } | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | // ownWindow reports whether xid is one of the XWM's own helper windows. | |
| 194 | + | func (x *XWM) ownWindow(xid uint32) bool { | |
| 195 | + | return xid != 0 && (xid == x.wmWin || xid == x.selWin || xid == x.xs.win) | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | // onCreateNotify records a new top-level window. Override-redirect windows | |
| 199 | + | // are kept (their geometry and serial matter later) but never reach the host. | |
| 200 | + | func (x *XWM) onCreateNotify(ev *xturbo.Event) { | |
| 201 | + | e := xproto.CreateNotifyOf(ev) | |
| 202 | + | xid := uint32(e.Window()) | |
| 203 | + | if x.ownWindow(xid) || x.tab.lookup(xid) != 0 { | |
| 204 | + | return | |
| 205 | + | } | |
| 206 | + | s := x.insert(xid) | |
| 207 | + | if s == 0 { | |
| 208 | + | return | |
| 209 | + | } | |
| 210 | + | r := x.tab.rec(s) | |
| 211 | + | override := e.OverrideRedirect() | |
| 212 | + | r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height()) | |
| 213 | + | if override { | |
| 214 | + | r.kind = kindUnmanaged | |
| 215 | + | x.stats.UnmanagedSeen++ | |
| 216 | + | return | |
| 217 | + | } | |
| 218 | + | if x.host != nil { | |
| 219 | + | x.adopt(s) | |
| 220 | + | } | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | // adopt announces a managed window to the host and starts watching it. | |
| 224 | + | func (x *XWM) adopt(s Slot) { | |
| 225 | + | r := x.tab.rec(s) | |
| 226 | + | r.id = x.host.Created(s, r.xid) | |
| 227 | + | var v xproto.ChangeWindowAttributesValues | |
| 228 | + | v.SetEventMask(xproto.EventMaskPropertyChange | xproto.EventMaskFocusChange) | |
| 229 | + | x.expect(xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(r.xid), &v), kindAttr, x.tab.tag(s), 0) | |
| 230 | + | x.log.Debug().Uint32("xid", r.xid).Uint32("slot", uint32(s)).Msg("x11 window created") | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | func (x *XWM) onDestroyNotify(ev *xturbo.Event) { | |
| 234 | + | xid := uint32(xproto.DestroyNotifyOf(ev).Window()) | |
| 235 | + | s := x.tab.lookup(xid) | |
| 236 | + | if s == 0 { | |
| 237 | + | return | |
| 238 | + | } | |
| 239 | + | x.forgetWindow(s) | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | // onTreeReply (boot): record every existing child and ask for its attributes. | |
| 243 | + | func (x *XWM) onTreeReply(r *xturbo.Reply) { | |
| 244 | + | q, ok := xproto.ParseQueryTreeReply(r.Raw) | |
| 245 | + | if !ok { | |
| 246 | + | return | |
| 247 | + | } | |
| 248 | + | children := q.Children() | |
| 249 | + | for i := 0; i < children.Len(); i++ { | |
| 250 | + | xid := uint32(children.At(i)) | |
| 251 | + | if x.ownWindow(xid) || x.tab.lookup(xid) != 0 { | |
| 252 | + | continue | |
| 253 | + | } | |
| 254 | + | s := x.insert(xid) | |
| 255 | + | if s == 0 { | |
| 256 | + | return | |
| 257 | + | } | |
| 258 | + | x.expect(xproto.GetWindowAttributes(x.conn.Buf(), xproto.Window(xid)), kindAttr, x.tab.tag(s), 0) | |
| 259 | + | } | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | // onAttrReply (boot): classify an existing window; viewable ones go through | |
| 263 | + | // the map flow once the host is attached. | |
| 264 | + | func (x *XWM) onAttrReply(rec *record, r *xturbo.Reply) { | |
| 265 | + | a, ok := xproto.ParseGetWindowAttributesReply(r.Raw) | |
| 266 | + | if !ok || x.host != nil { | |
| 267 | + | return | |
| 268 | + | } | |
| 269 | + | if a.OverrideRedirect() { | |
| 270 | + | rec.kind = kindUnmanaged | |
| 271 | + | rec.mapped = a.MapState() == xproto.MapStateViewable | |
| 272 | + | x.stats.UnmanagedSeen++ | |
| 273 | + | return | |
| 274 | + | } | |
| 275 | + | if a.MapState() == xproto.MapStateViewable { | |
| 276 | + | rec.mapped, rec.mapping = true, true | |
| 277 | + | } | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | func (x *XWM) onVersionReply(r *xturbo.Reply) { | |
| 281 | + | switch r.Args[0] { | |
| 282 | + | case versionComposite: | |
| 283 | + | if v, ok := composite.ParseQueryVersionReply(r.Raw); ok { | |
| 284 | + | x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("Composite") | |
| 285 | + | } | |
| 286 | + | case versionXFixes: | |
| 287 | + | if v, ok := xfixes.ParseQueryVersionReply(r.Raw); ok { | |
| 288 | + | x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("XFixes") | |
| 289 | + | } | |
| 290 | + | case versionRes: | |
| 291 | + | if v, ok := res.ParseQueryVersionReply(r.Raw); ok { | |
| 292 | + | x.log.Info().Uint16("major", v.ServerMajor()).Uint16("minor", v.ServerMinor()).Msg("X-Resource") | |
| 293 | + | } | |
| 294 | + | } | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | // noteTime records the server time of the latest timestamped event; 0 is | |
| 298 | + | // CurrentTime and carries none. | |
| 299 | + | func (x *XWM) noteTime(t uint32) { | |
| 300 | + | if t != 0 { | |
| 301 | + | x.lastTime = t | |
| 302 | + | } | |
| 303 | + | } | |
A internal/adapters/wayland/xwm/fdsource_mock_test.go +129 -0
| @@ -0,0 +1,129 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package xwm | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | mock "github.com/stretchr/testify/mock" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // newMockfdSource creates a new instance of mockfdSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 12 | + | // The first argument is typically a *testing.T value. | |
| 13 | + | func newMockfdSource(t interface { | |
| 14 | + | mock.TestingT | |
| 15 | + | Cleanup(func()) | |
| 16 | + | }) *mockfdSource { | |
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 18 | + | helper.Helper() | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | mock := &mockfdSource{} | |
| 22 | + | mock.Mock.Test(t) | |
| 23 | + | ||
| 24 | + | t.Cleanup(func() { | |
| 25 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 26 | + | helper.Helper() | |
| 27 | + | } | |
| 28 | + | mock.AssertExpectations(t) | |
| 29 | + | }) | |
| 30 | + | ||
| 31 | + | return mock | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // mockfdSource is an autogenerated mock type for the fdSource type | |
| 35 | + | type mockfdSource struct { | |
| 36 | + | mock.Mock | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | type mockfdSource_Expecter struct { | |
| 40 | + | mock *mock.Mock | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (_m *mockfdSource) EXPECT() *mockfdSource_Expecter { | |
| 44 | + | return &mockfdSource_Expecter{mock: &_m.Mock} | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // Remove provides a mock function for the type mockfdSource | |
| 48 | + | func (_mock *mockfdSource) Remove() { | |
| 49 | + | _mock.Called() | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // mockfdSource_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' | |
| 54 | + | type mockfdSource_Remove_Call struct { | |
| 55 | + | *mock.Call | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // Remove is a helper method to define mock.On call | |
| 59 | + | func (_e *mockfdSource_Expecter) Remove() *mockfdSource_Remove_Call { | |
| 60 | + | return &mockfdSource_Remove_Call{Call: _e.mock.On("Remove")} | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | func (_c *mockfdSource_Remove_Call) Run(run func()) *mockfdSource_Remove_Call { | |
| 64 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 65 | + | run() | |
| 66 | + | }) | |
| 67 | + | return _c | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func (_c *mockfdSource_Remove_Call) Return() *mockfdSource_Remove_Call { | |
| 71 | + | _c.Call.Return() | |
| 72 | + | return _c | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (_c *mockfdSource_Remove_Call) RunAndReturn(run func()) *mockfdSource_Remove_Call { | |
| 76 | + | _c.Run(run) | |
| 77 | + | return _c | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // SetWritable provides a mock function for the type mockfdSource | |
| 81 | + | func (_mock *mockfdSource) SetWritable(on bool) error { | |
| 82 | + | ret := _mock.Called(on) | |
| 83 | + | ||
| 84 | + | if len(ret) == 0 { | |
| 85 | + | panic("no return value specified for SetWritable") | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | var r0 error | |
| 89 | + | if returnFunc, ok := ret.Get(0).(func(bool) error); ok { | |
| 90 | + | r0 = returnFunc(on) | |
| 91 | + | } else { | |
| 92 | + | r0 = ret.Error(0) | |
| 93 | + | } | |
| 94 | + | return r0 | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // mockfdSource_SetWritable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWritable' | |
| 98 | + | type mockfdSource_SetWritable_Call struct { | |
| 99 | + | *mock.Call | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // SetWritable is a helper method to define mock.On call | |
| 103 | + | // - on bool | |
| 104 | + | func (_e *mockfdSource_Expecter) SetWritable(on any) *mockfdSource_SetWritable_Call { | |
| 105 | + | return &mockfdSource_SetWritable_Call{Call: _e.mock.On("SetWritable", on)} | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | func (_c *mockfdSource_SetWritable_Call) Run(run func(on bool)) *mockfdSource_SetWritable_Call { | |
| 109 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 110 | + | var arg0 bool | |
| 111 | + | if args[0] != nil { | |
| 112 | + | arg0 = args[0].(bool) | |
| 113 | + | } | |
| 114 | + | run( | |
| 115 | + | arg0, | |
| 116 | + | ) | |
| 117 | + | }) | |
| 118 | + | return _c | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | func (_c *mockfdSource_SetWritable_Call) Return(err error) *mockfdSource_SetWritable_Call { | |
| 122 | + | _c.Call.Return(err) | |
| 123 | + | return _c | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | func (_c *mockfdSource_SetWritable_Call) RunAndReturn(run func(on bool) error) *mockfdSource_SetWritable_Call { | |
| 127 | + | _c.Call.Return(run) | |
| 128 | + | return _c | |
| 129 | + | } | |
A internal/adapters/wayland/xwm/fixes_test.go +230 -0
| @@ -0,0 +1,230 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "bnema.dev/xturbo" | |
| 8 | + | "bnema.dev/xturbo/xproto" | |
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |
| 10 | + | "github.com/stretchr/testify/mock" | |
| 11 | + | "github.com/stretchr/testify/require" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | func getProps(p *xpeer, skip int, win uint32) int { | |
| 15 | + | n := 0 | |
| 16 | + | for _, r := range since(p, skip, opGetProperty) { | |
| 17 | + | if r.u32(4) == win { | |
| 18 | + | n++ | |
| 19 | + | } | |
| 20 | + | } | |
| 21 | + | return n | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // Our own _NET_WM_STATE writes must not be read back once the window is ready. | |
| 25 | + | func TestOwnNetWMStateWriteIsNotReadBack(t *testing.T) { | |
| 26 | + | hs := newHarness(t, nil) | |
| 27 | + | const win = 0x500001 | |
| 28 | + | hs.classic(win) | |
| 29 | + | s := hs.mapAndWait(win) | |
| 30 | + | ||
| 31 | + | hs.x.Configure(s, ports.ConfigureWindow{Width: 300, Height: 200, Fullscreen: true}) | |
| 32 | + | n := settle(t, hs.x, hs.p) | |
| 33 | + | require.True(t, hs.x.tab.rec(s).fullscreen, "the written state is the known state") | |
| 34 | + | st, _ := hs.p.prop(win, "_NET_WM_STATE") | |
| 35 | + | require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FULLSCREEN")}, atomsOf(st)) | |
| 36 | + | ||
| 37 | + | ev := hs.x.Stats().Events | |
| 38 | + | hs.p.propertyNotify(win, hs.p.atom("_NET_WM_STATE"), false) | |
| 39 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events > ev }) | |
| 40 | + | settle(t, hs.x, hs.p) | |
| 41 | + | require.Zero(t, getProps(hs.p, n, win), "no GetProperty") | |
| 42 | + | ||
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // A pre-map XMoveResize is honoured, floating or not. | |
| 46 | + | func TestConfigureRequestBeforeReadyIsHonoured(t *testing.T) { | |
| 47 | + | hs := newHarness(t, nil) | |
| 48 | + | const win = 0x500001 | |
| 49 | + | hs.classic(win) | |
| 50 | + | hs.create(win) | |
| 51 | + | n := settle(t, hs.x, hs.p) | |
| 52 | + | ||
| 53 | + | mask := uint16(xproto.ConfigWindowX | xproto.ConfigWindowY | xproto.ConfigWindowWidth | xproto.ConfigWindowHeight) | |
| 54 | + | hs.x.feed(t, hs.p, func() { sendConfigureRequest(hs.p, win, mask, 11, 22, 640, 70000&0xffff) }) | |
| 55 | + | settle(t, hs.x, hs.p) | |
| 56 | + | cw := since(hs.p, n, opConfigureWindow) | |
| 57 | + | require.Len(t, cw, 1) | |
| 58 | + | require.Equal(t, []uint32{11, 22, 640, 4464}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20), cw[0].u32(24)}) | |
| 59 | + | require.Len(t, sentEvents(hs.p, n, win), 1) | |
| 60 | + | ||
| 61 | + | hs.expectReady(win) | |
| 62 | + | hs.p.mapRequest(win) | |
| 63 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 64 | + | require.Equal(t, 640, hs.ready[win].Width) | |
| 65 | + | require.Equal(t, 4464, hs.ready[win].Height) | |
| 66 | + | require.False(t, hs.ready[win].Floating) | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // After a remap the state is rewritten from scratch, FOCUSED included. | |
| 70 | + | func TestRemapRewritesStateWithFocused(t *testing.T) { | |
| 71 | + | hs := newHarness(t, nil) | |
| 72 | + | const win = 0x500001 | |
| 73 | + | hs.classic(win) | |
| 74 | + | s := hs.mapAndWait(win) | |
| 75 | + | hs.x.Focus(s) | |
| 76 | + | settle(t, hs.x, hs.p) | |
| 77 | + | st, _ := hs.p.prop(win, "_NET_WM_STATE") | |
| 78 | + | require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |
| 79 | + | ||
| 80 | + | hs.h.EXPECT().Dissociate(s, false).Once() | |
| 81 | + | hs.p.unmapNotify(win, false) | |
| 82 | + | readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).mapping }) | |
| 83 | + | delete(hs.ready, win) | |
| 84 | + | hs.expectReady(win) | |
| 85 | + | hs.p.mapRequest(win) | |
| 86 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 87 | + | ||
| 88 | + | hs.x.Focus(s) | |
| 89 | + | settle(t, hs.x, hs.p) | |
| 90 | + | st, _ = hs.p.prop(win, "_NET_WM_STATE") | |
| 91 | + | require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // Replies of a previous map never satisfy the next one (xturbo usually retires | |
| 95 | + | // their slots first; the generation guards the rest). | |
| 96 | + | func TestStaleRepliesOfPreviousMapAreDropped(t *testing.T) { | |
| 97 | + | hs := newHarness(t, nil) | |
| 98 | + | const win = 0x500001 | |
| 99 | + | hs.classic(win) | |
| 100 | + | s := hs.create(win) | |
| 101 | + | hs.p.holdReplies() // the second map is never answered | |
| 102 | + | hs.p.mapRequest(win) | |
| 103 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapping }) | |
| 104 | + | oldTag := hs.x.tab.tag(s) | |
| 105 | + | hs.h.EXPECT().Dissociate(s, false).Once() | |
| 106 | + | hs.p.unmapNotify(win, false) | |
| 107 | + | readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).mapping }) | |
| 108 | + | hs.p.mapRequest(win) | |
| 109 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapping }) | |
| 110 | + | require.NotEqual(t, oldTag, hs.x.tab.tag(s)) | |
| 111 | + | ||
| 112 | + | rep := make([]byte, 32) | |
| 113 | + | rep[0] = 1 | |
| 114 | + | for idx := 0; idx < propCount; idx++ { | |
| 115 | + | hs.x.onReply(&xturbo.Reply{Kind: kindProp, Args: [4]uint32{oldTag, uint32(idx)}, Raw: rep}) | |
| 116 | + | } | |
| 117 | + | hs.x.onReply(&xturbo.Reply{Kind: kindGeom, Args: [4]uint32{oldTag}, Raw: rep}) | |
| 118 | + | require.EqualValues(t, propCount+1, hs.x.Stats().Stale) | |
| 119 | + | require.False(t, hs.x.tab.rec(s).ready) | |
| 120 | + | require.Equal(t, propMaskAll&^propClientIDs, hs.x.tab.rec(s).pending, "the new map still awaits its own replies") | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func TestGetGeometryBadDrawableIsNotReady(t *testing.T) { | |
| 124 | + | hs := newHarness(t, nil) | |
| 125 | + | const win = 0x500001 | |
| 126 | + | hs.classic(win) | |
| 127 | + | s := hs.create(win) | |
| 128 | + | hs.p.set(func(p *xpeer) { p.bad[win] = true }) | |
| 129 | + | hs.p.mapRequest(win) | |
| 130 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Errors >= uint64(propCount)+1 }) | |
| 131 | + | require.Zero(t, hs.x.Stats().Unattributed) | |
| 132 | + | require.False(t, hs.x.tab.rec(s).ready) | |
| 133 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |
| 134 | + | hs.p.destroyNotify(win) | |
| 135 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 }) | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // A host that refocuses the window while dissociating it must not leave a | |
| 139 | + | // dangling active slot. | |
| 140 | + | func TestForgetWindowClearsActiveSetByHost(t *testing.T) { | |
| 141 | + | x, p, h, _, _ := attached(t) | |
| 142 | + | const win = 0x500001 | |
| 143 | + | s := newWin(t, x, p, h, win) | |
| 144 | + | h.EXPECT().Dissociate(s, true).Run(func(sl Slot, _ bool) { x.Focus(sl) }).Once() | |
| 145 | + | p.destroyNotify(win) | |
| 146 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 }) | |
| 147 | + | require.Zero(t, x.active) | |
| 148 | + | require.False(t, x.refocus) | |
| 149 | + | settle(t, x, p) | |
| 150 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 151 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | func TestTitleAndAppIDAreValidUTF8(t *testing.T) { | |
| 155 | + | hs := newHarness(t, nil) | |
| 156 | + | const win = 0x500001 | |
| 157 | + | hs.classic(win) | |
| 158 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) }) | |
| 159 | + | hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("caf\xe9")) | |
| 160 | + | hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("i\x00C\xe9\x00")) | |
| 161 | + | s := hs.mapAndWait(win) | |
| 162 | + | require.Equal(t, "caf\uFFFD", hs.ready[win].Title) | |
| 163 | + | require.Equal(t, "C\uFFFD", hs.ready[win].AppID) | |
| 164 | + | ||
| 165 | + | // Unchanged after sanitising: no host call. | |
| 166 | + | hs.notifyWMName(win) | |
| 167 | + | // A changed truncated multibyte title reaches the host sanitised. | |
| 168 | + | hs.h.EXPECT().Title(s, "\u00e9\uFFFD").Once() | |
| 169 | + | hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("\xc3\xa9\xe2\x82")) | |
| 170 | + | hs.notifyWMName(win) | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | func TestSyntheticConfigureNotifyClampsCoordinates(t *testing.T) { | |
| 174 | + | x, p, h, _, _ := attached(t) | |
| 175 | + | const win = 0x500001 | |
| 176 | + | s := newWin(t, x, p, h, win) | |
| 177 | + | n := settle(t, x, p) | |
| 178 | + | r := x.tab.rec(s) | |
| 179 | + | r.x, r.y, r.w, r.h = 70000, -70000, 70000, 5 | |
| 180 | + | x.sendConfigureNotify(r) | |
| 181 | + | settle(t, x, p) | |
| 182 | + | ev := sentEvents(p, n, win) | |
| 183 | + | require.Len(t, ev, 1) | |
| 184 | + | require.EqualValues(t, 32767, int16(binary.LittleEndian.Uint16(ev[0][16:]))) | |
| 185 | + | require.EqualValues(t, -32768, int16(binary.LittleEndian.Uint16(ev[0][18:]))) | |
| 186 | + | require.EqualValues(t, 65535, binary.LittleEndian.Uint16(ev[0][20:])) | |
| 187 | + | require.EqualValues(t, 5, binary.LittleEndian.Uint16(ev[0][22:])) | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // A window withdrawn before Attach is not mapped by Attach; a pre-attach | |
| 191 | + | // ConfigureRequest is honoured. | |
| 192 | + | func TestUnmapAndConfigureBeforeAttach(t *testing.T) { | |
| 193 | + | x, p := bootPeer(t, nil) | |
| 194 | + | const gone, sized = 0x500001, 0x500002 | |
| 195 | + | for _, w := range []uint32{gone, sized} { | |
| 196 | + | p.setProp(w, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("a\x00B\x00")) | |
| 197 | + | p.geoms[w] = xpeerGeom{w: 300, h: 200} | |
| 198 | + | p.createNotify(w, false) | |
| 199 | + | p.mapRequest(w) | |
| 200 | + | } | |
| 201 | + | p.unmapNotify(gone, false) | |
| 202 | + | mask := uint16(xproto.ConfigWindowWidth | xproto.ConfigWindowHeight) | |
| 203 | + | sendConfigureRequest(p, sized, mask, 0, 0, 500, 400) | |
| 204 | + | readUntil(t, x, func() bool { return x.Stats().Events >= 6 }) | |
| 205 | + | n := len(p.requests()) | |
| 206 | + | ||
| 207 | + | h := newMockhost(t) | |
| 208 | + | src := newMockfdSource(t) | |
| 209 | + | src.EXPECT().Remove().Maybe() | |
| 210 | + | post, _ := newPost() | |
| 211 | + | var got WindowInfo | |
| 212 | + | done := false | |
| 213 | + | h.EXPECT().Created(mock.Anything, mock.Anything).Return(ports.WindowID(1)).Twice() | |
| 214 | + | h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, i WindowInfo) { got, done = i, true }).Once() | |
| 215 | + | x.attach(src, h, post) | |
| 216 | + | readUntil(t, x, func() bool { return done }) | |
| 217 | + | settle(t, x, p) | |
| 218 | + | for _, r := range since(p, n, opMapWindow) { | |
| 219 | + | require.NotEqual(t, uint32(gone), r.u32(4), "withdrawn window must not be mapped") | |
| 220 | + | } | |
| 221 | + | require.Equal(t, 500, got.Width) | |
| 222 | + | require.Equal(t, 400, got.Height) | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | func (hs *harness) notifyWMName(win uint32) { | |
| 226 | + | hs.t.Helper() | |
| 227 | + | before := hs.x.Stats().Replies | |
| 228 | + | hs.p.propertyNotify(win, uint32(xproto.AtomWmName), false) | |
| 229 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |
| 230 | + | } | |
A internal/adapters/wayland/xwm/focus.go +218 -0
| @@ -0,0 +1,218 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "slices" | |
| 6 | + | ||
| 7 | + | "bnema.dev/xturbo" | |
| 8 | + | "bnema.dev/xturbo/xproto" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Bits of XWM.listsDirty: the client lists to rewrite at flush. | |
| 12 | + | const ( | |
| 13 | + | listClients uint8 = 1 << iota | |
| 14 | + | listStacking | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // onFocusIn denies a focus change the XWM did not make: Wayland is the truth. | |
| 18 | + | // The active window's focus is re-issued once at the next flush. | |
| 19 | + | func (x *XWM) onFocusIn(ev *xturbo.Event) { | |
| 20 | + | e := xproto.FocusInOf(ev) | |
| 21 | + | if e.Detail() == xproto.NotifyDetailPointer { | |
| 22 | + | return | |
| 23 | + | } | |
| 24 | + | // Keyboard grabs are how menus steal focus legitimately (wlroots does | |
| 25 | + | // the same): fighting them would close every Steam menu. | |
| 26 | + | if e.Mode() == xproto.NotifyModeGrab || e.Mode() == xproto.NotifyModeUngrab { | |
| 27 | + | return | |
| 28 | + | } | |
| 29 | + | s := x.tab.lookup(uint32(e.Event())) | |
| 30 | + | if s == 0 || s == x.active { | |
| 31 | + | return | |
| 32 | + | } | |
| 33 | + | if r := x.tab.rec(s); r.kind == kindUnmanaged { | |
| 34 | + | return | |
| 35 | + | } | |
| 36 | + | x.refocus = true | |
| 37 | + | x.wake() | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // Focus gives keyboard focus to s; 0 means a native Wayland window or none. | |
| 41 | + | // Focus never uses PointerRoot: without an X window to focus it is None. | |
| 42 | + | func (x *XWM) Focus(s Slot) { | |
| 43 | + | if x.closed { | |
| 44 | + | return | |
| 45 | + | } | |
| 46 | + | r := x.managedRec(s) | |
| 47 | + | if r == nil { | |
| 48 | + | s = 0 | |
| 49 | + | } | |
| 50 | + | if s == x.active { | |
| 51 | + | return | |
| 52 | + | } | |
| 53 | + | if old := x.managedRec(x.active); old != nil { | |
| 54 | + | old.pend.activated = false | |
| 55 | + | x.markDirty(x.active, old) | |
| 56 | + | } | |
| 57 | + | x.active = s | |
| 58 | + | if r == nil { | |
| 59 | + | x.issueFocus(0, nil) | |
| 60 | + | x.setActive(0) | |
| 61 | + | x.wake() | |
| 62 | + | return | |
| 63 | + | } | |
| 64 | + | r.pend.activated = true | |
| 65 | + | x.markDirty(s, r) | |
| 66 | + | x.issueFocus(s, r) | |
| 67 | + | x.setActive(r.xid) | |
| 68 | + | x.raise(r) | |
| 69 | + | x.wake() | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | // issueFocus applies the ICCCM input model of r: Passive and Locally active | |
| 73 | + | // windows get SetInputFocus, Locally and Globally active ones WM_TAKE_FOCUS, | |
| 74 | + | // No-input windows nothing. A nil r clears the X focus. | |
| 75 | + | func (x *XWM) issueFocus(s Slot, r *record) { | |
| 76 | + | b := x.conn.Buf() | |
| 77 | + | if r == nil { | |
| 78 | + | xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.WindowNone, 0) | |
| 79 | + | return | |
| 80 | + | } | |
| 81 | + | if r.input { | |
| 82 | + | x.expect(xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.Window(r.xid), 0), kindFocus, x.tab.tag(s), 0) | |
| 83 | + | } | |
| 84 | + | if r.takeFocus { | |
| 85 | + | x.sendProtocol(r.xid, atomWMTakeFocus) | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // sendProtocol sends a WM_PROTOCOLS client message [proto, CurrentTime]. | |
| 90 | + | func (x *XWM) sendProtocol(xid uint32, proto int) { | |
| 91 | + | var raw [32]byte | |
| 92 | + | raw[0], raw[1] = 33, 32 // ClientMessage, format 32 | |
| 93 | + | binary.LittleEndian.PutUint32(raw[4:], xid) | |
| 94 | + | binary.LittleEndian.PutUint32(raw[8:], x.atoms[atomWMProtocols]) | |
| 95 | + | binary.LittleEndian.PutUint32(raw[12:], x.atoms[proto]) | |
| 96 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(xid), xproto.EventMaskNoEvent, &raw) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // setActive writes _NET_ACTIVE_WINDOW on the root. | |
| 100 | + | func (x *XWM) setActive(xid uint32) { | |
| 101 | + | v := [1]uint32{xid} | |
| 102 | + | x.setProp32(x.root, atomNetActiveWindow, xproto.AtomWindow, v[:]) | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // raise restacks r above its siblings and at the top of the stacking list. | |
| 106 | + | func (x *XWM) raise(r *record) { | |
| 107 | + | var v xproto.ConfigureWindowValues | |
| 108 | + | v.SetStackMode(xproto.StackModeAbove) | |
| 109 | + | xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v) | |
| 110 | + | if i := slices.Index(x.stacking, r.xid); i >= 0 { | |
| 111 | + | x.stacking = append(x.stacking[:i], x.stacking[i+1:]...) | |
| 112 | + | x.stacking = append(x.stacking, r.xid) | |
| 113 | + | x.listsDirty |= listStacking | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | // CloseWindow asks s to close (WM_DELETE_WINDOW), or kills its client. | |
| 118 | + | func (x *XWM) CloseWindow(s Slot) { | |
| 119 | + | r := x.managedRec(s) | |
| 120 | + | if r == nil { | |
| 121 | + | return | |
| 122 | + | } | |
| 123 | + | if r.deleteWindow { | |
| 124 | + | x.sendProtocol(r.xid, atomWMDeleteWindow) | |
| 125 | + | } else { | |
| 126 | + | xproto.KillClient(x.conn.Buf(), r.xid) | |
| 127 | + | } | |
| 128 | + | x.wake() | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // addClient puts a ready window at the end of both client lists. | |
| 132 | + | func (x *XWM) addClient(xid uint32) { | |
| 133 | + | if !slices.Contains(x.clients, xid) { | |
| 134 | + | x.clients = append(x.clients, xid) | |
| 135 | + | x.listsDirty |= listClients | |
| 136 | + | } | |
| 137 | + | if !slices.Contains(x.stacking, xid) { | |
| 138 | + | x.stacking = append(x.stacking, xid) | |
| 139 | + | x.listsDirty |= listStacking | |
| 140 | + | } | |
| 141 | + | x.wake() | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | // removeClient drops an unmapped or destroyed window from both client lists. | |
| 145 | + | func (x *XWM) removeClient(xid uint32) { | |
| 146 | + | if i := slices.Index(x.clients, xid); i >= 0 { | |
| 147 | + | x.clients = slices.Delete(x.clients, i, i+1) | |
| 148 | + | x.listsDirty |= listClients | |
| 149 | + | } | |
| 150 | + | if i := slices.Index(x.stacking, xid); i >= 0 { | |
| 151 | + | x.stacking = slices.Delete(x.stacking, i, i+1) | |
| 152 | + | x.listsDirty |= listStacking | |
| 153 | + | } | |
| 154 | + | x.wake() | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | // dropWindow takes s out of the client lists, the focus and the configure | |
| 158 | + | // queue when it stops being mapped (unmap, destroy). It is idempotent. | |
| 159 | + | func (x *XWM) dropWindow(s Slot) { | |
| 160 | + | r := x.tab.rec(s) | |
| 161 | + | if r != nil { | |
| 162 | + | r.ovQueued = false // a reused slot must not inherit a queued report | |
| 163 | + | } | |
| 164 | + | if r == nil || r.kind != kindManaged { | |
| 165 | + | return | |
| 166 | + | } | |
| 167 | + | x.removeClient(r.xid) | |
| 168 | + | r.pend.dirty = false // the queued slot is skipped at flush | |
| 169 | + | // A remap starts from a clean _NET_WM_STATE, so Focus writes FOCUSED again. | |
| 170 | + | r.pend.activated, r.sent.activated = false, false | |
| 171 | + | r.sent.fullscreen, r.stateSent = false, 0 | |
| 172 | + | if x.active == s { | |
| 173 | + | x.active = 0 | |
| 174 | + | x.refocus = false | |
| 175 | + | x.setActive(0) | |
| 176 | + | } | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // forgetWindow ends the life of s (destroy, reparent away): it is dropped, | |
| 180 | + | // the host is told if it knew the window, and the record is freed. | |
| 181 | + | func (x *XWM) forgetWindow(s Slot) { | |
| 182 | + | r := x.tab.rec(s) | |
| 183 | + | if r == nil || r.kind == kindFree { | |
| 184 | + | return | |
| 185 | + | } | |
| 186 | + | x.dropWindow(s) | |
| 187 | + | if r.kind == kindUnmanaged && r.ovPaired && x.host != nil { | |
| 188 | + | r.ovPaired = false | |
| 189 | + | x.host.OverrideGone(s) | |
| 190 | + | } | |
| 191 | + | if r.kind == kindManaged && r.id != 0 && x.host != nil { | |
| 192 | + | r.id = 0 | |
| 193 | + | x.host.Dissociate(s, true) | |
| 194 | + | if x.active == s { // the host re-focused the window while dissociating it | |
| 195 | + | x.active, x.refocus = 0, false | |
| 196 | + | x.issueFocus(0, nil) | |
| 197 | + | x.setActive(0) | |
| 198 | + | x.wake() | |
| 199 | + | } | |
| 200 | + | } | |
| 201 | + | x.tab.remove(s) | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | // flushFocus rewrites the client lists that changed and re-issues a denied | |
| 205 | + | // focus, once per display iteration. | |
| 206 | + | func (x *XWM) flushFocus() { | |
| 207 | + | if x.listsDirty&listClients != 0 { | |
| 208 | + | x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, x.clients) | |
| 209 | + | } | |
| 210 | + | if x.listsDirty&listStacking != 0 { | |
| 211 | + | x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, x.stacking) | |
| 212 | + | } | |
| 213 | + | x.listsDirty = 0 | |
| 214 | + | if x.refocus { | |
| 215 | + | x.refocus = false | |
| 216 | + | x.issueFocus(x.active, x.managedRec(x.active)) | |
| 217 | + | } | |
| 218 | + | } | |
A internal/adapters/wayland/xwm/host.go +82 -0
| @@ -0,0 +1,82 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "time" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // Slot is the XWM's window slot: table index + 1, stable until the window is | |
| 10 | + | // destroyed. 0 means none. | |
| 11 | + | type Slot uint32 | |
| 12 | + | ||
| 13 | + | // WindowInfo is what the XWM learned about a managed window by the time it is | |
| 14 | + | // ready to be mapped. The host copies it. | |
| 15 | + | type WindowInfo struct { | |
| 16 | + | PID int | |
| 17 | + | Floating bool // dialog/utility/splash/fixed size | |
| 18 | + | Width, Height int // requested size, logical (scale 1) | |
| 19 | + | Parent Slot | |
| 20 | + | Modal bool // _NET_WM_STATE_MODAL | |
| 21 | + | Fullscreen bool // _NET_WM_STATE_FULLSCREEN at map | |
| 22 | + | Bypass uint8 // _NET_WM_BYPASS_COMPOSITOR (0 unset, 1 bypass, 2 do not) | |
| 23 | + | // HasAt: the client asked for a rect equal to a whole output; AtX, AtY | |
| 24 | + | // is that output's layout origin. | |
| 25 | + | HasAt bool | |
| 26 | + | AtX, AtY int | |
| 27 | + | Title, AppID string | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // host is the wayland server as the XWM sees it. Display goroutine only. | |
| 31 | + | // All strings passed are owned by the XWM record and already deduplicated | |
| 32 | + | // (only passed when changed). | |
| 33 | + | type host interface { | |
| 34 | + | // Created: a managed (non-override-redirect) X window is known; the host | |
| 35 | + | // allocates a ports.WindowID and returns it. Not yet mapped. | |
| 36 | + | Created(slot Slot, xid uint32) ports.WindowID | |
| 37 | + | // Ready: properties read and MapWindow issued; the surface may still be | |
| 38 | + | // missing. Info is copied by the host. | |
| 39 | + | Ready(slot Slot, info WindowInfo) | |
| 40 | + | // Associate links the X window to the wl_surface with identity surfaceID. | |
| 41 | + | // The host maps the window on the surface's first buffer commit. | |
| 42 | + | Associate(slot Slot, surfaceID uint64) | |
| 43 | + | // Dissociate: UnmapNotify/DestroyNotify; the host emits WindowUnmapped if | |
| 44 | + | // mapped and drops the surface link. destroyed also frees the WindowID. | |
| 45 | + | Dissociate(slot Slot, destroyed bool) | |
| 46 | + | Title(slot Slot, title string) // changed only | |
| 47 | + | AppID(slot Slot, appID string) // changed only (WM_CLASS class) | |
| 48 | + | Parent(slot Slot, parent Slot) // 0 = none; after a WM_TRANSIENT_FOR change | |
| 49 | + | // Fullscreen forwards a fullscreen request (_NET_WM_STATE client message | |
| 50 | + | // or initial state); the host emits WindowFullscreenRequest. | |
| 51 | + | Fullscreen(slot Slot, on bool) | |
| 52 | + | ||
| 53 | + | // OverrideMapped reports an override-redirect window that is mapped, | |
| 54 | + | // typed and paired with its wl_surface. Rect is in layout coordinates. | |
| 55 | + | OverrideMapped(slot Slot, xid uint32, surfaceID uint64, x, y, w, h int, noInput bool) | |
| 56 | + | // OverrideMoved reports a changed layout rect of a mapped override window. | |
| 57 | + | OverrideMoved(slot Slot, x, y, w, h int) | |
| 58 | + | // OverrideGone ends a mapped override window: unmapped or destroyed. | |
| 59 | + | OverrideGone(slot Slot) | |
| 60 | + | ||
| 61 | + | // SelectionOwned: an X client owns kind (SelClipboard/SelPrimary) and | |
| 62 | + | // offers these MIME types, or none does any more (mimes nil). mimes is | |
| 63 | + | // scratch: the host copies it. | |
| 64 | + | SelectionOwned(kind int, mimes []string) | |
| 65 | + | ||
| 66 | + | // SelectionRead asks for the data of the current Wayland selection kind | |
| 67 | + | // as mime: it returns a non-blocking read end the XWM closes, or -1 when | |
| 68 | + | // there is none, it is protected, or mime is not offered. | |
| 69 | + | SelectionRead(kind int, mime string) (fd int) | |
| 70 | + | ||
| 71 | + | // After runs fn on the display goroutine once d has elapsed, unless the | |
| 72 | + | // returned cancel ran first. cancel is idempotent and display goroutine | |
| 73 | + | // only; fn may still run once after cancel when it was already queued, | |
| 74 | + | // so callers guard with their own generation. | |
| 75 | + | After(d time.Duration, fn func()) (cancel func()) | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | // fdSource is the display's event source for the X socket (*server.Source). | |
| 79 | + | type fdSource interface { | |
| 80 | + | SetWritable(on bool) error | |
| 81 | + | Remove() | |
| 82 | + | } | |
A internal/adapters/wayland/xwm/host_mock_test.go +775 -0
| @@ -0,0 +1,775 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package xwm | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |
| 11 | + | mock "github.com/stretchr/testify/mock" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // newMockhost creates a new instance of mockhost. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 15 | + | // The first argument is typically a *testing.T value. | |
| 16 | + | func newMockhost(t interface { | |
| 17 | + | mock.TestingT | |
| 18 | + | Cleanup(func()) | |
| 19 | + | }) *mockhost { | |
| 20 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 21 | + | helper.Helper() | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | mock := &mockhost{} | |
| 25 | + | mock.Mock.Test(t) | |
| 26 | + | ||
| 27 | + | t.Cleanup(func() { | |
| 28 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 29 | + | helper.Helper() | |
| 30 | + | } | |
| 31 | + | mock.AssertExpectations(t) | |
| 32 | + | }) | |
| 33 | + | ||
| 34 | + | return mock | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // mockhost is an autogenerated mock type for the host type | |
| 38 | + | type mockhost struct { | |
| 39 | + | mock.Mock | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | type mockhost_Expecter struct { | |
| 43 | + | mock *mock.Mock | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func (_m *mockhost) EXPECT() *mockhost_Expecter { | |
| 47 | + | return &mockhost_Expecter{mock: &_m.Mock} | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // After provides a mock function for the type mockhost | |
| 51 | + | func (_mock *mockhost) After(d time.Duration, fn func()) func() { | |
| 52 | + | ret := _mock.Called(d, fn) | |
| 53 | + | ||
| 54 | + | if len(ret) == 0 { | |
| 55 | + | panic("no return value specified for After") | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | var r0 func() | |
| 59 | + | if returnFunc, ok := ret.Get(0).(func(time.Duration, func()) func()); ok { | |
| 60 | + | r0 = returnFunc(d, fn) | |
| 61 | + | } else { | |
| 62 | + | if ret.Get(0) != nil { | |
| 63 | + | r0 = ret.Get(0).(func()) | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | return r0 | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // mockhost_After_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'After' | |
| 70 | + | type mockhost_After_Call struct { | |
| 71 | + | *mock.Call | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // After is a helper method to define mock.On call | |
| 75 | + | // - d time.Duration | |
| 76 | + | // - fn func() | |
| 77 | + | func (_e *mockhost_Expecter) After(d any, fn any) *mockhost_After_Call { | |
| 78 | + | return &mockhost_After_Call{Call: _e.mock.On("After", d, fn)} | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | func (_c *mockhost_After_Call) Run(run func(d time.Duration, fn func())) *mockhost_After_Call { | |
| 82 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 83 | + | var arg0 time.Duration | |
| 84 | + | if args[0] != nil { | |
| 85 | + | arg0 = args[0].(time.Duration) | |
| 86 | + | } | |
| 87 | + | var arg1 func() | |
| 88 | + | if args[1] != nil { | |
| 89 | + | arg1 = args[1].(func()) | |
| 90 | + | } | |
| 91 | + | run( | |
| 92 | + | arg0, | |
| 93 | + | arg1, | |
| 94 | + | ) | |
| 95 | + | }) | |
| 96 | + | return _c | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | func (_c *mockhost_After_Call) Return(cancel func()) *mockhost_After_Call { | |
| 100 | + | _c.Call.Return(cancel) | |
| 101 | + | return _c | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | func (_c *mockhost_After_Call) RunAndReturn(run func(d time.Duration, fn func()) func()) *mockhost_After_Call { | |
| 105 | + | _c.Call.Return(run) | |
| 106 | + | return _c | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | // AppID provides a mock function for the type mockhost | |
| 110 | + | func (_mock *mockhost) AppID(slot Slot, appID string) { | |
| 111 | + | _mock.Called(slot, appID) | |
| 112 | + | return | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | // mockhost_AppID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppID' | |
| 116 | + | type mockhost_AppID_Call struct { | |
| 117 | + | *mock.Call | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | // AppID is a helper method to define mock.On call | |
| 121 | + | // - slot Slot | |
| 122 | + | // - appID string | |
| 123 | + | func (_e *mockhost_Expecter) AppID(slot any, appID any) *mockhost_AppID_Call { | |
| 124 | + | return &mockhost_AppID_Call{Call: _e.mock.On("AppID", slot, appID)} | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | func (_c *mockhost_AppID_Call) Run(run func(slot Slot, appID string)) *mockhost_AppID_Call { | |
| 128 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 129 | + | var arg0 Slot | |
| 130 | + | if args[0] != nil { | |
| 131 | + | arg0 = args[0].(Slot) | |
| 132 | + | } | |
| 133 | + | var arg1 string | |
| 134 | + | if args[1] != nil { | |
| 135 | + | arg1 = args[1].(string) | |
| 136 | + | } | |
| 137 | + | run( | |
| 138 | + | arg0, | |
| 139 | + | arg1, | |
| 140 | + | ) | |
| 141 | + | }) | |
| 142 | + | return _c | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | func (_c *mockhost_AppID_Call) Return() *mockhost_AppID_Call { | |
| 146 | + | _c.Call.Return() | |
| 147 | + | return _c | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | func (_c *mockhost_AppID_Call) RunAndReturn(run func(slot Slot, appID string)) *mockhost_AppID_Call { | |
| 151 | + | _c.Run(run) | |
| 152 | + | return _c | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | // Associate provides a mock function for the type mockhost | |
| 156 | + | func (_mock *mockhost) Associate(slot Slot, surfaceID uint64) { | |
| 157 | + | _mock.Called(slot, surfaceID) | |
| 158 | + | return | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | // mockhost_Associate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Associate' | |
| 162 | + | type mockhost_Associate_Call struct { | |
| 163 | + | *mock.Call | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | // Associate is a helper method to define mock.On call | |
| 167 | + | // - slot Slot | |
| 168 | + | // - surfaceID uint64 | |
| 169 | + | func (_e *mockhost_Expecter) Associate(slot any, surfaceID any) *mockhost_Associate_Call { | |
| 170 | + | return &mockhost_Associate_Call{Call: _e.mock.On("Associate", slot, surfaceID)} | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | func (_c *mockhost_Associate_Call) Run(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call { | |
| 174 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 175 | + | var arg0 Slot | |
| 176 | + | if args[0] != nil { | |
| 177 | + | arg0 = args[0].(Slot) | |
| 178 | + | } | |
| 179 | + | var arg1 uint64 | |
| 180 | + | if args[1] != nil { | |
| 181 | + | arg1 = args[1].(uint64) | |
| 182 | + | } | |
| 183 | + | run( | |
| 184 | + | arg0, | |
| 185 | + | arg1, | |
| 186 | + | ) | |
| 187 | + | }) | |
| 188 | + | return _c | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | func (_c *mockhost_Associate_Call) Return() *mockhost_Associate_Call { | |
| 192 | + | _c.Call.Return() | |
| 193 | + | return _c | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | func (_c *mockhost_Associate_Call) RunAndReturn(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call { | |
| 197 | + | _c.Run(run) | |
| 198 | + | return _c | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | // Created provides a mock function for the type mockhost | |
| 202 | + | func (_mock *mockhost) Created(slot Slot, xid uint32) ports.WindowID { | |
| 203 | + | ret := _mock.Called(slot, xid) | |
| 204 | + | ||
| 205 | + | if len(ret) == 0 { | |
| 206 | + | panic("no return value specified for Created") | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | var r0 ports.WindowID | |
| 210 | + | if returnFunc, ok := ret.Get(0).(func(Slot, uint32) ports.WindowID); ok { | |
| 211 | + | r0 = returnFunc(slot, xid) | |
| 212 | + | } else { | |
| 213 | + | r0 = ret.Get(0).(ports.WindowID) | |
| 214 | + | } | |
| 215 | + | return r0 | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | // mockhost_Created_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Created' | |
| 219 | + | type mockhost_Created_Call struct { | |
| 220 | + | *mock.Call | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | // Created is a helper method to define mock.On call | |
| 224 | + | // - slot Slot | |
| 225 | + | // - xid uint32 | |
| 226 | + | func (_e *mockhost_Expecter) Created(slot any, xid any) *mockhost_Created_Call { | |
| 227 | + | return &mockhost_Created_Call{Call: _e.mock.On("Created", slot, xid)} | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | func (_c *mockhost_Created_Call) Run(run func(slot Slot, xid uint32)) *mockhost_Created_Call { | |
| 231 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 232 | + | var arg0 Slot | |
| 233 | + | if args[0] != nil { | |
| 234 | + | arg0 = args[0].(Slot) | |
| 235 | + | } | |
| 236 | + | var arg1 uint32 | |
| 237 | + | if args[1] != nil { | |
| 238 | + | arg1 = args[1].(uint32) | |
| 239 | + | } | |
| 240 | + | run( | |
| 241 | + | arg0, | |
| 242 | + | arg1, | |
| 243 | + | ) | |
| 244 | + | }) | |
| 245 | + | return _c | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | func (_c *mockhost_Created_Call) Return(windowID ports.WindowID) *mockhost_Created_Call { | |
| 249 | + | _c.Call.Return(windowID) | |
| 250 | + | return _c | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | func (_c *mockhost_Created_Call) RunAndReturn(run func(slot Slot, xid uint32) ports.WindowID) *mockhost_Created_Call { | |
| 254 | + | _c.Call.Return(run) | |
| 255 | + | return _c | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | // Dissociate provides a mock function for the type mockhost | |
| 259 | + | func (_mock *mockhost) Dissociate(slot Slot, destroyed bool) { | |
| 260 | + | _mock.Called(slot, destroyed) | |
| 261 | + | return | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | // mockhost_Dissociate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Dissociate' | |
| 265 | + | type mockhost_Dissociate_Call struct { | |
| 266 | + | *mock.Call | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | // Dissociate is a helper method to define mock.On call | |
| 270 | + | // - slot Slot | |
| 271 | + | // - destroyed bool | |
| 272 | + | func (_e *mockhost_Expecter) Dissociate(slot any, destroyed any) *mockhost_Dissociate_Call { | |
| 273 | + | return &mockhost_Dissociate_Call{Call: _e.mock.On("Dissociate", slot, destroyed)} | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | func (_c *mockhost_Dissociate_Call) Run(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call { | |
| 277 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 278 | + | var arg0 Slot | |
| 279 | + | if args[0] != nil { | |
| 280 | + | arg0 = args[0].(Slot) | |
| 281 | + | } | |
| 282 | + | var arg1 bool | |
| 283 | + | if args[1] != nil { | |
| 284 | + | arg1 = args[1].(bool) | |
| 285 | + | } | |
| 286 | + | run( | |
| 287 | + | arg0, | |
| 288 | + | arg1, | |
| 289 | + | ) | |
| 290 | + | }) | |
| 291 | + | return _c | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | func (_c *mockhost_Dissociate_Call) Return() *mockhost_Dissociate_Call { | |
| 295 | + | _c.Call.Return() | |
| 296 | + | return _c | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | func (_c *mockhost_Dissociate_Call) RunAndReturn(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call { | |
| 300 | + | _c.Run(run) | |
| 301 | + | return _c | |
| 302 | + | } | |
| 303 | + | ||
| 304 | + | // Fullscreen provides a mock function for the type mockhost | |
| 305 | + | func (_mock *mockhost) Fullscreen(slot Slot, on bool) { | |
| 306 | + | _mock.Called(slot, on) | |
| 307 | + | return | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | // mockhost_Fullscreen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fullscreen' | |
| 311 | + | type mockhost_Fullscreen_Call struct { | |
| 312 | + | *mock.Call | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | // Fullscreen is a helper method to define mock.On call | |
| 316 | + | // - slot Slot | |
| 317 | + | // - on bool | |
| 318 | + | func (_e *mockhost_Expecter) Fullscreen(slot any, on any) *mockhost_Fullscreen_Call { | |
| 319 | + | return &mockhost_Fullscreen_Call{Call: _e.mock.On("Fullscreen", slot, on)} | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | func (_c *mockhost_Fullscreen_Call) Run(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call { | |
| 323 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 324 | + | var arg0 Slot | |
| 325 | + | if args[0] != nil { | |
| 326 | + | arg0 = args[0].(Slot) | |
| 327 | + | } | |
| 328 | + | var arg1 bool | |
| 329 | + | if args[1] != nil { | |
| 330 | + | arg1 = args[1].(bool) | |
| 331 | + | } | |
| 332 | + | run( | |
| 333 | + | arg0, | |
| 334 | + | arg1, | |
| 335 | + | ) | |
| 336 | + | }) | |
| 337 | + | return _c | |
| 338 | + | } | |
| 339 | + | ||
| 340 | + | func (_c *mockhost_Fullscreen_Call) Return() *mockhost_Fullscreen_Call { | |
| 341 | + | _c.Call.Return() | |
| 342 | + | return _c | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | func (_c *mockhost_Fullscreen_Call) RunAndReturn(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call { | |
| 346 | + | _c.Run(run) | |
| 347 | + | return _c | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | // OverrideGone provides a mock function for the type mockhost | |
| 351 | + | func (_mock *mockhost) OverrideGone(slot Slot) { | |
| 352 | + | _mock.Called(slot) | |
| 353 | + | return | |
| 354 | + | } | |
| 355 | + | ||
| 356 | + | // mockhost_OverrideGone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideGone' | |
| 357 | + | type mockhost_OverrideGone_Call struct { | |
| 358 | + | *mock.Call | |
| 359 | + | } | |
| 360 | + | ||
| 361 | + | // OverrideGone is a helper method to define mock.On call | |
| 362 | + | // - slot Slot | |
| 363 | + | func (_e *mockhost_Expecter) OverrideGone(slot any) *mockhost_OverrideGone_Call { | |
| 364 | + | return &mockhost_OverrideGone_Call{Call: _e.mock.On("OverrideGone", slot)} | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | func (_c *mockhost_OverrideGone_Call) Run(run func(slot Slot)) *mockhost_OverrideGone_Call { | |
| 368 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 369 | + | var arg0 Slot | |
| 370 | + | if args[0] != nil { | |
| 371 | + | arg0 = args[0].(Slot) | |
| 372 | + | } | |
| 373 | + | run( | |
| 374 | + | arg0, | |
| 375 | + | ) | |
| 376 | + | }) | |
| 377 | + | return _c | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | func (_c *mockhost_OverrideGone_Call) Return() *mockhost_OverrideGone_Call { | |
| 381 | + | _c.Call.Return() | |
| 382 | + | return _c | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | func (_c *mockhost_OverrideGone_Call) RunAndReturn(run func(slot Slot)) *mockhost_OverrideGone_Call { | |
| 386 | + | _c.Run(run) | |
| 387 | + | return _c | |
| 388 | + | } | |
| 389 | + | ||
| 390 | + | // OverrideMapped provides a mock function for the type mockhost | |
| 391 | + | func (_mock *mockhost) OverrideMapped(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool) { | |
| 392 | + | _mock.Called(slot, xid, surfaceID, x, y, w, h, noInput) | |
| 393 | + | return | |
| 394 | + | } | |
| 395 | + | ||
| 396 | + | // mockhost_OverrideMapped_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideMapped' | |
| 397 | + | type mockhost_OverrideMapped_Call struct { | |
| 398 | + | *mock.Call | |
| 399 | + | } | |
| 400 | + | ||
| 401 | + | // OverrideMapped is a helper method to define mock.On call | |
| 402 | + | // - slot Slot | |
| 403 | + | // - xid uint32 | |
| 404 | + | // - surfaceID uint64 | |
| 405 | + | // - x int | |
| 406 | + | // - y int | |
| 407 | + | // - w int | |
| 408 | + | // - h int | |
| 409 | + | // - noInput bool | |
| 410 | + | func (_e *mockhost_Expecter) OverrideMapped(slot any, xid any, surfaceID any, x any, y any, w any, h any, noInput any) *mockhost_OverrideMapped_Call { | |
| 411 | + | return &mockhost_OverrideMapped_Call{Call: _e.mock.On("OverrideMapped", slot, xid, surfaceID, x, y, w, h, noInput)} | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | func (_c *mockhost_OverrideMapped_Call) Run(run func(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool)) *mockhost_OverrideMapped_Call { | |
| 415 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 416 | + | var arg0 Slot | |
| 417 | + | if args[0] != nil { | |
| 418 | + | arg0 = args[0].(Slot) | |
| 419 | + | } | |
| 420 | + | var arg1 uint32 | |
| 421 | + | if args[1] != nil { | |
| 422 | + | arg1 = args[1].(uint32) | |
| 423 | + | } | |
| 424 | + | var arg2 uint64 | |
| 425 | + | if args[2] != nil { | |
| 426 | + | arg2 = args[2].(uint64) | |
| 427 | + | } | |
| 428 | + | var arg3 int | |
| 429 | + | if args[3] != nil { | |
| 430 | + | arg3 = args[3].(int) | |
| 431 | + | } | |
| 432 | + | var arg4 int | |
| 433 | + | if args[4] != nil { | |
| 434 | + | arg4 = args[4].(int) | |
| 435 | + | } | |
| 436 | + | var arg5 int | |
| 437 | + | if args[5] != nil { | |
| 438 | + | arg5 = args[5].(int) | |
| 439 | + | } | |
| 440 | + | var arg6 int | |
| 441 | + | if args[6] != nil { | |
| 442 | + | arg6 = args[6].(int) | |
| 443 | + | } | |
| 444 | + | var arg7 bool | |
| 445 | + | if args[7] != nil { | |
| 446 | + | arg7 = args[7].(bool) | |
| 447 | + | } | |
| 448 | + | run( | |
| 449 | + | arg0, | |
| 450 | + | arg1, | |
| 451 | + | arg2, | |
| 452 | + | arg3, | |
| 453 | + | arg4, | |
| 454 | + | arg5, | |
| 455 | + | arg6, | |
| 456 | + | arg7, | |
| 457 | + | ) | |
| 458 | + | }) | |
| 459 | + | return _c | |
| 460 | + | } | |
| 461 | + | ||
| 462 | + | func (_c *mockhost_OverrideMapped_Call) Return() *mockhost_OverrideMapped_Call { | |
| 463 | + | _c.Call.Return() | |
| 464 | + | return _c | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | func (_c *mockhost_OverrideMapped_Call) RunAndReturn(run func(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool)) *mockhost_OverrideMapped_Call { | |
| 468 | + | _c.Run(run) | |
| 469 | + | return _c | |
| 470 | + | } | |
| 471 | + | ||
| 472 | + | // OverrideMoved provides a mock function for the type mockhost | |
| 473 | + | func (_mock *mockhost) OverrideMoved(slot Slot, x int, y int, w int, h int) { | |
| 474 | + | _mock.Called(slot, x, y, w, h) | |
| 475 | + | return | |
| 476 | + | } | |
| 477 | + | ||
| 478 | + | // mockhost_OverrideMoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideMoved' | |
| 479 | + | type mockhost_OverrideMoved_Call struct { | |
| 480 | + | *mock.Call | |
| 481 | + | } | |
| 482 | + | ||
| 483 | + | // OverrideMoved is a helper method to define mock.On call | |
| 484 | + | // - slot Slot | |
| 485 | + | // - x int | |
| 486 | + | // - y int | |
| 487 | + | // - w int | |
| 488 | + | // - h int | |
| 489 | + | func (_e *mockhost_Expecter) OverrideMoved(slot any, x any, y any, w any, h any) *mockhost_OverrideMoved_Call { | |
| 490 | + | return &mockhost_OverrideMoved_Call{Call: _e.mock.On("OverrideMoved", slot, x, y, w, h)} | |
| 491 | + | } | |
| 492 | + | ||
| 493 | + | func (_c *mockhost_OverrideMoved_Call) Run(run func(slot Slot, x int, y int, w int, h int)) *mockhost_OverrideMoved_Call { | |
| 494 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 495 | + | var arg0 Slot | |
| 496 | + | if args[0] != nil { | |
| 497 | + | arg0 = args[0].(Slot) | |
| 498 | + | } | |
| 499 | + | var arg1 int | |
| 500 | + | if args[1] != nil { | |
| 501 | + | arg1 = args[1].(int) | |
| 502 | + | } | |
| 503 | + | var arg2 int | |
| 504 | + | if args[2] != nil { | |
| 505 | + | arg2 = args[2].(int) | |
| 506 | + | } | |
| 507 | + | var arg3 int | |
| 508 | + | if args[3] != nil { | |
| 509 | + | arg3 = args[3].(int) | |
| 510 | + | } | |
| 511 | + | var arg4 int | |
| 512 | + | if args[4] != nil { | |
| 513 | + | arg4 = args[4].(int) | |
| 514 | + | } | |
| 515 | + | run( | |
| 516 | + | arg0, | |
| 517 | + | arg1, | |
| 518 | + | arg2, | |
| 519 | + | arg3, | |
| 520 | + | arg4, | |
| 521 | + | ) | |
| 522 | + | }) | |
| 523 | + | return _c | |
| 524 | + | } | |
| 525 | + | ||
| 526 | + | func (_c *mockhost_OverrideMoved_Call) Return() *mockhost_OverrideMoved_Call { | |
| 527 | + | _c.Call.Return() | |
| 528 | + | return _c | |
| 529 | + | } | |
| 530 | + | ||
| 531 | + | func (_c *mockhost_OverrideMoved_Call) RunAndReturn(run func(slot Slot, x int, y int, w int, h int)) *mockhost_OverrideMoved_Call { | |
| 532 | + | _c.Run(run) | |
| 533 | + | return _c | |
| 534 | + | } | |
| 535 | + | ||
| 536 | + | // Parent provides a mock function for the type mockhost | |
| 537 | + | func (_mock *mockhost) Parent(slot Slot, parent Slot) { | |
| 538 | + | _mock.Called(slot, parent) | |
| 539 | + | return | |
| 540 | + | } | |
| 541 | + | ||
| 542 | + | // mockhost_Parent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Parent' | |
| 543 | + | type mockhost_Parent_Call struct { | |
| 544 | + | *mock.Call | |
| 545 | + | } | |
| 546 | + | ||
| 547 | + | // Parent is a helper method to define mock.On call | |
| 548 | + | // - slot Slot | |
| 549 | + | // - parent Slot | |
| 550 | + | func (_e *mockhost_Expecter) Parent(slot any, parent any) *mockhost_Parent_Call { | |
| 551 | + | return &mockhost_Parent_Call{Call: _e.mock.On("Parent", slot, parent)} | |
| 552 | + | } | |
| 553 | + | ||
| 554 | + | func (_c *mockhost_Parent_Call) Run(run func(slot Slot, parent Slot)) *mockhost_Parent_Call { | |
| 555 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 556 | + | var arg0 Slot | |
| 557 | + | if args[0] != nil { | |
| 558 | + | arg0 = args[0].(Slot) | |
| 559 | + | } | |
| 560 | + | var arg1 Slot | |
| 561 | + | if args[1] != nil { | |
| 562 | + | arg1 = args[1].(Slot) | |
| 563 | + | } | |
| 564 | + | run( | |
| 565 | + | arg0, | |
| 566 | + | arg1, | |
| 567 | + | ) | |
| 568 | + | }) | |
| 569 | + | return _c | |
| 570 | + | } | |
| 571 | + | ||
| 572 | + | func (_c *mockhost_Parent_Call) Return() *mockhost_Parent_Call { | |
| 573 | + | _c.Call.Return() | |
| 574 | + | return _c | |
| 575 | + | } | |
| 576 | + | ||
| 577 | + | func (_c *mockhost_Parent_Call) RunAndReturn(run func(slot Slot, parent Slot)) *mockhost_Parent_Call { | |
| 578 | + | _c.Run(run) | |
| 579 | + | return _c | |
| 580 | + | } | |
| 581 | + | ||
| 582 | + | // Ready provides a mock function for the type mockhost | |
| 583 | + | func (_mock *mockhost) Ready(slot Slot, info WindowInfo) { | |
| 584 | + | _mock.Called(slot, info) | |
| 585 | + | return | |
| 586 | + | } | |
| 587 | + | ||
| 588 | + | // mockhost_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready' | |
| 589 | + | type mockhost_Ready_Call struct { | |
| 590 | + | *mock.Call | |
| 591 | + | } | |
| 592 | + | ||
| 593 | + | // Ready is a helper method to define mock.On call | |
| 594 | + | // - slot Slot | |
| 595 | + | // - info WindowInfo | |
| 596 | + | func (_e *mockhost_Expecter) Ready(slot any, info any) *mockhost_Ready_Call { | |
| 597 | + | return &mockhost_Ready_Call{Call: _e.mock.On("Ready", slot, info)} | |
| 598 | + | } | |
| 599 | + | ||
| 600 | + | func (_c *mockhost_Ready_Call) Run(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call { | |
| 601 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 602 | + | var arg0 Slot | |
| 603 | + | if args[0] != nil { | |
| 604 | + | arg0 = args[0].(Slot) | |
| 605 | + | } | |
| 606 | + | var arg1 WindowInfo | |
| 607 | + | if args[1] != nil { | |
| 608 | + | arg1 = args[1].(WindowInfo) | |
| 609 | + | } | |
| 610 | + | run( | |
| 611 | + | arg0, | |
| 612 | + | arg1, | |
| 613 | + | ) | |
| 614 | + | }) | |
| 615 | + | return _c | |
| 616 | + | } | |
| 617 | + | ||
| 618 | + | func (_c *mockhost_Ready_Call) Return() *mockhost_Ready_Call { | |
| 619 | + | _c.Call.Return() | |
| 620 | + | return _c | |
| 621 | + | } | |
| 622 | + | ||
| 623 | + | func (_c *mockhost_Ready_Call) RunAndReturn(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call { | |
| 624 | + | _c.Run(run) | |
| 625 | + | return _c | |
| 626 | + | } | |
| 627 | + | ||
| 628 | + | // SelectionOwned provides a mock function for the type mockhost | |
| 629 | + | func (_mock *mockhost) SelectionOwned(kind int, mimes []string) { | |
| 630 | + | _mock.Called(kind, mimes) | |
| 631 | + | return | |
| 632 | + | } | |
| 633 | + | ||
| 634 | + | // mockhost_SelectionOwned_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectionOwned' | |
| 635 | + | type mockhost_SelectionOwned_Call struct { | |
| 636 | + | *mock.Call | |
| 637 | + | } | |
| 638 | + | ||
| 639 | + | // SelectionOwned is a helper method to define mock.On call | |
| 640 | + | // - kind int | |
| 641 | + | // - mimes []string | |
| 642 | + | func (_e *mockhost_Expecter) SelectionOwned(kind any, mimes any) *mockhost_SelectionOwned_Call { | |
| 643 | + | return &mockhost_SelectionOwned_Call{Call: _e.mock.On("SelectionOwned", kind, mimes)} | |
| 644 | + | } | |
| 645 | + | ||
| 646 | + | func (_c *mockhost_SelectionOwned_Call) Run(run func(kind int, mimes []string)) *mockhost_SelectionOwned_Call { | |
| 647 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 648 | + | var arg0 int | |
| 649 | + | if args[0] != nil { | |
| 650 | + | arg0 = args[0].(int) | |
| 651 | + | } | |
| 652 | + | var arg1 []string | |
| 653 | + | if args[1] != nil { | |
| 654 | + | arg1 = args[1].([]string) | |
| 655 | + | } | |
| 656 | + | run( | |
| 657 | + | arg0, | |
| 658 | + | arg1, | |
| 659 | + | ) | |
| 660 | + | }) | |
| 661 | + | return _c | |
| 662 | + | } | |
| 663 | + | ||
| 664 | + | func (_c *mockhost_SelectionOwned_Call) Return() *mockhost_SelectionOwned_Call { | |
| 665 | + | _c.Call.Return() | |
| 666 | + | return _c | |
| 667 | + | } | |
| 668 | + | ||
| 669 | + | func (_c *mockhost_SelectionOwned_Call) RunAndReturn(run func(kind int, mimes []string)) *mockhost_SelectionOwned_Call { | |
| 670 | + | _c.Run(run) | |
| 671 | + | return _c | |
| 672 | + | } | |
| 673 | + | ||
| 674 | + | // SelectionRead provides a mock function for the type mockhost | |
| 675 | + | func (_mock *mockhost) SelectionRead(kind int, mime string) int { | |
| 676 | + | ret := _mock.Called(kind, mime) | |
| 677 | + | ||
| 678 | + | if len(ret) == 0 { | |
| 679 | + | panic("no return value specified for SelectionRead") | |
| 680 | + | } | |
| 681 | + | ||
| 682 | + | var r0 int | |
| 683 | + | if returnFunc, ok := ret.Get(0).(func(int, string) int); ok { | |
| 684 | + | r0 = returnFunc(kind, mime) | |
| 685 | + | } else { | |
| 686 | + | r0 = ret.Get(0).(int) | |
| 687 | + | } | |
| 688 | + | return r0 | |
| 689 | + | } | |
| 690 | + | ||
| 691 | + | // mockhost_SelectionRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectionRead' | |
| 692 | + | type mockhost_SelectionRead_Call struct { | |
| 693 | + | *mock.Call | |
| 694 | + | } | |
| 695 | + | ||
| 696 | + | // SelectionRead is a helper method to define mock.On call | |
| 697 | + | // - kind int | |
| 698 | + | // - mime string | |
| 699 | + | func (_e *mockhost_Expecter) SelectionRead(kind any, mime any) *mockhost_SelectionRead_Call { | |
| 700 | + | return &mockhost_SelectionRead_Call{Call: _e.mock.On("SelectionRead", kind, mime)} | |
| 701 | + | } | |
| 702 | + | ||
| 703 | + | func (_c *mockhost_SelectionRead_Call) Run(run func(kind int, mime string)) *mockhost_SelectionRead_Call { | |
| 704 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 705 | + | var arg0 int | |
| 706 | + | if args[0] != nil { | |
| 707 | + | arg0 = args[0].(int) | |
| 708 | + | } | |
| 709 | + | var arg1 string | |
| 710 | + | if args[1] != nil { | |
| 711 | + | arg1 = args[1].(string) | |
| 712 | + | } | |
| 713 | + | run( | |
| 714 | + | arg0, | |
| 715 | + | arg1, | |
| 716 | + | ) | |
| 717 | + | }) | |
| 718 | + | return _c | |
| 719 | + | } | |
| 720 | + | ||
| 721 | + | func (_c *mockhost_SelectionRead_Call) Return(fd int) *mockhost_SelectionRead_Call { | |
| 722 | + | _c.Call.Return(fd) | |
| 723 | + | return _c | |
| 724 | + | } | |
| 725 | + | ||
| 726 | + | func (_c *mockhost_SelectionRead_Call) RunAndReturn(run func(kind int, mime string) int) *mockhost_SelectionRead_Call { | |
| 727 | + | _c.Call.Return(run) | |
| 728 | + | return _c | |
| 729 | + | } | |
| 730 | + | ||
| 731 | + | // Title provides a mock function for the type mockhost | |
| 732 | + | func (_mock *mockhost) Title(slot Slot, title string) { | |
| 733 | + | _mock.Called(slot, title) | |
| 734 | + | return | |
| 735 | + | } | |
| 736 | + | ||
| 737 | + | // mockhost_Title_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Title' | |
| 738 | + | type mockhost_Title_Call struct { | |
| 739 | + | *mock.Call | |
| 740 | + | } | |
| 741 | + | ||
| 742 | + | // Title is a helper method to define mock.On call | |
| 743 | + | // - slot Slot | |
| 744 | + | // - title string | |
| 745 | + | func (_e *mockhost_Expecter) Title(slot any, title any) *mockhost_Title_Call { | |
| 746 | + | return &mockhost_Title_Call{Call: _e.mock.On("Title", slot, title)} | |
| 747 | + | } | |
| 748 | + | ||
| 749 | + | func (_c *mockhost_Title_Call) Run(run func(slot Slot, title string)) *mockhost_Title_Call { | |
| 750 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 751 | + | var arg0 Slot | |
| 752 | + | if args[0] != nil { | |
| 753 | + | arg0 = args[0].(Slot) | |
| 754 | + | } | |
| 755 | + | var arg1 string | |
| 756 | + | if args[1] != nil { | |
| 757 | + | arg1 = args[1].(string) | |
| 758 | + | } | |
| 759 | + | run( | |
| 760 | + | arg0, | |
| 761 | + | arg1, | |
| 762 | + | ) | |
| 763 | + | }) | |
| 764 | + | return _c | |
| 765 | + | } | |
| 766 | + | ||
| 767 | + | func (_c *mockhost_Title_Call) Return() *mockhost_Title_Call { | |
| 768 | + | _c.Call.Return() | |
| 769 | + | return _c | |
| 770 | + | } | |
| 771 | + | ||
| 772 | + | func (_c *mockhost_Title_Call) RunAndReturn(run func(slot Slot, title string)) *mockhost_Title_Call { | |
| 773 | + | _c.Run(run) | |
| 774 | + | return _c | |
| 775 | + | } | |
A internal/adapters/wayland/xwm/loop_mock_test.go +180 -0
| @@ -0,0 +1,180 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package xwm | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | mock "github.com/stretchr/testify/mock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // newMockloop creates a new instance of mockloop. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 14 | + | // The first argument is typically a *testing.T value. | |
| 15 | + | func newMockloop(t interface { | |
| 16 | + | mock.TestingT | |
| 17 | + | Cleanup(func()) | |
| 18 | + | }) *mockloop { | |
| 19 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 20 | + | helper.Helper() | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | mock := &mockloop{} | |
| 24 | + | mock.Mock.Test(t) | |
| 25 | + | ||
| 26 | + | t.Cleanup(func() { | |
| 27 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 28 | + | helper.Helper() | |
| 29 | + | } | |
| 30 | + | mock.AssertExpectations(t) | |
| 31 | + | }) | |
| 32 | + | ||
| 33 | + | return mock | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // mockloop is an autogenerated mock type for the loop type | |
| 37 | + | type mockloop struct { | |
| 38 | + | mock.Mock | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | type mockloop_Expecter struct { | |
| 42 | + | mock *mock.Mock | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func (_m *mockloop) EXPECT() *mockloop_Expecter { | |
| 46 | + | return &mockloop_Expecter{mock: &_m.Mock} | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // AddFD provides a mock function for the type mockloop | |
| 50 | + | func (_mock *mockloop) AddFD(fd int, onReadable func(), onWritable func()) (fdSource, error) { | |
| 51 | + | ret := _mock.Called(fd, onReadable, onWritable) | |
| 52 | + | ||
| 53 | + | if len(ret) == 0 { | |
| 54 | + | panic("no return value specified for AddFD") | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | var r0 fdSource | |
| 58 | + | var r1 error | |
| 59 | + | if returnFunc, ok := ret.Get(0).(func(int, func(), func()) (fdSource, error)); ok { | |
| 60 | + | return returnFunc(fd, onReadable, onWritable) | |
| 61 | + | } | |
| 62 | + | if returnFunc, ok := ret.Get(0).(func(int, func(), func()) fdSource); ok { | |
| 63 | + | r0 = returnFunc(fd, onReadable, onWritable) | |
| 64 | + | } else { | |
| 65 | + | if ret.Get(0) != nil { | |
| 66 | + | r0 = ret.Get(0).(fdSource) | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | if returnFunc, ok := ret.Get(1).(func(int, func(), func()) error); ok { | |
| 70 | + | r1 = returnFunc(fd, onReadable, onWritable) | |
| 71 | + | } else { | |
| 72 | + | r1 = ret.Error(1) | |
| 73 | + | } | |
| 74 | + | return r0, r1 | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // mockloop_AddFD_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFD' | |
| 78 | + | type mockloop_AddFD_Call struct { | |
| 79 | + | *mock.Call | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // AddFD is a helper method to define mock.On call | |
| 83 | + | // - fd int | |
| 84 | + | // - onReadable func() | |
| 85 | + | // - onWritable func() | |
| 86 | + | func (_e *mockloop_Expecter) AddFD(fd any, onReadable any, onWritable any) *mockloop_AddFD_Call { | |
| 87 | + | return &mockloop_AddFD_Call{Call: _e.mock.On("AddFD", fd, onReadable, onWritable)} | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | func (_c *mockloop_AddFD_Call) Run(run func(fd int, onReadable func(), onWritable func())) *mockloop_AddFD_Call { | |
| 91 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 92 | + | var arg0 int | |
| 93 | + | if args[0] != nil { | |
| 94 | + | arg0 = args[0].(int) | |
| 95 | + | } | |
| 96 | + | var arg1 func() | |
| 97 | + | if args[1] != nil { | |
| 98 | + | arg1 = args[1].(func()) | |
| 99 | + | } | |
| 100 | + | var arg2 func() | |
| 101 | + | if args[2] != nil { | |
| 102 | + | arg2 = args[2].(func()) | |
| 103 | + | } | |
| 104 | + | run( | |
| 105 | + | arg0, | |
| 106 | + | arg1, | |
| 107 | + | arg2, | |
| 108 | + | ) | |
| 109 | + | }) | |
| 110 | + | return _c | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | func (_c *mockloop_AddFD_Call) Return(fdSourceMoqParam fdSource, err error) *mockloop_AddFD_Call { | |
| 114 | + | _c.Call.Return(fdSourceMoqParam, err) | |
| 115 | + | return _c | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | func (_c *mockloop_AddFD_Call) RunAndReturn(run func(fd int, onReadable func(), onWritable func()) (fdSource, error)) *mockloop_AddFD_Call { | |
| 119 | + | _c.Call.Return(run) | |
| 120 | + | return _c | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | // After provides a mock function for the type mockloop | |
| 124 | + | func (_mock *mockloop) After(d time.Duration, fn func()) func() { | |
| 125 | + | ret := _mock.Called(d, fn) | |
| 126 | + | ||
| 127 | + | if len(ret) == 0 { | |
| 128 | + | panic("no return value specified for After") | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | var r0 func() | |
| 132 | + | if returnFunc, ok := ret.Get(0).(func(time.Duration, func()) func()); ok { | |
| 133 | + | r0 = returnFunc(d, fn) | |
| 134 | + | } else { | |
| 135 | + | if ret.Get(0) != nil { | |
| 136 | + | r0 = ret.Get(0).(func()) | |
| 137 | + | } | |
| 138 | + | } | |
| 139 | + | return r0 | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | // mockloop_After_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'After' | |
| 143 | + | type mockloop_After_Call struct { | |
| 144 | + | *mock.Call | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | // After is a helper method to define mock.On call | |
| 148 | + | // - d time.Duration | |
| 149 | + | // - fn func() | |
| 150 | + | func (_e *mockloop_Expecter) After(d any, fn any) *mockloop_After_Call { | |
| 151 | + | return &mockloop_After_Call{Call: _e.mock.On("After", d, fn)} | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | func (_c *mockloop_After_Call) Run(run func(d time.Duration, fn func())) *mockloop_After_Call { | |
| 155 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 156 | + | var arg0 time.Duration | |
| 157 | + | if args[0] != nil { | |
| 158 | + | arg0 = args[0].(time.Duration) | |
| 159 | + | } | |
| 160 | + | var arg1 func() | |
| 161 | + | if args[1] != nil { | |
| 162 | + | arg1 = args[1].(func()) | |
| 163 | + | } | |
| 164 | + | run( | |
| 165 | + | arg0, | |
| 166 | + | arg1, | |
| 167 | + | ) | |
| 168 | + | }) | |
| 169 | + | return _c | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | func (_c *mockloop_After_Call) Return(cancel func()) *mockloop_After_Call { | |
| 173 | + | _c.Call.Return(cancel) | |
| 174 | + | return _c | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | func (_c *mockloop_After_Call) RunAndReturn(run func(d time.Duration, fn func()) func()) *mockloop_After_Call { | |
| 178 | + | _c.Call.Return(run) | |
| 179 | + | return _c | |
| 180 | + | } | |
A internal/adapters/wayland/xwm/mapflow.go +359 -0
| @@ -0,0 +1,359 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bnema.dev/xturbo" | |
| 5 | + | "bnema.dev/xturbo/xproto" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // ICCCM WM_STATE values. | |
| 9 | + | const ( | |
| 10 | + | wmStateWithdrawn uint32 = 0 | |
| 11 | + | wmStateNormal uint32 = 1 | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // _NET_WM_STATE client message actions. | |
| 15 | + | const ( | |
| 16 | + | netStateRemove = 0 | |
| 17 | + | netStateAdd = 1 | |
| 18 | + | netStateToggle = 2 | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | // beginMap starts the map flow of a managed window: every property, the | |
| 22 | + | // geometry and MapWindow go out in one batch (wlroots order). Also called by | |
| 23 | + | // Attach for windows mapped before it. | |
| 24 | + | func (x *XWM) beginMap(s Slot) { | |
| 25 | + | rec := x.tab.rec(s) | |
| 26 | + | if rec.kind != kindManaged || rec.mapping { | |
| 27 | + | return | |
| 28 | + | } | |
| 29 | + | rec.mapping, rec.ready = true, false | |
| 30 | + | rec.hasNetName = false | |
| 31 | + | rec.pending = propGeom | |
| 32 | + | xid := rec.xid | |
| 33 | + | for idx := 0; idx < propCount; idx++ { | |
| 34 | + | x.requestProp(s, idx) | |
| 35 | + | } | |
| 36 | + | b := x.conn.Buf() | |
| 37 | + | x.expect(xproto.GetGeometry(b, xproto.Drawable(xid)), kindGeom, x.tab.tag(s), 0) | |
| 38 | + | xproto.MapWindow(b, xproto.Window(xid)) | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | func (x *XWM) onMapRequest(ev *xturbo.Event) { | |
| 42 | + | if s := x.tab.lookup(uint32(xproto.MapRequestOf(ev).Window())); s != 0 { | |
| 43 | + | x.beginMap(s) | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func (x *XWM) onMapNotify(ev *xturbo.Event) { | |
| 48 | + | s := x.tab.lookup(uint32(xproto.MapNotifyOf(ev).Window())) | |
| 49 | + | if s == 0 { | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | rec := x.tab.rec(s) | |
| 53 | + | again := rec.mapped | |
| 54 | + | rec.mapped = true | |
| 55 | + | if rec.kind == kindUnmanaged && !again { | |
| 56 | + | x.requestOverrideType(s) | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | // onUnmapNotify ends the mapped life of a managed window; a later MapRequest | |
| 61 | + | // starts over and re-reads the properties. Synthetic events (ICCCM unmap | |
| 62 | + | // requests) are not an unmap. | |
| 63 | + | func (x *XWM) onUnmapNotify(ev *xturbo.Event) { | |
| 64 | + | if ev.SendEvent { | |
| 65 | + | return | |
| 66 | + | } | |
| 67 | + | s := x.tab.lookup(uint32(xproto.UnmapNotifyOf(ev).Window())) | |
| 68 | + | if s == 0 { | |
| 69 | + | return | |
| 70 | + | } | |
| 71 | + | rec := x.tab.rec(s) | |
| 72 | + | rec.mapped = false | |
| 73 | + | if rec.kind == kindUnmanaged { | |
| 74 | + | x.overrideUnmapped(s, rec) | |
| 75 | + | return | |
| 76 | + | } | |
| 77 | + | if rec.kind != kindManaged { | |
| 78 | + | return | |
| 79 | + | } | |
| 80 | + | wasMapping := rec.mapping | |
| 81 | + | rec.ready, rec.mapping, rec.pending, rec.sized = false, false, 0, false | |
| 82 | + | rec.gen++ // replies still in flight for this map name a dead tag | |
| 83 | + | if rec.gen == 0 { | |
| 84 | + | rec.gen = 1 | |
| 85 | + | } | |
| 86 | + | rec.pend.dirty = false | |
| 87 | + | rec.serial, rec.surface = 0, 0 | |
| 88 | + | if wasMapping { | |
| 89 | + | xid := rec.xid | |
| 90 | + | x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateWithdrawn, 0}) | |
| 91 | + | xproto.DeleteProperty(x.conn.Buf(), xproto.Window(xid), xproto.Atom(x.atoms[atomNetWMState])) | |
| 92 | + | } | |
| 93 | + | x.dropWindow(s) | |
| 94 | + | if rec.id != 0 { | |
| 95 | + | x.host.Dissociate(s, false) | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // setProp32Typed is setProp32 with an explicit property type atom. | |
| 100 | + | func (x *XWM) setProp32Typed(win uint32, prop int, typ uint32, vals []uint32) { | |
| 101 | + | x.setProp32(win, prop, xproto.Atom(typ), vals) | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // checkReady fires Ready once every awaited reply is in. | |
| 105 | + | func (x *XWM) checkReady(s Slot) { | |
| 106 | + | rec := x.tab.rec(s) | |
| 107 | + | if rec == nil || rec.pending != 0 || rec.ready || !rec.mapping || rec.kind != kindManaged || x.host == nil { | |
| 108 | + | return | |
| 109 | + | } | |
| 110 | + | rec.ready = true | |
| 111 | + | x.classify(rec) | |
| 112 | + | info := x.info(s) | |
| 113 | + | xid := rec.xid | |
| 114 | + | ||
| 115 | + | var state [2]uint32 | |
| 116 | + | n := 0 | |
| 117 | + | if rec.modal { | |
| 118 | + | state[n] = x.atoms[atomNetWMStateModal] | |
| 119 | + | n++ | |
| 120 | + | } | |
| 121 | + | if rec.fullscreen { | |
| 122 | + | state[n] = x.atoms[atomNetWMStateFullscreen] | |
| 123 | + | n++ | |
| 124 | + | } | |
| 125 | + | x.setProp32(xid, atomNetWMState, xproto.AtomAtom, state[:n]) | |
| 126 | + | x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateNormal, 0}) | |
| 127 | + | x.addClient(xid) | |
| 128 | + | ||
| 129 | + | x.host.Ready(s, info) | |
| 130 | + | x.tryPair(s) | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | // info is what the host learns when a window is ready. | |
| 134 | + | func (x *XWM) info(s Slot) WindowInfo { | |
| 135 | + | rec := x.tab.rec(s) | |
| 136 | + | parent := x.managedSlot(rec.transient) | |
| 137 | + | info := WindowInfo{ | |
| 138 | + | PID: int(rec.pid), | |
| 139 | + | Floating: rec.floating, | |
| 140 | + | Parent: parent, | |
| 141 | + | Modal: rec.modal, | |
| 142 | + | Fullscreen: rec.fullscreen, | |
| 143 | + | Title: rec.title, | |
| 144 | + | AppID: rec.class, | |
| 145 | + | Bypass: rec.bypass, | |
| 146 | + | } | |
| 147 | + | if rec.sized && !info.Floating { | |
| 148 | + | info.AtX, info.AtY, info.HasAt = x.space.WholeOutput(int(rec.x), int(rec.y), int(rec.w), int(rec.h)) | |
| 149 | + | } | |
| 150 | + | if info.Floating || rec.sized { | |
| 151 | + | info.Width, info.Height = int(rec.w), int(rec.h) | |
| 152 | + | } | |
| 153 | + | return info | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func (x *XWM) onGeomReply(s Slot, rec *record, r *xturbo.Reply) { | |
| 157 | + | if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok { | |
| 158 | + | rec.x, rec.y, rec.w, rec.h = int32(g.X()), int32(g.Y()), uint32(g.Width()), uint32(g.Height()) | |
| 159 | + | } | |
| 160 | + | rec.pending &^= propGeom | |
| 161 | + | x.checkReady(s) | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | // onClientMessage handles WL_SURFACE_SERIAL (pairing), the legacy | |
| 165 | + | // WL_SURFACE_ID (counted, ignored) and _NET_WM_STATE requests. | |
| 166 | + | func (x *XWM) onClientMessage(ev *xturbo.Event) { | |
| 167 | + | e := xproto.ClientMessageOf(ev) | |
| 168 | + | if e.Format() != 32 { | |
| 169 | + | return | |
| 170 | + | } | |
| 171 | + | typ := uint32(e.Type()) | |
| 172 | + | switch typ { | |
| 173 | + | case x.atoms[atomWLSurfaceSerial]: | |
| 174 | + | s := x.tab.lookup(uint32(e.Window())) | |
| 175 | + | if s == 0 { | |
| 176 | + | return | |
| 177 | + | } | |
| 178 | + | rec := x.tab.rec(s) | |
| 179 | + | if rec.serial != 0 { | |
| 180 | + | x.log.Debug().Uint32("xid", rec.xid).Msg("duplicate WL_SURFACE_SERIAL ignored") | |
| 181 | + | return | |
| 182 | + | } | |
| 183 | + | d := e.Data().Data32() | |
| 184 | + | rec.serial = uint64(d[1])<<32 | uint64(d[0]) | |
| 185 | + | x.tryPair(s) | |
| 186 | + | case x.atoms[atomWLSurfaceID]: | |
| 187 | + | x.stats.LegacyID++ | |
| 188 | + | if x.stats.LegacyID == 1 { | |
| 189 | + | x.log.Warn().Msg("WL_SURFACE_ID received: Xwayland did not bind xwayland_shell_v1, window ignored") | |
| 190 | + | } | |
| 191 | + | case x.atoms[atomNetWMState]: | |
| 192 | + | x.onNetWMState(e) | |
| 193 | + | case x.atoms[atomWMChangeState]: | |
| 194 | + | // Iconify requests are ignored: windows stay mapped. | |
| 195 | + | } | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | // onNetWMState forwards fullscreen requests to the host and records them; the | |
| 199 | + | // property itself is written when core answers with a configure. | |
| 200 | + | func (x *XWM) onNetWMState(e xproto.ClientMessageEvent) { | |
| 201 | + | s := x.tab.lookup(uint32(e.Window())) | |
| 202 | + | if s == 0 { | |
| 203 | + | return | |
| 204 | + | } | |
| 205 | + | rec := x.tab.rec(s) | |
| 206 | + | if rec.kind != kindManaged { | |
| 207 | + | return | |
| 208 | + | } | |
| 209 | + | d := e.Data().Data32() | |
| 210 | + | apply := func(cur bool) bool { | |
| 211 | + | switch d[0] { | |
| 212 | + | case netStateRemove: | |
| 213 | + | return false | |
| 214 | + | case netStateAdd: | |
| 215 | + | return true | |
| 216 | + | case netStateToggle: | |
| 217 | + | return !cur | |
| 218 | + | } | |
| 219 | + | return cur | |
| 220 | + | } | |
| 221 | + | for _, a := range d[1:3] { | |
| 222 | + | switch { | |
| 223 | + | case a == 0: | |
| 224 | + | case a == x.atoms[atomNetWMStateModal]: | |
| 225 | + | rec.modal = apply(rec.modal) | |
| 226 | + | case a == x.atoms[atomNetWMStateFullscreen]: | |
| 227 | + | on := apply(rec.fullscreen) | |
| 228 | + | if on == rec.fullscreen { | |
| 229 | + | continue | |
| 230 | + | } | |
| 231 | + | rec.fullscreen = on | |
| 232 | + | if rec.ready { | |
| 233 | + | x.host.Fullscreen(s, on) | |
| 234 | + | rec = x.tab.rec(s) | |
| 235 | + | } | |
| 236 | + | } | |
| 237 | + | } | |
| 238 | + | } | |
| 239 | + | ||
| 240 | + | // onReparentNotify keeps the table to root's direct children: a window moved | |
| 241 | + | // away is forgotten, one moved into root is treated as newly created. | |
| 242 | + | func (x *XWM) onReparentNotify(ev *xturbo.Event) { | |
| 243 | + | e := xproto.ReparentNotifyOf(ev) | |
| 244 | + | xid := uint32(e.Window()) | |
| 245 | + | if xid == x.wmWin { | |
| 246 | + | return | |
| 247 | + | } | |
| 248 | + | s := x.tab.lookup(xid) | |
| 249 | + | if uint32(e.Parent()) != x.root { | |
| 250 | + | if s != 0 { | |
| 251 | + | x.forgetWindow(s) | |
| 252 | + | } | |
| 253 | + | return | |
| 254 | + | } | |
| 255 | + | if s != 0 { | |
| 256 | + | return | |
| 257 | + | } | |
| 258 | + | s = x.insert(xid) | |
| 259 | + | if s == 0 { | |
| 260 | + | return | |
| 261 | + | } | |
| 262 | + | rec := x.tab.rec(s) | |
| 263 | + | rec.x, rec.y = int32(e.X()), int32(e.Y()) | |
| 264 | + | if e.OverrideRedirect() { | |
| 265 | + | rec.kind = kindUnmanaged | |
| 266 | + | x.stats.UnmanagedSeen++ | |
| 267 | + | return | |
| 268 | + | } | |
| 269 | + | if x.host != nil { | |
| 270 | + | x.adopt(s) | |
| 271 | + | } | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | // tryPair links the window to the wl_surface carrying its serial, once both | |
| 275 | + | // are known and the window is ready (the host maps on the surface's first | |
| 276 | + | // buffer, so it must know the window's info by then). Unmanaged windows keep | |
| 277 | + | // the surface but are never associated. | |
| 278 | + | func (x *XWM) tryPair(s Slot) { | |
| 279 | + | rec := x.tab.rec(s) | |
| 280 | + | if rec.serial == 0 || rec.surface != 0 { | |
| 281 | + | return | |
| 282 | + | } | |
| 283 | + | if rec.kind == kindManaged && !rec.ready { | |
| 284 | + | return | |
| 285 | + | } | |
| 286 | + | for i := 0; i < x.unpairedN; i++ { | |
| 287 | + | if x.unpaired[i].serial != rec.serial { | |
| 288 | + | continue | |
| 289 | + | } | |
| 290 | + | rec.surface = x.unpaired[i].surface | |
| 291 | + | x.unpairedN-- | |
| 292 | + | x.unpaired[i] = x.unpaired[x.unpairedN] | |
| 293 | + | if rec.kind == kindManaged { | |
| 294 | + | x.log.Debug().Uint32("xid", rec.xid).Uint64("surface", rec.surface).Msg("x11 window paired") | |
| 295 | + | x.host.Associate(s, rec.surface) | |
| 296 | + | } else { | |
| 297 | + | x.overrideMapped(s, rec) | |
| 298 | + | } | |
| 299 | + | return | |
| 300 | + | } | |
| 301 | + | } | |
| 302 | + | ||
| 303 | + | // SetSerial records the serial of a wl_surface (xwayland_surface_v1.set_serial) | |
| 304 | + | // and pairs it with the window announcing the same serial. | |
| 305 | + | func (x *XWM) SetSerial(surfaceID uint64, serial uint64) { | |
| 306 | + | if serial == 0 || x.closed { | |
| 307 | + | return | |
| 308 | + | } | |
| 309 | + | for i := 0; i < x.unpairedN; i++ { | |
| 310 | + | if x.unpaired[i].serial == serial { | |
| 311 | + | x.unpaired[i].surface = surfaceID | |
| 312 | + | x.pairSerial(serial) | |
| 313 | + | return | |
| 314 | + | } | |
| 315 | + | } | |
| 316 | + | if x.unpairedN == unpairedCap { | |
| 317 | + | copy(x.unpaired[:], x.unpaired[1:]) | |
| 318 | + | x.unpairedN-- | |
| 319 | + | if !x.warnedUnpaired { | |
| 320 | + | x.warnedUnpaired = true | |
| 321 | + | x.log.Warn().Msg("too many unpaired surfaces, dropping the oldest") | |
| 322 | + | } | |
| 323 | + | } | |
| 324 | + | x.unpaired[x.unpairedN] = struct{ serial, surface uint64 }{serial, surfaceID} | |
| 325 | + | x.unpairedN++ | |
| 326 | + | x.pairSerial(serial) | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | // pairSerial offers a newly known surface serial to the windows. | |
| 330 | + | func (x *XWM) pairSerial(serial uint64) { | |
| 331 | + | for i := range x.tab.recs { | |
| 332 | + | if r := &x.tab.recs[i]; r.kind != kindFree && r.serial == serial && r.surface == 0 { | |
| 333 | + | x.tryPair(Slot(i + 1)) | |
| 334 | + | break | |
| 335 | + | } | |
| 336 | + | } | |
| 337 | + | x.wake() | |
| 338 | + | } | |
| 339 | + | ||
| 340 | + | // SurfaceGone forgets a destroyed wl_surface. | |
| 341 | + | func (x *XWM) SurfaceGone(surfaceID uint64) { | |
| 342 | + | for i := 0; i < x.unpairedN; i++ { | |
| 343 | + | if x.unpaired[i].surface == surfaceID { | |
| 344 | + | x.unpairedN-- | |
| 345 | + | x.unpaired[i] = x.unpaired[x.unpairedN] | |
| 346 | + | break | |
| 347 | + | } | |
| 348 | + | } | |
| 349 | + | for i := range x.tab.recs { | |
| 350 | + | if r := &x.tab.recs[i]; r.kind != kindFree && r.surface == surfaceID { | |
| 351 | + | r.surface = 0 | |
| 352 | + | if r.kind == kindUnmanaged && r.ovPaired { | |
| 353 | + | r.ovPaired = false | |
| 354 | + | x.host.OverrideGone(Slot(i + 1)) | |
| 355 | + | } | |
| 356 | + | } | |
| 357 | + | } | |
| 358 | + | x.wake() | |
| 359 | + | } | |
A internal/adapters/wayland/xwm/mapflow_test.go +806 -0
| @@ -0,0 +1,806 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "bnema.dev/xturbo" | |
| 9 | + | "bnema.dev/xturbo/xproto" | |
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |
| 11 | + | "github.com/stretchr/testify/mock" | |
| 12 | + | "github.com/stretchr/testify/require" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // harness bundles an attached XWM, its peer and the host mock. | |
| 16 | + | type harness struct { | |
| 17 | + | t *testing.T | |
| 18 | + | x *XWM | |
| 19 | + | p *xpeer | |
| 20 | + | h *mockhost | |
| 21 | + | ||
| 22 | + | ready map[uint32]WindowInfo // by xid, filled by Ready | |
| 23 | + | next ports.WindowID | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | func newHarness(t *testing.T, mutate func(p *xpeer)) *harness { | |
| 27 | + | t.Helper() | |
| 28 | + | x, p := bootPeer(t, mutate) | |
| 29 | + | hs := &harness{t: t, x: x, p: p, h: newMockhost(t), ready: map[uint32]WindowInfo{}} | |
| 30 | + | src := newMockfdSource(t) | |
| 31 | + | src.EXPECT().Remove().Maybe() | |
| 32 | + | src.EXPECT().SetWritable(mock.Anything).Return(nil).Maybe() | |
| 33 | + | post, _ := newPost() | |
| 34 | + | x.attach(src, hs.h, post) | |
| 35 | + | return hs | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func (hs *harness) expectCreated(win uint32) { | |
| 39 | + | hs.next++ | |
| 40 | + | hs.h.EXPECT().Created(mock.Anything, win).Return(hs.next).Once() | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (hs *harness) expectReady(win uint32) { | |
| 44 | + | hs.h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, info WindowInfo) { | |
| 45 | + | hs.ready[win] = info | |
| 46 | + | }).Once() | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | func (hs *harness) create(win uint32) Slot { | |
| 50 | + | hs.t.Helper() | |
| 51 | + | hs.expectCreated(win) | |
| 52 | + | hs.p.createNotify(win, false) | |
| 53 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.tab.lookup(win) != 0 }) | |
| 54 | + | return hs.x.tab.lookup(win) | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // mapAndWait creates and maps win and waits for Ready. | |
| 58 | + | func (hs *harness) mapAndWait(win uint32) Slot { | |
| 59 | + | hs.t.Helper() | |
| 60 | + | s := hs.create(win) | |
| 61 | + | hs.expectReady(win) | |
| 62 | + | hs.p.mapRequest(win) | |
| 63 | + | readUntil(hs.t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 64 | + | return s | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func (hs *harness) str(win uint32, atom string, typ uint32, s string) { | |
| 68 | + | hs.p.setProp(win, hs.p.atom(atom), typ, 8, []byte(s)) | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | func (hs *harness) words(win uint32, atom string, typ uint32, w ...uint32) { | |
| 72 | + | hs.p.setProp(win, hs.p.atom(atom), typ, 32, le32(w...)) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (hs *harness) atoms(win uint32, atom string, names ...string) { | |
| 76 | + | var w []uint32 | |
| 77 | + | for _, n := range names { | |
| 78 | + | w = append(w, hs.p.atom(n)) | |
| 79 | + | } | |
| 80 | + | hs.words(win, atom, uint32(xproto.AtomAtom), w...) | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | func (hs *harness) classic(win uint32) { | |
| 84 | + | hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("inst\x00Class\x00")) | |
| 85 | + | hs.str(win, "_NET_WM_NAME", hs.p.atom("UTF8_STRING"), "net title") | |
| 86 | + | hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("old title")) | |
| 87 | + | hs.words(win, "_NET_WM_PID", uint32(xproto.AtomCardinal), 4242) | |
| 88 | + | hs.p.set(func(p *xpeer) { p.geoms[win] = xpeerGeom{w: 300, h: 200} }) | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | func (hs *harness) count(match func(r xpeerReq) bool) int { | |
| 92 | + | n := 0 | |
| 93 | + | for _, r := range hs.p.requests() { | |
| 94 | + | if match(r) { | |
| 95 | + | n++ | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | return n | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | func (hs *harness) waitMap(win uint32) { | |
| 102 | + | hs.t.Helper() | |
| 103 | + | readUntil(hs.t, hs.x, func() bool { | |
| 104 | + | return hs.count(func(r xpeerReq) bool { return r.major == opMapWindow && r.u32(4) == win }) > 0 | |
| 105 | + | }) | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | func TestMapFlowBatchesRequestsAndReadyFiresOnce(t *testing.T) { | |
| 109 | + | hs := newHarness(t, nil) | |
| 110 | + | const win = 0x500001 | |
| 111 | + | hs.classic(win) | |
| 112 | + | hs.create(win) | |
| 113 | + | base := len(hs.p.requests()) | |
| 114 | + | ||
| 115 | + | hs.p.holdReplies() | |
| 116 | + | hs.p.mapRequest(win) | |
| 117 | + | hs.expectReady(win) | |
| 118 | + | hs.waitMap(win) | |
| 119 | + | ||
| 120 | + | reqs := hs.p.requests()[base:] | |
| 121 | + | want := []uint32{ | |
| 122 | + | uint32(xproto.AtomWmClass), uint32(xproto.AtomWmName), hs.p.atom("_NET_WM_NAME"), uint32(xproto.AtomWmHints), | |
| 123 | + | uint32(xproto.AtomWmNormalHints), hs.p.atom("WM_PROTOCOLS"), uint32(xproto.AtomWmTransientFor), | |
| 124 | + | hs.p.atom("_NET_WM_WINDOW_TYPE"), hs.p.atom("_NET_WM_STATE"), hs.p.atom("_NET_WM_PID"), hs.p.atom("_NET_WM_BYPASS_COMPOSITOR"), | |
| 125 | + | } | |
| 126 | + | require.GreaterOrEqual(t, len(reqs), len(want)+2) | |
| 127 | + | for i, a := range want { | |
| 128 | + | require.EqualValues(t, opGetProperty, reqs[i].major, i) | |
| 129 | + | require.Equal(t, uint32(win), reqs[i].u32(4)) | |
| 130 | + | require.Equal(t, a, reqs[i].u32(8), i) | |
| 131 | + | require.EqualValues(t, 1024, reqs[i].u32(20), "long length: one 4 KiB read") | |
| 132 | + | } | |
| 133 | + | require.EqualValues(t, opGetGeometry, reqs[len(want)].major) | |
| 134 | + | require.EqualValues(t, opMapWindow, reqs[len(want)+1].major, "MapWindow in the same batch") | |
| 135 | + | ||
| 136 | + | hs.p.release() // every reply arrives after MapWindow went out | |
| 137 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 138 | + | info := hs.ready[win] | |
| 139 | + | require.Equal(t, WindowInfo{PID: 4242, Title: "net title", AppID: "Class"}, info, "_NET_WM_NAME wins, class is the second string") | |
| 140 | + | ||
| 141 | + | state, ok := hs.p.prop(win, "WM_STATE") | |
| 142 | + | require.True(t, ok) | |
| 143 | + | require.Equal(t, []uint32{1, 0}, atomsOf(state)) | |
| 144 | + | list, ok := hs.p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 145 | + | require.True(t, ok) | |
| 146 | + | require.Equal(t, []uint32{win}, atomsOf(list)) | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | func TestMapFlowFloatingAndModal(t *testing.T) { | |
| 150 | + | hint := func(minw, minh, maxw, maxh uint32) []byte { | |
| 151 | + | return le32(hintMinSize|hintMaxSize, 0, 0, 0, 0, minw, minh, maxw, maxh, 0, 0, 0, 0, 0, 0, 0, 0, 0) | |
| 152 | + | } | |
| 153 | + | cases := []struct { | |
| 154 | + | name string | |
| 155 | + | setup func(hs *harness, win uint32) | |
| 156 | + | want func(info WindowInfo) | |
| 157 | + | }{ | |
| 158 | + | {"dialog type", func(hs *harness, w uint32) { | |
| 159 | + | hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_DIALOG") | |
| 160 | + | }, func(i WindowInfo) { | |
| 161 | + | require.True(t, i.Floating) | |
| 162 | + | require.Equal(t, 300, i.Width) | |
| 163 | + | require.Equal(t, 200, i.Height) | |
| 164 | + | }}, | |
| 165 | + | {"utility type", func(hs *harness, w uint32) { | |
| 166 | + | hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_UTILITY") | |
| 167 | + | }, func(i WindowInfo) { require.True(t, i.Floating) }}, | |
| 168 | + | {"splash type", func(hs *harness, w uint32) { | |
| 169 | + | hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_SPLASH") | |
| 170 | + | }, func(i WindowInfo) { require.True(t, i.Floating) }}, | |
| 171 | + | {"normal type", func(hs *harness, w uint32) { | |
| 172 | + | hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_NORMAL") | |
| 173 | + | }, func(i WindowInfo) { require.False(t, i.Floating); require.Zero(t, i.Width) }}, | |
| 174 | + | {"fixed size", func(hs *harness, w uint32) { | |
| 175 | + | hs.p.setProp(w, uint32(xproto.AtomWmNormalHints), uint32(xproto.AtomWmSizeHints), 32, hint(200, 100, 200, 100)) | |
| 176 | + | }, func(i WindowInfo) { require.True(t, i.Floating) }}, | |
| 177 | + | {"resizable", func(hs *harness, w uint32) { | |
| 178 | + | hs.p.setProp(w, uint32(xproto.AtomWmNormalHints), uint32(xproto.AtomWmSizeHints), 32, hint(100, 100, 800, 600)) | |
| 179 | + | }, func(i WindowInfo) { require.False(t, i.Floating) }}, | |
| 180 | + | {"modal", func(hs *harness, w uint32) { | |
| 181 | + | hs.atoms(w, "_NET_WM_STATE", "_NET_WM_STATE_MODAL") | |
| 182 | + | }, func(i WindowInfo) { require.True(t, i.Modal); require.False(t, i.Fullscreen) }}, | |
| 183 | + | {"bypass 1", func(hs *harness, w uint32) { | |
| 184 | + | hs.words(w, "_NET_WM_BYPASS_COMPOSITOR", uint32(xproto.AtomCardinal), 1) | |
| 185 | + | }, func(i WindowInfo) { require.EqualValues(t, 1, i.Bypass) }}, | |
| 186 | + | {"bypass 2 kept", func(hs *harness, w uint32) { | |
| 187 | + | hs.words(w, "_NET_WM_BYPASS_COMPOSITOR", uint32(xproto.AtomCardinal), 2) | |
| 188 | + | }, func(i WindowInfo) { require.EqualValues(t, 2, i.Bypass) }}, | |
| 189 | + | {"bypass absent", func(hs *harness, w uint32) {}, func(i WindowInfo) { require.Zero(t, i.Bypass) }}, | |
| 190 | + | {"class steam_app", func(hs *harness, w uint32) { | |
| 191 | + | hs.p.setProp(w, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("steam_app_892970\x00steam_app_892970\x00")) | |
| 192 | + | }, func(i WindowInfo) { require.Equal(t, "steam_app_892970", i.AppID) }}, | |
| 193 | + | {"fullscreen at map", func(hs *harness, w uint32) { | |
| 194 | + | hs.atoms(w, "_NET_WM_STATE", "_NET_WM_STATE_FULLSCREEN") | |
| 195 | + | }, func(i WindowInfo) { require.True(t, i.Fullscreen) }}, | |
| 196 | + | } | |
| 197 | + | for _, c := range cases { | |
| 198 | + | t.Run(c.name, func(t *testing.T) { | |
| 199 | + | hs := newHarness(t, nil) | |
| 200 | + | const win = 0x500001 | |
| 201 | + | hs.classic(win) | |
| 202 | + | c.setup(hs, win) | |
| 203 | + | hs.mapAndWait(win) | |
| 204 | + | c.want(hs.ready[win]) | |
| 205 | + | }) | |
| 206 | + | } | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | func TestMapFlowTransientForManagedWindowFloats(t *testing.T) { | |
| 210 | + | hs := newHarness(t, nil) | |
| 211 | + | const parent, child = 0x500001, 0x500002 | |
| 212 | + | hs.classic(parent) | |
| 213 | + | ps := hs.mapAndWait(parent) | |
| 214 | + | hs.classic(child) | |
| 215 | + | hs.p.setProp(child, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(parent)) | |
| 216 | + | hs.mapAndWait(child) | |
| 217 | + | require.True(t, hs.ready[child].Floating) | |
| 218 | + | require.Equal(t, ps, hs.ready[child].Parent) | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | func TestMapFlowTransientForUnknownWindowIsNotAParent(t *testing.T) { | |
| 222 | + | hs := newHarness(t, nil) | |
| 223 | + | const win = 0x500001 | |
| 224 | + | hs.classic(win) | |
| 225 | + | hs.p.setProp(win, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(0x777777)) | |
| 226 | + | hs.mapAndWait(win) | |
| 227 | + | require.Zero(t, hs.ready[win].Parent) | |
| 228 | + | require.False(t, hs.ready[win].Floating) | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | func TestMapFlowClassFallsBackToInstance(t *testing.T) { | |
| 232 | + | hs := newHarness(t, nil) | |
| 233 | + | const win = 0x500001 | |
| 234 | + | hs.classic(win) | |
| 235 | + | hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("onlyinstance\x00")) | |
| 236 | + | hs.mapAndWait(win) | |
| 237 | + | require.Equal(t, "onlyinstance", hs.ready[win].AppID) | |
| 238 | + | } | |
| 239 | + | ||
| 240 | + | func TestMapFlowTitleFallsBackToWMName(t *testing.T) { | |
| 241 | + | hs := newHarness(t, nil) | |
| 242 | + | const win = 0x500001 | |
| 243 | + | hs.classic(win) | |
| 244 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) }) | |
| 245 | + | hs.mapAndWait(win) | |
| 246 | + | require.Equal(t, "old title", hs.ready[win].Title) | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | func TestMapFlowPIDFromXRes(t *testing.T) { | |
| 250 | + | hs := newHarness(t, func(p *xpeer) { p.pids = map[uint32]uint32{0x500001: 777} }) | |
| 251 | + | const win = 0x500001 | |
| 252 | + | hs.classic(win) | |
| 253 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) }) | |
| 254 | + | hs.create(win) | |
| 255 | + | hs.expectReady(win) | |
| 256 | + | hs.p.mapRequest(win) | |
| 257 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 258 | + | require.Equal(t, 777, hs.ready[win].PID, "Ready waited for the QueryClientIds reply") | |
| 259 | + | require.Equal(t, 1, hs.count(func(r xpeerReq) bool { return r.major == 144 && r.minor == 4 })) | |
| 260 | + | require.Zero(t, hs.x.tab.rec(hs.x.tab.lookup(win)).pending) | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | func TestMapFlowPIDUnknownStillReady(t *testing.T) { | |
| 264 | + | hs := newHarness(t, nil) | |
| 265 | + | const win = 0x500001 | |
| 266 | + | hs.classic(win) | |
| 267 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) }) | |
| 268 | + | hs.mapAndWait(win) | |
| 269 | + | require.Zero(t, hs.ready[win].PID) | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | func TestMapFlowNoXResAsksNothing(t *testing.T) { | |
| 273 | + | exts := fullPeerExts() | |
| 274 | + | delete(exts, "X-Resource") | |
| 275 | + | fd, p := startXPeer(t, exts) | |
| 276 | + | x, err := Boot(context.Background(), fd, testLog()) | |
| 277 | + | require.NoError(t, err) | |
| 278 | + | t.Cleanup(x.CloseConn) | |
| 279 | + | h := newMockhost(t) | |
| 280 | + | src := newMockfdSource(t) | |
| 281 | + | src.EXPECT().Remove().Maybe() | |
| 282 | + | post, _ := newPost() | |
| 283 | + | x.attach(src, h, post) | |
| 284 | + | hs := &harness{t: t, x: x, p: p, h: h, ready: map[uint32]WindowInfo{}} | |
| 285 | + | const win = 0x500001 | |
| 286 | + | hs.classic(win) | |
| 287 | + | p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) }) | |
| 288 | + | hs.mapAndWait(win) | |
| 289 | + | require.Zero(t, hs.count(func(r xpeerReq) bool { return r.major == 144 })) | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | func TestMapFlowBadWindowOnPropertiesNeverReady(t *testing.T) { | |
| 293 | + | hs := newHarness(t, nil) | |
| 294 | + | const win = 0x500001 | |
| 295 | + | hs.classic(win) | |
| 296 | + | hs.p.set(func(p *xpeer) { p.bad[win] = true }) | |
| 297 | + | s := hs.create(win) | |
| 298 | + | hs.p.mapRequest(win) | |
| 299 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Errors >= uint64(propCount)+1 }) | |
| 300 | + | require.Zero(t, hs.x.tab.rec(s).pending, "failed replies are no longer awaited") | |
| 301 | + | require.False(t, hs.x.tab.rec(s).ready) | |
| 302 | + | require.Zero(t, hs.x.Stats().Unattributed) | |
| 303 | + | ||
| 304 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |
| 305 | + | hs.p.destroyNotify(win) | |
| 306 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 }) | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | func TestMapRequestTwiceBeforeReadyAsksOnce(t *testing.T) { | |
| 310 | + | hs := newHarness(t, nil) | |
| 311 | + | const win = 0x500001 | |
| 312 | + | hs.classic(win) | |
| 313 | + | hs.create(win) | |
| 314 | + | hs.p.holdReplies() | |
| 315 | + | hs.p.mapRequest(win) | |
| 316 | + | hs.p.mapRequest(win) | |
| 317 | + | hs.expectReady(win) | |
| 318 | + | hs.waitMap(win) | |
| 319 | + | hs.p.release() | |
| 320 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 321 | + | require.Equal(t, 1, hs.count(func(r xpeerReq) bool { return r.major == opMapWindow && r.u32(4) == win })) | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | func TestMapNotifyMarksMapped(t *testing.T) { | |
| 325 | + | hs := newHarness(t, nil) | |
| 326 | + | const win = 0x500001 | |
| 327 | + | hs.classic(win) | |
| 328 | + | s := hs.mapAndWait(win) | |
| 329 | + | hs.p.mapNotify(win) | |
| 330 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapped }) | |
| 331 | + | } | |
| 332 | + | ||
| 333 | + | // Property changes after Ready. | |
| 334 | + | ||
| 335 | + | func (hs *harness) notify(win uint32, atom string, deleted bool) { | |
| 336 | + | hs.t.Helper() | |
| 337 | + | before := hs.x.Stats().Replies | |
| 338 | + | hs.p.propertyNotify(win, hs.p.atom(atom), deleted) | |
| 339 | + | if deleted { | |
| 340 | + | hs.p.propertyNotify(win, hs.p.atom("WM_PROTOCOLS"), false) // sentinel: one reply follows | |
| 341 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |
| 342 | + | return | |
| 343 | + | } | |
| 344 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |
| 345 | + | } | |
| 346 | + | ||
| 347 | + | func TestPropertyChangedTitleReachesHostOnce(t *testing.T) { | |
| 348 | + | hs := newHarness(t, nil) | |
| 349 | + | const win = 0x500001 | |
| 350 | + | hs.classic(win) | |
| 351 | + | s := hs.mapAndWait(win) | |
| 352 | + | ||
| 353 | + | hs.h.EXPECT().Title(s, "renamed").Once() | |
| 354 | + | hs.str(win, "_NET_WM_NAME", hs.p.atom("UTF8_STRING"), "renamed") | |
| 355 | + | hs.notify(win, "_NET_WM_NAME", false) | |
| 356 | + | require.Equal(t, "renamed", hs.x.tab.rec(s).title) | |
| 357 | + | ||
| 358 | + | // Same value again: no host call (the mock fails on an unexpected one). | |
| 359 | + | hs.notify(win, "_NET_WM_NAME", false) | |
| 360 | + | ||
| 361 | + | // WM_NAME is ignored while _NET_WM_NAME is set. | |
| 362 | + | hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("legacy")) | |
| 363 | + | before := hs.x.Stats().Events | |
| 364 | + | hs.p.propertyNotify(win, uint32(xproto.AtomWmName), false) | |
| 365 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events > before }) | |
| 366 | + | require.Equal(t, "renamed", hs.x.tab.rec(s).title) | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | func TestPropertyNetNameDeletedFallsBackToWMName(t *testing.T) { | |
| 370 | + | hs := newHarness(t, nil) | |
| 371 | + | const win = 0x500001 | |
| 372 | + | hs.classic(win) | |
| 373 | + | s := hs.mapAndWait(win) | |
| 374 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) }) | |
| 375 | + | hs.h.EXPECT().Title(s, "old title").Once() | |
| 376 | + | hs.p.propertyNotify(win, hs.p.atom("_NET_WM_NAME"), true) | |
| 377 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).title == "old title" }) | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | func TestPropertyClassChangeReachesHost(t *testing.T) { | |
| 381 | + | hs := newHarness(t, nil) | |
| 382 | + | const win = 0x500001 | |
| 383 | + | hs.classic(win) | |
| 384 | + | s := hs.mapAndWait(win) | |
| 385 | + | hs.h.EXPECT().AppID(s, "Other").Once() | |
| 386 | + | hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("i\x00Other\x00")) | |
| 387 | + | hs.p.propertyNotify(win, uint32(xproto.AtomWmClass), false) | |
| 388 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).class == "Other" }) | |
| 389 | + | } | |
| 390 | + | ||
| 391 | + | func TestPropertyTransientChangeReachesHost(t *testing.T) { | |
| 392 | + | hs := newHarness(t, nil) | |
| 393 | + | const a, b = 0x500001, 0x500002 | |
| 394 | + | hs.classic(a) | |
| 395 | + | as := hs.mapAndWait(a) | |
| 396 | + | hs.classic(b) | |
| 397 | + | bs := hs.mapAndWait(b) | |
| 398 | + | hs.h.EXPECT().Parent(bs, as).Once() | |
| 399 | + | hs.p.setProp(b, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(a)) | |
| 400 | + | hs.p.propertyNotify(b, uint32(xproto.AtomWmTransientFor), false) | |
| 401 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(bs).transient == a }) | |
| 402 | + | } | |
| 403 | + | ||
| 404 | + | func TestPropertyInputProtocolsAndHints(t *testing.T) { | |
| 405 | + | hs := newHarness(t, nil) | |
| 406 | + | const win = 0x500001 | |
| 407 | + | hs.classic(win) | |
| 408 | + | hs.atoms(win, "WM_PROTOCOLS", "WM_DELETE_WINDOW", "WM_TAKE_FOCUS") | |
| 409 | + | hs.p.setProp(win, uint32(xproto.AtomWmHints), uint32(xproto.AtomWmHints), 32, le32(hintInput, 0, 0, 0, 0, 0, 0, 0, 0)) | |
| 410 | + | s := hs.mapAndWait(win) | |
| 411 | + | r := hs.x.tab.rec(s) | |
| 412 | + | require.True(t, r.deleteWindow) | |
| 413 | + | require.True(t, r.takeFocus) | |
| 414 | + | require.False(t, r.input, "WM_HINTS input = false") | |
| 415 | + | ||
| 416 | + | // No WM_HINTS: input defaults to true. | |
| 417 | + | const other = 0x500002 | |
| 418 | + | hs.classic(other) | |
| 419 | + | o := hs.mapAndWait(other) | |
| 420 | + | require.True(t, hs.x.tab.rec(o).input) | |
| 421 | + | require.False(t, hs.x.tab.rec(o).deleteWindow) | |
| 422 | + | } | |
| 423 | + | ||
| 424 | + | func TestPropertyUnwatchedAtomAsksNothing(t *testing.T) { | |
| 425 | + | hs := newHarness(t, nil) | |
| 426 | + | const win = 0x500001 | |
| 427 | + | hs.classic(win) | |
| 428 | + | hs.mapAndWait(win) | |
| 429 | + | n := len(hs.p.requests()) | |
| 430 | + | hs.p.propertyNotify(win, hs.p.atom("_MOTIF_WM_HINTS"), false) | |
| 431 | + | hs.p.propertyNotify(0x999999, hs.p.atom("_NET_WM_NAME"), false) // unknown window | |
| 432 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 }) | |
| 433 | + | require.Len(t, hs.p.requests(), n) | |
| 434 | + | } | |
| 435 | + | ||
| 436 | + | // Pairing. | |
| 437 | + | ||
| 438 | + | func (hs *harness) serialMsg(win uint32, serial uint64) { | |
| 439 | + | hs.p.clientMessage(win, hs.p.atom("WL_SURFACE_SERIAL"), uint32(serial), uint32(serial>>32)) | |
| 440 | + | hs.t.Helper() | |
| 441 | + | before := hs.x.Stats().Events | |
| 442 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Events > before }) | |
| 443 | + | } | |
| 444 | + | ||
| 445 | + | func TestPairingSerialBeforeSetSerial(t *testing.T) { | |
| 446 | + | hs := newHarness(t, nil) | |
| 447 | + | const win = 0x500001 | |
| 448 | + | hs.classic(win) | |
| 449 | + | s := hs.mapAndWait(win) | |
| 450 | + | hs.serialMsg(win, 0x1_0000_0002) | |
| 451 | + | require.EqualValues(t, 0x1_0000_0002, hs.x.tab.rec(s).serial, "high word in data32[1]") | |
| 452 | + | hs.h.EXPECT().Associate(s, uint64(99)).Once() | |
| 453 | + | hs.x.SetSerial(99, 0x1_0000_0002) | |
| 454 | + | require.EqualValues(t, 99, hs.x.tab.rec(s).surface) | |
| 455 | + | require.Zero(t, hs.x.unpairedN) | |
| 456 | + | } | |
| 457 | + | ||
| 458 | + | func TestPairingSetSerialBeforeSerial(t *testing.T) { | |
| 459 | + | hs := newHarness(t, nil) | |
| 460 | + | const win = 0x500001 | |
| 461 | + | hs.classic(win) | |
| 462 | + | s := hs.mapAndWait(win) | |
| 463 | + | hs.x.SetSerial(99, 7) | |
| 464 | + | require.Equal(t, 1, hs.x.unpairedN) | |
| 465 | + | hs.h.EXPECT().Associate(s, uint64(99)).Once() | |
| 466 | + | hs.serialMsg(win, 7) | |
| 467 | + | require.Zero(t, hs.x.unpairedN) | |
| 468 | + | } | |
| 469 | + | ||
| 470 | + | func TestPairingWaitsForReady(t *testing.T) { | |
| 471 | + | hs := newHarness(t, nil) | |
| 472 | + | const win = 0x500001 | |
| 473 | + | hs.classic(win) | |
| 474 | + | s := hs.create(win) | |
| 475 | + | hs.x.SetSerial(5, 3) | |
| 476 | + | hs.serialMsg(win, 3) // not mapped yet: no Associate (the mock fails on one) | |
| 477 | + | require.Zero(t, hs.x.tab.rec(s).surface) | |
| 478 | + | ||
| 479 | + | hs.h.EXPECT().Associate(s, uint64(5)).Once() | |
| 480 | + | hs.expectReady(win) | |
| 481 | + | hs.p.mapRequest(win) | |
| 482 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).surface != 0 }) | |
| 483 | + | } | |
| 484 | + | ||
| 485 | + | func TestPairingSerialForUnknownWindowIgnored(t *testing.T) { | |
| 486 | + | hs := newHarness(t, nil) | |
| 487 | + | hs.x.SetSerial(5, 3) | |
| 488 | + | hs.serialMsg(0x888888, 3) | |
| 489 | + | require.Equal(t, 1, hs.x.unpairedN, "stays unpaired") | |
| 490 | + | } | |
| 491 | + | ||
| 492 | + | func TestPairingSurfaceGoneBeforePair(t *testing.T) { | |
| 493 | + | hs := newHarness(t, nil) | |
| 494 | + | const win = 0x500001 | |
| 495 | + | hs.classic(win) | |
| 496 | + | s := hs.mapAndWait(win) | |
| 497 | + | hs.x.SetSerial(5, 3) | |
| 498 | + | hs.x.SurfaceGone(5) | |
| 499 | + | require.Zero(t, hs.x.unpairedN) | |
| 500 | + | hs.serialMsg(win, 3) // the surface is gone: nothing to pair with | |
| 501 | + | require.Zero(t, hs.x.tab.rec(s).surface) | |
| 502 | + | } | |
| 503 | + | ||
| 504 | + | func TestPairingSurfaceGoneAfterPairClearsLink(t *testing.T) { | |
| 505 | + | hs := newHarness(t, nil) | |
| 506 | + | const win = 0x500001 | |
| 507 | + | hs.classic(win) | |
| 508 | + | s := hs.mapAndWait(win) | |
| 509 | + | hs.serialMsg(win, 3) | |
| 510 | + | hs.h.EXPECT().Associate(s, uint64(5)).Once() | |
| 511 | + | hs.x.SetSerial(5, 3) | |
| 512 | + | hs.x.SurfaceGone(5) | |
| 513 | + | require.Zero(t, hs.x.tab.rec(s).surface) | |
| 514 | + | } | |
| 515 | + | ||
| 516 | + | func TestPairingDuplicateSerialIgnored(t *testing.T) { | |
| 517 | + | hs := newHarness(t, nil) | |
| 518 | + | const win = 0x500001 | |
| 519 | + | hs.classic(win) | |
| 520 | + | s := hs.mapAndWait(win) | |
| 521 | + | hs.serialMsg(win, 3) | |
| 522 | + | hs.serialMsg(win, 4) | |
| 523 | + | require.EqualValues(t, 3, hs.x.tab.rec(s).serial) | |
| 524 | + | hs.x.SetSerial(8, 4) // nobody announces 4: stays unpaired | |
| 525 | + | require.Equal(t, 1, hs.x.unpairedN) | |
| 526 | + | } | |
| 527 | + | ||
| 528 | + | func TestPairingUnmanagedKeepsSurfaceWithoutAssociate(t *testing.T) { | |
| 529 | + | hs := newHarness(t, nil) | |
| 530 | + | hs.p.createNotify(0x500009, true) | |
| 531 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 }) | |
| 532 | + | hs.serialMsg(0x500009, 3) | |
| 533 | + | hs.x.SetSerial(5, 3) // the mock fails on an Associate | |
| 534 | + | require.EqualValues(t, 5, hs.x.tab.rec(hs.x.tab.lookup(0x500009)).surface) | |
| 535 | + | } | |
| 536 | + | ||
| 537 | + | func TestPairingTableOverflowDropsOldest(t *testing.T) { | |
| 538 | + | hs := newHarness(t, nil) | |
| 539 | + | for i := uint64(1); i <= unpairedCap+1; i++ { | |
| 540 | + | hs.x.SetSerial(100+i, i) | |
| 541 | + | } | |
| 542 | + | require.Equal(t, unpairedCap, hs.x.unpairedN) | |
| 543 | + | for i := 0; i < hs.x.unpairedN; i++ { | |
| 544 | + | require.NotEqual(t, uint64(1), hs.x.unpaired[i].serial, "oldest dropped") | |
| 545 | + | } | |
| 546 | + | } | |
| 547 | + | ||
| 548 | + | func TestPairingZeroSerialIgnored(t *testing.T) { | |
| 549 | + | hs := newHarness(t, nil) | |
| 550 | + | hs.x.SetSerial(5, 0) | |
| 551 | + | require.Zero(t, hs.x.unpairedN) | |
| 552 | + | } | |
| 553 | + | ||
| 554 | + | func TestLegacySurfaceIDCountedAndIgnored(t *testing.T) { | |
| 555 | + | hs := newHarness(t, nil) | |
| 556 | + | const win = 0x500001 | |
| 557 | + | hs.classic(win) | |
| 558 | + | s := hs.mapAndWait(win) | |
| 559 | + | hs.p.clientMessage(win, hs.p.atom("WL_SURFACE_ID"), 12) | |
| 560 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().LegacyID == 1 }) | |
| 561 | + | require.Zero(t, hs.x.tab.rec(s).serial) | |
| 562 | + | } | |
| 563 | + | ||
| 564 | + | func TestNetWMStateClientMessageForwardsFullscreen(t *testing.T) { | |
| 565 | + | hs := newHarness(t, nil) | |
| 566 | + | const win = 0x500001 | |
| 567 | + | hs.classic(win) | |
| 568 | + | s := hs.mapAndWait(win) | |
| 569 | + | hs.h.EXPECT().Fullscreen(s, true).Once() | |
| 570 | + | hs.p.clientMessage(win, hs.p.atom("_NET_WM_STATE"), 1, hs.p.atom("_NET_WM_STATE_FULLSCREEN")) | |
| 571 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).fullscreen }) | |
| 572 | + | hs.h.EXPECT().Fullscreen(s, false).Once() // toggle | |
| 573 | + | hs.p.clientMessage(win, hs.p.atom("_NET_WM_STATE"), 2, hs.p.atom("_NET_WM_STATE_FULLSCREEN")) | |
| 574 | + | readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).fullscreen }) | |
| 575 | + | // Not written back by the message itself. | |
| 576 | + | st, ok := hs.p.prop(win, "_NET_WM_STATE") | |
| 577 | + | require.True(t, ok) | |
| 578 | + | require.Empty(t, st.data) | |
| 579 | + | } | |
| 580 | + | ||
| 581 | + | // Unmap, destroy, remap. | |
| 582 | + | ||
| 583 | + | func TestUnmapDissociatesAndRemapRereadsProperties(t *testing.T) { | |
| 584 | + | hs := newHarness(t, nil) | |
| 585 | + | const win = 0x500001 | |
| 586 | + | hs.classic(win) | |
| 587 | + | s := hs.mapAndWait(win) | |
| 588 | + | ||
| 589 | + | // A synthetic UnmapNotify (ICCCM withdraw request) is not an unmap. | |
| 590 | + | hs.p.unmapNotify(win, true) | |
| 591 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 }) | |
| 592 | + | require.True(t, hs.x.tab.rec(s).ready) | |
| 593 | + | ||
| 594 | + | hs.h.EXPECT().Dissociate(s, false).Once() | |
| 595 | + | hs.p.unmapNotify(win, false) | |
| 596 | + | readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).ready }) | |
| 597 | + | r := hs.x.tab.rec(s) | |
| 598 | + | require.False(t, r.mapping) | |
| 599 | + | require.Zero(t, r.serial) | |
| 600 | + | hs.p.waitReq(0, func(q xpeerReq) bool { | |
| 601 | + | return q.major == opSetInputFocus || q.major == opChangeProperty && q.u32(4) == win | |
| 602 | + | }) | |
| 603 | + | readUntil(t, hs.x, func() bool { | |
| 604 | + | v, ok := hs.p.prop(win, "WM_STATE") | |
| 605 | + | return ok && len(v.data) == 8 && binary.LittleEndian.Uint32(v.data) == 0 | |
| 606 | + | }) | |
| 607 | + | _, ok := hs.p.prop(win, "_NET_WM_STATE") | |
| 608 | + | require.False(t, ok, "_NET_WM_STATE deleted") | |
| 609 | + | list, _ := hs.p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 610 | + | require.Empty(t, list.data) | |
| 611 | + | ||
| 612 | + | // Remap starts over. | |
| 613 | + | delete(hs.ready, win) | |
| 614 | + | hs.expectReady(win) | |
| 615 | + | n := hs.count(func(q xpeerReq) bool { return q.major == opGetProperty && q.u32(4) == win }) | |
| 616 | + | hs.p.mapRequest(win) | |
| 617 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 618 | + | require.Equal(t, n+propCount, hs.count(func(q xpeerReq) bool { return q.major == opGetProperty && q.u32(4) == win })) | |
| 619 | + | list, _ = hs.p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 620 | + | require.Equal(t, []uint32{win}, atomsOf(list)) | |
| 621 | + | } | |
| 622 | + | ||
| 623 | + | func TestDestroyWhileMappingNeverReady(t *testing.T) { | |
| 624 | + | hs := newHarness(t, nil) | |
| 625 | + | const win = 0x500001 | |
| 626 | + | hs.classic(win) | |
| 627 | + | s := hs.create(win) | |
| 628 | + | hs.p.holdReplies() | |
| 629 | + | hs.p.mapRequest(win) | |
| 630 | + | hs.waitMap(win) | |
| 631 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |
| 632 | + | hs.p.destroyNotify(win) | |
| 633 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 }) | |
| 634 | + | replies := hs.x.Stats().Replies | |
| 635 | + | hs.p.release() // replies for a window that is gone: xturbo retired their slots | |
| 636 | + | readUntil(t, hs.x, func() bool { return hs.x.conn.Stats().DroppedReplies >= uint64(propCount) }) | |
| 637 | + | require.Equal(t, replies, hs.x.Stats().Replies, "none reaches the XWM") | |
| 638 | + | } | |
| 639 | + | ||
| 640 | + | func TestStaleRepliesAfterSlotReuseAreIgnored(t *testing.T) { | |
| 641 | + | hs := newHarness(t, nil) | |
| 642 | + | const old, fresh = 0x500001, 0x500002 | |
| 643 | + | hs.classic(old) | |
| 644 | + | hs.classic(fresh) | |
| 645 | + | s := hs.create(old) | |
| 646 | + | hs.p.holdReplies() | |
| 647 | + | hs.p.mapRequest(old) | |
| 648 | + | hs.waitMap(old) | |
| 649 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |
| 650 | + | hs.p.destroyNotify(old) | |
| 651 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(old) == 0 }) | |
| 652 | + | ||
| 653 | + | hs.expectCreated(fresh) | |
| 654 | + | hs.p.createNotify(fresh, false) | |
| 655 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(fresh) != 0 }) | |
| 656 | + | require.Equal(t, s, hs.x.tab.lookup(fresh), "the freed slot is reused") | |
| 657 | + | ||
| 658 | + | hs.p.release() | |
| 659 | + | readUntil(t, hs.x, func() bool { return hs.x.conn.Stats().DroppedReplies >= uint64(propCount) }) | |
| 660 | + | ||
| 661 | + | // A reply that does get through with the old tag (slot reused, generation | |
| 662 | + | // bumped) is ignored. | |
| 663 | + | oldTag := s | Slot(hs.x.tab.rec(s).gen-1)<<16 | |
| 664 | + | rep := make([]byte, 32) | |
| 665 | + | rep[0] = 1 | |
| 666 | + | hs.x.onReply(&xturbo.Reply{Kind: kindProp, Args: [4]uint32{uint32(oldTag), propClass}, Raw: rep}) | |
| 667 | + | hs.x.onReply(&xturbo.Reply{Kind: kindGeom, Args: [4]uint32{uint32(oldTag)}, Raw: rep}) | |
| 668 | + | require.EqualValues(t, 2, hs.x.Stats().Stale) | |
| 669 | + | require.False(t, hs.x.tab.rec(s).ready, "stale replies must not complete the new window") | |
| 670 | + | require.Zero(t, hs.x.tab.rec(s).pending) | |
| 671 | + | } | |
| 672 | + | ||
| 673 | + | // Reparenting. | |
| 674 | + | ||
| 675 | + | func TestReparentAwayFromRootRemovesManagedWindow(t *testing.T) { | |
| 676 | + | hs := newHarness(t, nil) | |
| 677 | + | const win = 0x500001 | |
| 678 | + | hs.classic(win) | |
| 679 | + | s := hs.mapAndWait(win) | |
| 680 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |
| 681 | + | hs.p.reparentNotify(win, 0x600000) | |
| 682 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 }) | |
| 683 | + | list, _ := hs.p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 684 | + | require.Empty(t, list.data) | |
| 685 | + | require.Zero(t, hs.x.tab.n) | |
| 686 | + | } | |
| 687 | + | ||
| 688 | + | func TestReparentAwayOfUnmanagedWindowRemovesIt(t *testing.T) { | |
| 689 | + | hs := newHarness(t, nil) | |
| 690 | + | hs.p.createNotify(0x500009, true) | |
| 691 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 }) | |
| 692 | + | hs.p.reparentNotify(0x500009, 0x600000) | |
| 693 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) == 0 }) | |
| 694 | + | } | |
| 695 | + | ||
| 696 | + | func TestReparentIntoRootAdoptsWindow(t *testing.T) { | |
| 697 | + | hs := newHarness(t, nil) | |
| 698 | + | const win = 0x500001 | |
| 699 | + | hs.expectCreated(win) | |
| 700 | + | hs.p.reparentNotify(win, peerRoot) | |
| 701 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) != 0 }) | |
| 702 | + | require.Equal(t, kindManaged, hs.x.tab.rec(hs.x.tab.lookup(win)).kind) | |
| 703 | + | ||
| 704 | + | // Already known: no second Created. | |
| 705 | + | hs.p.reparentNotify(win, peerRoot) | |
| 706 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 }) | |
| 707 | + | } | |
| 708 | + | ||
| 709 | + | func TestReparentIntoRootOfOverrideRedirectWindowStaysUnmanaged(t *testing.T) { | |
| 710 | + | hs := newHarness(t, nil) | |
| 711 | + | hs.p.event(21, func(b *[32]byte) { | |
| 712 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |
| 713 | + | binary.LittleEndian.PutUint32(b[8:], 0x500009) | |
| 714 | + | binary.LittleEndian.PutUint32(b[12:], peerRoot) | |
| 715 | + | b[20] = 1 | |
| 716 | + | }) | |
| 717 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 }) | |
| 718 | + | require.Equal(t, kindUnmanaged, hs.x.tab.rec(hs.x.tab.lookup(0x500009)).kind) | |
| 719 | + | } | |
| 720 | + | ||
| 721 | + | func TestReparentOfWMWindowIgnored(t *testing.T) { | |
| 722 | + | hs := newHarness(t, nil) | |
| 723 | + | hs.p.reparentNotify(hs.x.wmWin, peerRoot) | |
| 724 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 1 }) | |
| 725 | + | require.Zero(t, hs.x.tab.lookup(hs.x.wmWin)) | |
| 726 | + | } | |
| 727 | + | ||
| 728 | + | // Window known only before Attach: Attach replays the map flow. | |
| 729 | + | func TestAttachMapsWindowsRequestedBeforeAttach(t *testing.T) { | |
| 730 | + | x, p := bootPeer(t, nil) | |
| 731 | + | const win = 0x500001 | |
| 732 | + | p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("a\x00B\x00")) | |
| 733 | + | p.createNotify(win, false) | |
| 734 | + | p.mapRequest(win) | |
| 735 | + | readUntil(t, x, func() bool { return x.Stats().Events >= 2 }) | |
| 736 | + | h := newMockhost(t) | |
| 737 | + | src := newMockfdSource(t) | |
| 738 | + | src.EXPECT().Remove().Maybe() | |
| 739 | + | post, _ := newPost() | |
| 740 | + | var got WindowInfo | |
| 741 | + | done := false | |
| 742 | + | h.EXPECT().Created(mock.Anything, uint32(win)).Return(ports.WindowID(1)).Once() | |
| 743 | + | h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, i WindowInfo) { got, done = i, true }).Once() | |
| 744 | + | x.attach(src, h, post) | |
| 745 | + | readUntil(t, x, func() bool { return done }) | |
| 746 | + | require.Equal(t, "B", got.AppID) | |
| 747 | + | } | |
| 748 | + | ||
| 749 | + | // Replies complete a map in any order (a server may answer the geometry | |
| 750 | + | // before the properties it was asked for first): Ready fires on the last one. | |
| 751 | + | func TestMapFlowReadyFiresOnLastReplyInAnyOrder(t *testing.T) { | |
| 752 | + | hs := newHarness(t, nil) | |
| 753 | + | const win = 0x500001 | |
| 754 | + | hs.classic(win) | |
| 755 | + | s := hs.create(win) | |
| 756 | + | r := hs.x.tab.rec(s) | |
| 757 | + | r.mapping, r.pending = true, propMaskAll&^propClientIDs | |
| 758 | + | hs.expectReady(win) | |
| 759 | + | order := []int{propCount, propPID, propBypass, propClass, propNetName, propName, propHints, propNormalHints, propProtocols, propTransientFor, propWindowType, propState} | |
| 760 | + | for i, bit := range order { | |
| 761 | + | require.False(t, hs.x.tab.rec(s).ready, i) | |
| 762 | + | if bit == propCount { // the geometry | |
| 763 | + | hs.x.tab.rec(s).w, hs.x.tab.rec(s).h = 300, 200 | |
| 764 | + | hs.x.tab.rec(s).pending &^= propGeom | |
| 765 | + | hs.x.checkReady(s) | |
| 766 | + | continue | |
| 767 | + | } | |
| 768 | + | var val []byte | |
| 769 | + | var format uint8 = 8 | |
| 770 | + | switch bit { | |
| 771 | + | case propClass: | |
| 772 | + | val = []byte("a\x00B\x00") | |
| 773 | + | case propNetName: | |
| 774 | + | val = []byte("t") | |
| 775 | + | case propPID: | |
| 776 | + | val, format = le32(5), 32 | |
| 777 | + | default: | |
| 778 | + | format = 32 | |
| 779 | + | } | |
| 780 | + | x := hs.x | |
| 781 | + | x.applyProp(s, bit, format, val) | |
| 782 | + | x.tab.rec(s).pending &^= 1 << bit | |
| 783 | + | x.checkReady(s) | |
| 784 | + | } | |
| 785 | + | require.True(t, hs.x.tab.rec(s).ready) | |
| 786 | + | require.Equal(t, "B", hs.ready[win].AppID) | |
| 787 | + | require.Equal(t, "t", hs.ready[win].Title) | |
| 788 | + | require.Equal(t, 5, hs.ready[win].PID) | |
| 789 | + | } | |
| 790 | + | ||
| 791 | + | func TestMapFlowWholeOutputRectSetsHint(t *testing.T) { | |
| 792 | + | hs := newHarness(t, nil) | |
| 793 | + | hs.x.space.Set(ports.Layout{out(0, 0, 1920, 1080), out(1920, 0, 1280, 1024)}) | |
| 794 | + | const win = 0x500001 | |
| 795 | + | hs.classic(win) | |
| 796 | + | s := hs.create(win) | |
| 797 | + | r := hs.x.tab.rec(s) | |
| 798 | + | r.x, r.y, r.w, r.h, r.sized = 1920, 0, 1280, 1024, true | |
| 799 | + | info := hs.x.info(s) | |
| 800 | + | require.True(t, info.HasAt) | |
| 801 | + | require.Equal(t, 1920, info.AtX) | |
| 802 | + | r.h = 1000 | |
| 803 | + | require.False(t, hs.x.info(s).HasAt) | |
| 804 | + | r.h, r.sized = 1024, false | |
| 805 | + | require.False(t, hs.x.info(s).HasAt) | |
| 806 | + | } | |
A internal/adapters/wayland/xwm/props.go +376 -0
| @@ -0,0 +1,376 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "strings" | |
| 7 | + | ||
| 8 | + | "bnema.dev/xturbo" | |
| 9 | + | "bnema.dev/xturbo/res" | |
| 10 | + | "bnema.dev/xturbo/xproto" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // Property indices (second Expect argument of kindProp) and their bits in | |
| 14 | + | // record.pending. propGeom is the GetGeometry bit; propClientIDs is raised | |
| 15 | + | // while a QueryClientIds reply stands in for a missing _NET_WM_PID. | |
| 16 | + | const ( | |
| 17 | + | propClass = iota | |
| 18 | + | propName | |
| 19 | + | propNetName | |
| 20 | + | propHints | |
| 21 | + | propNormalHints | |
| 22 | + | propProtocols | |
| 23 | + | propTransientFor | |
| 24 | + | propWindowType | |
| 25 | + | propState | |
| 26 | + | propPID | |
| 27 | + | propBypass | |
| 28 | + | propCount | |
| 29 | + | ) | |
| 30 | + | ||
| 31 | + | const ( | |
| 32 | + | propGeom uint16 = 1 << propCount | |
| 33 | + | propClientIDs uint16 = propGeom << 1 | |
| 34 | + | // propMaskAll is what a map waits for: every property and the geometry. | |
| 35 | + | propMaskAll uint16 = propGeom<<1 - 1 | |
| 36 | + | ) | |
| 37 | + | ||
| 38 | + | // WM_NORMAL_HINTS flags and WM_HINTS flags. | |
| 39 | + | const ( | |
| 40 | + | hintInput = 1 << 0 | |
| 41 | + | hintMinSize = 1 << 4 | |
| 42 | + | hintMaxSize = 1 << 5 | |
| 43 | + | ) | |
| 44 | + | ||
| 45 | + | // maxPropWords bounds one GetProperty (4 KiB), so titles are truncated there. | |
| 46 | + | const maxPropWords = 1024 | |
| 47 | + | ||
| 48 | + | // propIndex maps a property atom to its index, or -1 when it is not watched. | |
| 49 | + | func (x *XWM) propIndex(a uint32) int { | |
| 50 | + | switch a { | |
| 51 | + | case uint32(xproto.AtomWmClass): | |
| 52 | + | return propClass | |
| 53 | + | case uint32(xproto.AtomWmName): | |
| 54 | + | return propName | |
| 55 | + | case uint32(xproto.AtomWmHints): | |
| 56 | + | return propHints | |
| 57 | + | case uint32(xproto.AtomWmNormalHints): | |
| 58 | + | return propNormalHints | |
| 59 | + | case uint32(xproto.AtomWmTransientFor): | |
| 60 | + | return propTransientFor | |
| 61 | + | case x.atoms[atomNetWMName]: | |
| 62 | + | return propNetName | |
| 63 | + | case x.atoms[atomWMProtocols]: | |
| 64 | + | return propProtocols | |
| 65 | + | case x.atoms[atomNetWMWindowType]: | |
| 66 | + | return propWindowType | |
| 67 | + | case x.atoms[atomNetWMState]: | |
| 68 | + | return propState | |
| 69 | + | case x.atoms[atomNetWMPID]: | |
| 70 | + | return propPID | |
| 71 | + | case x.atoms[atomNetWMBypassCompositor]: | |
| 72 | + | return propBypass | |
| 73 | + | } | |
| 74 | + | return -1 | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | func (x *XWM) propAtom(idx int) xproto.Atom { | |
| 78 | + | switch idx { | |
| 79 | + | case propClass: | |
| 80 | + | return xproto.AtomWmClass | |
| 81 | + | case propName: | |
| 82 | + | return xproto.AtomWmName | |
| 83 | + | case propHints: | |
| 84 | + | return xproto.AtomWmHints | |
| 85 | + | case propNormalHints: | |
| 86 | + | return xproto.AtomWmNormalHints | |
| 87 | + | case propTransientFor: | |
| 88 | + | return xproto.AtomWmTransientFor | |
| 89 | + | case propNetName: | |
| 90 | + | return xproto.Atom(x.atoms[atomNetWMName]) | |
| 91 | + | case propProtocols: | |
| 92 | + | return xproto.Atom(x.atoms[atomWMProtocols]) | |
| 93 | + | case propWindowType: | |
| 94 | + | return xproto.Atom(x.atoms[atomNetWMWindowType]) | |
| 95 | + | case propState: | |
| 96 | + | return xproto.Atom(x.atoms[atomNetWMState]) | |
| 97 | + | case propBypass: | |
| 98 | + | return xproto.Atom(x.atoms[atomNetWMBypassCompositor]) | |
| 99 | + | default: | |
| 100 | + | return xproto.Atom(x.atoms[atomNetWMPID]) | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // requestProp queues a GetProperty for property idx of s. | |
| 105 | + | func (x *XWM) requestProp(s Slot, idx int) { | |
| 106 | + | rec := x.tab.rec(s) | |
| 107 | + | seq := xproto.GetProperty(x.conn.Buf(), false, xproto.Window(rec.xid), x.propAtom(idx), xproto.AtomAny, 0, maxPropWords) | |
| 108 | + | rec.pending |= 1 << idx | |
| 109 | + | x.expect(seq, kindProp, x.tab.tag(s), uint32(idx)) | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | // onPropertyNotify handles a changed property of a managed window that is | |
| 113 | + | // being mapped or is mapped; unwatched atoms cost nothing. | |
| 114 | + | func (x *XWM) onPropertyNotify(ev *xturbo.Event) { | |
| 115 | + | e := xproto.PropertyNotifyOf(ev) | |
| 116 | + | if uint32(e.Window()) == x.selWin && x.selWin != 0 { | |
| 117 | + | x.onSelProperty(ev) | |
| 118 | + | return | |
| 119 | + | } | |
| 120 | + | if x.onRequestorProperty(e) { | |
| 121 | + | return | |
| 122 | + | } | |
| 123 | + | s := x.tab.lookup(uint32(e.Window())) | |
| 124 | + | if s == 0 { | |
| 125 | + | return | |
| 126 | + | } | |
| 127 | + | if rec := x.tab.rec(s); rec.kind != kindManaged || !rec.mapping { | |
| 128 | + | return | |
| 129 | + | } else if rec.ready && x.propIndex(uint32(e.Atom())) == propState { | |
| 130 | + | // The XWM owns _NET_WM_STATE of a mapped window: our own writes | |
| 131 | + | // would feed back, and clients change it with ClientMessages. | |
| 132 | + | return | |
| 133 | + | } | |
| 134 | + | idx := x.propIndex(uint32(e.Atom())) | |
| 135 | + | if idx < 0 { | |
| 136 | + | return | |
| 137 | + | } | |
| 138 | + | if e.State() == xproto.PropertyDelete { | |
| 139 | + | x.applyProp(s, idx, 0, nil) | |
| 140 | + | return | |
| 141 | + | } | |
| 142 | + | x.requestProp(s, idx) | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | // onPropReply decodes one GetProperty reply into the record. An absent | |
| 146 | + | // property (format 0) clears its fields. | |
| 147 | + | func (x *XWM) onPropReply(s Slot, _ *record, r *xturbo.Reply) { | |
| 148 | + | idx := int(r.Args[1]) | |
| 149 | + | if idx < 0 || idx >= propCount { | |
| 150 | + | return | |
| 151 | + | } | |
| 152 | + | var format uint8 | |
| 153 | + | var val []byte | |
| 154 | + | if rep, ok := xproto.ParseGetPropertyReply(r.Raw); ok { | |
| 155 | + | format, val = rep.Format(), rep.Value() | |
| 156 | + | } | |
| 157 | + | x.applyProp(s, idx, format, val) | |
| 158 | + | x.tab.rec(s).pending &^= 1 << idx | |
| 159 | + | x.checkReady(s) | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | func word(b []byte, i int) uint32 { return binary.LittleEndian.Uint32(b[4*i:]) } | |
| 163 | + | ||
| 164 | + | // applyProp stores one decoded property. It does not allocate unless a string | |
| 165 | + | // changed. Host calls come last: the record is not touched after them. | |
| 166 | + | func (x *XWM) applyProp(s Slot, idx int, format uint8, val []byte) { | |
| 167 | + | rec := x.tab.rec(s) | |
| 168 | + | if idx == propClass || idx == propName || idx == propNetName { | |
| 169 | + | if format != 8 { | |
| 170 | + | val = nil | |
| 171 | + | } | |
| 172 | + | } else if format != 32 { | |
| 173 | + | val = nil | |
| 174 | + | } | |
| 175 | + | switch idx { | |
| 176 | + | case propClass: | |
| 177 | + | inst, class := val, []byte(nil) | |
| 178 | + | if i := bytes.IndexByte(val, 0); i >= 0 { | |
| 179 | + | inst, class = val[:i], val[i+1:] | |
| 180 | + | if j := bytes.IndexByte(class, 0); j >= 0 { | |
| 181 | + | class = class[:j] | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | app := class | |
| 185 | + | if len(app) == 0 { | |
| 186 | + | app = inst | |
| 187 | + | } | |
| 188 | + | if rec.class == string(app) { | |
| 189 | + | return | |
| 190 | + | } | |
| 191 | + | cls := strings.ToValidUTF8(string(app), "\uFFFD") | |
| 192 | + | if rec.class == cls { | |
| 193 | + | return | |
| 194 | + | } | |
| 195 | + | rec.class = cls | |
| 196 | + | if rec.ready { | |
| 197 | + | x.host.AppID(s, rec.class) | |
| 198 | + | } | |
| 199 | + | case propName: | |
| 200 | + | if !rec.hasNetName { | |
| 201 | + | x.setTitle(s, val) | |
| 202 | + | } | |
| 203 | + | case propNetName: | |
| 204 | + | had := rec.hasNetName | |
| 205 | + | rec.hasNetName = len(val) > 0 | |
| 206 | + | if rec.hasNetName { | |
| 207 | + | x.setTitle(s, val) | |
| 208 | + | } else if had && rec.mapping { | |
| 209 | + | x.requestProp(s, propName) | |
| 210 | + | } | |
| 211 | + | case propHints: | |
| 212 | + | rec.input = true | |
| 213 | + | if len(val) >= 8 && word(val, 0)&hintInput != 0 { | |
| 214 | + | rec.input = word(val, 1) != 0 | |
| 215 | + | } | |
| 216 | + | case propNormalHints: | |
| 217 | + | rec.minW, rec.minH, rec.maxW, rec.maxH = 0, 0, 0, 0 | |
| 218 | + | if len(val) >= 36 { | |
| 219 | + | fl := word(val, 0) | |
| 220 | + | if fl&hintMinSize != 0 { | |
| 221 | + | rec.minW, rec.minH = int32(word(val, 5)), int32(word(val, 6)) | |
| 222 | + | } | |
| 223 | + | if fl&hintMaxSize != 0 { | |
| 224 | + | rec.maxW, rec.maxH = int32(word(val, 7)), int32(word(val, 8)) | |
| 225 | + | } | |
| 226 | + | } | |
| 227 | + | x.classify(rec) | |
| 228 | + | case propProtocols: | |
| 229 | + | rec.deleteWindow, rec.takeFocus = false, false | |
| 230 | + | for i := 0; i+4 <= len(val); i += 4 { | |
| 231 | + | switch binary.LittleEndian.Uint32(val[i:]) { | |
| 232 | + | case x.atoms[atomWMDeleteWindow]: | |
| 233 | + | rec.deleteWindow = true | |
| 234 | + | case x.atoms[atomWMTakeFocus]: | |
| 235 | + | rec.takeFocus = true | |
| 236 | + | } | |
| 237 | + | } | |
| 238 | + | case propTransientFor: | |
| 239 | + | var t uint32 | |
| 240 | + | if len(val) >= 4 { | |
| 241 | + | t = word(val, 0) | |
| 242 | + | } | |
| 243 | + | if t == rec.transient { | |
| 244 | + | return | |
| 245 | + | } | |
| 246 | + | rec.transient = t | |
| 247 | + | x.classify(rec) | |
| 248 | + | if rec.ready { | |
| 249 | + | x.host.Parent(s, x.managedSlot(t)) | |
| 250 | + | } | |
| 251 | + | case propWindowType: | |
| 252 | + | rec.wtype = 0 | |
| 253 | + | for i := 0; i+4 <= len(val); i += 4 { | |
| 254 | + | if t := x.windowType(binary.LittleEndian.Uint32(val[i:])); t != 0 { | |
| 255 | + | rec.wtype = t | |
| 256 | + | break | |
| 257 | + | } | |
| 258 | + | } | |
| 259 | + | x.classify(rec) | |
| 260 | + | case propState: | |
| 261 | + | var modal, fs bool | |
| 262 | + | for i := 0; i+4 <= len(val); i += 4 { | |
| 263 | + | switch binary.LittleEndian.Uint32(val[i:]) { | |
| 264 | + | case x.atoms[atomNetWMStateModal]: | |
| 265 | + | modal = true | |
| 266 | + | case x.atoms[atomNetWMStateFullscreen]: | |
| 267 | + | fs = true | |
| 268 | + | } | |
| 269 | + | } | |
| 270 | + | changed := fs != rec.fullscreen | |
| 271 | + | rec.modal, rec.fullscreen = modal, fs | |
| 272 | + | if changed && rec.ready { | |
| 273 | + | x.host.Fullscreen(s, fs) | |
| 274 | + | } | |
| 275 | + | case propBypass: | |
| 276 | + | // Kept for a later VRR/tearing policy; direct scanout and tearing | |
| 277 | + | // do not depend on it. 2 ("do not bypass") is ignored on purpose. | |
| 278 | + | rec.bypass = 0 | |
| 279 | + | if len(val) >= 4 { | |
| 280 | + | rec.bypass = uint8(min(word(val, 0), 2)) | |
| 281 | + | } | |
| 282 | + | x.log.Debug().Uint32("xid", rec.xid).Uint8("bypass", rec.bypass).Msg("_NET_WM_BYPASS_COMPOSITOR") | |
| 283 | + | case propPID: | |
| 284 | + | if len(val) >= 4 { | |
| 285 | + | rec.pid = int32(word(val, 0)) | |
| 286 | + | return | |
| 287 | + | } | |
| 288 | + | rec.pid = 0 | |
| 289 | + | if !rec.ready && rec.pending&propClientIDs == 0 && x.conn.Ext().Info(xturbo.ExtRes).Present { | |
| 290 | + | x.queryClientIDs(s) | |
| 291 | + | } | |
| 292 | + | } | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | // setTitle stores a title when it differs; only a change allocates. | |
| 296 | + | func (x *XWM) setTitle(s Slot, val []byte) { | |
| 297 | + | rec := x.tab.rec(s) | |
| 298 | + | if rec.title == string(val) { | |
| 299 | + | return | |
| 300 | + | } | |
| 301 | + | t := strings.ToValidUTF8(string(val), "\uFFFD") | |
| 302 | + | if rec.title == t { | |
| 303 | + | return | |
| 304 | + | } | |
| 305 | + | rec.title = t | |
| 306 | + | if rec.ready { | |
| 307 | + | x.host.Title(s, rec.title) | |
| 308 | + | } | |
| 309 | + | } | |
| 310 | + | ||
| 311 | + | // windowType returns the atom enum of a recognised _NET_WM_WINDOW_TYPE atom, | |
| 312 | + | // or 0. | |
| 313 | + | func (x *XWM) windowType(a uint32) uint8 { | |
| 314 | + | for t := atomNetWMWindowTypeNormal; t <= atomNetWMWindowTypeDND; t++ { | |
| 315 | + | if x.atoms[t] == a { | |
| 316 | + | return uint8(t) | |
| 317 | + | } | |
| 318 | + | } | |
| 319 | + | return 0 | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | // floatingType reports whether a window type floats by itself. | |
| 323 | + | func floatingType(t uint8) bool { | |
| 324 | + | switch int(t) { | |
| 325 | + | case atomNetWMWindowTypeDialog, atomNetWMWindowTypeUtility, atomNetWMWindowTypeSplash, atomNetWMWindowTypeToolbar: | |
| 326 | + | return true | |
| 327 | + | } | |
| 328 | + | return false | |
| 329 | + | } | |
| 330 | + | ||
| 331 | + | // classify sets rec.floating, the single source of truth: a dialog-like | |
| 332 | + | // window type, a transient for a managed window, or a fixed size. | |
| 333 | + | func (x *XWM) classify(rec *record) { | |
| 334 | + | fixed := rec.minW > 0 && rec.minH > 0 && rec.minW == rec.maxW && rec.minH == rec.maxH | |
| 335 | + | rec.floating = floatingType(rec.wtype) || x.managedSlot(rec.transient) != 0 || fixed | |
| 336 | + | } | |
| 337 | + | ||
| 338 | + | // managedSlot returns the slot of the managed window xid, or 0. | |
| 339 | + | func (x *XWM) managedSlot(xid uint32) Slot { | |
| 340 | + | if xid == 0 { | |
| 341 | + | return 0 | |
| 342 | + | } | |
| 343 | + | if s := x.tab.lookup(xid); s != 0 && x.tab.rec(s).kind == kindManaged { | |
| 344 | + | return s | |
| 345 | + | } | |
| 346 | + | return 0 | |
| 347 | + | } | |
| 348 | + | ||
| 349 | + | // queryClientIDs asks XRes for the PID of the client owning s, which stands | |
| 350 | + | // in for a missing _NET_WM_PID: Ready waits for it (propClientIDs). | |
| 351 | + | func (x *XWM) queryClientIDs(s Slot) { | |
| 352 | + | rec := x.tab.rec(s) | |
| 353 | + | specs := [1]res.ClientIdSpec{{Client: rec.xid, Mask: res.ClientIdMaskLocalClientPID}} | |
| 354 | + | seq := res.QueryClientIds(x.conn.Buf(), specs[:]) | |
| 355 | + | if seq == 0 { | |
| 356 | + | return | |
| 357 | + | } | |
| 358 | + | rec.pending |= propClientIDs | |
| 359 | + | x.expect(seq, kindClientIds, x.tab.tag(s), 0) | |
| 360 | + | } | |
| 361 | + | ||
| 362 | + | // onClientIdsReply stores the PID XRes found and releases the map. | |
| 363 | + | func (x *XWM) onClientIdsReply(s Slot, rec *record, r *xturbo.Reply) { | |
| 364 | + | if q, ok := res.ParseQueryClientIdsReply(r.Raw); ok { | |
| 365 | + | it := q.Ids() | |
| 366 | + | for it.Next() { | |
| 367 | + | v := it.Value() | |
| 368 | + | if v.Spec().Mask == res.ClientIdMaskLocalClientPID && v.Value().Len() > 0 { | |
| 369 | + | rec.pid = int32(v.Value().At(0)) | |
| 370 | + | break | |
| 371 | + | } | |
| 372 | + | } | |
| 373 | + | } | |
| 374 | + | rec.pending &^= propClientIDs | |
| 375 | + | x.checkReady(s) | |
| 376 | + | } | |
A internal/adapters/wayland/xwm/selection.go +1050 -0
| @@ -0,0 +1,1050 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "errors" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "bnema.dev/xturbo" | |
| 9 | + | "bnema.dev/xturbo/xfixes" | |
| 10 | + | "bnema.dev/xturbo/xproto" | |
| 11 | + | "golang.org/x/sys/unix" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // The selection bridge: text-only CLIPBOARD and PRIMARY between X clients and | |
| 15 | + | // the Wayland clipboard. Display goroutine only; nothing here is locked. | |
| 16 | + | ||
| 17 | + | // Selection kinds, in the order clipboard.go uses. | |
| 18 | + | const ( | |
| 19 | + | SelClipboard = iota | |
| 20 | + | SelPrimary | |
| 21 | + | selKinds | |
| 22 | + | ) | |
| 23 | + | ||
| 24 | + | // textMimes are the MIME types the bridge offers, in the order of preference; | |
| 25 | + | // mimeTarget gives the atom an X owner must advertise for each. | |
| 26 | + | var ( | |
| 27 | + | textMimes = [5]string{"text/plain;charset=utf-8", "text/plain", "UTF8_STRING", "STRING", "TEXT"} | |
| 28 | + | mimeTarget = [5]int{atomUTF8String, atomString, atomUTF8String, atomString, atomUTF8String} | |
| 29 | + | ) | |
| 30 | + | ||
| 31 | + | // selState is one selection. | |
| 32 | + | type selState struct { | |
| 33 | + | atom uint32 | |
| 34 | + | gen uint32 // bumped on every ownership change; stales TARGETS probes | |
| 35 | + | probe uint32 // gen when the TARGETS ConvertSelection was issued | |
| 36 | + | owner uint32 // X owner window other than selWin, 0 none | |
| 37 | + | ownTime uint32 // lastTime when selWin took ownership, answered for TIMESTAMP | |
| 38 | + | ours bool // selWin owns the selection (Wayland -> X) | |
| 39 | + | // wl is what the Wayland side offers while ours: raw text availability. | |
| 40 | + | wlUTF8, wlPlain bool | |
| 41 | + | wlMime [len(textMimes)]string // offered text MIMEs, in textMimes order, "" absent | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | const ( | |
| 45 | + | chunk = 65536 // every transfer step moves at most this many bytes | |
| 46 | + | nXfer = 4 // concurrent transfers per direction, and pool buffers | |
| 47 | + | tickEvery = time.Second | |
| 48 | + | xIdleMax = 10 // ticks without X-side progress | |
| 49 | + | rIdleMax = 30 // ticks of a reader that does not drain its pipe | |
| 50 | + | ) | |
| 51 | + | ||
| 52 | + | // bufPool is the fixed set of transfer buffers. | |
| 53 | + | type bufPool struct { | |
| 54 | + | bufs [nXfer][chunk]byte | |
| 55 | + | free [nXfer]int8 | |
| 56 | + | n int | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | func (p *bufPool) init() { | |
| 60 | + | for i := range p.free { | |
| 61 | + | p.free[i] = int8(i) | |
| 62 | + | } | |
| 63 | + | p.n = nXfer | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // get returns a free buffer index, -1 when the pool is exhausted. | |
| 67 | + | func (p *bufPool) get() int { | |
| 68 | + | if p.n == 0 { | |
| 69 | + | return -1 | |
| 70 | + | } | |
| 71 | + | p.n-- | |
| 72 | + | return int(p.free[p.n]) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (p *bufPool) put(i int) { | |
| 76 | + | p.free[p.n] = int8(i) | |
| 77 | + | p.n++ | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // X -> Wayland transfer states. | |
| 81 | + | const ( | |
| 82 | + | inFree uint8 = iota | |
| 83 | + | inQueued // waiting for the conversion property to be free | |
| 84 | + | inConverting | |
| 85 | + | inWaitIncr // waiting for the next INCR chunk (PropertyNotify) | |
| 86 | + | inWriting // a buffer is pending on the Wayland reader's pipe | |
| 87 | + | inReading // a GetProperty is outstanding | |
| 88 | + | ) | |
| 89 | + | ||
| 90 | + | // xin is one X -> Wayland transfer. | |
| 91 | + | type xin struct { | |
| 92 | + | x *XWM | |
| 93 | + | idx int | |
| 94 | + | state uint8 | |
| 95 | + | kind int | |
| 96 | + | fd int | |
| 97 | + | target uint32 | |
| 98 | + | order uint64 | |
| 99 | + | gen uint32 // stales replies of an aborted transfer | |
| 100 | + | incr bool | |
| 101 | + | buf int | |
| 102 | + | off, n int | |
| 103 | + | words uint32 // long offset of the next piece of a long property | |
| 104 | + | more bool // the last piece left bytes in the property | |
| 105 | + | ready bool // the owner wrote the next INCR chunk while a buffer was pending | |
| 106 | + | idle int | |
| 107 | + | src fdSource | |
| 108 | + | onR func() // pipe hangup | |
| 109 | + | onW func() // pipe writable | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | // selBridge is the XWM's selection state. | |
| 113 | + | type selBridge struct { | |
| 114 | + | st [selKinds]selState | |
| 115 | + | mimes [len(textMimes)]string // scratch handed to SelectionOwned | |
| 116 | + | pool bufPool | |
| 117 | + | in [nXfer]xin | |
| 118 | + | out [nXfer]xout | |
| 119 | + | order uint64 | |
| 120 | + | // active is the transfer that owns the conversion property, -1 none. | |
| 121 | + | active int | |
| 122 | + | timerOn bool | |
| 123 | + | closing bool // selClose ran: no transfer may start | |
| 124 | + | cancel func() | |
| 125 | + | tickFn func() | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | func (x *XWM) initSelection() { | |
| 129 | + | b := &x.sel | |
| 130 | + | b.pool.init() | |
| 131 | + | b.active = -1 | |
| 132 | + | b.tickFn = x.selTick | |
| 133 | + | for i := range b.in { | |
| 134 | + | t := &b.in[i] | |
| 135 | + | t.x, t.idx, t.buf = x, i, -1 | |
| 136 | + | t.onR = func() { t.x.inAbort(t.idx, true) } | |
| 137 | + | t.onW = func() { t.x.inWritable(t.idx) } | |
| 138 | + | } | |
| 139 | + | for i := range b.out { | |
| 140 | + | o := &b.out[i] | |
| 141 | + | o.x, o.idx, o.buf, o.fd = x, i, -1, -1 | |
| 142 | + | o.onR = func() { o.x.outReadable(o.idx) } | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | // bootSelection creates the selection window and asks for ownership events. | |
| 147 | + | // Without XFixes the bridge stays off. | |
| 148 | + | func (x *XWM) bootSelection(root xproto.Window) { | |
| 149 | + | if !x.conn.Ext().Info(xturbo.ExtXFixes).Present { | |
| 150 | + | x.log.Warn().Msg("X server has no XFixes: clipboard bridge disabled") | |
| 151 | + | return | |
| 152 | + | } | |
| 153 | + | xid := x.selWin | |
| 154 | + | b := x.conn.Buf() | |
| 155 | + | var wv xproto.CreateWindowValues | |
| 156 | + | wv.SetEventMask(xproto.EventMaskPropertyChange) | |
| 157 | + | xproto.CreateWindow(b, 0, xproto.Window(xid), root, -1, -1, 1, 1, 0, xproto.WindowClassInputOnly, 0, &wv) | |
| 158 | + | x.sel.st[SelClipboard].atom = x.atoms[atomClipboard] | |
| 159 | + | x.sel.st[SelPrimary].atom = x.atoms[atomPrimary] | |
| 160 | + | const mask = xfixes.SelectionEventMaskSetSelectionOwner | xfixes.SelectionEventMaskSelectionWindowDestroy | xfixes.SelectionEventMaskSelectionClientClose | |
| 161 | + | for k := range x.sel.st { | |
| 162 | + | xfixes.SelectSelectionInput(b, root, xproto.Atom(x.sel.st[k].atom), mask) | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | // selKind maps a selection atom to its kind, -1 when it is neither. | |
| 167 | + | func (x *XWM) selKind(atom uint32) int { | |
| 168 | + | for k := range x.sel.st { | |
| 169 | + | if x.sel.st[k].atom == atom && atom != 0 { | |
| 170 | + | return k | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | return -1 | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | // onSelectionEvent handles the selection events; others are ignored. | |
| 177 | + | func (x *XWM) onSelectionEvent(ev *xturbo.Event) { | |
| 178 | + | if x.selWin == 0 { | |
| 179 | + | return | |
| 180 | + | } | |
| 181 | + | switch ev.Kind { | |
| 182 | + | case xfixes.KindSelectionNotify: | |
| 183 | + | x.onXFixesSelection(ev) | |
| 184 | + | case xproto.KindSelectionNotify: | |
| 185 | + | x.onSelectionNotify(ev) | |
| 186 | + | case xproto.KindSelectionRequest: | |
| 187 | + | x.onSelectionRequest(ev) | |
| 188 | + | case xproto.KindSelectionClear: | |
| 189 | + | x.onSelectionClear(ev) | |
| 190 | + | } | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | // onXFixesSelection: the owner of a selection changed. | |
| 194 | + | func (x *XWM) onXFixesSelection(ev *xturbo.Event) { | |
| 195 | + | e := xfixes.SelectionNotifyOf(ev) | |
| 196 | + | k := x.selKind(uint32(e.Selection())) | |
| 197 | + | if k < 0 { | |
| 198 | + | return | |
| 199 | + | } | |
| 200 | + | st := &x.sel.st[k] | |
| 201 | + | owner := uint32(e.Owner()) | |
| 202 | + | if owner == x.selWin { // our own SetSelectionOwner echoed back | |
| 203 | + | return | |
| 204 | + | } | |
| 205 | + | st.gen++ | |
| 206 | + | st.owner = owner | |
| 207 | + | if owner == 0 { | |
| 208 | + | x.log.Debug().Int("kind", k).Msg("selection: no X owner") | |
| 209 | + | x.host.SelectionOwned(k, nil) | |
| 210 | + | return | |
| 211 | + | } | |
| 212 | + | st.probe = st.gen | |
| 213 | + | x.log.Debug().Int("kind", k).Uint32("owner", owner).Msg("selection: X owner") | |
| 214 | + | xproto.ConvertSelection(x.conn.Buf(), xproto.Window(x.selWin), xproto.Atom(st.atom), | |
| 215 | + | xproto.Atom(x.atoms[atomTargets]), xproto.Atom(x.atoms[atomNeferwlTargets]), 0) | |
| 216 | + | x.wake() | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | // onSelectionNotify: a ConvertSelection we issued was answered. | |
| 220 | + | func (x *XWM) onSelectionNotify(ev *xturbo.Event) { | |
| 221 | + | e := xproto.SelectionNotifyOf(ev) | |
| 222 | + | if uint32(e.Requestor()) != x.selWin { | |
| 223 | + | return | |
| 224 | + | } | |
| 225 | + | k := x.selKind(uint32(e.Selection())) | |
| 226 | + | if k < 0 { | |
| 227 | + | return | |
| 228 | + | } | |
| 229 | + | if uint32(e.Target()) != x.atoms[atomTargets] { | |
| 230 | + | x.onDataNotify(e) | |
| 231 | + | return | |
| 232 | + | } | |
| 233 | + | { | |
| 234 | + | st := &x.sel.st[k] | |
| 235 | + | if st.ours || st.owner == 0 || st.probe != st.gen { // stale probe | |
| 236 | + | return | |
| 237 | + | } | |
| 238 | + | if e.Property() == 0 { | |
| 239 | + | x.host.SelectionOwned(k, nil) | |
| 240 | + | return | |
| 241 | + | } | |
| 242 | + | seq := xproto.GetProperty(x.conn.Buf(), true, xproto.Window(x.selWin), xproto.Atom(x.atoms[atomNeferwlTargets]), 0, 0, 256) | |
| 243 | + | x.expect(seq, kindSelTargets, uint32(k), x.sel.st[k].gen) | |
| 244 | + | x.wake() | |
| 245 | + | return | |
| 246 | + | } | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | // onTargetsReply turns the owner's TARGETS list into the MIME types offered | |
| 250 | + | // to Wayland: the text subset, empty for images, files and the like. | |
| 251 | + | func (x *XWM) onTargetsReply(r *xturbo.Reply) { | |
| 252 | + | k := int(r.Args[0]) | |
| 253 | + | if k < 0 || k >= selKinds || r.Args[1] != x.sel.st[k].gen || x.sel.st[k].ours || x.sel.st[k].owner == 0 || x.sel.st[k].probe != r.Args[1] { | |
| 254 | + | return | |
| 255 | + | } | |
| 256 | + | g, ok := xproto.ParseGetPropertyReply(r.Raw) | |
| 257 | + | if !ok { | |
| 258 | + | return | |
| 259 | + | } | |
| 260 | + | var have [atomCount]bool | |
| 261 | + | if g.Format() == 32 { | |
| 262 | + | v := g.Value() | |
| 263 | + | for i := 0; i+4 <= len(v); i += 4 { | |
| 264 | + | a := binary.LittleEndian.Uint32(v[i:]) | |
| 265 | + | for _, t := range [...]int{atomUTF8String, atomString, atomText} { | |
| 266 | + | if a == x.atoms[t] { | |
| 267 | + | have[t] = true | |
| 268 | + | } | |
| 269 | + | } | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | n := 0 | |
| 273 | + | for i, m := range textMimes { | |
| 274 | + | if have[mimeTarget[i]] { | |
| 275 | + | x.sel.mimes[n] = m | |
| 276 | + | n++ | |
| 277 | + | } | |
| 278 | + | } | |
| 279 | + | if n == 0 { | |
| 280 | + | x.host.SelectionOwned(k, nil) | |
| 281 | + | return | |
| 282 | + | } | |
| 283 | + | x.host.SelectionOwned(k, x.sel.mimes[:n]) | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | // ---- X -> Wayland ---- | |
| 287 | + | ||
| 288 | + | // SelectionReceive: a Wayland reader wants the X selection kind as mime. The | |
| 289 | + | // XWM takes fd (a write end it now owns) and closes it when done. | |
| 290 | + | func (x *XWM) SelectionReceive(kind int, mime string, fd int) { | |
| 291 | + | target := uint32(0) | |
| 292 | + | for i, m := range textMimes { | |
| 293 | + | if m == mime { | |
| 294 | + | target = x.atoms[mimeTarget[i]] | |
| 295 | + | break | |
| 296 | + | } | |
| 297 | + | } | |
| 298 | + | if x.selWin == 0 || x.closed || kind < 0 || kind >= selKinds || target == 0 || x.sel.st[kind].owner == 0 { | |
| 299 | + | _ = unix.Close(fd) | |
| 300 | + | return | |
| 301 | + | } | |
| 302 | + | if unix.SetNonblock(fd, true) != nil { | |
| 303 | + | _ = unix.Close(fd) | |
| 304 | + | return | |
| 305 | + | } | |
| 306 | + | for i := range x.sel.in { | |
| 307 | + | t := &x.sel.in[i] | |
| 308 | + | if t.state != inFree { | |
| 309 | + | continue | |
| 310 | + | } | |
| 311 | + | x.sel.order++ | |
| 312 | + | t.state, t.kind, t.fd, t.target, t.order = inQueued, kind, fd, target, x.sel.order | |
| 313 | + | t.incr, t.buf, t.off, t.n, t.words, t.more, t.ready, t.idle, t.src = false, -1, 0, 0, 0, false, false, 0, nil | |
| 314 | + | x.selEnsureTimer() | |
| 315 | + | x.inStartNext() | |
| 316 | + | return | |
| 317 | + | } | |
| 318 | + | x.log.Warn().Msg("selection: too many transfers, paste refused") | |
| 319 | + | _ = unix.Close(fd) | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | // inStartNext starts the oldest queued transfer when none is converting. | |
| 323 | + | func (x *XWM) inStartNext() { | |
| 324 | + | b := &x.sel | |
| 325 | + | if b.active >= 0 || b.closing { | |
| 326 | + | return | |
| 327 | + | } | |
| 328 | + | best := -1 | |
| 329 | + | for i := range b.in { | |
| 330 | + | if b.in[i].state == inQueued && (best < 0 || b.in[i].order < b.in[best].order) { | |
| 331 | + | best = i | |
| 332 | + | } | |
| 333 | + | } | |
| 334 | + | if best < 0 { | |
| 335 | + | return | |
| 336 | + | } | |
| 337 | + | t := &b.in[best] | |
| 338 | + | t.state, t.idle = inConverting, 0 | |
| 339 | + | b.active = best | |
| 340 | + | xproto.ConvertSelection(x.conn.Buf(), xproto.Window(x.selWin), xproto.Atom(b.st[t.kind].atom), | |
| 341 | + | xproto.Atom(t.target), xproto.Atom(x.atoms[atomNeferwlData]), 0) | |
| 342 | + | x.wake() | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | // inFinish ends transfer i and starts the next one. | |
| 346 | + | func (x *XWM) inFinish(i int) { | |
| 347 | + | b := &x.sel | |
| 348 | + | t := &b.in[i] | |
| 349 | + | if t.src != nil { | |
| 350 | + | t.src.Remove() | |
| 351 | + | t.src = nil | |
| 352 | + | } | |
| 353 | + | if t.fd >= 0 { | |
| 354 | + | _ = unix.Close(t.fd) | |
| 355 | + | } | |
| 356 | + | if t.buf >= 0 { | |
| 357 | + | b.pool.put(t.buf) | |
| 358 | + | t.buf = -1 | |
| 359 | + | } | |
| 360 | + | t.gen++ | |
| 361 | + | t.state, t.fd = inFree, -1 | |
| 362 | + | if b.active == i { | |
| 363 | + | b.active = -1 | |
| 364 | + | } | |
| 365 | + | x.inStartNext() | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | // inAbort drops transfer i, deleting the conversion property when X may still | |
| 369 | + | // be writing it. | |
| 370 | + | func (x *XWM) inAbort(i int, del bool) { | |
| 371 | + | t := &x.sel.in[i] | |
| 372 | + | if t.state == inFree { | |
| 373 | + | return | |
| 374 | + | } | |
| 375 | + | if del && x.sel.active == i && !x.closed { | |
| 376 | + | xproto.DeleteProperty(x.conn.Buf(), xproto.Window(x.selWin), xproto.Atom(x.atoms[atomNeferwlData])) | |
| 377 | + | x.wake() | |
| 378 | + | } | |
| 379 | + | x.inFinish(i) | |
| 380 | + | } | |
| 381 | + | ||
| 382 | + | // onDataNotify: the owner answered our data ConvertSelection. | |
| 383 | + | func (x *XWM) onDataNotify(e xproto.SelectionNotifyEvent) { | |
| 384 | + | i := x.sel.active | |
| 385 | + | if i < 0 { | |
| 386 | + | return | |
| 387 | + | } | |
| 388 | + | t := &x.sel.in[i] | |
| 389 | + | if t.state != inConverting || uint32(e.Target()) != t.target || uint32(e.Selection()) != x.sel.st[t.kind].atom { | |
| 390 | + | return | |
| 391 | + | } | |
| 392 | + | if p := uint32(e.Property()); p != 0 && p != x.atoms[atomNeferwlData] { | |
| 393 | + | return | |
| 394 | + | } | |
| 395 | + | if e.Property() == 0 { | |
| 396 | + | x.inFinish(i) | |
| 397 | + | return | |
| 398 | + | } | |
| 399 | + | t.words = 0 | |
| 400 | + | x.inRequest(i) | |
| 401 | + | } | |
| 402 | + | ||
| 403 | + | // inRequest asks for the next piece of the property; delete makes INCR and | |
| 404 | + | // whole-value reads advance. | |
| 405 | + | func (x *XWM) inRequest(i int) { | |
| 406 | + | t := &x.sel.in[i] | |
| 407 | + | t.state = inReading | |
| 408 | + | seq := xproto.GetProperty(x.conn.Buf(), true, xproto.Window(x.selWin), xproto.Atom(x.atoms[atomNeferwlData]), | |
| 409 | + | 0, t.words, chunk/4) | |
| 410 | + | if seq != 0 { | |
| 411 | + | if err := x.conn.Expect(seq, kindSelData, uint32(i), t.gen, 0, 0); err != nil { | |
| 412 | + | x.stats.RingFull++ | |
| 413 | + | } | |
| 414 | + | } | |
| 415 | + | x.wake() | |
| 416 | + | } | |
| 417 | + | ||
| 418 | + | // onSelDataReply handles one piece of the converted property. | |
| 419 | + | func (x *XWM) onSelDataReply(r *xturbo.Reply) { | |
| 420 | + | i := int(r.Args[0]) | |
| 421 | + | if i < 0 || i >= nXfer { | |
| 422 | + | return | |
| 423 | + | } | |
| 424 | + | t := &x.sel.in[i] | |
| 425 | + | if t.state != inReading || t.gen != r.Args[1] { | |
| 426 | + | return | |
| 427 | + | } | |
| 428 | + | g, ok := xproto.ParseGetPropertyReply(r.Raw) | |
| 429 | + | if !ok || g.Format() == 0 { | |
| 430 | + | x.inAbort(i, false) | |
| 431 | + | return | |
| 432 | + | } | |
| 433 | + | t.idle = 0 | |
| 434 | + | if uint32(g.Type()) == x.atoms[atomIncr] { | |
| 435 | + | t.incr, t.state, t.words, t.more = true, inWaitIncr, 0, false | |
| 436 | + | return | |
| 437 | + | } | |
| 438 | + | v := g.Value() | |
| 439 | + | if g.Format() != 8 || len(v) > chunk { | |
| 440 | + | x.inAbort(i, true) | |
| 441 | + | return | |
| 442 | + | } | |
| 443 | + | t.more = g.BytesAfter() > 0 | |
| 444 | + | if len(v) == 0 { | |
| 445 | + | if t.more { // cannot happen: bytes remain only after a non-empty read | |
| 446 | + | x.inAbort(i, true) | |
| 447 | + | return | |
| 448 | + | } | |
| 449 | + | x.inNext(i) | |
| 450 | + | return | |
| 451 | + | } | |
| 452 | + | b := x.sel.pool.get() | |
| 453 | + | if b < 0 { | |
| 454 | + | x.log.Warn().Msg("selection: no free buffer, paste aborted") | |
| 455 | + | x.inAbort(i, true) | |
| 456 | + | return | |
| 457 | + | } | |
| 458 | + | t.buf = b | |
| 459 | + | t.n = copy(x.sel.pool.bufs[b][:], v) | |
| 460 | + | t.off = 0 | |
| 461 | + | t.words += uint32(t.n) / 4 | |
| 462 | + | t.state = inWriting | |
| 463 | + | x.inDrain(i) | |
| 464 | + | } | |
| 465 | + | ||
| 466 | + | // inDrain writes the pending buffer to the reader's pipe. | |
| 467 | + | func (x *XWM) inDrain(i int) { | |
| 468 | + | t := &x.sel.in[i] | |
| 469 | + | buf := &x.sel.pool.bufs[t.buf] | |
| 470 | + | for t.off < t.n { | |
| 471 | + | n, err := unix.Write(t.fd, buf[t.off:t.n]) | |
| 472 | + | switch { | |
| 473 | + | case err == nil: | |
| 474 | + | t.off += n | |
| 475 | + | t.idle = 0 | |
| 476 | + | case errors.Is(err, unix.EINTR): | |
| 477 | + | case errors.Is(err, unix.EAGAIN): | |
| 478 | + | x.inWatch(i, true) | |
| 479 | + | return | |
| 480 | + | default: // EPIPE: the reader went away | |
| 481 | + | x.inAbort(i, true) | |
| 482 | + | return | |
| 483 | + | } | |
| 484 | + | } | |
| 485 | + | x.sel.pool.put(t.buf) | |
| 486 | + | t.buf = -1 | |
| 487 | + | if t.src != nil { | |
| 488 | + | x.inWatch(i, false) | |
| 489 | + | } | |
| 490 | + | x.inNext(i) | |
| 491 | + | } | |
| 492 | + | ||
| 493 | + | // inWatch arms or disarms writable interest on the reader's pipe. | |
| 494 | + | func (x *XWM) inWatch(i int, on bool) { | |
| 495 | + | t := &x.sel.in[i] | |
| 496 | + | if t.src == nil { | |
| 497 | + | if !on || x.lp == nil { | |
| 498 | + | if on { | |
| 499 | + | x.inAbort(i, true) | |
| 500 | + | } | |
| 501 | + | return | |
| 502 | + | } | |
| 503 | + | src, err := x.lp.AddFD(t.fd, t.onR, t.onW) | |
| 504 | + | if err != nil { | |
| 505 | + | x.inAbort(i, true) | |
| 506 | + | return | |
| 507 | + | } | |
| 508 | + | t.src = src | |
| 509 | + | } | |
| 510 | + | if err := t.src.SetWritable(on); err != nil { | |
| 511 | + | x.inAbort(i, true) | |
| 512 | + | } | |
| 513 | + | } | |
| 514 | + | ||
| 515 | + | // inWritable: the pipe has room again. | |
| 516 | + | func (x *XWM) inWritable(i int) { | |
| 517 | + | t := &x.sel.in[i] | |
| 518 | + | if t.state == inWriting && t.buf >= 0 { | |
| 519 | + | x.inDrain(i) | |
| 520 | + | } | |
| 521 | + | } | |
| 522 | + | ||
| 523 | + | // inNext decides what follows a drained (or empty) piece. | |
| 524 | + | func (x *XWM) inNext(i int) { | |
| 525 | + | t := &x.sel.in[i] | |
| 526 | + | switch { | |
| 527 | + | case t.more: // the rest of this property | |
| 528 | + | x.inRequest(i) | |
| 529 | + | case t.incr && t.n > 0: | |
| 530 | + | t.state, t.n, t.words = inWaitIncr, 0, 0 | |
| 531 | + | if t.ready { | |
| 532 | + | t.ready = false | |
| 533 | + | x.inRequest(i) | |
| 534 | + | } | |
| 535 | + | default: // single value done, or the empty INCR terminator | |
| 536 | + | x.inFinish(i) | |
| 537 | + | } | |
| 538 | + | } | |
| 539 | + | ||
| 540 | + | // onSelProperty: PropertyNotify on the selection window. | |
| 541 | + | func (x *XWM) onSelProperty(ev *xturbo.Event) { | |
| 542 | + | e := xproto.PropertyNotifyOf(ev) | |
| 543 | + | if e.State() != xproto.PropertyNewValue || uint32(e.Atom()) != x.atoms[atomNeferwlData] { | |
| 544 | + | return | |
| 545 | + | } | |
| 546 | + | // (a Deleted state on a requestor is handled by onRequestorProperty) | |
| 547 | + | i := x.sel.active | |
| 548 | + | if i < 0 || !x.sel.in[i].incr { | |
| 549 | + | return | |
| 550 | + | } | |
| 551 | + | t := &x.sel.in[i] | |
| 552 | + | switch t.state { | |
| 553 | + | case inWaitIncr: | |
| 554 | + | t.idle = 0 | |
| 555 | + | t.n = 0 | |
| 556 | + | x.inRequest(i) | |
| 557 | + | case inWriting, inReading: | |
| 558 | + | t.ready = true // no request while a buffer is pending: backpressure | |
| 559 | + | } | |
| 560 | + | } | |
| 561 | + | ||
| 562 | + | // onSelError: a selection request failed; the awaited reply never comes. | |
| 563 | + | func (x *XWM) onSelError(e *xturbo.XError) { | |
| 564 | + | if e.Kind == kindSelOut { | |
| 565 | + | if i := int(e.Args[0]); i >= 0 && i < nXfer && x.sel.out[i].gen == e.Args[1] && x.sel.out[i].state != outFree { | |
| 566 | + | x.outAbort(i) | |
| 567 | + | } | |
| 568 | + | return | |
| 569 | + | } | |
| 570 | + | if e.Kind == kindSelData { | |
| 571 | + | if i := int(e.Args[0]); i >= 0 && i < nXfer && x.sel.in[i].gen == e.Args[1] && x.sel.in[i].state == inReading { | |
| 572 | + | x.inAbort(i, false) | |
| 573 | + | } | |
| 574 | + | } | |
| 575 | + | } | |
| 576 | + | ||
| 577 | + | // ---- timeouts ---- | |
| 578 | + | ||
| 579 | + | func (x *XWM) selEnsureTimer() { | |
| 580 | + | b := &x.sel | |
| 581 | + | if b.timerOn || x.lp == nil { | |
| 582 | + | return | |
| 583 | + | } | |
| 584 | + | b.timerOn = true | |
| 585 | + | b.cancel = x.lp.After(tickEvery, b.tickFn) | |
| 586 | + | } | |
| 587 | + | ||
| 588 | + | // selTick ages every transfer by one second and aborts the stalled ones. | |
| 589 | + | func (x *XWM) selTick() { | |
| 590 | + | b := &x.sel | |
| 591 | + | b.timerOn, b.cancel = false, nil | |
| 592 | + | if x.closed { | |
| 593 | + | return | |
| 594 | + | } | |
| 595 | + | live := false | |
| 596 | + | for i := range b.in { | |
| 597 | + | t := &b.in[i] | |
| 598 | + | if t.state == inFree { | |
| 599 | + | continue | |
| 600 | + | } | |
| 601 | + | t.idle++ | |
| 602 | + | limit := xIdleMax | |
| 603 | + | if t.state == inWriting { | |
| 604 | + | limit = rIdleMax | |
| 605 | + | } | |
| 606 | + | if t.state != inQueued && t.idle >= limit { | |
| 607 | + | x.log.Warn().Int("kind", t.kind).Msg("selection: transfer timed out") | |
| 608 | + | x.inAbort(i, true) | |
| 609 | + | continue | |
| 610 | + | } | |
| 611 | + | live = true | |
| 612 | + | } | |
| 613 | + | for i := range b.out { | |
| 614 | + | o := &b.out[i] | |
| 615 | + | if o.state == outFree { | |
| 616 | + | continue | |
| 617 | + | } | |
| 618 | + | o.idle++ | |
| 619 | + | limit := rIdleMax | |
| 620 | + | if o.incr && o.n > 0 && !o.credit { | |
| 621 | + | limit = xIdleMax // the requestor does not delete the property | |
| 622 | + | } | |
| 623 | + | if o.idle >= limit { | |
| 624 | + | x.log.Warn().Int("kind", o.kind).Msg("selection: transfer timed out") | |
| 625 | + | x.outAbort(i) | |
| 626 | + | continue | |
| 627 | + | } | |
| 628 | + | live = true | |
| 629 | + | } | |
| 630 | + | if live { | |
| 631 | + | x.selEnsureTimer() | |
| 632 | + | } | |
| 633 | + | } | |
| 634 | + | ||
| 635 | + | // selClose aborts every transfer. | |
| 636 | + | func (x *XWM) selClose() { | |
| 637 | + | x.sel.closing = true | |
| 638 | + | for i := range x.sel.out { | |
| 639 | + | if x.sel.out[i].state != outFree { | |
| 640 | + | x.outAbort(i) | |
| 641 | + | } | |
| 642 | + | } | |
| 643 | + | for i := range x.sel.in { | |
| 644 | + | if x.sel.in[i].state != inFree { | |
| 645 | + | x.inAbort(i, false) | |
| 646 | + | } | |
| 647 | + | } | |
| 648 | + | if x.sel.cancel != nil { | |
| 649 | + | x.sel.cancel() | |
| 650 | + | x.sel.cancel = nil | |
| 651 | + | } | |
| 652 | + | x.sel.timerOn = false | |
| 653 | + | } | |
| 654 | + | ||
| 655 | + | // ---- Wayland -> X ---- | |
| 656 | + | ||
| 657 | + | // Wayland -> X transfer states. | |
| 658 | + | const ( | |
| 659 | + | outFree uint8 = iota | |
| 660 | + | outActive | |
| 661 | + | ) | |
| 662 | + | ||
| 663 | + | // xout is one Wayland -> X transfer. | |
| 664 | + | type xout struct { | |
| 665 | + | x *XWM | |
| 666 | + | idx int | |
| 667 | + | state uint8 | |
| 668 | + | kind int | |
| 669 | + | requestor uint32 | |
| 670 | + | property uint32 | |
| 671 | + | target uint32 | |
| 672 | + | time uint32 | |
| 673 | + | fd int | |
| 674 | + | buf int | |
| 675 | + | n int | |
| 676 | + | eof bool | |
| 677 | + | incr bool // INCR announced | |
| 678 | + | credit bool // the requestor deleted the property: the next chunk may go | |
| 679 | + | notified bool // the requestor got its SelectionNotify | |
| 680 | + | idle int | |
| 681 | + | gen uint32 | |
| 682 | + | src fdSource | |
| 683 | + | onR func() | |
| 684 | + | } | |
| 685 | + | ||
| 686 | + | // WaylandSelection: a Wayland client owns kind with these MIME types, or none | |
| 687 | + | // does (mimes nil). The XWM takes X ownership when text is offered. | |
| 688 | + | func (x *XWM) WaylandSelection(kind int, mimes []string) { | |
| 689 | + | if x.selWin == 0 || x.closed || kind < 0 || kind >= selKinds || x.host == nil { | |
| 690 | + | return | |
| 691 | + | } | |
| 692 | + | st := &x.sel.st[kind] | |
| 693 | + | st.wlUTF8, st.wlPlain = false, false | |
| 694 | + | st.wlMime = [len(textMimes)]string{} | |
| 695 | + | for _, m := range mimes { | |
| 696 | + | for i, tm := range textMimes { | |
| 697 | + | if m != tm { | |
| 698 | + | continue | |
| 699 | + | } | |
| 700 | + | st.wlMime[i] = m | |
| 701 | + | if mimeTarget[i] == atomUTF8String { | |
| 702 | + | st.wlUTF8 = true | |
| 703 | + | } else { | |
| 704 | + | st.wlPlain = true | |
| 705 | + | } | |
| 706 | + | } | |
| 707 | + | } | |
| 708 | + | for i := range x.sel.out { // the data being served is stale | |
| 709 | + | if x.sel.out[i].state != outFree && x.sel.out[i].kind == kind { | |
| 710 | + | x.outAbort(i) | |
| 711 | + | } | |
| 712 | + | } | |
| 713 | + | b := x.conn.Buf() | |
| 714 | + | if !st.wlUTF8 && !st.wlPlain { | |
| 715 | + | if st.ours { | |
| 716 | + | st.ours = false | |
| 717 | + | xproto.SetSelectionOwner(b, 0, xproto.Atom(st.atom), 0) | |
| 718 | + | x.wake() | |
| 719 | + | } | |
| 720 | + | return | |
| 721 | + | } | |
| 722 | + | st.ours = true | |
| 723 | + | st.ownTime = x.lastTime | |
| 724 | + | st.gen++ | |
| 725 | + | st.owner = 0 | |
| 726 | + | xproto.SetSelectionOwner(b, xproto.Window(x.selWin), xproto.Atom(st.atom), 0) | |
| 727 | + | x.log.Debug().Int("kind", kind).Msg("selection: Wayland owner") | |
| 728 | + | x.wake() | |
| 729 | + | } | |
| 730 | + | ||
| 731 | + | // onSelectionClear: someone else took a selection from selWin. | |
| 732 | + | func (x *XWM) onSelectionClear(ev *xturbo.Event) { | |
| 733 | + | e := xproto.SelectionClearOf(ev) | |
| 734 | + | k := x.selKind(uint32(e.Selection())) | |
| 735 | + | if k < 0 || uint32(e.Owner()) != x.selWin { | |
| 736 | + | return | |
| 737 | + | } | |
| 738 | + | x.sel.st[k].ours = false | |
| 739 | + | for i := range x.sel.out { | |
| 740 | + | if x.sel.out[i].state != outFree && x.sel.out[i].kind == k { | |
| 741 | + | x.outAbort(i) | |
| 742 | + | } | |
| 743 | + | } | |
| 744 | + | } | |
| 745 | + | ||
| 746 | + | // notify sends a SelectionNotify to the requestor; property 0 refuses. | |
| 747 | + | func (x *XWM) notify(requestor, selection, target, property, time uint32) { | |
| 748 | + | d := xproto.SelectionNotifyEventData{ | |
| 749 | + | Time: xproto.Timestamp(time), Requestor: xproto.Window(requestor), | |
| 750 | + | Selection: xproto.Atom(selection), Target: xproto.Atom(target), Property: xproto.Atom(property), | |
| 751 | + | } | |
| 752 | + | var ev [32]byte | |
| 753 | + | d.Put(&ev) | |
| 754 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(requestor), 0, &ev) | |
| 755 | + | x.wake() | |
| 756 | + | } | |
| 757 | + | ||
| 758 | + | // onSelectionRequest: an X client asks selWin for the Wayland selection. | |
| 759 | + | func (x *XWM) onSelectionRequest(ev *xturbo.Event) { | |
| 760 | + | e := xproto.SelectionRequestOf(ev) | |
| 761 | + | if uint32(e.Owner()) != x.selWin { | |
| 762 | + | return | |
| 763 | + | } | |
| 764 | + | sel, req, target, prop, time := uint32(e.Selection()), uint32(e.Requestor()), uint32(e.Target()), uint32(e.Property()), uint32(e.Time()) | |
| 765 | + | k := x.selKind(sel) | |
| 766 | + | if k < 0 || !x.sel.st[k].ours { | |
| 767 | + | x.notify(req, sel, target, 0, time) | |
| 768 | + | return | |
| 769 | + | } | |
| 770 | + | if prop == 0 { // obsolete client | |
| 771 | + | prop = target | |
| 772 | + | } | |
| 773 | + | st := &x.sel.st[k] | |
| 774 | + | switch target { | |
| 775 | + | case x.atoms[atomTargets]: | |
| 776 | + | var list [6]uint32 | |
| 777 | + | n := 0 | |
| 778 | + | list[n], n = x.atoms[atomTargets], n+1 | |
| 779 | + | list[n], n = x.atoms[atomTimestamp], n+1 | |
| 780 | + | if st.wlUTF8 { | |
| 781 | + | list[n], n = x.atoms[atomUTF8String], n+1 | |
| 782 | + | list[n], n = x.atoms[atomText], n+1 | |
| 783 | + | } | |
| 784 | + | list[n], n = x.atoms[atomString], n+1 | |
| 785 | + | xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(req), xproto.Atom(prop), | |
| 786 | + | xproto.AtomAtom, 32, uint32(n), x.encode32(list[:n])) | |
| 787 | + | x.notify(req, sel, target, prop, time) | |
| 788 | + | return | |
| 789 | + | case x.atoms[atomTimestamp]: | |
| 790 | + | xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(req), xproto.Atom(prop), | |
| 791 | + | xproto.AtomInteger, 32, 1, x.encode32([]uint32{st.ownTime})) | |
| 792 | + | x.notify(req, sel, target, prop, time) | |
| 793 | + | return | |
| 794 | + | case x.atoms[atomUTF8String], x.atoms[atomText], x.atoms[atomString]: | |
| 795 | + | default: | |
| 796 | + | x.notify(req, sel, target, 0, time) | |
| 797 | + | return | |
| 798 | + | } | |
| 799 | + | // Choose the Wayland MIME: UTF-8 targets prefer the UTF-8 types; STRING | |
| 800 | + | // prefers plain text and falls back to the UTF-8 bytes unchanged. | |
| 801 | + | mime := "" | |
| 802 | + | order := [...]int{0, 2, 4, 1, 3} // utf8 types, then plain | |
| 803 | + | if target == x.atoms[atomString] { | |
| 804 | + | order = [...]int{1, 3, 0, 2, 4} | |
| 805 | + | } | |
| 806 | + | for _, i := range order { | |
| 807 | + | if st.wlMime[i] != "" && (target == x.atoms[atomString] || mimeTarget[i] == atomUTF8String) { | |
| 808 | + | mime = st.wlMime[i] | |
| 809 | + | break | |
| 810 | + | } | |
| 811 | + | } | |
| 812 | + | if mime == "" { | |
| 813 | + | x.notify(req, sel, target, 0, time) | |
| 814 | + | return | |
| 815 | + | } | |
| 816 | + | for i := range x.sel.out { // one transfer per (requestor, property) | |
| 817 | + | o := &x.sel.out[i] | |
| 818 | + | if o.state != outFree && o.requestor == req && o.property == prop { | |
| 819 | + | x.notify(req, sel, target, 0, time) | |
| 820 | + | return | |
| 821 | + | } | |
| 822 | + | } | |
| 823 | + | slot := -1 | |
| 824 | + | for i := range x.sel.out { | |
| 825 | + | if x.sel.out[i].state == outFree { | |
| 826 | + | slot = i | |
| 827 | + | break | |
| 828 | + | } | |
| 829 | + | } | |
| 830 | + | if slot < 0 || x.sel.pool.n == 0 { | |
| 831 | + | x.log.Warn().Msg("selection: too many transfers, request refused") | |
| 832 | + | x.notify(req, sel, target, 0, time) | |
| 833 | + | return | |
| 834 | + | } | |
| 835 | + | fd := x.host.SelectionRead(k, mime) | |
| 836 | + | if fd < 0 { | |
| 837 | + | x.notify(req, sel, target, 0, time) | |
| 838 | + | return | |
| 839 | + | } | |
| 840 | + | x.startOut(slot, k, req, prop, target, time, fd) | |
| 841 | + | } | |
| 842 | + | ||
| 843 | + | // startOut begins serving the data on fd (a non-blocking read end it now | |
| 844 | + | // owns) to requestor. | |
| 845 | + | func (x *XWM) startOut(slot, k int, req, prop, target, time uint32, fd int) { | |
| 846 | + | o := &x.sel.out[slot] | |
| 847 | + | o.state, o.kind, o.requestor, o.property, o.target, o.time = outActive, k, req, prop, target, time | |
| 848 | + | o.fd, o.buf, o.n, o.eof, o.incr, o.credit, o.notified, o.idle, o.src = fd, x.sel.pool.get(), 0, false, false, false, false, 0, nil | |
| 849 | + | x.selEnsureTimer() | |
| 850 | + | x.outRead(slot) // data may already be waiting | |
| 851 | + | } | |
| 852 | + | ||
| 853 | + | // encode32 packs vals into the shared scratch. | |
| 854 | + | func (x *XWM) encode32(vals []uint32) []byte { | |
| 855 | + | n := 4 * len(vals) | |
| 856 | + | if cap(x.scratch) < n { | |
| 857 | + | x.scratch = make([]byte, 0, n) | |
| 858 | + | } | |
| 859 | + | p := x.scratch[:n] | |
| 860 | + | for i, v := range vals { | |
| 861 | + | binary.LittleEndian.PutUint32(p[4*i:], v) | |
| 862 | + | } | |
| 863 | + | return p | |
| 864 | + | } | |
| 865 | + | ||
| 866 | + | // outWatch registers the pipe's read end with the display. | |
| 867 | + | func (x *XWM) outWatch(i int) { | |
| 868 | + | o := &x.sel.out[i] | |
| 869 | + | if o.src != nil { | |
| 870 | + | return | |
| 871 | + | } | |
| 872 | + | if x.lp == nil { | |
| 873 | + | x.outAbort(i) | |
| 874 | + | return | |
| 875 | + | } | |
| 876 | + | src, err := x.lp.AddFD(o.fd, o.onR, nil) | |
| 877 | + | if err != nil { | |
| 878 | + | x.outAbort(i) | |
| 879 | + | return | |
| 880 | + | } | |
| 881 | + | o.src = src | |
| 882 | + | } | |
| 883 | + | ||
| 884 | + | // outUnwatch drops read interest: the buffer is full and nothing may be read | |
| 885 | + | // until the requestor consumed the previous chunk. | |
| 886 | + | func (x *XWM) outUnwatch(i int) { | |
| 887 | + | o := &x.sel.out[i] | |
| 888 | + | if o.src != nil { | |
| 889 | + | o.src.Remove() | |
| 890 | + | o.src = nil | |
| 891 | + | } | |
| 892 | + | } | |
| 893 | + | ||
| 894 | + | // outReadable is the pipe's read callback. | |
| 895 | + | func (x *XWM) outReadable(i int) { | |
| 896 | + | if x.sel.out[i].state == outFree { | |
| 897 | + | return | |
| 898 | + | } | |
| 899 | + | x.outRead(i) | |
| 900 | + | } | |
| 901 | + | ||
| 902 | + | // outRead fills the buffer from the pipe and advances the transfer. | |
| 903 | + | func (x *XWM) outRead(i int) { | |
| 904 | + | o := &x.sel.out[i] | |
| 905 | + | buf := &x.sel.pool.bufs[o.buf] | |
| 906 | + | for o.n < chunk && !o.eof { | |
| 907 | + | n, err := unix.Read(o.fd, buf[o.n:]) | |
| 908 | + | switch { | |
| 909 | + | case err == nil && n == 0: | |
| 910 | + | o.eof = true | |
| 911 | + | case err == nil: | |
| 912 | + | o.n += n | |
| 913 | + | o.idle = 0 | |
| 914 | + | case errors.Is(err, unix.EINTR): | |
| 915 | + | continue | |
| 916 | + | case errors.Is(err, unix.EAGAIN): | |
| 917 | + | x.outPump(i) | |
| 918 | + | return | |
| 919 | + | default: | |
| 920 | + | x.outAbort(i) | |
| 921 | + | return | |
| 922 | + | } | |
| 923 | + | } | |
| 924 | + | x.outPump(i) | |
| 925 | + | } | |
| 926 | + | ||
| 927 | + | // outPump sends what the state allows. | |
| 928 | + | func (x *XWM) outPump(i int) { | |
| 929 | + | o := &x.sel.out[i] | |
| 930 | + | if o.state == outFree { | |
| 931 | + | return | |
| 932 | + | } | |
| 933 | + | full := o.n == chunk | |
| 934 | + | if !o.incr { | |
| 935 | + | switch { | |
| 936 | + | case o.eof: // whole value | |
| 937 | + | x.outSetProp(i, o.n) | |
| 938 | + | x.notify(o.requestor, x.sel.st[o.kind].atom, o.target, o.property, o.time) | |
| 939 | + | x.outFinish(i) | |
| 940 | + | case full: // does not fit one request: INCR | |
| 941 | + | o.incr, o.notified = true, true | |
| 942 | + | var v xproto.ChangeWindowAttributesValues | |
| 943 | + | v.SetEventMask(xproto.EventMaskPropertyChange) | |
| 944 | + | xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(o.requestor), &v) | |
| 945 | + | xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(o.requestor), xproto.Atom(o.property), | |
| 946 | + | xproto.Atom(x.atoms[atomIncr]), 32, 1, x.encode32([]uint32{0})) | |
| 947 | + | x.notify(o.requestor, x.sel.st[o.kind].atom, o.target, o.property, o.time) | |
| 948 | + | x.outUnwatch(i) | |
| 949 | + | default: | |
| 950 | + | x.outWatch(i) | |
| 951 | + | } | |
| 952 | + | return | |
| 953 | + | } | |
| 954 | + | if o.credit && (full || o.eof) { | |
| 955 | + | o.credit = false | |
| 956 | + | o.idle = 0 | |
| 957 | + | x.outSetProp(i, o.n) | |
| 958 | + | if o.n == 0 { // the empty chunk ends the transfer | |
| 959 | + | x.outFinish(i) | |
| 960 | + | return | |
| 961 | + | } | |
| 962 | + | o.n = 0 | |
| 963 | + | } | |
| 964 | + | if o.n < chunk && !o.eof { | |
| 965 | + | x.outWatch(i) | |
| 966 | + | } else { | |
| 967 | + | x.outUnwatch(i) | |
| 968 | + | } | |
| 969 | + | } | |
| 970 | + | ||
| 971 | + | // outSetProp writes the first n buffered bytes as the property value. | |
| 972 | + | func (x *XWM) outSetProp(i, n int) { | |
| 973 | + | o := &x.sel.out[i] | |
| 974 | + | seq := xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(o.requestor), xproto.Atom(o.property), | |
| 975 | + | xproto.Atom(o.target), 8, uint32(n), x.sel.pool.bufs[o.buf][:n]) | |
| 976 | + | if seq != 0 { | |
| 977 | + | if err := x.conn.Expect(seq, kindSelOut, uint32(i), o.gen, 0, 0); err != nil { | |
| 978 | + | x.stats.RingFull++ | |
| 979 | + | } | |
| 980 | + | } | |
| 981 | + | x.wake() | |
| 982 | + | } | |
| 983 | + | ||
| 984 | + | // outFinish ends a completed transfer. | |
| 985 | + | func (x *XWM) outFinish(i int) { | |
| 986 | + | o := &x.sel.out[i] | |
| 987 | + | if o.incr && !x.closed { | |
| 988 | + | var v xproto.ChangeWindowAttributesValues | |
| 989 | + | v.SetEventMask(0) | |
| 990 | + | xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(o.requestor), &v) | |
| 991 | + | x.wake() | |
| 992 | + | } | |
| 993 | + | x.outRelease(i) | |
| 994 | + | } | |
| 995 | + | ||
| 996 | + | // outRelease frees the transfer's resources. | |
| 997 | + | func (x *XWM) outRelease(i int) { | |
| 998 | + | o := &x.sel.out[i] | |
| 999 | + | x.outUnwatch(i) | |
| 1000 | + | if o.fd >= 0 { | |
| 1001 | + | _ = unix.Close(o.fd) | |
| 1002 | + | } | |
| 1003 | + | if o.buf >= 0 { | |
| 1004 | + | x.sel.pool.put(o.buf) | |
| 1005 | + | } | |
| 1006 | + | o.fd, o.buf = -1, -1 | |
| 1007 | + | o.gen++ | |
| 1008 | + | o.state = outFree | |
| 1009 | + | } | |
| 1010 | + | ||
| 1011 | + | // outAbort ends a transfer early: the requestor is refused (before its | |
| 1012 | + | // notification) or released (INCR: mask dropped, property deleted). | |
| 1013 | + | func (x *XWM) outAbort(i int) { | |
| 1014 | + | o := &x.sel.out[i] | |
| 1015 | + | if o.state == outFree { | |
| 1016 | + | return | |
| 1017 | + | } | |
| 1018 | + | if !x.closed { | |
| 1019 | + | switch { | |
| 1020 | + | case !o.notified: | |
| 1021 | + | x.notify(o.requestor, x.sel.st[o.kind].atom, o.target, 0, o.time) | |
| 1022 | + | case o.incr: | |
| 1023 | + | var v xproto.ChangeWindowAttributesValues | |
| 1024 | + | v.SetEventMask(0) | |
| 1025 | + | xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(o.requestor), &v) | |
| 1026 | + | xproto.DeleteProperty(x.conn.Buf(), xproto.Window(o.requestor), xproto.Atom(o.property)) | |
| 1027 | + | x.wake() | |
| 1028 | + | } | |
| 1029 | + | } | |
| 1030 | + | x.outRelease(i) | |
| 1031 | + | } | |
| 1032 | + | ||
| 1033 | + | // onRequestorProperty: PropertyNotify on a window we serve by INCR. | |
| 1034 | + | func (x *XWM) onRequestorProperty(e xproto.PropertyNotifyEvent) bool { | |
| 1035 | + | if e.State() != xproto.PropertyDelete { | |
| 1036 | + | return false | |
| 1037 | + | } | |
| 1038 | + | hit := false | |
| 1039 | + | for i := range x.sel.out { | |
| 1040 | + | o := &x.sel.out[i] | |
| 1041 | + | if o.state == outFree || !o.incr || o.requestor != uint32(e.Window()) || o.property != uint32(e.Atom()) { | |
| 1042 | + | continue | |
| 1043 | + | } | |
| 1044 | + | hit = true | |
| 1045 | + | o.credit = true | |
| 1046 | + | o.idle = 0 | |
| 1047 | + | x.outPump(i) | |
| 1048 | + | } | |
| 1049 | + | return hit | |
| 1050 | + | } | |
A internal/adapters/wayland/xwm/selection_test.go +801 -0
| @@ -0,0 +1,801 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "io" | |
| 7 | + | "os" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "bnema.dev/xturbo" | |
| 12 | + | "bnema.dev/xturbo/xproto" | |
| 13 | + | "github.com/stretchr/testify/mock" | |
| 14 | + | "github.com/stretchr/testify/require" | |
| 15 | + | "golang.org/x/sys/unix" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | const peerXFixesFirstEvent = 87 | |
| 19 | + | ||
| 20 | + | // xfixesSelection sends an XFixesSelectionNotify. | |
| 21 | + | func (p *xpeer) xfixesSelection(owner, selection uint32) { | |
| 22 | + | p.event(peerXFixesFirstEvent, func(b *[32]byte) { | |
| 23 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |
| 24 | + | binary.LittleEndian.PutUint32(b[8:], owner) | |
| 25 | + | binary.LittleEndian.PutUint32(b[12:], selection) | |
| 26 | + | }) | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // selectionNotify sends a core SelectionNotify to the requestor. | |
| 30 | + | func (p *xpeer) selectionNotify(requestor, selection, target, property uint32) { | |
| 31 | + | p.event(31, func(b *[32]byte) { | |
| 32 | + | binary.LittleEndian.PutUint32(b[8:], requestor) | |
| 33 | + | binary.LittleEndian.PutUint32(b[12:], selection) | |
| 34 | + | binary.LittleEndian.PutUint32(b[16:], target) | |
| 35 | + | binary.LittleEndian.PutUint32(b[20:], property) | |
| 36 | + | }) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // pumpReq dispatches X events until a request matching match was sent after | |
| 40 | + | // the first skip requests. | |
| 41 | + | func pumpReq(t *testing.T, x *XWM, p *xpeer, skip int, match func(xpeerReq) bool) (xpeerReq, int) { | |
| 42 | + | t.Helper() | |
| 43 | + | var found xpeerReq | |
| 44 | + | next := skip | |
| 45 | + | readUntil(t, x, func() bool { | |
| 46 | + | x.flush() | |
| 47 | + | reqs := p.requests() | |
| 48 | + | for i := skip; i < len(reqs); i++ { | |
| 49 | + | if match(reqs[i]) { | |
| 50 | + | found, next = reqs[i], i+1 | |
| 51 | + | return true | |
| 52 | + | } | |
| 53 | + | } | |
| 54 | + | return false | |
| 55 | + | }) | |
| 56 | + | return found, next | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | func TestSelectionBoot(t *testing.T) { | |
| 60 | + | x, p := bootPeer(t, nil) | |
| 61 | + | require.NotZero(t, x.selWin) | |
| 62 | + | reqs := p.requests() | |
| 63 | + | var created, selected int | |
| 64 | + | for _, r := range reqs { | |
| 65 | + | switch { | |
| 66 | + | case r.major == opCreateWindow && r.u32(4) == x.selWin: | |
| 67 | + | created++ | |
| 68 | + | require.EqualValues(t, 2, binary.LittleEndian.Uint16(r.raw[22:]), "InputOnly") | |
| 69 | + | case r.major == 138 && r.minor == 2: // SelectSelectionInput | |
| 70 | + | selected++ | |
| 71 | + | require.EqualValues(t, peerRoot, r.u32(4)) | |
| 72 | + | require.Contains(t, []uint32{p.atom("CLIPBOARD"), p.atom("PRIMARY")}, r.u32(8)) | |
| 73 | + | require.EqualValues(t, 7, r.u32(12)) | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | require.Equal(t, 1, created) | |
| 77 | + | require.Equal(t, 2, selected) | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | func TestSelectionXOwnsAnnounces(t *testing.T) { | |
| 81 | + | x, p, h, _, _ := attached(t) | |
| 82 | + | const owner = 0x500010 | |
| 83 | + | clip, prim := p.atom("CLIPBOARD"), p.atom("PRIMARY") | |
| 84 | + | tgt := p.atom("TARGETS") | |
| 85 | + | ||
| 86 | + | // Owner with text targets: TARGETS probe, then the intersection. | |
| 87 | + | p.xfixesSelection(owner, clip) | |
| 88 | + | req, n := pumpReq(t, x, p, 0, func(r xpeerReq) bool { return r.major == 24 }) // ConvertSelection | |
| 89 | + | require.Equal(t, x.selWin, req.u32(4)) | |
| 90 | + | require.Equal(t, clip, req.u32(8)) | |
| 91 | + | require.Equal(t, tgt, req.u32(12)) | |
| 92 | + | prop := req.u32(16) | |
| 93 | + | require.Equal(t, p.atom("_NEFERWL_TARGETS"), prop) | |
| 94 | + | p.setProp(x.selWin, prop, 4, 32, le32(tgt, p.atom("UTF8_STRING"), p.atom("image/png"), p.atom("TEXT"))) | |
| 95 | + | got := make(chan []string, 8) | |
| 96 | + | h.EXPECT().SelectionOwned(SelClipboard, []string{"text/plain;charset=utf-8", "UTF8_STRING", "TEXT"}).Run(func(int, []string) { got <- nil }).Once() | |
| 97 | + | p.selectionNotify(x.selWin, clip, tgt, prop) | |
| 98 | + | readUntil(t, x, func() bool { return len(got) == 1 }) | |
| 99 | + | pumpReq(t, x, p, n, func(r xpeerReq) bool { return r.major == opGetProperty && r.u32(8) == prop }) | |
| 100 | + | ||
| 101 | + | // Only image/png: nothing offered. | |
| 102 | + | p.xfixesSelection(owner, prim) | |
| 103 | + | pumpReq(t, x, p, n, func(r xpeerReq) bool { return r.major == 24 && r.u32(8) == prim }) | |
| 104 | + | p.setProp(x.selWin, prop, 4, 32, le32(tgt, p.atom("image/png"))) | |
| 105 | + | h.EXPECT().SelectionOwned(SelPrimary, []string(nil)).Run(func(int, []string) { got <- nil }).Once() | |
| 106 | + | p.selectionNotify(x.selWin, prim, tgt, prop) | |
| 107 | + | readUntil(t, x, func() bool { return len(got) == 2 }) | |
| 108 | + | ||
| 109 | + | // Owner None. | |
| 110 | + | h.EXPECT().SelectionOwned(SelClipboard, []string(nil)).Run(func(int, []string) { got <- nil }).Once() | |
| 111 | + | p.xfixesSelection(0, clip) | |
| 112 | + | readUntil(t, x, func() bool { return len(got) == 3 }) | |
| 113 | + | ||
| 114 | + | // Our own window: ignored (the mock fails on any host call). | |
| 115 | + | before := len(p.requests()) | |
| 116 | + | p.xfixesSelection(x.selWin, clip) | |
| 117 | + | ev := x.Stats().Events | |
| 118 | + | readUntil(t, x, func() bool { return x.Stats().Events > ev }) | |
| 119 | + | x.flush() | |
| 120 | + | require.Equal(t, before, len(p.requests())) | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | // ---- X -> Wayland ---- | |
| 124 | + | ||
| 125 | + | type selRig struct { | |
| 126 | + | x *XWM | |
| 127 | + | p *xpeer | |
| 128 | + | h *mockhost | |
| 129 | + | clip uint32 | |
| 130 | + | utf8 uint32 | |
| 131 | + | data uint32 | |
| 132 | + | skip int | |
| 133 | + | timer func() | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | // ownedRig attaches an XWM whose CLIPBOARD is owned by an X client offering text. | |
| 137 | + | func ownedRig(t *testing.T) *selRig { | |
| 138 | + | t.Helper() | |
| 139 | + | x, p, h, _, _ := attached(t) | |
| 140 | + | r := &selRig{x: x, p: p, h: h, clip: p.atom("CLIPBOARD"), utf8: p.atom("UTF8_STRING"), data: p.atom("_NEFERWL_DATA")} | |
| 141 | + | h.EXPECT().After(mock.Anything, mock.Anything).Run(func(_ time.Duration, fn func()) { r.timer = fn }).Return(func() {}).Maybe() | |
| 142 | + | got := 0 | |
| 143 | + | h.EXPECT().SelectionOwned(SelClipboard, mock.Anything).Run(func(int, []string) { got++ }).Once() | |
| 144 | + | p.xfixesSelection(0x500010, r.clip) | |
| 145 | + | _, n := pumpReq(t, x, p, 0, func(q xpeerReq) bool { return q.major == 24 }) | |
| 146 | + | tgt := p.atom("TARGETS") | |
| 147 | + | p.setProp(x.selWin, p.atom("_NEFERWL_TARGETS"), 4, 32, le32(tgt, r.utf8)) | |
| 148 | + | p.selectionNotify(x.selWin, r.clip, tgt, p.atom("_NEFERWL_TARGETS")) | |
| 149 | + | readUntil(t, x, func() bool { return got == 1 }) | |
| 150 | + | r.skip = len(p.requests()) | |
| 151 | + | _ = n | |
| 152 | + | return r | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | // receive starts a paste and returns the read end of its pipe. | |
| 156 | + | func (r *selRig) receive(t *testing.T, size int) *os.File { | |
| 157 | + | t.Helper() | |
| 158 | + | fds := make([]int, 2) | |
| 159 | + | require.NoError(t, unix.Pipe2(fds, unix.O_CLOEXEC)) | |
| 160 | + | if size > 0 { | |
| 161 | + | _, err := unix.FcntlInt(uintptr(fds[0]), unix.F_SETPIPE_SZ, size) | |
| 162 | + | require.NoError(t, err) | |
| 163 | + | } | |
| 164 | + | r.x.SelectionReceive(SelClipboard, "text/plain;charset=utf-8", fds[1]) | |
| 165 | + | r.x.flush() | |
| 166 | + | f := os.NewFile(uintptr(fds[0]), "pipe-r") | |
| 167 | + | t.Cleanup(func() { f.Close() }) | |
| 168 | + | return f | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | func (r *selRig) waitGet(t *testing.T) { | |
| 172 | + | t.Helper() | |
| 173 | + | _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opGetProperty && q.u32(8) == r.data }) | |
| 174 | + | r.skip = n | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | func pattern(n int) []byte { | |
| 178 | + | b := make([]byte, n) | |
| 179 | + | for i := range b { | |
| 180 | + | b[i] = byte(i*7 + i/251) | |
| 181 | + | } | |
| 182 | + | return b | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | func TestSelectionXToWaylandSmall(t *testing.T) { | |
| 186 | + | r := ownedRig(t) | |
| 187 | + | f := r.receive(t, 0) | |
| 188 | + | req, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 }) | |
| 189 | + | require.Equal(t, r.x.selWin, req.u32(4)) | |
| 190 | + | require.Equal(t, r.clip, req.u32(8)) | |
| 191 | + | require.Equal(t, r.utf8, req.u32(12)) | |
| 192 | + | require.Equal(t, r.data, req.u32(16)) | |
| 193 | + | r.skip = n | |
| 194 | + | r.p.setProp(r.x.selWin, r.data, r.utf8, 8, []byte("hello clipboard")) | |
| 195 | + | r.p.selectionNotify(r.x.selWin, r.clip, r.utf8, r.data) | |
| 196 | + | r.waitGet(t) | |
| 197 | + | buf := make([]byte, 64) | |
| 198 | + | var got []byte | |
| 199 | + | readUntil(t, r.x, func() bool { | |
| 200 | + | m, _ := unix.Read(int(f.Fd()), buf[:0:0]) | |
| 201 | + | _ = m | |
| 202 | + | return r.x.sel.in[0].state == inFree | |
| 203 | + | }) | |
| 204 | + | m, err := f.Read(buf) | |
| 205 | + | require.NoError(t, err) | |
| 206 | + | got = buf[:m] | |
| 207 | + | require.Equal(t, "hello clipboard", string(got)) | |
| 208 | + | _, ok := r.p.prop(r.x.selWin, "_NEFERWL_DATA") | |
| 209 | + | require.False(t, ok, "property deleted by the read") | |
| 210 | + | _, err = f.Read(buf) | |
| 211 | + | require.ErrorIs(t, err, io.EOF, "write end closed") | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | func TestSelectionXToWaylandRefused(t *testing.T) { | |
| 215 | + | r := ownedRig(t) | |
| 216 | + | f := r.receive(t, 0) | |
| 217 | + | _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 }) | |
| 218 | + | r.skip = n | |
| 219 | + | r.p.selectionNotify(r.x.selWin, r.clip, r.utf8, 0) | |
| 220 | + | readUntil(t, r.x, func() bool { return r.x.sel.in[0].state == inFree }) | |
| 221 | + | _, err := f.Read(make([]byte, 8)) | |
| 222 | + | require.ErrorIs(t, err, io.EOF) | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | func TestSelectionXToWaylandINCR(t *testing.T) { | |
| 226 | + | r := ownedRig(t) | |
| 227 | + | const total = 1 << 20 | |
| 228 | + | want := pattern(total) | |
| 229 | + | f := r.receive(t, 1<<20) | |
| 230 | + | _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 }) | |
| 231 | + | r.skip = n | |
| 232 | + | r.p.setProp(r.x.selWin, r.data, r.p.atom("INCR"), 32, le32(total)) | |
| 233 | + | r.p.selectionNotify(r.x.selWin, r.clip, r.utf8, r.data) | |
| 234 | + | r.waitGet(t) // reads and deletes the INCR marker | |
| 235 | + | gets := 1 | |
| 236 | + | for off := 0; off <= total; off += chunk { | |
| 237 | + | end := min(off+chunk, total) | |
| 238 | + | r.p.setProp(r.x.selWin, r.data, r.utf8, 8, want[off:end]) | |
| 239 | + | r.p.propertyNotify(r.x.selWin, r.data, false) | |
| 240 | + | r.waitGet(t) | |
| 241 | + | gets++ | |
| 242 | + | } | |
| 243 | + | readUntil(t, r.x, func() bool { return r.x.sel.in[0].state == inFree }) | |
| 244 | + | got, err := io.ReadAll(f) | |
| 245 | + | require.NoError(t, err) | |
| 246 | + | require.True(t, bytes.Equal(want, got), "byte-exact, got %d", len(got)) | |
| 247 | + | require.Equal(t, 1+total/chunk+1, gets, "marker, 16 chunks, empty terminator") | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | // A reader that does not drain: no further GetProperty until the pipe has room. | |
| 251 | + | func TestSelectionXToWaylandBackpressure(t *testing.T) { | |
| 252 | + | r := ownedRig(t) | |
| 253 | + | src := newMockfdSource(t) | |
| 254 | + | var onW func() | |
| 255 | + | r.x.lp = newFDLoop(t, r, src, nil, &onW) | |
| 256 | + | src.EXPECT().SetWritable(true).Return(nil).Once() | |
| 257 | + | src.EXPECT().SetWritable(false).Return(nil).Once() | |
| 258 | + | src.EXPECT().Remove().Return().Once() | |
| 259 | + | f := r.receive(t, 0) // default 64 KiB pipe | |
| 260 | + | _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 }) | |
| 261 | + | r.skip = n | |
| 262 | + | r.p.setProp(r.x.selWin, r.data, r.p.atom("INCR"), 32, le32(3*chunk)) | |
| 263 | + | r.p.selectionNotify(r.x.selWin, r.clip, r.utf8, r.data) | |
| 264 | + | r.waitGet(t) | |
| 265 | + | want := pattern(3 * chunk) | |
| 266 | + | chunkN := func(k int) { | |
| 267 | + | r.p.setProp(r.x.selWin, r.data, r.utf8, 8, want[k*chunk:(k+1)*chunk]) | |
| 268 | + | r.p.propertyNotify(r.x.selWin, r.data, false) | |
| 269 | + | } | |
| 270 | + | chunkN(0) // fills the pipe exactly | |
| 271 | + | r.waitGet(t) | |
| 272 | + | readUntil(t, r.x, func() bool { return r.x.sel.in[0].state == inWaitIncr }) | |
| 273 | + | chunkN(1) // nobody reads: EAGAIN, the buffer stays pending | |
| 274 | + | r.waitGet(t) | |
| 275 | + | readUntil(t, r.x, func() bool { return r.x.sel.in[0].state == inWriting }) | |
| 276 | + | require.NotNil(t, onW) | |
| 277 | + | ||
| 278 | + | gets := getProps(r.p, 0, r.x.selWin) | |
| 279 | + | chunkN(2) // announced while a buffer is pending | |
| 280 | + | ev := r.x.Stats().Events | |
| 281 | + | readUntil(t, r.x, func() bool { return r.x.Stats().Events > ev }) | |
| 282 | + | r.x.flush() | |
| 283 | + | require.Equal(t, gets, getProps(r.p, 0, r.x.selWin), "no GetProperty while a buffer is pending") | |
| 284 | + | ||
| 285 | + | got := make([]byte, 0, 3*chunk) | |
| 286 | + | buf := make([]byte, chunk) | |
| 287 | + | m, err := f.Read(buf) // the reader wakes: room again | |
| 288 | + | require.NoError(t, err) | |
| 289 | + | got = append(got, buf[:m]...) | |
| 290 | + | onW() | |
| 291 | + | require.Equal(t, inReading, r.x.sel.in[0].state, "the pending announcement is served once drained") | |
| 292 | + | r.waitGet(t) | |
| 293 | + | _ = got | |
| 294 | + | r.x.selClose() | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | func TestSelectionTimeouts(t *testing.T) { | |
| 298 | + | r := ownedRig(t) | |
| 299 | + | f := r.receive(t, 0) | |
| 300 | + | _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 }) | |
| 301 | + | r.skip = n | |
| 302 | + | require.NotNil(t, r.timer) | |
| 303 | + | // X stays silent after ConvertSelection: nine ticks keep it alive, the tenth aborts. | |
| 304 | + | for i := 0; i < xIdleMax-1; i++ { | |
| 305 | + | fn := r.timer | |
| 306 | + | fn() | |
| 307 | + | require.Equal(t, inConverting, r.x.sel.in[0].state, i) | |
| 308 | + | } | |
| 309 | + | r.timer() | |
| 310 | + | require.Equal(t, inFree, r.x.sel.in[0].state) | |
| 311 | + | _, err := f.Read(make([]byte, 8)) | |
| 312 | + | require.ErrorIs(t, err, io.EOF, "fd closed") | |
| 313 | + | pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opDeleteProperty && q.u32(8) == r.data }) | |
| 314 | + | require.Equal(t, nXfer, r.x.sel.pool.n) | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | func TestSelectionPoolExhausted(t *testing.T) { | |
| 318 | + | r := ownedRig(t) | |
| 319 | + | var files []*os.File | |
| 320 | + | for i := 0; i < nXfer; i++ { | |
| 321 | + | files = append(files, r.receive(t, 0)) | |
| 322 | + | } | |
| 323 | + | fifth := r.receive(t, 0) | |
| 324 | + | _, err := fifth.Read(make([]byte, 8)) | |
| 325 | + | require.ErrorIs(t, err, io.EOF, "the fifth paste is refused with its pipe closed") | |
| 326 | + | for i := range r.x.sel.in { | |
| 327 | + | require.NotEqual(t, inFree, r.x.sel.in[i].state, i) | |
| 328 | + | } | |
| 329 | + | _ = files | |
| 330 | + | r.x.selClose() | |
| 331 | + | for i := range r.x.sel.in { | |
| 332 | + | require.Equal(t, inFree, r.x.sel.in[i].state, i) | |
| 333 | + | } | |
| 334 | + | } | |
| 335 | + | ||
| 336 | + | // allocRig is a rig whose peer sinks requests and whose timer seam is inert. | |
| 337 | + | func allocRig(t *testing.T) *selRig { | |
| 338 | + | r := ownedRig(t) | |
| 339 | + | sinkPeer(t, r.x, r.p) | |
| 340 | + | r.x.lp = newTimerLoop(t) | |
| 341 | + | r.x.sel.timerOn = true // no timer re-arming in the measured loop | |
| 342 | + | return r | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | func dataReply(r *selRig, typ uint32, payload []byte) *xturbo.Reply { | |
| 346 | + | raw := make([]byte, 32+(len(payload)+3)&^3) | |
| 347 | + | raw[0], raw[1] = 1, 8 | |
| 348 | + | binary.LittleEndian.PutUint32(raw[8:], typ) | |
| 349 | + | binary.LittleEndian.PutUint32(raw[16:], uint32(len(payload))) | |
| 350 | + | copy(raw[32:], payload) | |
| 351 | + | return &xturbo.Reply{Kind: kindSelData, Raw: raw} | |
| 352 | + | } | |
| 353 | + | ||
| 354 | + | func TestSelectionCopyAllocations(t *testing.T) { | |
| 355 | + | r := allocRig(t) | |
| 356 | + | x := r.x | |
| 357 | + | fds := make([]int, 2) | |
| 358 | + | require.NoError(t, unix.Pipe2(fds, unix.O_CLOEXEC)) | |
| 359 | + | t.Cleanup(func() { unix.Close(fds[0]); unix.Close(fds[1]) }) | |
| 360 | + | payload := pattern(4096) | |
| 361 | + | rep := dataReply(r, r.utf8, payload) | |
| 362 | + | var note xturbo.Event | |
| 363 | + | note.Kind = xproto.KindSelectionNotify | |
| 364 | + | binary.LittleEndian.PutUint32(note.Raw[8:], x.selWin) | |
| 365 | + | binary.LittleEndian.PutUint32(note.Raw[12:], r.clip) | |
| 366 | + | binary.LittleEndian.PutUint32(note.Raw[16:], r.utf8) | |
| 367 | + | binary.LittleEndian.PutUint32(note.Raw[20:], r.data) | |
| 368 | + | sink := make([]byte, 8192) | |
| 369 | + | mime := textMimes[0] | |
| 370 | + | round := func() { | |
| 371 | + | fd, err := unix.FcntlInt(uintptr(fds[1]), unix.F_DUPFD_CLOEXEC, 0) | |
| 372 | + | if err != nil { | |
| 373 | + | t.Fatal("dup") | |
| 374 | + | } | |
| 375 | + | x.SelectionReceive(SelClipboard, mime, fd) | |
| 376 | + | x.onSelectionNotify(¬e) | |
| 377 | + | rep.Args = [4]uint32{0, x.sel.in[0].gen} | |
| 378 | + | x.onSelDataReply(rep) | |
| 379 | + | if m, err := unix.Read(fds[0], sink); err != nil || m != 4096 { | |
| 380 | + | t.Fatal("read") | |
| 381 | + | } | |
| 382 | + | x.posted = false | |
| 383 | + | x.flush() | |
| 384 | + | } | |
| 385 | + | round() | |
| 386 | + | require.Zero(t, testing.AllocsPerRun(100, round)) | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | func TestSelectionINCRChunkAllocations(t *testing.T) { | |
| 390 | + | r := allocRig(t) | |
| 391 | + | x := r.x | |
| 392 | + | fds := make([]int, 2) | |
| 393 | + | require.NoError(t, unix.Pipe2(fds, unix.O_CLOEXEC)) | |
| 394 | + | t.Cleanup(func() { unix.Close(fds[0]); unix.Close(fds[1]) }) | |
| 395 | + | x.SelectionReceive(SelClipboard, textMimes[0], fds[1]) | |
| 396 | + | var note xturbo.Event | |
| 397 | + | note.Kind = xproto.KindSelectionNotify | |
| 398 | + | binary.LittleEndian.PutUint32(note.Raw[8:], x.selWin) | |
| 399 | + | binary.LittleEndian.PutUint32(note.Raw[12:], r.clip) | |
| 400 | + | binary.LittleEndian.PutUint32(note.Raw[16:], r.utf8) | |
| 401 | + | binary.LittleEndian.PutUint32(note.Raw[20:], r.data) | |
| 402 | + | x.onSelectionNotify(¬e) | |
| 403 | + | x.onSelDataReply(dataReply(r, r.p.atom("INCR"), le32(1<<20))) | |
| 404 | + | require.Equal(t, inWaitIncr, x.sel.in[0].state) | |
| 405 | + | rep := dataReply(r, r.utf8, pattern(chunk)) | |
| 406 | + | var pn xturbo.Event | |
| 407 | + | pn.Kind = xproto.KindPropertyNotify | |
| 408 | + | binary.LittleEndian.PutUint32(pn.Raw[4:], x.selWin) | |
| 409 | + | binary.LittleEndian.PutUint32(pn.Raw[8:], r.data) | |
| 410 | + | sink := make([]byte, chunk) | |
| 411 | + | step := func() { | |
| 412 | + | x.onSelProperty(&pn) | |
| 413 | + | rep.Args = [4]uint32{0, x.sel.in[0].gen} | |
| 414 | + | x.onSelDataReply(rep) | |
| 415 | + | if m, err := unix.Read(fds[0], sink); err != nil || m != chunk { | |
| 416 | + | t.Fatal("read") | |
| 417 | + | } | |
| 418 | + | x.posted = false | |
| 419 | + | x.flush() | |
| 420 | + | } | |
| 421 | + | step() | |
| 422 | + | require.Zero(t, testing.AllocsPerRun(100, step)) | |
| 423 | + | require.Equal(t, inWaitIncr, x.sel.in[0].state) | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | // ---- Wayland -> X ---- | |
| 427 | + | ||
| 428 | + | const reqWin = 0x500020 | |
| 429 | + | ||
| 430 | + | func (p *xpeer) selectionRequest(owner, requestor, selection, target, property uint32) { | |
| 431 | + | p.event(30, func(b *[32]byte) { | |
| 432 | + | binary.LittleEndian.PutUint32(b[8:], owner) | |
| 433 | + | binary.LittleEndian.PutUint32(b[12:], requestor) | |
| 434 | + | binary.LittleEndian.PutUint32(b[16:], selection) | |
| 435 | + | binary.LittleEndian.PutUint32(b[20:], target) | |
| 436 | + | binary.LittleEndian.PutUint32(b[24:], property) | |
| 437 | + | }) | |
| 438 | + | } | |
| 439 | + | ||
| 440 | + | func (p *xpeer) selectionClear(owner, selection uint32) { | |
| 441 | + | p.event(29, func(b *[32]byte) { | |
| 442 | + | binary.LittleEndian.PutUint32(b[8:], owner) | |
| 443 | + | binary.LittleEndian.PutUint32(b[12:], selection) | |
| 444 | + | }) | |
| 445 | + | } | |
| 446 | + | ||
| 447 | + | // sentNotify returns the property of the SelectionNotify SendEvent after skip. | |
| 448 | + | func sentNotify(t *testing.T, r *selRig) (property uint32) { | |
| 449 | + | t.Helper() | |
| 450 | + | req, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opSendEvent && q.raw[12] == 31 }) | |
| 451 | + | r.skip = n | |
| 452 | + | require.EqualValues(t, reqWin, req.u32(4)) | |
| 453 | + | require.Equal(t, r.clip, binary.LittleEndian.Uint32(req.raw[12+12:])) | |
| 454 | + | return binary.LittleEndian.Uint32(req.raw[12+20:]) | |
| 455 | + | } | |
| 456 | + | ||
| 457 | + | // ownedByWayland attaches an XWM and gives CLIPBOARD to a Wayland client offering mimes. | |
| 458 | + | func ownedByWayland(t *testing.T, mimes ...string) *selRig { | |
| 459 | + | t.Helper() | |
| 460 | + | x, p, h, _, _ := attached(t) | |
| 461 | + | r := &selRig{x: x, p: p, h: h, clip: p.atom("CLIPBOARD"), utf8: p.atom("UTF8_STRING"), data: p.atom("_NEFERWL_DATA")} | |
| 462 | + | h.EXPECT().After(mock.Anything, mock.Anything).Run(func(_ time.Duration, fn func()) { r.timer = fn }).Return(func() {}).Maybe() | |
| 463 | + | x.WaylandSelection(SelClipboard, mimes) | |
| 464 | + | x.flush() | |
| 465 | + | req, n := pumpReq(t, x, p, 0, func(q xpeerReq) bool { return q.major == opSetSelectionOwner && q.u32(8) == r.clip }) | |
| 466 | + | require.Equal(t, x.selWin, req.u32(4)) | |
| 467 | + | r.skip = n | |
| 468 | + | return r | |
| 469 | + | } | |
| 470 | + | ||
| 471 | + | // serve queues a SelectionRequest; SelectionRead hands out the read end of a | |
| 472 | + | // pipe whose write end is returned. | |
| 473 | + | func (r *selRig) serve(t *testing.T, target uint32) (w *os.File) { | |
| 474 | + | t.Helper() | |
| 475 | + | fds := make([]int, 2) | |
| 476 | + | require.NoError(t, unix.Pipe2(fds, unix.O_CLOEXEC|unix.O_NONBLOCK)) | |
| 477 | + | r.h.EXPECT().SelectionRead(SelClipboard, mock.Anything).Return(fds[0]).Once() | |
| 478 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, target, r.data) | |
| 479 | + | w = os.NewFile(uintptr(fds[1]), "pipe-w") | |
| 480 | + | t.Cleanup(func() { w.Close() }) | |
| 481 | + | return w | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | func TestSelectionWaylandToXTargets(t *testing.T) { | |
| 485 | + | r := ownedByWayland(t, "text/plain") | |
| 486 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("TARGETS"), r.data) | |
| 487 | + | require.Equal(t, r.data, sentNotify(t, r)) | |
| 488 | + | v, ok := r.p.prop(reqWin, "_NEFERWL_DATA") | |
| 489 | + | require.True(t, ok) | |
| 490 | + | require.Equal(t, []uint32{r.p.atom("TARGETS"), r.p.atom("TIMESTAMP"), r.p.atom("STRING")}, atomsOf(v), "no UTF8 target for text/plain only") | |
| 491 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("image/png"), r.data) | |
| 492 | + | require.Zero(t, sentNotify(t, r), "unknown target refused") | |
| 493 | + | // UTF8_STRING is not offered by a text/plain-only source, but STRING reads it. | |
| 494 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.utf8, r.data) | |
| 495 | + | require.Zero(t, sentNotify(t, r), "no UTF-8 text offered") | |
| 496 | + | } | |
| 497 | + | ||
| 498 | + | func TestSelectionWaylandToXSmall(t *testing.T) { | |
| 499 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |
| 500 | + | w := r.serve(t, r.utf8) | |
| 501 | + | data := pattern(4096) | |
| 502 | + | _, err := w.Write(data) | |
| 503 | + | require.NoError(t, err) | |
| 504 | + | require.NoError(t, w.Close()) | |
| 505 | + | require.Equal(t, r.data, sentNotify(t, r)) | |
| 506 | + | v, ok := r.p.prop(reqWin, "_NEFERWL_DATA") | |
| 507 | + | require.True(t, ok) | |
| 508 | + | require.Equal(t, r.utf8, v.typ) | |
| 509 | + | require.True(t, bytes.Equal(data, v.data)) | |
| 510 | + | require.Equal(t, nXfer, r.x.sel.pool.n) | |
| 511 | + | } | |
| 512 | + | ||
| 513 | + | func TestSelectionWaylandToXINCR(t *testing.T) { | |
| 514 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |
| 515 | + | src := newMockfdSource(t) | |
| 516 | + | var onR func() | |
| 517 | + | removes := 0 | |
| 518 | + | r.x.lp = newFDLoop(t, r, src, &onR, nil) | |
| 519 | + | src.EXPECT().Remove().Run(func() { removes++ }).Return().Maybe() | |
| 520 | + | const total = 1<<20 + 123 | |
| 521 | + | data := pattern(total) | |
| 522 | + | w := r.serve(t, r.utf8) | |
| 523 | + | require.NoError(t, unix.SetNonblock(int(w.Fd()), false)) | |
| 524 | + | go func() { _, _ = w.Write(data); w.Close() }() | |
| 525 | + | ||
| 526 | + | // The first fill is a whole chunk: INCR is announced. | |
| 527 | + | var got []byte | |
| 528 | + | deadline := time.Now().Add(5 * time.Second) | |
| 529 | + | for !r.x.sel.out[0].incr { | |
| 530 | + | require.True(t, time.Now().Before(deadline), "INCR never announced") | |
| 531 | + | r.x.onReadable() | |
| 532 | + | if onR != nil && r.x.sel.out[0].state == outActive { | |
| 533 | + | onR() | |
| 534 | + | } | |
| 535 | + | time.Sleep(time.Millisecond) | |
| 536 | + | } | |
| 537 | + | require.Equal(t, r.data, sentNotify(t, r)) | |
| 538 | + | v, _ := r.p.prop(reqWin, "_NEFERWL_DATA") | |
| 539 | + | require.Equal(t, r.p.atom("INCR"), v.typ) | |
| 540 | + | mask, _ := pumpReq(t, r.x, r.p, 0, func(q xpeerReq) bool { | |
| 541 | + | return q.major == opChangeWindowAttributes && q.u32(4) == reqWin && q.u32(12) == uint32(xproto.EventMaskPropertyChange) | |
| 542 | + | }) | |
| 543 | + | require.NotZero(t, mask.raw) | |
| 544 | + | ||
| 545 | + | chunks := 0 | |
| 546 | + | for { | |
| 547 | + | // The requestor deletes the property: the next chunk goes out. | |
| 548 | + | r.p.propertyNotify(reqWin, r.data, true) | |
| 549 | + | before := len(r.p.requests()) | |
| 550 | + | if onR != nil { | |
| 551 | + | time.Sleep(time.Millisecond) | |
| 552 | + | } | |
| 553 | + | var cp xpeerProp | |
| 554 | + | readUntil(t, r.x, func() bool { | |
| 555 | + | if onR != nil && r.x.sel.out[0].state != outFree && r.x.sel.out[0].n < chunk && !r.x.sel.out[0].eof { | |
| 556 | + | onR() | |
| 557 | + | } | |
| 558 | + | r.x.flush() | |
| 559 | + | for _, q := range r.p.requests()[before:] { | |
| 560 | + | if q.major == opChangeProperty && q.u32(4) == reqWin { | |
| 561 | + | return true | |
| 562 | + | } | |
| 563 | + | } | |
| 564 | + | return false | |
| 565 | + | }) | |
| 566 | + | cp, _ = r.p.prop(reqWin, "_NEFERWL_DATA") | |
| 567 | + | chunks++ | |
| 568 | + | if len(cp.data) == 0 { | |
| 569 | + | break | |
| 570 | + | } | |
| 571 | + | require.Equal(t, r.utf8, cp.typ) | |
| 572 | + | got = append(got, cp.data...) | |
| 573 | + | } | |
| 574 | + | require.True(t, bytes.Equal(data, got), "byte-exact: %d of %d", len(got), total) | |
| 575 | + | require.Equal(t, total/chunk+2, chunks, "16 full chunks, the tail, then the empty terminator") | |
| 576 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].state == outFree }) | |
| 577 | + | // The mask is removed at the end. | |
| 578 | + | reqs := r.p.requests() | |
| 579 | + | last := reqs[len(reqs)-1] | |
| 580 | + | for i := len(reqs) - 1; i >= 0; i-- { | |
| 581 | + | if reqs[i].major == opChangeWindowAttributes && reqs[i].u32(4) == reqWin { | |
| 582 | + | last = reqs[i] | |
| 583 | + | break | |
| 584 | + | } | |
| 585 | + | } | |
| 586 | + | require.Zero(t, last.u32(12), "event mask dropped") | |
| 587 | + | require.Equal(t, nXfer, r.x.sel.pool.n) | |
| 588 | + | } | |
| 589 | + | ||
| 590 | + | func TestSelectionWaylandToXTimeoutAndClear(t *testing.T) { | |
| 591 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |
| 592 | + | src := newMockfdSource(t) | |
| 593 | + | src.EXPECT().Remove().Return().Maybe() | |
| 594 | + | r.x.lp = newFDLoop(t, r, src, nil, nil) | |
| 595 | + | w := r.serve(t, r.utf8) | |
| 596 | + | _, err := w.Write(pattern(chunk)) // a full chunk: INCR | |
| 597 | + | require.NoError(t, err) | |
| 598 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].incr }) | |
| 599 | + | require.Equal(t, r.data, sentNotify(t, r)) | |
| 600 | + | require.NotNil(t, r.timer) | |
| 601 | + | for i := 0; i < xIdleMax; i++ { | |
| 602 | + | require.Equal(t, outActive, r.x.sel.out[0].state, i) | |
| 603 | + | r.timer() | |
| 604 | + | } | |
| 605 | + | require.Equal(t, outFree, r.x.sel.out[0].state, "no Deleted for 10 s aborts") | |
| 606 | + | pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opDeleteProperty && q.u32(4) == reqWin }) | |
| 607 | + | ||
| 608 | + | // SelectionClear aborts a running transfer and refuses further requests. | |
| 609 | + | w2 := r.serve(t, r.utf8) | |
| 610 | + | _, err = w2.Write(pattern(chunk)) | |
| 611 | + | require.NoError(t, err) | |
| 612 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].state == outActive }) | |
| 613 | + | r.p.selectionClear(r.x.selWin, r.clip) | |
| 614 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].state == outFree }) | |
| 615 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.utf8, r.data) | |
| 616 | + | require.Equal(t, r.data, sentNotify(t, r), "INCR announcement of the aborted transfer") | |
| 617 | + | require.Zero(t, sentNotify(t, r), "request after the clear is refused") | |
| 618 | + | } | |
| 619 | + | ||
| 620 | + | // Replacing the Wayland selection mid-transfer aborts it; nil releases X ownership. | |
| 621 | + | func TestSelectionClearAndRace(t *testing.T) { | |
| 622 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |
| 623 | + | src := newMockfdSource(t) | |
| 624 | + | src.EXPECT().Remove().Return().Maybe() | |
| 625 | + | r.x.lp = newFDLoop(t, r, src, nil, nil) | |
| 626 | + | w := r.serve(t, r.utf8) | |
| 627 | + | _, err := w.Write(pattern(chunk)) | |
| 628 | + | require.NoError(t, err) | |
| 629 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].incr }) | |
| 630 | + | r.x.WaylandSelection(SelClipboard, []string{"text/plain"}) | |
| 631 | + | require.Equal(t, outFree, r.x.sel.out[0].state) | |
| 632 | + | r.x.WaylandSelection(SelClipboard, nil) // protected or cleared | |
| 633 | + | r.x.flush() | |
| 634 | + | req, _ := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opSetSelectionOwner && q.u32(4) == 0 }) | |
| 635 | + | require.Equal(t, r.clip, req.u32(8), "ownership released") | |
| 636 | + | require.False(t, r.x.sel.st[SelClipboard].ours) | |
| 637 | + | } | |
| 638 | + | ||
| 639 | + | func TestSelectionCopyAllocationsWaylandToX(t *testing.T) { | |
| 640 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |
| 641 | + | x := r.x | |
| 642 | + | sinkPeer(t, x, r.p) | |
| 643 | + | x.lp = newTimerLoop(t) | |
| 644 | + | x.sel.timerOn = true | |
| 645 | + | data := pattern(4096) | |
| 646 | + | var ev xturbo.Event | |
| 647 | + | ev.Kind = xproto.KindSelectionRequest | |
| 648 | + | binary.LittleEndian.PutUint32(ev.Raw[8:], x.selWin) | |
| 649 | + | binary.LittleEndian.PutUint32(ev.Raw[12:], reqWin) | |
| 650 | + | binary.LittleEndian.PutUint32(ev.Raw[16:], r.clip) | |
| 651 | + | binary.LittleEndian.PutUint32(ev.Raw[20:], r.utf8) | |
| 652 | + | binary.LittleEndian.PutUint32(ev.Raw[24:], r.data) | |
| 653 | + | fds := make([]int, 2) | |
| 654 | + | x.sel.st[SelClipboard].wlMime = [len(textMimes)]string{"text/plain;charset=utf-8"} | |
| 655 | + | r.h.EXPECT().SelectionRead(SelClipboard, mock.Anything).RunAndReturn(func(int, string) int { return fds[0] }).Maybe() | |
| 656 | + | // The host mock allocates per call (call recording); its cost is measured | |
| 657 | + | // alone and subtracted, so the request path itself must add nothing. | |
| 658 | + | x.post = nil // the test post queue grows by append | |
| 659 | + | base := testing.AllocsPerRun(100, func() { r.h.Calls = r.h.Calls[:0]; x.host.SelectionRead(SelClipboard, "text/plain;charset=utf-8") }) | |
| 660 | + | allocs := testing.AllocsPerRun(100, func() { | |
| 661 | + | if unix.Pipe2(fds, unix.O_CLOEXEC|unix.O_NONBLOCK) != nil { | |
| 662 | + | t.Fatal("pipe") | |
| 663 | + | } | |
| 664 | + | if _, err := unix.Write(fds[1], data); err != nil { | |
| 665 | + | t.Fatal("write") | |
| 666 | + | } | |
| 667 | + | unix.Close(fds[1]) | |
| 668 | + | r.h.Calls = r.h.Calls[:0] // the mock's call log would grow | |
| 669 | + | x.onSelectionRequest(&ev) | |
| 670 | + | x.posted = false | |
| 671 | + | x.flush() | |
| 672 | + | }) | |
| 673 | + | // Pipe2 itself does not allocate; the measured path is read + ChangeProperty + SendEvent. | |
| 674 | + | // The slack covers the in-process X peer goroutine reading the flushed bytes. | |
| 675 | + | require.LessOrEqual(t, allocs, base+8, "request path allocates beyond the host call") | |
| 676 | + | require.Equal(t, outFree, x.sel.out[0].state) | |
| 677 | + | } | |
| 678 | + | ||
| 679 | + | // A TARGETS reply that arrives after Wayland took the selection is dropped. | |
| 680 | + | func TestSelectionStaleTargetsAfterWaylandOwner(t *testing.T) { | |
| 681 | + | x, p, h, _, _ := attached(t) | |
| 682 | + | const owner = 0x500010 | |
| 683 | + | clip := p.atom("CLIPBOARD") | |
| 684 | + | tgt := p.atom("TARGETS") | |
| 685 | + | p.xfixesSelection(owner, clip) | |
| 686 | + | req, _ := pumpReq(t, x, p, 0, func(r xpeerReq) bool { return r.major == 24 }) | |
| 687 | + | prop := req.u32(16) | |
| 688 | + | x.WaylandSelection(SelClipboard, []string{"text/plain"}) | |
| 689 | + | p.setProp(x.selWin, prop, 4, 32, le32(tgt, p.atom("UTF8_STRING"))) | |
| 690 | + | p.selectionNotify(x.selWin, clip, tgt, prop) // the mock fails on SelectionOwned | |
| 691 | + | ev := x.Stats().Events | |
| 692 | + | readUntil(t, x, func() bool { return x.Stats().Events > ev }) | |
| 693 | + | x.flush() | |
| 694 | + | require.True(t, x.sel.st[SelClipboard].ours) | |
| 695 | + | for i := 0; i < 20; i++ { // let any (wrong) GetProperty round trip finish | |
| 696 | + | x.onReadable() | |
| 697 | + | x.flush() | |
| 698 | + | time.Sleep(time.Millisecond) | |
| 699 | + | } | |
| 700 | + | require.Zero(t, getProps(p, 0, x.selWin), "the stale TARGETS reply is not fetched") | |
| 701 | + | _ = h | |
| 702 | + | } | |
| 703 | + | ||
| 704 | + | // newFDLoop is a loop whose AddFD hands out src and captures the callbacks; | |
| 705 | + | // timers are inert. | |
| 706 | + | func newFDLoop(t *testing.T, rig *selRig, src fdSource, onR, onW *func()) *mockloop { | |
| 707 | + | l := newMockloop(t) | |
| 708 | + | l.EXPECT().AddFD(mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 709 | + | func(_ int, r, w func()) (fdSource, error) { | |
| 710 | + | if onR != nil { | |
| 711 | + | *onR = r | |
| 712 | + | } | |
| 713 | + | if onW != nil { | |
| 714 | + | *onW = w | |
| 715 | + | } | |
| 716 | + | return src, nil | |
| 717 | + | }).Maybe() | |
| 718 | + | l.EXPECT().After(mock.Anything, mock.Anything).RunAndReturn( | |
| 719 | + | func(_ time.Duration, fn func()) func() { rig.timer = fn; return func() {} }).Maybe() | |
| 720 | + | return l | |
| 721 | + | } | |
| 722 | + | ||
| 723 | + | // newTimerLoop is a loop with inert timers. | |
| 724 | + | func newTimerLoop(t *testing.T) *mockloop { | |
| 725 | + | l := newMockloop(t) | |
| 726 | + | l.EXPECT().After(mock.Anything, mock.Anything).RunAndReturn( | |
| 727 | + | func(time.Duration, func()) func() { return func() {} }).Maybe() | |
| 728 | + | return l | |
| 729 | + | } | |
| 730 | + | ||
| 731 | + | // A late SelectionNotify of an aborted transfer (other selection or property) | |
| 732 | + | // is not taken by the next transfer. | |
| 733 | + | func TestSelectionLateReplyIgnored(t *testing.T) { | |
| 734 | + | r := ownedRig(t) | |
| 735 | + | _ = r.receive(t, 0) | |
| 736 | + | _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 }) | |
| 737 | + | r.skip = n | |
| 738 | + | for i := 0; i < xIdleMax; i++ { | |
| 739 | + | r.timer() | |
| 740 | + | } | |
| 741 | + | require.Equal(t, inFree, r.x.sel.in[0].state) | |
| 742 | + | _ = r.receive(t, 0) | |
| 743 | + | require.Equal(t, inConverting, r.x.sel.in[0].state) | |
| 744 | + | prim := r.p.atom("PRIMARY") | |
| 745 | + | r.p.selectionNotify(r.x.selWin, prim, r.utf8, r.data) | |
| 746 | + | r.p.selectionNotify(r.x.selWin, r.clip, r.utf8, r.p.atom("_NEFERWL_TARGETS")) | |
| 747 | + | ev := r.x.Stats().Events | |
| 748 | + | readUntil(t, r.x, func() bool { return r.x.Stats().Events >= ev+2 }) | |
| 749 | + | require.Equal(t, inConverting, r.x.sel.in[0].state) | |
| 750 | + | } | |
| 751 | + | ||
| 752 | + | // CloseConn closes the fds of in-flight transfers. | |
| 753 | + | func TestSelectionCloseConnClosesFDs(t *testing.T) { | |
| 754 | + | r := ownedRig(t) | |
| 755 | + | f := r.receive(t, 0) | |
| 756 | + | r.x.CloseConn() | |
| 757 | + | _, err := f.Read(make([]byte, 8)) | |
| 758 | + | require.ErrorIs(t, err, io.EOF) | |
| 759 | + | } | |
| 760 | + | ||
| 761 | + | // Closing never starts a queued transfer's ConvertSelection. | |
| 762 | + | func TestSelectionCloseDoesNotStartQueued(t *testing.T) { | |
| 763 | + | r := ownedRig(t) | |
| 764 | + | _ = r.receive(t, 0) | |
| 765 | + | _ = r.receive(t, 0) | |
| 766 | + | _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 }) | |
| 767 | + | r.x.selClose() | |
| 768 | + | r.x.flush() | |
| 769 | + | time.Sleep(50 * time.Millisecond) | |
| 770 | + | cs := 0 | |
| 771 | + | for _, q := range since(r.p, r.skip, 24) { | |
| 772 | + | cs++ | |
| 773 | + | _ = q | |
| 774 | + | } | |
| 775 | + | _ = n | |
| 776 | + | require.Equal(t, 1, cs) | |
| 777 | + | require.False(t, r.x.sel.timerOn) | |
| 778 | + | } | |
| 779 | + | ||
| 780 | + | func TestSelectionWaylandToXTimestamp(t *testing.T) { | |
| 781 | + | r := ownedByWayland(t, "text/plain") | |
| 782 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("TIMESTAMP"), r.data) | |
| 783 | + | require.Equal(t, r.data, sentNotify(t, r)) | |
| 784 | + | v, ok := r.p.prop(reqWin, "_NEFERWL_DATA") | |
| 785 | + | require.True(t, ok) | |
| 786 | + | require.EqualValues(t, 32, v.format) | |
| 787 | + | require.EqualValues(t, 19, v.typ, "INTEGER") | |
| 788 | + | require.Len(t, v.data, 4) | |
| 789 | + | } | |
| 790 | + | ||
| 791 | + | func TestSelectionOwnershipTimeFromLastEvent(t *testing.T) { | |
| 792 | + | x, p, _, _, _ := attached(t) | |
| 793 | + | p.event(28, func(b *[32]byte) { // PropertyNotify | |
| 794 | + | binary.LittleEndian.PutUint32(b[4:], x.selWin) | |
| 795 | + | binary.LittleEndian.PutUint32(b[8:], 1) | |
| 796 | + | binary.LittleEndian.PutUint32(b[12:], 4242) | |
| 797 | + | }) | |
| 798 | + | readUntil(t, x, func() bool { return x.lastTime == 4242 }) | |
| 799 | + | x.WaylandSelection(SelClipboard, []string{"text/plain"}) | |
| 800 | + | require.EqualValues(t, 4242, x.sel.st[SelClipboard].ownTime) | |
| 801 | + | } | |
A internal/adapters/wayland/xwm/selection_xsettings.go +198 -0
| @@ -0,0 +1,198 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "unicode/utf8" | |
| 6 | + | ||
| 7 | + | "bnema.dev/xturbo" | |
| 8 | + | "bnema.dev/xturbo/xproto" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // XSETTINGS 0.5 manager (selection _XSETTINGS_S0). The window is the XWM's | |
| 12 | + | // own, independent of any selection bridge window. | |
| 13 | + | ||
| 14 | + | const ( | |
| 15 | + | xsBufSize = 1024 | |
| 16 | + | xsMaxString = 256 // longest theme name published | |
| 17 | + | xsInt = 0 | |
| 18 | + | xsString = 1 | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | // xsettings is the manager state. Settings are fixed, sorted by name. | |
| 22 | + | type xsettings struct { | |
| 23 | + | win uint32 | |
| 24 | + | active bool // we own _XSETTINGS_S0 | |
| 25 | + | taken bool // another manager owns it (boot check) | |
| 26 | + | ownerNo uint32 | |
| 27 | + | argb uint32 // ARGB32 picture format, 0 unknown | |
| 28 | + | ||
| 29 | + | serial uint32 | |
| 30 | + | // Last published values and the serial of their last change. | |
| 31 | + | theme string | |
| 32 | + | size int32 | |
| 33 | + | fixedSerial uint32 // serial of the constant settings (scale 1, 96 dpi) | |
| 34 | + | themeSerial uint32 | |
| 35 | + | sizeSerial uint32 | |
| 36 | + | ||
| 37 | + | buf [xsBufSize]byte | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // X11 clients see outputs at their logical size and the compositor upscales | |
| 41 | + | // their windows, so the scale announced is always 1 at 96 dpi. | |
| 42 | + | const xsDPI = 96 * 1024 | |
| 43 | + | ||
| 44 | + | func (x *XWM) cursorThemeName() string { | |
| 45 | + | t := x.cfg.CursorTheme | |
| 46 | + | if t == "" { | |
| 47 | + | t = "default" | |
| 48 | + | } | |
| 49 | + | if len(t) > xsMaxString { | |
| 50 | + | n := xsMaxString | |
| 51 | + | for n > 0 && !utf8.RuneStart(t[n]) { // back off to a rune boundary | |
| 52 | + | n-- | |
| 53 | + | } | |
| 54 | + | t = t[:n] | |
| 55 | + | } | |
| 56 | + | return t | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | func (x *XWM) cursorThemeSize() int32 { | |
| 60 | + | if s := x.cfg.CursorSize; s >= 8 && s <= 256 { | |
| 61 | + | return int32(s) | |
| 62 | + | } | |
| 63 | + | return 24 | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // bootSettings runs at the end of boot: the XSETTINGS manager and the root | |
| 67 | + | // cursor. Failures are never fatal. | |
| 68 | + | func (x *XWM) bootSettings() error { | |
| 69 | + | if err := x.bootXSettings(); err != nil { | |
| 70 | + | return err | |
| 71 | + | } | |
| 72 | + | return x.bootRootCursor() | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (x *XWM) bootXSettings() error { | |
| 76 | + | if !x.cfg.XSettings { | |
| 77 | + | return nil | |
| 78 | + | } | |
| 79 | + | conn, b := x.conn, x.conn.Buf() | |
| 80 | + | win, err := conn.NewXID() | |
| 81 | + | if err != nil { | |
| 82 | + | return err | |
| 83 | + | } | |
| 84 | + | x.xs.win = win | |
| 85 | + | var wv xproto.CreateWindowValues | |
| 86 | + | wv.SetEventMask(0) | |
| 87 | + | xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(x.root), -1, -1, 1, 1, 0, xproto.WindowClassInputOnly, 0, &wv) | |
| 88 | + | x.expect(xproto.GetSelectionOwner(b, xproto.Atom(x.atoms[atomXSettingsS0])), kindSelOwner, 0, 0) | |
| 89 | + | if err := conn.Sync(&x.h); err != nil { | |
| 90 | + | return err | |
| 91 | + | } | |
| 92 | + | if x.bootErr != nil { | |
| 93 | + | return x.bootErr | |
| 94 | + | } | |
| 95 | + | if x.xs.taken { | |
| 96 | + | x.log.Warn().Uint32("owner", x.xs.ownerNo).Msg("another XSETTINGS manager runs, not taking _XSETTINGS_S0") | |
| 97 | + | xproto.DestroyWindow(b, xproto.Window(win)) | |
| 98 | + | x.xs.win = 0 | |
| 99 | + | return nil | |
| 100 | + | } | |
| 101 | + | x.publishXSettings(true) | |
| 102 | + | sel := x.atoms[atomXSettingsS0] | |
| 103 | + | xproto.SetSelectionOwner(b, xproto.Window(win), xproto.Atom(sel), 0) | |
| 104 | + | // ICCCM §2.8: announce the new manager to the root. | |
| 105 | + | var ev [32]byte | |
| 106 | + | ev[0], ev[1] = 33, 32 // ClientMessage, format 32 | |
| 107 | + | binary.LittleEndian.PutUint32(ev[4:], x.root) | |
| 108 | + | binary.LittleEndian.PutUint32(ev[8:], x.atoms[atomManager]) | |
| 109 | + | binary.LittleEndian.PutUint32(ev[16:], sel) | |
| 110 | + | binary.LittleEndian.PutUint32(ev[20:], win) | |
| 111 | + | xproto.SendEvent(b, false, xproto.Window(x.root), xproto.EventMaskStructureNotify, &ev) | |
| 112 | + | x.xs.active = true | |
| 113 | + | return nil | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | // onSelOwnerReply: the boot GetSelectionOwner of _XSETTINGS_S0. | |
| 117 | + | func (x *XWM) onSelOwnerReply(r *xturbo.Reply) { | |
| 118 | + | if v, ok := xproto.ParseGetSelectionOwnerReply(r.Raw); ok && v.Owner() != 0 && uint32(v.Owner()) != x.xs.win { | |
| 119 | + | x.xs.taken, x.xs.ownerNo = true, uint32(v.Owner()) | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | // onXSettingsClear: another client took _XSETTINGS_S0. | |
| 124 | + | func (x *XWM) onXSettingsClear(ev *xturbo.Event) { | |
| 125 | + | e := xproto.SelectionClearEvent(xproto.SelectionClearOf(ev)) | |
| 126 | + | if x.xs.active && uint32(e.Owner()) == x.xs.win && uint32(e.Selection()) == x.atoms[atomXSettingsS0] { | |
| 127 | + | x.xs.active = false | |
| 128 | + | x.log.Warn().Msg("XSETTINGS manager replaced by another client, settings no longer updated") | |
| 129 | + | } | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // publishXSettings encodes and writes the property when a value changed | |
| 133 | + | // (force: the first time). It reports whether it wrote. Unchanged values cost | |
| 134 | + | // no allocation and no request. | |
| 135 | + | func (x *XWM) publishXSettings(force bool) bool { | |
| 136 | + | s := &x.xs | |
| 137 | + | theme, size := x.cursorThemeName(), x.cursorThemeSize() | |
| 138 | + | if !force && s.serial != 0 && theme == s.theme && size == s.size { | |
| 139 | + | return false | |
| 140 | + | } | |
| 141 | + | next := s.serial + 1 // wraps | |
| 142 | + | if force || s.serial == 0 { | |
| 143 | + | s.fixedSerial = next | |
| 144 | + | } | |
| 145 | + | if force || s.serial == 0 || theme != s.theme { | |
| 146 | + | s.themeSerial = next | |
| 147 | + | } | |
| 148 | + | if force || s.serial == 0 || size != s.size { | |
| 149 | + | s.sizeSerial = next | |
| 150 | + | } | |
| 151 | + | s.serial, s.theme, s.size = next, theme, size | |
| 152 | + | n := s.encode() | |
| 153 | + | xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(s.win), | |
| 154 | + | xproto.Atom(x.atoms[atomXSettingsSettings]), xproto.Atom(x.atoms[atomXSettingsSettings]), 8, uint32(n), s.buf[:n]) | |
| 155 | + | return true | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | func pad4(n int) int { return (n + 3) &^ 3 } | |
| 159 | + | ||
| 160 | + | // encode writes the settings, sorted by name, into s.buf. | |
| 161 | + | func (s *xsettings) encode() int { | |
| 162 | + | le := binary.LittleEndian | |
| 163 | + | b := s.buf[:] | |
| 164 | + | clear(b[:16]) | |
| 165 | + | b[0] = 0 // LSBFirst | |
| 166 | + | le.PutUint32(b[4:], s.serial) | |
| 167 | + | le.PutUint32(b[8:], 5) | |
| 168 | + | n := 12 | |
| 169 | + | putName := func(typ byte, name string, serial uint32) { | |
| 170 | + | b[n], b[n+1] = typ, 0 | |
| 171 | + | le.PutUint16(b[n+2:], uint16(len(name))) | |
| 172 | + | n += 4 | |
| 173 | + | copy(b[n:], name) | |
| 174 | + | clear(b[n+len(name) : n+pad4(len(name))]) | |
| 175 | + | n += pad4(len(name)) | |
| 176 | + | le.PutUint32(b[n:], serial) | |
| 177 | + | n += 4 | |
| 178 | + | } | |
| 179 | + | putName(xsInt, "Gdk/UnscaledDPI", s.fixedSerial) | |
| 180 | + | le.PutUint32(b[n:], xsDPI) | |
| 181 | + | n += 4 | |
| 182 | + | putName(xsInt, "Gdk/WindowScalingFactor", s.fixedSerial) | |
| 183 | + | le.PutUint32(b[n:], 1) | |
| 184 | + | n += 4 | |
| 185 | + | putName(xsString, "Gtk/CursorThemeName", s.themeSerial) | |
| 186 | + | le.PutUint32(b[n:], uint32(len(s.theme))) | |
| 187 | + | n += 4 | |
| 188 | + | copy(b[n:], s.theme) | |
| 189 | + | clear(b[n+len(s.theme) : n+pad4(len(s.theme))]) | |
| 190 | + | n += pad4(len(s.theme)) | |
| 191 | + | putName(xsInt, "Gtk/CursorThemeSize", s.sizeSerial) | |
| 192 | + | le.PutUint32(b[n:], uint32(s.size)) | |
| 193 | + | n += 4 | |
| 194 | + | putName(xsInt, "Xft/DPI", s.fixedSerial) | |
| 195 | + | le.PutUint32(b[n:], xsDPI) | |
| 196 | + | n += 4 | |
| 197 | + | return n | |
| 198 | + | } | |
A internal/adapters/wayland/xwm/space.go +158 -0
| @@ -0,0 +1,158 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import "github.com/bnema/neferwl/internal/ports" | |
| 4 | + | ||
| 5 | + | const maxRegions = 16 | |
| 6 | + | ||
| 7 | + | // Region is one output's part of the layout and where it is in X space. | |
| 8 | + | // X = layout - Origin. | |
| 9 | + | type Region struct { | |
| 10 | + | Layout, X ports.Rect // logical layout rect; X rect in X pixels | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | // Space maps layout coordinates (which may be negative) to X coordinates | |
| 14 | + | // (non-negative: the layout top-left is X (0,0)). Fixed storage, no allocs. | |
| 15 | + | type Space struct { | |
| 16 | + | regions [maxRegions]Region | |
| 17 | + | n int | |
| 18 | + | dropped int // outputs beyond maxRegions in the last Set | |
| 19 | + | ox, oy int // layout point that is X (0,0) | |
| 20 | + | w, h int // X screen size: the extents | |
| 21 | + | scale float64 // scale of the first output; not part of the change test | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // Origin is the layout point that is X (0,0): the top-left of the layout | |
| 25 | + | // extents. | |
| 26 | + | func (s *Space) Origin() (ox, oy int) { return s.ox, s.oy } | |
| 27 | + | ||
| 28 | + | // Dropped is how many outputs of the last Set did not fit. | |
| 29 | + | func (s *Space) Dropped() int { return s.dropped } | |
| 30 | + | ||
| 31 | + | // Set recomputes the mapping and reports whether it changed. | |
| 32 | + | func (s *Space) Set(l ports.Layout) bool { | |
| 33 | + | var next Space | |
| 34 | + | n := len(l) | |
| 35 | + | if n > maxRegions { | |
| 36 | + | next.dropped = n - maxRegions | |
| 37 | + | n = maxRegions | |
| 38 | + | } | |
| 39 | + | next.n = n | |
| 40 | + | var maxX, maxY int | |
| 41 | + | for i := 0; i < n; i++ { | |
| 42 | + | o := l[i] | |
| 43 | + | if i == 0 || o.X < next.ox { | |
| 44 | + | next.ox = o.X | |
| 45 | + | } | |
| 46 | + | if i == 0 || o.Y < next.oy { | |
| 47 | + | next.oy = o.Y | |
| 48 | + | } | |
| 49 | + | if i == 0 || o.X+o.Width > maxX { | |
| 50 | + | maxX = o.X + o.Width | |
| 51 | + | } | |
| 52 | + | if i == 0 || o.Y+o.Height > maxY { | |
| 53 | + | maxY = o.Y + o.Height | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | for i := 0; i < n; i++ { | |
| 57 | + | o := l[i] | |
| 58 | + | next.regions[i] = Region{ | |
| 59 | + | Layout: ports.Rect{X: o.X, Y: o.Y, W: o.Width, H: o.Height}, | |
| 60 | + | X: ports.Rect{X: o.X - next.ox, Y: o.Y - next.oy, W: o.Width, H: o.Height}, | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | next.w, next.h = maxX-next.ox, maxY-next.oy | |
| 64 | + | if n > 0 { | |
| 65 | + | next.scale = l[0].Scale | |
| 66 | + | } | |
| 67 | + | if next.regions == s.regions && next.n == s.n && next.ox == s.ox && next.oy == s.oy && | |
| 68 | + | next.w == s.w && next.h == s.h { | |
| 69 | + | s.dropped, s.scale = next.dropped, next.scale | |
| 70 | + | return false | |
| 71 | + | } | |
| 72 | + | *s = next | |
| 73 | + | return true | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | // Scale is the scale of the first output of the layout, 1 when unset. | |
| 77 | + | func (s *Space) Scale() float64 { | |
| 78 | + | if s.scale <= 0 { | |
| 79 | + | return 1 | |
| 80 | + | } | |
| 81 | + | return s.scale | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // Size is the X screen size. | |
| 85 | + | func (s *Space) Size() (w, h int) { return s.w, s.h } | |
| 86 | + | ||
| 87 | + | // nearest returns the index of the region containing the layout point, or | |
| 88 | + | // the closest one by squared distance when it is in a hole. -1 when empty. | |
| 89 | + | func (s *Space) nearest(lx, ly int) int { | |
| 90 | + | best, bestD := -1, 0 | |
| 91 | + | for i := 0; i < s.n; i++ { | |
| 92 | + | r := s.regions[i].Layout | |
| 93 | + | dx := axisGap(lx, r.X, r.X+r.W) | |
| 94 | + | dy := axisGap(ly, r.Y, r.Y+r.H) | |
| 95 | + | d := dx*dx + dy*dy | |
| 96 | + | if best < 0 || d < bestD { | |
| 97 | + | best, bestD = i, d | |
| 98 | + | } | |
| 99 | + | } | |
| 100 | + | return best | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | // nearestX is nearest in X space. | |
| 104 | + | func (s *Space) nearestX(x, y int) int { | |
| 105 | + | best, bestD := -1, 0 | |
| 106 | + | for i := 0; i < s.n; i++ { | |
| 107 | + | r := s.regions[i].X | |
| 108 | + | dx := axisGap(x, r.X, r.X+r.W) | |
| 109 | + | dy := axisGap(y, r.Y, r.Y+r.H) | |
| 110 | + | d := dx*dx + dy*dy | |
| 111 | + | if best < 0 || d < bestD { | |
| 112 | + | best, bestD = i, d | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | return best | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // axisGap is the distance from v to [lo, hi): 0 inside. | |
| 119 | + | func axisGap(v, lo, hi int) int { | |
| 120 | + | if v < lo { | |
| 121 | + | return lo - v | |
| 122 | + | } | |
| 123 | + | if v >= hi { | |
| 124 | + | return v - hi + 1 | |
| 125 | + | } | |
| 126 | + | return 0 | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | // WholeOutput reports whether the X rect is exactly one output's, and that | |
| 130 | + | // output's layout origin. | |
| 131 | + | func (s *Space) WholeOutput(x, y, w, h int) (lx, ly int, ok bool) { | |
| 132 | + | for i := 0; i < s.n; i++ { | |
| 133 | + | if r := s.regions[i]; r.X.X == x && r.X.Y == y && r.X.W == w && r.X.H == h { | |
| 134 | + | return r.Layout.X, r.Layout.Y, true | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | return 0, 0, false | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | // ToX converts a layout point to X, through the nearest region. | |
| 141 | + | func (s *Space) ToX(lx, ly int) (x, y int) { | |
| 142 | + | i := s.nearest(lx, ly) | |
| 143 | + | if i < 0 { | |
| 144 | + | return lx, ly | |
| 145 | + | } | |
| 146 | + | r := &s.regions[i] | |
| 147 | + | return r.X.X + lx - r.Layout.X, r.X.Y + ly - r.Layout.Y | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | // ToLayout converts an X point to layout coordinates. | |
| 151 | + | func (s *Space) ToLayout(x, y int) (lx, ly int) { | |
| 152 | + | i := s.nearestX(x, y) | |
| 153 | + | if i < 0 { | |
| 154 | + | return x, y | |
| 155 | + | } | |
| 156 | + | r := &s.regions[i] | |
| 157 | + | return r.Layout.X + x - r.X.X, r.Layout.Y + y - r.X.Y | |
| 158 | + | } | |
A internal/adapters/wayland/xwm/space_test.go +110 -0
| @@ -0,0 +1,110 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func out(x, y, w, h int) ports.OutputPlacement { | |
| 10 | + | return ports.OutputPlacement{X: x, Y: y, Width: w, Height: h, Scale: 1} | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | func TestSpaceLayouts(t *testing.T) { | |
| 14 | + | tests := []struct { | |
| 15 | + | name string | |
| 16 | + | l ports.Layout | |
| 17 | + | w, h int | |
| 18 | + | lx, ly, x, y int | |
| 19 | + | }{ | |
| 20 | + | {"single", ports.Layout{out(0, 0, 1920, 1080)}, 1920, 1080, 10, 20, 10, 20}, | |
| 21 | + | {"side by side", ports.Layout{out(0, 0, 1920, 1080), out(1920, 0, 1280, 1024)}, 3200, 1080, 2000, 5, 2000, 5}, | |
| 22 | + | {"left-of negative origin", ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)}, 2560, 1080, -630, 20, 10, 20}, | |
| 23 | + | {"above", ports.Layout{out(0, -500, 800, 500), out(0, 0, 800, 600)}, 800, 1100, 3, -400, 3, 100}, | |
| 24 | + | {"below", ports.Layout{out(0, 0, 800, 600), out(0, 600, 800, 400)}, 800, 1000, 3, 700, 3, 700}, | |
| 25 | + | {"hole nearest", ports.Layout{out(0, 0, 100, 100), out(300, 0, 100, 100)}, 400, 100, 120, 10, 120, 10}, | |
| 26 | + | } | |
| 27 | + | for _, tc := range tests { | |
| 28 | + | t.Run(tc.name, func(t *testing.T) { | |
| 29 | + | var s Space | |
| 30 | + | if !s.Set(tc.l) { | |
| 31 | + | t.Fatal("first Set not changed") | |
| 32 | + | } | |
| 33 | + | if w, h := s.Size(); w != tc.w || h != tc.h { | |
| 34 | + | t.Fatalf("size %dx%d, want %dx%d", w, h, tc.w, tc.h) | |
| 35 | + | } | |
| 36 | + | x, y := s.ToX(tc.lx, tc.ly) | |
| 37 | + | if x != tc.x || y != tc.y { | |
| 38 | + | t.Fatalf("ToX = %d,%d want %d,%d", x, y, tc.x, tc.y) | |
| 39 | + | } | |
| 40 | + | if lx, ly := s.ToLayout(x, y); lx != tc.lx || ly != tc.ly { | |
| 41 | + | t.Fatalf("round trip = %d,%d want %d,%d", lx, ly, tc.lx, tc.ly) | |
| 42 | + | } | |
| 43 | + | if s.Set(tc.l) { | |
| 44 | + | t.Fatal("identical Set reported changed") | |
| 45 | + | } | |
| 46 | + | }) | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func TestSpaceEmptyAndOrigin(t *testing.T) { | |
| 51 | + | var s Space | |
| 52 | + | if x, y := s.ToX(5, 6); x != 5 || y != 6 { | |
| 53 | + | t.Fatalf("empty ToX = %d,%d", x, y) | |
| 54 | + | } | |
| 55 | + | s.Set(ports.Layout{out(-640, -20, 640, 480), out(0, 0, 1920, 1080)}) | |
| 56 | + | if ox, oy := s.Origin(); ox != -640 || oy != -20 { | |
| 57 | + | t.Fatalf("origin = %d,%d", ox, oy) | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | func TestSpaceSeventeenOutputs(t *testing.T) { | |
| 62 | + | l := make(ports.Layout, 17) | |
| 63 | + | for i := range l { | |
| 64 | + | l[i] = out(i*100, 0, 100, 100) | |
| 65 | + | } | |
| 66 | + | var s Space | |
| 67 | + | s.Set(l) | |
| 68 | + | if s.Dropped() != 1 { | |
| 69 | + | t.Fatalf("dropped = %d", s.Dropped()) | |
| 70 | + | } | |
| 71 | + | if w, _ := s.Size(); w != 1600 { | |
| 72 | + | t.Fatalf("width = %d", w) | |
| 73 | + | } | |
| 74 | + | if x, _ := s.ToX(1550, 5); x != 1550 { | |
| 75 | + | t.Fatalf("ToX = %d", x) | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | func TestSpaceAllocations(t *testing.T) { | |
| 80 | + | l := ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)} | |
| 81 | + | var s Space | |
| 82 | + | s.Set(l) | |
| 83 | + | n := testing.AllocsPerRun(100, func() { | |
| 84 | + | s.Set(l) | |
| 85 | + | x, y := s.ToX(-600, 10) | |
| 86 | + | x, y = s.ToLayout(x, y) | |
| 87 | + | _, _ = x, y | |
| 88 | + | _, _ = s.Origin() | |
| 89 | + | _, _ = s.Size() | |
| 90 | + | }) | |
| 91 | + | if n != 0 { | |
| 92 | + | t.Fatalf("allocs = %v", n) | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | func TestSpaceWholeOutputMatchesPlace(t *testing.T) { | |
| 97 | + | var s Space | |
| 98 | + | s.Set(ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)}) | |
| 99 | + | lx, ly, ok := s.WholeOutput(0, 0, 640, 480) | |
| 100 | + | if !ok || lx != -640 || ly != 0 { | |
| 101 | + | t.Fatalf("WholeOutput = %d,%d,%v", lx, ly, ok) | |
| 102 | + | } | |
| 103 | + | // Placing the window at that origin lands on the same X point. | |
| 104 | + | if x, y := s.ToX(lx, ly); x != 0 || y != 0 { | |
| 105 | + | t.Fatalf("ToX = %d,%d", x, y) | |
| 106 | + | } | |
| 107 | + | if _, _, ok := s.WholeOutput(0, 0, 640, 481); ok { | |
| 108 | + | t.Fatal("partial rect matched") | |
| 109 | + | } | |
| 110 | + | } | |
A internal/adapters/wayland/xwm/streams_test.go +197 -0
| @@ -0,0 +1,197 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/hex" | |
| 5 | + | "os" | |
| 6 | + | "path/filepath" | |
| 7 | + | "runtime" | |
| 8 | + | "runtime/debug" | |
| 9 | + | "strings" | |
| 10 | + | "testing" | |
| 11 | + | "time" | |
| 12 | + | ||
| 13 | + | "bnema.dev/xturbo/xproto" | |
| 14 | + | "github.com/stretchr/testify/require" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // The recorded streams are the bytes an X server wrote to the XWM during a | |
| 18 | + | // steady-state session of one mapped window, captured from the scripted xpeer | |
| 19 | + | // (real encoded X packets: events, GetProperty replies). The scenario is | |
| 20 | + | // deterministic, so a replay meets the request sequence numbers it recorded. | |
| 21 | + | // They are not xtrace captures of a real Xwayland: xpeer speaks the same wire | |
| 22 | + | // protocol and keeps the fixture reproducible. Regenerate with | |
| 23 | + | // | |
| 24 | + | // XWM_UPDATE_STREAMS=1 go test ./internal/adapters/wayland/xwm -run TestRecordedStreams | |
| 25 | + | const ( | |
| 26 | + | streamCycles = 400 | |
| 27 | + | streamWarm = 20 // cycles replayed before measuring | |
| 28 | + | ) | |
| 29 | + | ||
| 30 | + | const streamWin = 0x500001 | |
| 31 | + | ||
| 32 | + | type streamKind struct { | |
| 33 | + | name string | |
| 34 | + | // perCycle is Events+Replies the XWM sees per cycle. | |
| 35 | + | perCycle int | |
| 36 | + | cycle func(hs *harness) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | var streamKinds = []streamKind{ | |
| 40 | + | // A title notification the XWM answers with one GetProperty whose reply | |
| 41 | + | // equals the stored title. | |
| 42 | + | {"property_unchanged", 2, func(hs *harness) { | |
| 43 | + | before := hs.x.Stats().Replies | |
| 44 | + | hs.p.propertyNotify(streamWin, hs.p.atom("_NET_WM_NAME"), false) | |
| 45 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |
| 46 | + | }}, | |
| 47 | + | // The same plus ConfigureNotify, FocusIn and MapNotify. | |
| 48 | + | {"steady", 5, func(hs *harness) { | |
| 49 | + | before := hs.x.Stats().Replies | |
| 50 | + | hs.p.propertyNotify(streamWin, hs.p.atom("_NET_WM_NAME"), false) | |
| 51 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |
| 52 | + | events := hs.x.Stats().Events | |
| 53 | + | hs.p.configureNotify(streamWin) | |
| 54 | + | hs.p.focusIn(streamWin) | |
| 55 | + | hs.p.mapNotify(streamWin) | |
| 56 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Events >= events+3 }) | |
| 57 | + | }}, | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | // streamScenario boots, maps one window and parks the peer behind a poke so | |
| 61 | + | // nothing but the test writes to the XWM afterwards (an allocation count is | |
| 62 | + | // process-wide). The poke is part of the scenario, recorded or replayed. | |
| 63 | + | func streamScenario(t *testing.T) *harness { | |
| 64 | + | t.Helper() | |
| 65 | + | hs := newHarness(t, nil) | |
| 66 | + | hs.classic(streamWin) | |
| 67 | + | hs.mapAndWait(streamWin) | |
| 68 | + | // The steady state is a focused window: a FocusIn on it is not denied, | |
| 69 | + | // so no request depends on how events are batched into flushes. The | |
| 70 | + | // poke makes sure the focus requests are out before the peer pauses. | |
| 71 | + | hs.x.Focus(hs.x.tab.lookup(streamWin)) | |
| 72 | + | poke(hs) | |
| 73 | + | return hs | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | func poke(hs *harness) { | |
| 77 | + | hs.t.Helper() | |
| 78 | + | n := len(hs.p.requests()) | |
| 79 | + | xproto.GetInputFocus(hs.x.conn.Buf()) | |
| 80 | + | hs.x.flush() | |
| 81 | + | hs.p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus }) | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | func recordStream(t *testing.T, k streamKind) []byte { | |
| 85 | + | hs := streamScenario(t) | |
| 86 | + | poke(hs) | |
| 87 | + | hs.p.mu.Lock() | |
| 88 | + | hs.p.record = true | |
| 89 | + | hs.p.mu.Unlock() | |
| 90 | + | for i := 0; i < streamCycles; i++ { | |
| 91 | + | k.cycle(hs) | |
| 92 | + | } | |
| 93 | + | hs.p.mu.Lock() | |
| 94 | + | defer hs.p.mu.Unlock() | |
| 95 | + | hs.p.record = false | |
| 96 | + | return append([]byte(nil), hs.p.tap...) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | func streamPath(k streamKind) string { | |
| 100 | + | return filepath.Join("testdata", "streams", k.name+".hex") | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | func loadStream(t *testing.T, k streamKind) []byte { | |
| 104 | + | t.Helper() | |
| 105 | + | text, err := os.ReadFile(streamPath(k)) | |
| 106 | + | require.NoError(t, err) | |
| 107 | + | b, err := hex.DecodeString(strings.Join(strings.Fields(string(text)), "")) | |
| 108 | + | require.NoError(t, err) | |
| 109 | + | return b | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | func encodeHex(b []byte) string { | |
| 113 | + | var sb strings.Builder | |
| 114 | + | for len(b) > 0 { | |
| 115 | + | n := min(32, len(b)) | |
| 116 | + | sb.WriteString(hex.EncodeToString(b[:n])) | |
| 117 | + | sb.WriteByte('\n') | |
| 118 | + | b = b[n:] | |
| 119 | + | } | |
| 120 | + | return sb.String() | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func TestRecordedStreams(t *testing.T) { | |
| 124 | + | for _, k := range streamKinds { | |
| 125 | + | t.Run(k.name, func(t *testing.T) { | |
| 126 | + | got := recordStream(t, k) | |
| 127 | + | if os.Getenv("XWM_UPDATE_STREAMS") != "" { | |
| 128 | + | require.NoError(t, os.MkdirAll(filepath.Dir(streamPath(k)), 0o755)) | |
| 129 | + | require.NoError(t, os.WriteFile(streamPath(k), []byte(encodeHex(got)), 0o644)) | |
| 130 | + | } | |
| 131 | + | require.Equal(t, loadStream(t, k), got, "the committed stream no longer matches the protocol the XWM speaks") | |
| 132 | + | }) | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | // replay feeds the recorded stream and reports the mallocs of the part after | |
| 137 | + | // the warm-up cycles. | |
| 138 | + | func replay(t *testing.T, k streamKind) (mallocs uint64, hs *harness) { | |
| 139 | + | t.Helper() | |
| 140 | + | stream := loadStream(t, k) | |
| 141 | + | hs = streamScenario(t) | |
| 142 | + | hs.p.pause() | |
| 143 | + | // A peer blocked in its read consumes one more request before it parks | |
| 144 | + | // (this one, or an earlier one still in flight); a parked peer leaves it | |
| 145 | + | // unread. Either way the peer ends up parked, with nothing half read. | |
| 146 | + | xproto.GetInputFocus(hs.x.conn.Buf()) | |
| 147 | + | hs.x.flush() | |
| 148 | + | hs.p.waitParked() | |
| 149 | + | _, err := hs.p.f.Write(stream) | |
| 150 | + | require.NoError(t, err) | |
| 151 | + | ||
| 152 | + | x := hs.x | |
| 153 | + | // The harness post queue grows by append, at batching-dependent moments. | |
| 154 | + | x.post = func(func()) {} | |
| 155 | + | base := x.Stats().Events + x.Stats().Replies | |
| 156 | + | drive := func(until uint64) { | |
| 157 | + | deadline := time.Now().Add(10 * time.Second) | |
| 158 | + | for x.Stats().Events+x.Stats().Replies < until { | |
| 159 | + | if time.Now().After(deadline) { // no formatting arguments: boxing them would count as mallocs | |
| 160 | + | t.Fatal("stream stalled") | |
| 161 | + | } | |
| 162 | + | before := x.Stats().Events + x.Stats().Replies | |
| 163 | + | x.onReadable() | |
| 164 | + | if x.Stats().Events+x.Stats().Replies == before { | |
| 165 | + | runtime.Gosched() | |
| 166 | + | } | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | drive(base + uint64(streamWarm*k.perCycle)) | |
| 170 | + | target := base + uint64(streamCycles*k.perCycle) | |
| 171 | + | ||
| 172 | + | var m0, m1 runtime.MemStats | |
| 173 | + | // A GC cycle finishing inside the window allocates runtime-internal | |
| 174 | + | // objects (sudogs), which Mallocs counts: keep the collector off. | |
| 175 | + | defer debug.SetGCPercent(debug.SetGCPercent(-1)) | |
| 176 | + | // Like testing.AllocsPerRun: one P, so no other goroutine's runtime | |
| 177 | + | // bookkeeping (timers, netpoll) lands in the window. | |
| 178 | + | defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) | |
| 179 | + | runtime.GC() | |
| 180 | + | runtime.ReadMemStats(&m0) | |
| 181 | + | drive(target) | |
| 182 | + | runtime.ReadMemStats(&m1) | |
| 183 | + | require.Equal(t, target, x.Stats().Events+x.Stats().Replies) | |
| 184 | + | return m1.Mallocs - m0.Mallocs, hs | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | func TestPropertyUnchangedAllocations(t *testing.T) { | |
| 188 | + | m, hs := replay(t, streamKinds[0]) | |
| 189 | + | require.Zero(t, m, "unchanged property cycles must not allocate") | |
| 190 | + | require.Equal(t, "net title", hs.x.tab.rec(hs.x.tab.lookup(streamWin)).title) | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | func TestDispatchAllocations(t *testing.T) { | |
| 194 | + | m, hs := replay(t, streamKinds[1]) | |
| 195 | + | require.Zero(t, m, "steady-state event dispatch must not allocate") | |
| 196 | + | require.True(t, hs.x.tab.rec(hs.x.tab.lookup(streamWin)).mapped) | |
| 197 | + | } | |
A internal/adapters/wayland/xwm/table.go +242 -0
| @@ -0,0 +1,242 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "math/bits" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // Record kinds. | |
| 10 | + | const ( | |
| 11 | + | kindFree uint8 = iota | |
| 12 | + | kindManaged | |
| 13 | + | kindUnmanaged | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | const ( | |
| 17 | + | initialSlots = 256 | |
| 18 | + | // maxSlots bounds the table: a Slot travels in the low 16 bits of an | |
| 19 | + | // Expect argument. | |
| 20 | + | maxSlots = 1<<16 - 1 | |
| 21 | + | indexGone = 0xFFFFFFFF // tombstone in table.index | |
| 22 | + | ) | |
| 23 | + | ||
| 24 | + | // pending is the configure state not yet (or last) sent to the X server. | |
| 25 | + | type pending struct { | |
| 26 | + | x, y int32 | |
| 27 | + | w, h uint32 | |
| 28 | + | fullscreen, activated, dirty bool | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // ovRect is a layout rectangle. | |
| 32 | + | type ovRect struct{ x, y, w, h int32 } | |
| 33 | + | ||
| 34 | + | // record is one X window known to the XWM. Strings are the only per-record | |
| 35 | + | // heap data. | |
| 36 | + | type record struct { | |
| 37 | + | xid uint32 | |
| 38 | + | id ports.WindowID | |
| 39 | + | kind uint8 | |
| 40 | + | gen uint16 // bumped on insert; stale Expect replies are told apart by it | |
| 41 | + | ||
| 42 | + | // X state | |
| 43 | + | mapped, mapping, ready bool | |
| 44 | + | x, y int32 | |
| 45 | + | w, h uint32 | |
| 46 | + | ||
| 47 | + | // properties | |
| 48 | + | pending uint16 // propMask bits still awaited | |
| 49 | + | input, deleteWindow, takeFocus bool | |
| 50 | + | wtype uint8 | |
| 51 | + | modal, fullscreen bool | |
| 52 | + | transient uint32 // xid | |
| 53 | + | pid int32 | |
| 54 | + | minW, minH, maxW, maxH int32 | |
| 55 | + | title, class string // owned, replaced only on change | |
| 56 | + | hasNetName bool | |
| 57 | + | floating bool // dialog-like type; set with the properties | |
| 58 | + | bypass uint8 // _NET_WM_BYPASS_COMPOSITOR, 0..2 | |
| 59 | + | sized bool // the client asked for its size before Ready | |
| 60 | + | ||
| 61 | + | // pairing | |
| 62 | + | serial uint64 | |
| 63 | + | surface uint64 | |
| 64 | + | ||
| 65 | + | // layout position of a managed window, from Place | |
| 66 | + | lx, ly int32 | |
| 67 | + | placed bool | |
| 68 | + | ||
| 69 | + | // unmanaged (override-redirect) windows | |
| 70 | + | noInput bool // tooltip, DND, notification: never takes the pointer | |
| 71 | + | ovPaired bool // OverrideMapped sent, OverrideGone owed | |
| 72 | + | ovQueued bool // slot is in XWM.ovDirty | |
| 73 | + | ov ovRect // layout rect last sent to the host | |
| 74 | + | ||
| 75 | + | // configure | |
| 76 | + | pend, sent pending | |
| 77 | + | stateSent uint8 // _NET_WM_STATE bits last written | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // table is a slab of records with an open-addressing index by X id. No Go map. | |
| 81 | + | type table struct { | |
| 82 | + | recs []record // slab, len == cap, power of two | |
| 83 | + | free []uint32 // free slot indices (LIFO) | |
| 84 | + | index []uint32 // slot+1, 0 empty, indexGone tombstone; len == 2*len(recs) | |
| 85 | + | shift uint8 | |
| 86 | + | n int | |
| 87 | + | tombs int | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | func newTable(size int) *table { | |
| 91 | + | t := &table{} | |
| 92 | + | t.resize(size) | |
| 93 | + | return t | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // resize reallocates the slab, free list and index for size records and | |
| 97 | + | // reindexes the live ones. | |
| 98 | + | func (t *table) resize(size int) { | |
| 99 | + | old := t.recs | |
| 100 | + | t.recs = make([]record, size) | |
| 101 | + | copy(t.recs, old) | |
| 102 | + | t.free = make([]uint32, 0, size) | |
| 103 | + | for i := min(size, maxSlots) - 1; i >= 0; i-- { | |
| 104 | + | if i >= len(old) || old[i].kind == kindFree { | |
| 105 | + | t.free = append(t.free, uint32(i)) | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | t.index = make([]uint32, 2*size) | |
| 109 | + | t.shift = uint8(32 - bits.TrailingZeros(uint(len(t.index)))) | |
| 110 | + | t.reindex() | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // reindex rebuilds the index from the live records, dropping tombstones. | |
| 114 | + | // It does not allocate. | |
| 115 | + | func (t *table) reindex() { | |
| 116 | + | clear(t.index) | |
| 117 | + | t.tombs = 0 | |
| 118 | + | for i := range t.recs { | |
| 119 | + | if t.recs[i].kind != kindFree { | |
| 120 | + | t.indexAdd(t.recs[i].xid, uint32(i)+1) | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | func (t *table) hash(xid uint32) uint32 { return (xid * 0x9E3779B1) >> t.shift } | |
| 126 | + | ||
| 127 | + | // indexAdd places slot for xid, which must be absent, in the first empty or | |
| 128 | + | // tombstone position of its probe sequence. | |
| 129 | + | func (t *table) indexAdd(xid, slot uint32) { | |
| 130 | + | mask := uint32(len(t.index) - 1) | |
| 131 | + | for i := t.hash(xid); ; i = (i + 1) & mask { | |
| 132 | + | switch t.index[i] { | |
| 133 | + | case 0: | |
| 134 | + | t.index[i] = slot | |
| 135 | + | return | |
| 136 | + | case indexGone: | |
| 137 | + | t.index[i] = slot | |
| 138 | + | t.tombs-- | |
| 139 | + | return | |
| 140 | + | } | |
| 141 | + | } | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | // grow doubles the table. It reports false at maxSlots. | |
| 145 | + | func (t *table) grow() bool { | |
| 146 | + | if len(t.recs) >= 1<<16 { | |
| 147 | + | return false | |
| 148 | + | } | |
| 149 | + | t.resize(2 * len(t.recs)) | |
| 150 | + | return len(t.free) > 0 | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // insert returns the slot of xid, creating a managed record (generation | |
| 154 | + | // bumped) when absent. It returns 0 when the table is full. Growth is the | |
| 155 | + | // only allocation. | |
| 156 | + | func (t *table) insert(xid uint32) Slot { | |
| 157 | + | if s := t.lookup(xid); s != 0 { | |
| 158 | + | return s | |
| 159 | + | } | |
| 160 | + | if len(t.free) == 0 && !t.grow() { | |
| 161 | + | return 0 | |
| 162 | + | } | |
| 163 | + | i := t.free[len(t.free)-1] | |
| 164 | + | t.free = t.free[:len(t.free)-1] | |
| 165 | + | if t.n+t.tombs+1 > len(t.index)/4*3 { // keep an empty index cell for lookup to stop at | |
| 166 | + | t.reindex() | |
| 167 | + | } | |
| 168 | + | r := &t.recs[i] | |
| 169 | + | gen := r.gen + 1 | |
| 170 | + | if gen == 0 { | |
| 171 | + | gen = 1 | |
| 172 | + | } | |
| 173 | + | *r = record{xid: xid, kind: kindManaged, gen: gen, input: true} | |
| 174 | + | t.indexAdd(xid, i+1) | |
| 175 | + | t.n++ | |
| 176 | + | return Slot(i + 1) | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // remove frees s. The record is zeroed except its generation. | |
| 180 | + | func (t *table) remove(s Slot) { | |
| 181 | + | r := t.rec(s) | |
| 182 | + | if r == nil || r.kind == kindFree { | |
| 183 | + | return | |
| 184 | + | } | |
| 185 | + | mask := uint32(len(t.index) - 1) | |
| 186 | + | for i := t.hash(r.xid); ; i = (i + 1) & mask { | |
| 187 | + | v := t.index[i] | |
| 188 | + | if v == 0 { | |
| 189 | + | break | |
| 190 | + | } | |
| 191 | + | if v == uint32(s) { | |
| 192 | + | t.index[i] = indexGone | |
| 193 | + | t.tombs++ | |
| 194 | + | break | |
| 195 | + | } | |
| 196 | + | } | |
| 197 | + | *r = record{gen: r.gen} | |
| 198 | + | t.free = append(t.free, uint32(s)-1) | |
| 199 | + | t.n-- | |
| 200 | + | if t.n+t.tombs > len(t.index)/4*3 { | |
| 201 | + | t.reindex() | |
| 202 | + | } | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | // lookup returns the slot of xid, or 0. | |
| 206 | + | func (t *table) lookup(xid uint32) Slot { | |
| 207 | + | mask := uint32(len(t.index) - 1) | |
| 208 | + | for i := t.hash(xid); ; i = (i + 1) & mask { | |
| 209 | + | v := t.index[i] | |
| 210 | + | if v == 0 { | |
| 211 | + | return 0 | |
| 212 | + | } | |
| 213 | + | if v != indexGone && t.recs[v-1].xid == xid { | |
| 214 | + | return Slot(v) | |
| 215 | + | } | |
| 216 | + | } | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | // rec returns the record of s, or nil when s is out of range. The pointer is | |
| 220 | + | // valid only until the next insert, host call or anything else that can grow | |
| 221 | + | // the table or free s: never hold it across those, fetch it again with rec(s). | |
| 222 | + | func (t *table) rec(s Slot) *record { | |
| 223 | + | if s == 0 || int(s) > len(t.recs) { | |
| 224 | + | return nil | |
| 225 | + | } | |
| 226 | + | return &t.recs[s-1] | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | // tag is the Expect argument naming s: slot in the low 16 bits, generation | |
| 230 | + | // in the high 16. | |
| 231 | + | func (t *table) tag(s Slot) uint32 { return uint32(s) | uint32(t.recs[s-1].gen)<<16 } | |
| 232 | + | ||
| 233 | + | // live resolves an Expect tag. It returns nil when the slot was freed or | |
| 234 | + | // reused since the request was sent. | |
| 235 | + | func (t *table) live(tag uint32) (Slot, *record) { | |
| 236 | + | s := Slot(tag & 0xFFFF) | |
| 237 | + | r := t.rec(s) | |
| 238 | + | if r == nil || r.kind == kindFree || r.gen != uint16(tag>>16) { | |
| 239 | + | return 0, nil | |
| 240 | + | } | |
| 241 | + | return s, r | |
| 242 | + | } | |
A internal/adapters/wayland/xwm/table_test.go +138 -0
| @@ -0,0 +1,138 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "math/rand/v2" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestTableAgainstMap(t *testing.T) { | |
| 11 | + | rng := rand.New(rand.NewPCG(1, 2)) | |
| 12 | + | tab := newTable(initialSlots) | |
| 13 | + | model := map[uint32]Slot{} | |
| 14 | + | var xids []uint32 | |
| 15 | + | for i := 0; i < 10000; i++ { | |
| 16 | + | switch op := rng.IntN(3); { | |
| 17 | + | case op < 2 || len(xids) == 0: | |
| 18 | + | xid := rng.Uint32()&0x1fffff | 0x200000 | |
| 19 | + | if _, ok := model[xid]; ok { | |
| 20 | + | continue | |
| 21 | + | } | |
| 22 | + | s := tab.insert(xid) | |
| 23 | + | require.NotZero(t, s) | |
| 24 | + | model[xid] = s | |
| 25 | + | xids = append(xids, xid) | |
| 26 | + | default: | |
| 27 | + | k := rng.IntN(len(xids)) | |
| 28 | + | xid := xids[k] | |
| 29 | + | xids[k] = xids[len(xids)-1] | |
| 30 | + | xids = xids[:len(xids)-1] | |
| 31 | + | tab.remove(model[xid]) | |
| 32 | + | delete(model, xid) | |
| 33 | + | } | |
| 34 | + | if i%500 == 0 { | |
| 35 | + | for xid, s := range model { | |
| 36 | + | require.Equal(t, s, tab.lookup(xid)) | |
| 37 | + | } | |
| 38 | + | require.Zero(t, tab.lookup(rng.Uint32()|1<<31)) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | require.Equal(t, len(model), tab.n) | |
| 42 | + | for xid, s := range model { | |
| 43 | + | require.Equal(t, s, tab.lookup(xid)) | |
| 44 | + | require.Equal(t, xid, tab.rec(s).xid) | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func TestTableGrowth(t *testing.T) { | |
| 49 | + | tab := newTable(initialSlots) | |
| 50 | + | slots := make([]Slot, 0, initialSlots+1) | |
| 51 | + | for i := 0; i < initialSlots; i++ { | |
| 52 | + | slots = append(slots, tab.insert(uint32(0x200000+i))) | |
| 53 | + | } | |
| 54 | + | require.Len(t, tab.recs, initialSlots) | |
| 55 | + | tab.rec(slots[7]).title = "kept" | |
| 56 | + | slots = append(slots, tab.insert(0x300000)) | |
| 57 | + | require.Len(t, tab.recs, 2*initialSlots) | |
| 58 | + | require.Len(t, tab.index, 4*initialSlots) | |
| 59 | + | for i, s := range slots[:initialSlots] { | |
| 60 | + | require.Equal(t, s, tab.lookup(uint32(0x200000+i))) | |
| 61 | + | } | |
| 62 | + | require.Equal(t, "kept", tab.rec(slots[7]).title, "growth keeps records") | |
| 63 | + | require.Equal(t, slots[initialSlots], tab.lookup(0x300000)) | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func TestTableTombstoneReuse(t *testing.T) { | |
| 67 | + | tab := newTable(initialSlots) | |
| 68 | + | for i := 0; i < 5000; i++ { // far more churn than slots: tombstones must be recycled | |
| 69 | + | s := tab.insert(uint32(0x200000 + i)) | |
| 70 | + | require.NotZero(t, s) | |
| 71 | + | require.Equal(t, s, tab.lookup(uint32(0x200000+i))) | |
| 72 | + | tab.remove(s) | |
| 73 | + | require.Zero(t, tab.lookup(uint32(0x200000+i))) | |
| 74 | + | } | |
| 75 | + | require.Len(t, tab.recs, initialSlots) | |
| 76 | + | require.Zero(t, tab.n) | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | func TestTableGenerationAndTag(t *testing.T) { | |
| 80 | + | tab := newTable(initialSlots) | |
| 81 | + | s := tab.insert(0x200001) | |
| 82 | + | tag := tab.tag(s) | |
| 83 | + | got, rec := tab.live(tag) | |
| 84 | + | require.Equal(t, s, got) | |
| 85 | + | require.NotNil(t, rec) | |
| 86 | + | tab.remove(s) | |
| 87 | + | _, rec = tab.live(tag) | |
| 88 | + | require.Nil(t, rec, "freed slot") | |
| 89 | + | s2 := tab.insert(0x200002) | |
| 90 | + | require.Equal(t, s, s2, "LIFO free list reuses the slot") | |
| 91 | + | _, rec = tab.live(tag) | |
| 92 | + | require.Nil(t, rec, "stale tag after slot reuse") | |
| 93 | + | _, rec = tab.live(tab.tag(s2)) | |
| 94 | + | require.NotNil(t, rec) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | func TestTableFull(t *testing.T) { | |
| 98 | + | tab := newTable(initialSlots) | |
| 99 | + | for i := 0; i < maxSlots; i++ { | |
| 100 | + | require.NotZero(t, tab.insert(uint32(i+1)), i) | |
| 101 | + | } | |
| 102 | + | require.Zero(t, tab.insert(0xFFFFFF), "table is bounded by the 16-bit slot in Expect tags") | |
| 103 | + | require.Equal(t, Slot(maxSlots), tab.lookup(maxSlots)) | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | func TestTableInsertExisting(t *testing.T) { | |
| 107 | + | tab := newTable(initialSlots) | |
| 108 | + | s := tab.insert(5) | |
| 109 | + | require.Equal(t, s, tab.insert(5)) | |
| 110 | + | require.Equal(t, 1, tab.n) | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | func TestTableLookupAllocations(t *testing.T) { | |
| 114 | + | tab := newTable(initialSlots) | |
| 115 | + | for i := 0; i < 200; i++ { | |
| 116 | + | tab.insert(uint32(0x200000 + i)) | |
| 117 | + | } | |
| 118 | + | var sink Slot | |
| 119 | + | n := testing.AllocsPerRun(1000, func() { | |
| 120 | + | sink += tab.lookup(0x200010) | |
| 121 | + | sink += tab.lookup(0x7fffffff) // absent | |
| 122 | + | _, r := tab.live(tab.tag(3)) | |
| 123 | + | if r != nil { | |
| 124 | + | sink++ | |
| 125 | + | } | |
| 126 | + | }) | |
| 127 | + | require.Zero(t, n) | |
| 128 | + | require.NotZero(t, sink) | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | func TestTableInsertRemoveAllocations(t *testing.T) { | |
| 132 | + | tab := newTable(initialSlots) | |
| 133 | + | n := testing.AllocsPerRun(1000, func() { | |
| 134 | + | s := tab.insert(0x200001) | |
| 135 | + | tab.remove(s) | |
| 136 | + | }) | |
| 137 | + | require.Zero(t, n) | |
| 138 | + | } | |
A internal/adapters/wayland/xwm/testdata/streams/property_unchanged.hex +950 -0
| @@ -0,0 +1,950 @@ | |||
| 1 | + | 1c00740001005000f30300000000000000000000000000000000000000000000 | |
| 2 | + | 0108750003000000ef0300000000000009000000000000000000000000000000 | |
| 3 | + | 6e6574207469746c650000001c00750001005000f30300000000000000000000 | |
| 4 | + | 0000000000000000000000000108760003000000ef0300000000000009000000 | |
| 5 | + | 0000000000000000000000006e6574207469746c650000001c00760001005000 | |
| 6 | + | f303000000000000000000000000000000000000000000000108770003000000 | |
| 7 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 8 | + | 650000001c00770001005000f303000000000000000000000000000000000000 | |
| 9 | + | 000000000108780003000000ef03000000000000090000000000000000000000 | |
| 10 | + | 000000006e6574207469746c650000001c00780001005000f303000000000000 | |
| 11 | + | 000000000000000000000000000000000108790003000000ef03000000000000 | |
| 12 | + | 090000000000000000000000000000006e6574207469746c650000001c007900 | |
| 13 | + | 01005000f3030000000000000000000000000000000000000000000001087a00 | |
| 14 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 15 | + | 7469746c650000001c007a0001005000f3030000000000000000000000000000 | |
| 16 | + | 000000000000000001087b0003000000ef030000000000000900000000000000 | |
| 17 | + | 00000000000000006e6574207469746c650000001c007b0001005000f3030000 | |
| 18 | + | 000000000000000000000000000000000000000001087c0003000000ef030000 | |
| 19 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 20 | + | 1c007c0001005000f30300000000000000000000000000000000000000000000 | |
| 21 | + | 01087d0003000000ef0300000000000009000000000000000000000000000000 | |
| 22 | + | 6e6574207469746c650000001c007d0001005000f30300000000000000000000 | |
| 23 | + | 00000000000000000000000001087e0003000000ef0300000000000009000000 | |
| 24 | + | 0000000000000000000000006e6574207469746c650000001c007e0001005000 | |
| 25 | + | f3030000000000000000000000000000000000000000000001087f0003000000 | |
| 26 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 27 | + | 650000001c007f0001005000f303000000000000000000000000000000000000 | |
| 28 | + | 000000000108800003000000ef03000000000000090000000000000000000000 | |
| 29 | + | 000000006e6574207469746c650000001c00800001005000f303000000000000 | |
| 30 | + | 000000000000000000000000000000000108810003000000ef03000000000000 | |
| 31 | + | 090000000000000000000000000000006e6574207469746c650000001c008100 | |
| 32 | + | 01005000f3030000000000000000000000000000000000000000000001088200 | |
| 33 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 34 | + | 7469746c650000001c00820001005000f3030000000000000000000000000000 | |
| 35 | + | 00000000000000000108830003000000ef030000000000000900000000000000 | |
| 36 | + | 00000000000000006e6574207469746c650000001c00830001005000f3030000 | |
| 37 | + | 00000000000000000000000000000000000000000108840003000000ef030000 | |
| 38 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 39 | + | 1c00840001005000f30300000000000000000000000000000000000000000000 | |
| 40 | + | 0108850003000000ef0300000000000009000000000000000000000000000000 | |
| 41 | + | 6e6574207469746c650000001c00850001005000f30300000000000000000000 | |
| 42 | + | 0000000000000000000000000108860003000000ef0300000000000009000000 | |
| 43 | + | 0000000000000000000000006e6574207469746c650000001c00860001005000 | |
| 44 | + | f303000000000000000000000000000000000000000000000108870003000000 | |
| 45 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 46 | + | 650000001c00870001005000f303000000000000000000000000000000000000 | |
| 47 | + | 000000000108880003000000ef03000000000000090000000000000000000000 | |
| 48 | + | 000000006e6574207469746c650000001c00880001005000f303000000000000 | |
| 49 | + | 000000000000000000000000000000000108890003000000ef03000000000000 | |
| 50 | + | 090000000000000000000000000000006e6574207469746c650000001c008900 | |
| 51 | + | 01005000f3030000000000000000000000000000000000000000000001088a00 | |
| 52 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 53 | + | 7469746c650000001c008a0001005000f3030000000000000000000000000000 | |
| 54 | + | 000000000000000001088b0003000000ef030000000000000900000000000000 | |
| 55 | + | 00000000000000006e6574207469746c650000001c008b0001005000f3030000 | |
| 56 | + | 000000000000000000000000000000000000000001088c0003000000ef030000 | |
| 57 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 58 | + | 1c008c0001005000f30300000000000000000000000000000000000000000000 | |
| 59 | + | 01088d0003000000ef0300000000000009000000000000000000000000000000 | |
| 60 | + | 6e6574207469746c650000001c008d0001005000f30300000000000000000000 | |
| 61 | + | 00000000000000000000000001088e0003000000ef0300000000000009000000 | |
| 62 | + | 0000000000000000000000006e6574207469746c650000001c008e0001005000 | |
| 63 | + | f3030000000000000000000000000000000000000000000001088f0003000000 | |
| 64 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 65 | + | 650000001c008f0001005000f303000000000000000000000000000000000000 | |
| 66 | + | 000000000108900003000000ef03000000000000090000000000000000000000 | |
| 67 | + | 000000006e6574207469746c650000001c00900001005000f303000000000000 | |
| 68 | + | 000000000000000000000000000000000108910003000000ef03000000000000 | |
| 69 | + | 090000000000000000000000000000006e6574207469746c650000001c009100 | |
| 70 | + | 01005000f3030000000000000000000000000000000000000000000001089200 | |
| 71 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 72 | + | 7469746c650000001c00920001005000f3030000000000000000000000000000 | |
| 73 | + | 00000000000000000108930003000000ef030000000000000900000000000000 | |
| 74 | + | 00000000000000006e6574207469746c650000001c00930001005000f3030000 | |
| 75 | + | 00000000000000000000000000000000000000000108940003000000ef030000 | |
| 76 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 77 | + | 1c00940001005000f30300000000000000000000000000000000000000000000 | |
| 78 | + | 0108950003000000ef0300000000000009000000000000000000000000000000 | |
| 79 | + | 6e6574207469746c650000001c00950001005000f30300000000000000000000 | |
| 80 | + | 0000000000000000000000000108960003000000ef0300000000000009000000 | |
| 81 | + | 0000000000000000000000006e6574207469746c650000001c00960001005000 | |
| 82 | + | f303000000000000000000000000000000000000000000000108970003000000 | |
| 83 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 84 | + | 650000001c00970001005000f303000000000000000000000000000000000000 | |
| 85 | + | 000000000108980003000000ef03000000000000090000000000000000000000 | |
| 86 | + | 000000006e6574207469746c650000001c00980001005000f303000000000000 | |
| 87 | + | 000000000000000000000000000000000108990003000000ef03000000000000 | |
| 88 | + | 090000000000000000000000000000006e6574207469746c650000001c009900 | |
| 89 | + | 01005000f3030000000000000000000000000000000000000000000001089a00 | |
| 90 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 91 | + | 7469746c650000001c009a0001005000f3030000000000000000000000000000 | |
| 92 | + | 000000000000000001089b0003000000ef030000000000000900000000000000 | |
| 93 | + | 00000000000000006e6574207469746c650000001c009b0001005000f3030000 | |
| 94 | + | 000000000000000000000000000000000000000001089c0003000000ef030000 | |
| 95 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 96 | + | 1c009c0001005000f30300000000000000000000000000000000000000000000 | |
| 97 | + | 01089d0003000000ef0300000000000009000000000000000000000000000000 | |
| 98 | + | 6e6574207469746c650000001c009d0001005000f30300000000000000000000 | |
| 99 | + | 00000000000000000000000001089e0003000000ef0300000000000009000000 | |
| 100 | + | 0000000000000000000000006e6574207469746c650000001c009e0001005000 | |
| 101 | + | f3030000000000000000000000000000000000000000000001089f0003000000 | |
| 102 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 103 | + | 650000001c009f0001005000f303000000000000000000000000000000000000 | |
| 104 | + | 000000000108a00003000000ef03000000000000090000000000000000000000 | |
| 105 | + | 000000006e6574207469746c650000001c00a00001005000f303000000000000 | |
| 106 | + | 000000000000000000000000000000000108a10003000000ef03000000000000 | |
| 107 | + | 090000000000000000000000000000006e6574207469746c650000001c00a100 | |
| 108 | + | 01005000f303000000000000000000000000000000000000000000000108a200 | |
| 109 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 110 | + | 7469746c650000001c00a20001005000f3030000000000000000000000000000 | |
| 111 | + | 00000000000000000108a30003000000ef030000000000000900000000000000 | |
| 112 | + | 00000000000000006e6574207469746c650000001c00a30001005000f3030000 | |
| 113 | + | 00000000000000000000000000000000000000000108a40003000000ef030000 | |
| 114 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 115 | + | 1c00a40001005000f30300000000000000000000000000000000000000000000 | |
| 116 | + | 0108a50003000000ef0300000000000009000000000000000000000000000000 | |
| 117 | + | 6e6574207469746c650000001c00a50001005000f30300000000000000000000 | |
| 118 | + | 0000000000000000000000000108a60003000000ef0300000000000009000000 | |
| 119 | + | 0000000000000000000000006e6574207469746c650000001c00a60001005000 | |
| 120 | + | f303000000000000000000000000000000000000000000000108a70003000000 | |
| 121 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 122 | + | 650000001c00a70001005000f303000000000000000000000000000000000000 | |
| 123 | + | 000000000108a80003000000ef03000000000000090000000000000000000000 | |
| 124 | + | 000000006e6574207469746c650000001c00a80001005000f303000000000000 | |
| 125 | + | 000000000000000000000000000000000108a90003000000ef03000000000000 | |
| 126 | + | 090000000000000000000000000000006e6574207469746c650000001c00a900 | |
| 127 | + | 01005000f303000000000000000000000000000000000000000000000108aa00 | |
| 128 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 129 | + | 7469746c650000001c00aa0001005000f3030000000000000000000000000000 | |
| 130 | + | 00000000000000000108ab0003000000ef030000000000000900000000000000 | |
| 131 | + | 00000000000000006e6574207469746c650000001c00ab0001005000f3030000 | |
| 132 | + | 00000000000000000000000000000000000000000108ac0003000000ef030000 | |
| 133 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 134 | + | 1c00ac0001005000f30300000000000000000000000000000000000000000000 | |
| 135 | + | 0108ad0003000000ef0300000000000009000000000000000000000000000000 | |
| 136 | + | 6e6574207469746c650000001c00ad0001005000f30300000000000000000000 | |
| 137 | + | 0000000000000000000000000108ae0003000000ef0300000000000009000000 | |
| 138 | + | 0000000000000000000000006e6574207469746c650000001c00ae0001005000 | |
| 139 | + | f303000000000000000000000000000000000000000000000108af0003000000 | |
| 140 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 141 | + | 650000001c00af0001005000f303000000000000000000000000000000000000 | |
| 142 | + | 000000000108b00003000000ef03000000000000090000000000000000000000 | |
| 143 | + | 000000006e6574207469746c650000001c00b00001005000f303000000000000 | |
| 144 | + | 000000000000000000000000000000000108b10003000000ef03000000000000 | |
| 145 | + | 090000000000000000000000000000006e6574207469746c650000001c00b100 | |
| 146 | + | 01005000f303000000000000000000000000000000000000000000000108b200 | |
| 147 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 148 | + | 7469746c650000001c00b20001005000f3030000000000000000000000000000 | |
| 149 | + | 00000000000000000108b30003000000ef030000000000000900000000000000 | |
| 150 | + | 00000000000000006e6574207469746c650000001c00b30001005000f3030000 | |
| 151 | + | 00000000000000000000000000000000000000000108b40003000000ef030000 | |
| 152 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 153 | + | 1c00b40001005000f30300000000000000000000000000000000000000000000 | |
| 154 | + | 0108b50003000000ef0300000000000009000000000000000000000000000000 | |
| 155 | + | 6e6574207469746c650000001c00b50001005000f30300000000000000000000 | |
| 156 | + | 0000000000000000000000000108b60003000000ef0300000000000009000000 | |
| 157 | + | 0000000000000000000000006e6574207469746c650000001c00b60001005000 | |
| 158 | + | f303000000000000000000000000000000000000000000000108b70003000000 | |
| 159 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 160 | + | 650000001c00b70001005000f303000000000000000000000000000000000000 | |
| 161 | + | 000000000108b80003000000ef03000000000000090000000000000000000000 | |
| 162 | + | 000000006e6574207469746c650000001c00b80001005000f303000000000000 | |
| 163 | + | 000000000000000000000000000000000108b90003000000ef03000000000000 | |
| 164 | + | 090000000000000000000000000000006e6574207469746c650000001c00b900 | |
| 165 | + | 01005000f303000000000000000000000000000000000000000000000108ba00 | |
| 166 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 167 | + | 7469746c650000001c00ba0001005000f3030000000000000000000000000000 | |
| 168 | + | 00000000000000000108bb0003000000ef030000000000000900000000000000 | |
| 169 | + | 00000000000000006e6574207469746c650000001c00bb0001005000f3030000 | |
| 170 | + | 00000000000000000000000000000000000000000108bc0003000000ef030000 | |
| 171 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 172 | + | 1c00bc0001005000f30300000000000000000000000000000000000000000000 | |
| 173 | + | 0108bd0003000000ef0300000000000009000000000000000000000000000000 | |
| 174 | + | 6e6574207469746c650000001c00bd0001005000f30300000000000000000000 | |
| 175 | + | 0000000000000000000000000108be0003000000ef0300000000000009000000 | |
| 176 | + | 0000000000000000000000006e6574207469746c650000001c00be0001005000 | |
| 177 | + | f303000000000000000000000000000000000000000000000108bf0003000000 | |
| 178 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 179 | + | 650000001c00bf0001005000f303000000000000000000000000000000000000 | |
| 180 | + | 000000000108c00003000000ef03000000000000090000000000000000000000 | |
| 181 | + | 000000006e6574207469746c650000001c00c00001005000f303000000000000 | |
| 182 | + | 000000000000000000000000000000000108c10003000000ef03000000000000 | |
| 183 | + | 090000000000000000000000000000006e6574207469746c650000001c00c100 | |
| 184 | + | 01005000f303000000000000000000000000000000000000000000000108c200 | |
| 185 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 186 | + | 7469746c650000001c00c20001005000f3030000000000000000000000000000 | |
| 187 | + | 00000000000000000108c30003000000ef030000000000000900000000000000 | |
| 188 | + | 00000000000000006e6574207469746c650000001c00c30001005000f3030000 | |
| 189 | + | 00000000000000000000000000000000000000000108c40003000000ef030000 | |
| 190 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 191 | + | 1c00c40001005000f30300000000000000000000000000000000000000000000 | |
| 192 | + | 0108c50003000000ef0300000000000009000000000000000000000000000000 | |
| 193 | + | 6e6574207469746c650000001c00c50001005000f30300000000000000000000 | |
| 194 | + | 0000000000000000000000000108c60003000000ef0300000000000009000000 | |
| 195 | + | 0000000000000000000000006e6574207469746c650000001c00c60001005000 | |
| 196 | + | f303000000000000000000000000000000000000000000000108c70003000000 | |
| 197 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 198 | + | 650000001c00c70001005000f303000000000000000000000000000000000000 | |
| 199 | + | 000000000108c80003000000ef03000000000000090000000000000000000000 | |
| 200 | + | 000000006e6574207469746c650000001c00c80001005000f303000000000000 | |
| 201 | + | 000000000000000000000000000000000108c90003000000ef03000000000000 | |
| 202 | + | 090000000000000000000000000000006e6574207469746c650000001c00c900 | |
| 203 | + | 01005000f303000000000000000000000000000000000000000000000108ca00 | |
| 204 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 205 | + | 7469746c650000001c00ca0001005000f3030000000000000000000000000000 | |
| 206 | + | 00000000000000000108cb0003000000ef030000000000000900000000000000 | |
| 207 | + | 00000000000000006e6574207469746c650000001c00cb0001005000f3030000 | |
| 208 | + | 00000000000000000000000000000000000000000108cc0003000000ef030000 | |
| 209 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 210 | + | 1c00cc0001005000f30300000000000000000000000000000000000000000000 | |
| 211 | + | 0108cd0003000000ef0300000000000009000000000000000000000000000000 | |
| 212 | + | 6e6574207469746c650000001c00cd0001005000f30300000000000000000000 | |
| 213 | + | 0000000000000000000000000108ce0003000000ef0300000000000009000000 | |
| 214 | + | 0000000000000000000000006e6574207469746c650000001c00ce0001005000 | |
| 215 | + | f303000000000000000000000000000000000000000000000108cf0003000000 | |
| 216 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 217 | + | 650000001c00cf0001005000f303000000000000000000000000000000000000 | |
| 218 | + | 000000000108d00003000000ef03000000000000090000000000000000000000 | |
| 219 | + | 000000006e6574207469746c650000001c00d00001005000f303000000000000 | |
| 220 | + | 000000000000000000000000000000000108d10003000000ef03000000000000 | |
| 221 | + | 090000000000000000000000000000006e6574207469746c650000001c00d100 | |
| 222 | + | 01005000f303000000000000000000000000000000000000000000000108d200 | |
| 223 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 224 | + | 7469746c650000001c00d20001005000f3030000000000000000000000000000 | |
| 225 | + | 00000000000000000108d30003000000ef030000000000000900000000000000 | |
| 226 | + | 00000000000000006e6574207469746c650000001c00d30001005000f3030000 | |
| 227 | + | 00000000000000000000000000000000000000000108d40003000000ef030000 | |
| 228 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 229 | + | 1c00d40001005000f30300000000000000000000000000000000000000000000 | |
| 230 | + | 0108d50003000000ef0300000000000009000000000000000000000000000000 | |
| 231 | + | 6e6574207469746c650000001c00d50001005000f30300000000000000000000 | |
| 232 | + | 0000000000000000000000000108d60003000000ef0300000000000009000000 | |
| 233 | + | 0000000000000000000000006e6574207469746c650000001c00d60001005000 | |
| 234 | + | f303000000000000000000000000000000000000000000000108d70003000000 | |
| 235 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 236 | + | 650000001c00d70001005000f303000000000000000000000000000000000000 | |
| 237 | + | 000000000108d80003000000ef03000000000000090000000000000000000000 | |
| 238 | + | 000000006e6574207469746c650000001c00d80001005000f303000000000000 | |
| 239 | + | 000000000000000000000000000000000108d90003000000ef03000000000000 | |
| 240 | + | 090000000000000000000000000000006e6574207469746c650000001c00d900 | |
| 241 | + | 01005000f303000000000000000000000000000000000000000000000108da00 | |
| 242 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 243 | + | 7469746c650000001c00da0001005000f3030000000000000000000000000000 | |
| 244 | + | 00000000000000000108db0003000000ef030000000000000900000000000000 | |
| 245 | + | 00000000000000006e6574207469746c650000001c00db0001005000f3030000 | |
| 246 | + | 00000000000000000000000000000000000000000108dc0003000000ef030000 | |
| 247 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 248 | + | 1c00dc0001005000f30300000000000000000000000000000000000000000000 | |
| 249 | + | 0108dd0003000000ef0300000000000009000000000000000000000000000000 | |
| 250 | + | 6e6574207469746c650000001c00dd0001005000f30300000000000000000000 | |
| 251 | + | 0000000000000000000000000108de0003000000ef0300000000000009000000 | |
| 252 | + | 0000000000000000000000006e6574207469746c650000001c00de0001005000 | |
| 253 | + | f303000000000000000000000000000000000000000000000108df0003000000 | |
| 254 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 255 | + | 650000001c00df0001005000f303000000000000000000000000000000000000 | |
| 256 | + | 000000000108e00003000000ef03000000000000090000000000000000000000 | |
| 257 | + | 000000006e6574207469746c650000001c00e00001005000f303000000000000 | |
| 258 | + | 000000000000000000000000000000000108e10003000000ef03000000000000 | |
| 259 | + | 090000000000000000000000000000006e6574207469746c650000001c00e100 | |
| 260 | + | 01005000f303000000000000000000000000000000000000000000000108e200 | |
| 261 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 262 | + | 7469746c650000001c00e20001005000f3030000000000000000000000000000 | |
| 263 | + | 00000000000000000108e30003000000ef030000000000000900000000000000 | |
| 264 | + | 00000000000000006e6574207469746c650000001c00e30001005000f3030000 | |
| 265 | + | 00000000000000000000000000000000000000000108e40003000000ef030000 | |
| 266 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 267 | + | 1c00e40001005000f30300000000000000000000000000000000000000000000 | |
| 268 | + | 0108e50003000000ef0300000000000009000000000000000000000000000000 | |
| 269 | + | 6e6574207469746c650000001c00e50001005000f30300000000000000000000 | |
| 270 | + | 0000000000000000000000000108e60003000000ef0300000000000009000000 | |
| 271 | + | 0000000000000000000000006e6574207469746c650000001c00e60001005000 | |
| 272 | + | f303000000000000000000000000000000000000000000000108e70003000000 | |
| 273 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 274 | + | 650000001c00e70001005000f303000000000000000000000000000000000000 | |
| 275 | + | 000000000108e80003000000ef03000000000000090000000000000000000000 | |
| 276 | + | 000000006e6574207469746c650000001c00e80001005000f303000000000000 | |
| 277 | + | 000000000000000000000000000000000108e90003000000ef03000000000000 | |
| 278 | + | 090000000000000000000000000000006e6574207469746c650000001c00e900 | |
| 279 | + | 01005000f303000000000000000000000000000000000000000000000108ea00 | |
| 280 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 281 | + | 7469746c650000001c00ea0001005000f3030000000000000000000000000000 | |
| 282 | + | 00000000000000000108eb0003000000ef030000000000000900000000000000 | |
| 283 | + | 00000000000000006e6574207469746c650000001c00eb0001005000f3030000 | |
| 284 | + | 00000000000000000000000000000000000000000108ec0003000000ef030000 | |
| 285 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 286 | + | 1c00ec0001005000f30300000000000000000000000000000000000000000000 | |
| 287 | + | 0108ed0003000000ef0300000000000009000000000000000000000000000000 | |
| 288 | + | 6e6574207469746c650000001c00ed0001005000f30300000000000000000000 | |
| 289 | + | 0000000000000000000000000108ee0003000000ef0300000000000009000000 | |
| 290 | + | 0000000000000000000000006e6574207469746c650000001c00ee0001005000 | |
| 291 | + | f303000000000000000000000000000000000000000000000108ef0003000000 | |
| 292 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 293 | + | 650000001c00ef0001005000f303000000000000000000000000000000000000 | |
| 294 | + | 000000000108f00003000000ef03000000000000090000000000000000000000 | |
| 295 | + | 000000006e6574207469746c650000001c00f00001005000f303000000000000 | |
| 296 | + | 000000000000000000000000000000000108f10003000000ef03000000000000 | |
| 297 | + | 090000000000000000000000000000006e6574207469746c650000001c00f100 | |
| 298 | + | 01005000f303000000000000000000000000000000000000000000000108f200 | |
| 299 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 300 | + | 7469746c650000001c00f20001005000f3030000000000000000000000000000 | |
| 301 | + | 00000000000000000108f30003000000ef030000000000000900000000000000 | |
| 302 | + | 00000000000000006e6574207469746c650000001c00f30001005000f3030000 | |
| 303 | + | 00000000000000000000000000000000000000000108f40003000000ef030000 | |
| 304 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 305 | + | 1c00f40001005000f30300000000000000000000000000000000000000000000 | |
| 306 | + | 0108f50003000000ef0300000000000009000000000000000000000000000000 | |
| 307 | + | 6e6574207469746c650000001c00f50001005000f30300000000000000000000 | |
| 308 | + | 0000000000000000000000000108f60003000000ef0300000000000009000000 | |
| 309 | + | 0000000000000000000000006e6574207469746c650000001c00f60001005000 | |
| 310 | + | f303000000000000000000000000000000000000000000000108f70003000000 | |
| 311 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 312 | + | 650000001c00f70001005000f303000000000000000000000000000000000000 | |
| 313 | + | 000000000108f80003000000ef03000000000000090000000000000000000000 | |
| 314 | + | 000000006e6574207469746c650000001c00f80001005000f303000000000000 | |
| 315 | + | 000000000000000000000000000000000108f90003000000ef03000000000000 | |
| 316 | + | 090000000000000000000000000000006e6574207469746c650000001c00f900 | |
| 317 | + | 01005000f303000000000000000000000000000000000000000000000108fa00 | |
| 318 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 319 | + | 7469746c650000001c00fa0001005000f3030000000000000000000000000000 | |
| 320 | + | 00000000000000000108fb0003000000ef030000000000000900000000000000 | |
| 321 | + | 00000000000000006e6574207469746c650000001c00fb0001005000f3030000 | |
| 322 | + | 00000000000000000000000000000000000000000108fc0003000000ef030000 | |
| 323 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 324 | + | 1c00fc0001005000f30300000000000000000000000000000000000000000000 | |
| 325 | + | 0108fd0003000000ef0300000000000009000000000000000000000000000000 | |
| 326 | + | 6e6574207469746c650000001c00fd0001005000f30300000000000000000000 | |
| 327 | + | 0000000000000000000000000108fe0003000000ef0300000000000009000000 | |
| 328 | + | 0000000000000000000000006e6574207469746c650000001c00fe0001005000 | |
| 329 | + | f303000000000000000000000000000000000000000000000108ff0003000000 | |
| 330 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 331 | + | 650000001c00ff0001005000f303000000000000000000000000000000000000 | |
| 332 | + | 000000000108000103000000ef03000000000000090000000000000000000000 | |
| 333 | + | 000000006e6574207469746c650000001c00000101005000f303000000000000 | |
| 334 | + | 000000000000000000000000000000000108010103000000ef03000000000000 | |
| 335 | + | 090000000000000000000000000000006e6574207469746c650000001c000101 | |
| 336 | + | 01005000f3030000000000000000000000000000000000000000000001080201 | |
| 337 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 338 | + | 7469746c650000001c00020101005000f3030000000000000000000000000000 | |
| 339 | + | 00000000000000000108030103000000ef030000000000000900000000000000 | |
| 340 | + | 00000000000000006e6574207469746c650000001c00030101005000f3030000 | |
| 341 | + | 00000000000000000000000000000000000000000108040103000000ef030000 | |
| 342 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 343 | + | 1c00040101005000f30300000000000000000000000000000000000000000000 | |
| 344 | + | 0108050103000000ef0300000000000009000000000000000000000000000000 | |
| 345 | + | 6e6574207469746c650000001c00050101005000f30300000000000000000000 | |
| 346 | + | 0000000000000000000000000108060103000000ef0300000000000009000000 | |
| 347 | + | 0000000000000000000000006e6574207469746c650000001c00060101005000 | |
| 348 | + | f303000000000000000000000000000000000000000000000108070103000000 | |
| 349 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 350 | + | 650000001c00070101005000f303000000000000000000000000000000000000 | |
| 351 | + | 000000000108080103000000ef03000000000000090000000000000000000000 | |
| 352 | + | 000000006e6574207469746c650000001c00080101005000f303000000000000 | |
| 353 | + | 000000000000000000000000000000000108090103000000ef03000000000000 | |
| 354 | + | 090000000000000000000000000000006e6574207469746c650000001c000901 | |
| 355 | + | 01005000f3030000000000000000000000000000000000000000000001080a01 | |
| 356 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 357 | + | 7469746c650000001c000a0101005000f3030000000000000000000000000000 | |
| 358 | + | 000000000000000001080b0103000000ef030000000000000900000000000000 | |
| 359 | + | 00000000000000006e6574207469746c650000001c000b0101005000f3030000 | |
| 360 | + | 000000000000000000000000000000000000000001080c0103000000ef030000 | |
| 361 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 362 | + | 1c000c0101005000f30300000000000000000000000000000000000000000000 | |
| 363 | + | 01080d0103000000ef0300000000000009000000000000000000000000000000 | |
| 364 | + | 6e6574207469746c650000001c000d0101005000f30300000000000000000000 | |
| 365 | + | 00000000000000000000000001080e0103000000ef0300000000000009000000 | |
| 366 | + | 0000000000000000000000006e6574207469746c650000001c000e0101005000 | |
| 367 | + | f3030000000000000000000000000000000000000000000001080f0103000000 | |
| 368 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 369 | + | 650000001c000f0101005000f303000000000000000000000000000000000000 | |
| 370 | + | 000000000108100103000000ef03000000000000090000000000000000000000 | |
| 371 | + | 000000006e6574207469746c650000001c00100101005000f303000000000000 | |
| 372 | + | 000000000000000000000000000000000108110103000000ef03000000000000 | |
| 373 | + | 090000000000000000000000000000006e6574207469746c650000001c001101 | |
| 374 | + | 01005000f3030000000000000000000000000000000000000000000001081201 | |
| 375 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 376 | + | 7469746c650000001c00120101005000f3030000000000000000000000000000 | |
| 377 | + | 00000000000000000108130103000000ef030000000000000900000000000000 | |
| 378 | + | 00000000000000006e6574207469746c650000001c00130101005000f3030000 | |
| 379 | + | 00000000000000000000000000000000000000000108140103000000ef030000 | |
| 380 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 381 | + | 1c00140101005000f30300000000000000000000000000000000000000000000 | |
| 382 | + | 0108150103000000ef0300000000000009000000000000000000000000000000 | |
| 383 | + | 6e6574207469746c650000001c00150101005000f30300000000000000000000 | |
| 384 | + | 0000000000000000000000000108160103000000ef0300000000000009000000 | |
| 385 | + | 0000000000000000000000006e6574207469746c650000001c00160101005000 | |
| 386 | + | f303000000000000000000000000000000000000000000000108170103000000 | |
| 387 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 388 | + | 650000001c00170101005000f303000000000000000000000000000000000000 | |
| 389 | + | 000000000108180103000000ef03000000000000090000000000000000000000 | |
| 390 | + | 000000006e6574207469746c650000001c00180101005000f303000000000000 | |
| 391 | + | 000000000000000000000000000000000108190103000000ef03000000000000 | |
| 392 | + | 090000000000000000000000000000006e6574207469746c650000001c001901 | |
| 393 | + | 01005000f3030000000000000000000000000000000000000000000001081a01 | |
| 394 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 395 | + | 7469746c650000001c001a0101005000f3030000000000000000000000000000 | |
| 396 | + | 000000000000000001081b0103000000ef030000000000000900000000000000 | |
| 397 | + | 00000000000000006e6574207469746c650000001c001b0101005000f3030000 | |
| 398 | + | 000000000000000000000000000000000000000001081c0103000000ef030000 | |
| 399 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 400 | + | 1c001c0101005000f30300000000000000000000000000000000000000000000 | |
| 401 | + | 01081d0103000000ef0300000000000009000000000000000000000000000000 | |
| 402 | + | 6e6574207469746c650000001c001d0101005000f30300000000000000000000 | |
| 403 | + | 00000000000000000000000001081e0103000000ef0300000000000009000000 | |
| 404 | + | 0000000000000000000000006e6574207469746c650000001c001e0101005000 | |
| 405 | + | f3030000000000000000000000000000000000000000000001081f0103000000 | |
| 406 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 407 | + | 650000001c001f0101005000f303000000000000000000000000000000000000 | |
| 408 | + | 000000000108200103000000ef03000000000000090000000000000000000000 | |
| 409 | + | 000000006e6574207469746c650000001c00200101005000f303000000000000 | |
| 410 | + | 000000000000000000000000000000000108210103000000ef03000000000000 | |
| 411 | + | 090000000000000000000000000000006e6574207469746c650000001c002101 | |
| 412 | + | 01005000f3030000000000000000000000000000000000000000000001082201 | |
| 413 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 414 | + | 7469746c650000001c00220101005000f3030000000000000000000000000000 | |
| 415 | + | 00000000000000000108230103000000ef030000000000000900000000000000 | |
| 416 | + | 00000000000000006e6574207469746c650000001c00230101005000f3030000 | |
| 417 | + | 00000000000000000000000000000000000000000108240103000000ef030000 | |
| 418 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 419 | + | 1c00240101005000f30300000000000000000000000000000000000000000000 | |
| 420 | + | 0108250103000000ef0300000000000009000000000000000000000000000000 | |
| 421 | + | 6e6574207469746c650000001c00250101005000f30300000000000000000000 | |
| 422 | + | 0000000000000000000000000108260103000000ef0300000000000009000000 | |
| 423 | + | 0000000000000000000000006e6574207469746c650000001c00260101005000 | |
| 424 | + | f303000000000000000000000000000000000000000000000108270103000000 | |
| 425 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 426 | + | 650000001c00270101005000f303000000000000000000000000000000000000 | |
| 427 | + | 000000000108280103000000ef03000000000000090000000000000000000000 | |
| 428 | + | 000000006e6574207469746c650000001c00280101005000f303000000000000 | |
| 429 | + | 000000000000000000000000000000000108290103000000ef03000000000000 | |
| 430 | + | 090000000000000000000000000000006e6574207469746c650000001c002901 | |
| 431 | + | 01005000f3030000000000000000000000000000000000000000000001082a01 | |
| 432 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 433 | + | 7469746c650000001c002a0101005000f3030000000000000000000000000000 | |
| 434 | + | 000000000000000001082b0103000000ef030000000000000900000000000000 | |
| 435 | + | 00000000000000006e6574207469746c650000001c002b0101005000f3030000 | |
| 436 | + | 000000000000000000000000000000000000000001082c0103000000ef030000 | |
| 437 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 438 | + | 1c002c0101005000f30300000000000000000000000000000000000000000000 | |
| 439 | + | 01082d0103000000ef0300000000000009000000000000000000000000000000 | |
| 440 | + | 6e6574207469746c650000001c002d0101005000f30300000000000000000000 | |
| 441 | + | 00000000000000000000000001082e0103000000ef0300000000000009000000 | |
| 442 | + | 0000000000000000000000006e6574207469746c650000001c002e0101005000 | |
| 443 | + | f3030000000000000000000000000000000000000000000001082f0103000000 | |
| 444 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 445 | + | 650000001c002f0101005000f303000000000000000000000000000000000000 | |
| 446 | + | 000000000108300103000000ef03000000000000090000000000000000000000 | |
| 447 | + | 000000006e6574207469746c650000001c00300101005000f303000000000000 | |
| 448 | + | 000000000000000000000000000000000108310103000000ef03000000000000 | |
| 449 | + | 090000000000000000000000000000006e6574207469746c650000001c003101 | |
| 450 | + | 01005000f3030000000000000000000000000000000000000000000001083201 | |
| 451 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 452 | + | 7469746c650000001c00320101005000f3030000000000000000000000000000 | |
| 453 | + | 00000000000000000108330103000000ef030000000000000900000000000000 | |
| 454 | + | 00000000000000006e6574207469746c650000001c00330101005000f3030000 | |
| 455 | + | 00000000000000000000000000000000000000000108340103000000ef030000 | |
| 456 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 457 | + | 1c00340101005000f30300000000000000000000000000000000000000000000 | |
| 458 | + | 0108350103000000ef0300000000000009000000000000000000000000000000 | |
| 459 | + | 6e6574207469746c650000001c00350101005000f30300000000000000000000 | |
| 460 | + | 0000000000000000000000000108360103000000ef0300000000000009000000 | |
| 461 | + | 0000000000000000000000006e6574207469746c650000001c00360101005000 | |
| 462 | + | f303000000000000000000000000000000000000000000000108370103000000 | |
| 463 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 464 | + | 650000001c00370101005000f303000000000000000000000000000000000000 | |
| 465 | + | 000000000108380103000000ef03000000000000090000000000000000000000 | |
| 466 | + | 000000006e6574207469746c650000001c00380101005000f303000000000000 | |
| 467 | + | 000000000000000000000000000000000108390103000000ef03000000000000 | |
| 468 | + | 090000000000000000000000000000006e6574207469746c650000001c003901 | |
| 469 | + | 01005000f3030000000000000000000000000000000000000000000001083a01 | |
| 470 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 471 | + | 7469746c650000001c003a0101005000f3030000000000000000000000000000 | |
| 472 | + | 000000000000000001083b0103000000ef030000000000000900000000000000 | |
| 473 | + | 00000000000000006e6574207469746c650000001c003b0101005000f3030000 | |
| 474 | + | 000000000000000000000000000000000000000001083c0103000000ef030000 | |
| 475 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 476 | + | 1c003c0101005000f30300000000000000000000000000000000000000000000 | |
| 477 | + | 01083d0103000000ef0300000000000009000000000000000000000000000000 | |
| 478 | + | 6e6574207469746c650000001c003d0101005000f30300000000000000000000 | |
| 479 | + | 00000000000000000000000001083e0103000000ef0300000000000009000000 | |
| 480 | + | 0000000000000000000000006e6574207469746c650000001c003e0101005000 | |
| 481 | + | f3030000000000000000000000000000000000000000000001083f0103000000 | |
| 482 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 483 | + | 650000001c003f0101005000f303000000000000000000000000000000000000 | |
| 484 | + | 000000000108400103000000ef03000000000000090000000000000000000000 | |
| 485 | + | 000000006e6574207469746c650000001c00400101005000f303000000000000 | |
| 486 | + | 000000000000000000000000000000000108410103000000ef03000000000000 | |
| 487 | + | 090000000000000000000000000000006e6574207469746c650000001c004101 | |
| 488 | + | 01005000f3030000000000000000000000000000000000000000000001084201 | |
| 489 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 490 | + | 7469746c650000001c00420101005000f3030000000000000000000000000000 | |
| 491 | + | 00000000000000000108430103000000ef030000000000000900000000000000 | |
| 492 | + | 00000000000000006e6574207469746c650000001c00430101005000f3030000 | |
| 493 | + | 00000000000000000000000000000000000000000108440103000000ef030000 | |
| 494 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 495 | + | 1c00440101005000f30300000000000000000000000000000000000000000000 | |
| 496 | + | 0108450103000000ef0300000000000009000000000000000000000000000000 | |
| 497 | + | 6e6574207469746c650000001c00450101005000f30300000000000000000000 | |
| 498 | + | 0000000000000000000000000108460103000000ef0300000000000009000000 | |
| 499 | + | 0000000000000000000000006e6574207469746c650000001c00460101005000 | |
| 500 | + | f303000000000000000000000000000000000000000000000108470103000000 | |
| 501 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 502 | + | 650000001c00470101005000f303000000000000000000000000000000000000 | |
| 503 | + | 000000000108480103000000ef03000000000000090000000000000000000000 | |
| 504 | + | 000000006e6574207469746c650000001c00480101005000f303000000000000 | |
| 505 | + | 000000000000000000000000000000000108490103000000ef03000000000000 | |
| 506 | + | 090000000000000000000000000000006e6574207469746c650000001c004901 | |
| 507 | + | 01005000f3030000000000000000000000000000000000000000000001084a01 | |
| 508 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 509 | + | 7469746c650000001c004a0101005000f3030000000000000000000000000000 | |
| 510 | + | 000000000000000001084b0103000000ef030000000000000900000000000000 | |
| 511 | + | 00000000000000006e6574207469746c650000001c004b0101005000f3030000 | |
| 512 | + | 000000000000000000000000000000000000000001084c0103000000ef030000 | |
| 513 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 514 | + | 1c004c0101005000f30300000000000000000000000000000000000000000000 | |
| 515 | + | 01084d0103000000ef0300000000000009000000000000000000000000000000 | |
| 516 | + | 6e6574207469746c650000001c004d0101005000f30300000000000000000000 | |
| 517 | + | 00000000000000000000000001084e0103000000ef0300000000000009000000 | |
| 518 | + | 0000000000000000000000006e6574207469746c650000001c004e0101005000 | |
| 519 | + | f3030000000000000000000000000000000000000000000001084f0103000000 | |
| 520 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 521 | + | 650000001c004f0101005000f303000000000000000000000000000000000000 | |
| 522 | + | 000000000108500103000000ef03000000000000090000000000000000000000 | |
| 523 | + | 000000006e6574207469746c650000001c00500101005000f303000000000000 | |
| 524 | + | 000000000000000000000000000000000108510103000000ef03000000000000 | |
| 525 | + | 090000000000000000000000000000006e6574207469746c650000001c005101 | |
| 526 | + | 01005000f3030000000000000000000000000000000000000000000001085201 | |
| 527 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 528 | + | 7469746c650000001c00520101005000f3030000000000000000000000000000 | |
| 529 | + | 00000000000000000108530103000000ef030000000000000900000000000000 | |
| 530 | + | 00000000000000006e6574207469746c650000001c00530101005000f3030000 | |
| 531 | + | 00000000000000000000000000000000000000000108540103000000ef030000 | |
| 532 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 533 | + | 1c00540101005000f30300000000000000000000000000000000000000000000 | |
| 534 | + | 0108550103000000ef0300000000000009000000000000000000000000000000 | |
| 535 | + | 6e6574207469746c650000001c00550101005000f30300000000000000000000 | |
| 536 | + | 0000000000000000000000000108560103000000ef0300000000000009000000 | |
| 537 | + | 0000000000000000000000006e6574207469746c650000001c00560101005000 | |
| 538 | + | f303000000000000000000000000000000000000000000000108570103000000 | |
| 539 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 540 | + | 650000001c00570101005000f303000000000000000000000000000000000000 | |
| 541 | + | 000000000108580103000000ef03000000000000090000000000000000000000 | |
| 542 | + | 000000006e6574207469746c650000001c00580101005000f303000000000000 | |
| 543 | + | 000000000000000000000000000000000108590103000000ef03000000000000 | |
| 544 | + | 090000000000000000000000000000006e6574207469746c650000001c005901 | |
| 545 | + | 01005000f3030000000000000000000000000000000000000000000001085a01 | |
| 546 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 547 | + | 7469746c650000001c005a0101005000f3030000000000000000000000000000 | |
| 548 | + | 000000000000000001085b0103000000ef030000000000000900000000000000 | |
| 549 | + | 00000000000000006e6574207469746c650000001c005b0101005000f3030000 | |
| 550 | + | 000000000000000000000000000000000000000001085c0103000000ef030000 | |
| 551 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 552 | + | 1c005c0101005000f30300000000000000000000000000000000000000000000 | |
| 553 | + | 01085d0103000000ef0300000000000009000000000000000000000000000000 | |
| 554 | + | 6e6574207469746c650000001c005d0101005000f30300000000000000000000 | |
| 555 | + | 00000000000000000000000001085e0103000000ef0300000000000009000000 | |
| 556 | + | 0000000000000000000000006e6574207469746c650000001c005e0101005000 | |
| 557 | + | f3030000000000000000000000000000000000000000000001085f0103000000 | |
| 558 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 559 | + | 650000001c005f0101005000f303000000000000000000000000000000000000 | |
| 560 | + | 000000000108600103000000ef03000000000000090000000000000000000000 | |
| 561 | + | 000000006e6574207469746c650000001c00600101005000f303000000000000 | |
| 562 | + | 000000000000000000000000000000000108610103000000ef03000000000000 | |
| 563 | + | 090000000000000000000000000000006e6574207469746c650000001c006101 | |
| 564 | + | 01005000f3030000000000000000000000000000000000000000000001086201 | |
| 565 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 566 | + | 7469746c650000001c00620101005000f3030000000000000000000000000000 | |
| 567 | + | 00000000000000000108630103000000ef030000000000000900000000000000 | |
| 568 | + | 00000000000000006e6574207469746c650000001c00630101005000f3030000 | |
| 569 | + | 00000000000000000000000000000000000000000108640103000000ef030000 | |
| 570 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 571 | + | 1c00640101005000f30300000000000000000000000000000000000000000000 | |
| 572 | + | 0108650103000000ef0300000000000009000000000000000000000000000000 | |
| 573 | + | 6e6574207469746c650000001c00650101005000f30300000000000000000000 | |
| 574 | + | 0000000000000000000000000108660103000000ef0300000000000009000000 | |
| 575 | + | 0000000000000000000000006e6574207469746c650000001c00660101005000 | |
| 576 | + | f303000000000000000000000000000000000000000000000108670103000000 | |
| 577 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 578 | + | 650000001c00670101005000f303000000000000000000000000000000000000 | |
| 579 | + | 000000000108680103000000ef03000000000000090000000000000000000000 | |
| 580 | + | 000000006e6574207469746c650000001c00680101005000f303000000000000 | |
| 581 | + | 000000000000000000000000000000000108690103000000ef03000000000000 | |
| 582 | + | 090000000000000000000000000000006e6574207469746c650000001c006901 | |
| 583 | + | 01005000f3030000000000000000000000000000000000000000000001086a01 | |
| 584 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 585 | + | 7469746c650000001c006a0101005000f3030000000000000000000000000000 | |
| 586 | + | 000000000000000001086b0103000000ef030000000000000900000000000000 | |
| 587 | + | 00000000000000006e6574207469746c650000001c006b0101005000f3030000 | |
| 588 | + | 000000000000000000000000000000000000000001086c0103000000ef030000 | |
| 589 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 590 | + | 1c006c0101005000f30300000000000000000000000000000000000000000000 | |
| 591 | + | 01086d0103000000ef0300000000000009000000000000000000000000000000 | |
| 592 | + | 6e6574207469746c650000001c006d0101005000f30300000000000000000000 | |
| 593 | + | 00000000000000000000000001086e0103000000ef0300000000000009000000 | |
| 594 | + | 0000000000000000000000006e6574207469746c650000001c006e0101005000 | |
| 595 | + | f3030000000000000000000000000000000000000000000001086f0103000000 | |
| 596 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 597 | + | 650000001c006f0101005000f303000000000000000000000000000000000000 | |
| 598 | + | 000000000108700103000000ef03000000000000090000000000000000000000 | |
| 599 | + | 000000006e6574207469746c650000001c00700101005000f303000000000000 | |
| 600 | + | 000000000000000000000000000000000108710103000000ef03000000000000 | |
| 601 | + | 090000000000000000000000000000006e6574207469746c650000001c007101 | |
| 602 | + | 01005000f3030000000000000000000000000000000000000000000001087201 | |
| 603 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 604 | + | 7469746c650000001c00720101005000f3030000000000000000000000000000 | |
| 605 | + | 00000000000000000108730103000000ef030000000000000900000000000000 | |
| 606 | + | 00000000000000006e6574207469746c650000001c00730101005000f3030000 | |
| 607 | + | 00000000000000000000000000000000000000000108740103000000ef030000 | |
| 608 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 609 | + | 1c00740101005000f30300000000000000000000000000000000000000000000 | |
| 610 | + | 0108750103000000ef0300000000000009000000000000000000000000000000 | |
| 611 | + | 6e6574207469746c650000001c00750101005000f30300000000000000000000 | |
| 612 | + | 0000000000000000000000000108760103000000ef0300000000000009000000 | |
| 613 | + | 0000000000000000000000006e6574207469746c650000001c00760101005000 | |
| 614 | + | f303000000000000000000000000000000000000000000000108770103000000 | |
| 615 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 616 | + | 650000001c00770101005000f303000000000000000000000000000000000000 | |
| 617 | + | 000000000108780103000000ef03000000000000090000000000000000000000 | |
| 618 | + | 000000006e6574207469746c650000001c00780101005000f303000000000000 | |
| 619 | + | 000000000000000000000000000000000108790103000000ef03000000000000 | |
| 620 | + | 090000000000000000000000000000006e6574207469746c650000001c007901 | |
| 621 | + | 01005000f3030000000000000000000000000000000000000000000001087a01 | |
| 622 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 623 | + | 7469746c650000001c007a0101005000f3030000000000000000000000000000 | |
| 624 | + | 000000000000000001087b0103000000ef030000000000000900000000000000 | |
| 625 | + | 00000000000000006e6574207469746c650000001c007b0101005000f3030000 | |
| 626 | + | 000000000000000000000000000000000000000001087c0103000000ef030000 | |
| 627 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 628 | + | 1c007c0101005000f30300000000000000000000000000000000000000000000 | |
| 629 | + | 01087d0103000000ef0300000000000009000000000000000000000000000000 | |
| 630 | + | 6e6574207469746c650000001c007d0101005000f30300000000000000000000 | |
| 631 | + | 00000000000000000000000001087e0103000000ef0300000000000009000000 | |
| 632 | + | 0000000000000000000000006e6574207469746c650000001c007e0101005000 | |
| 633 | + | f3030000000000000000000000000000000000000000000001087f0103000000 | |
| 634 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 635 | + | 650000001c007f0101005000f303000000000000000000000000000000000000 | |
| 636 | + | 000000000108800103000000ef03000000000000090000000000000000000000 | |
| 637 | + | 000000006e6574207469746c650000001c00800101005000f303000000000000 | |
| 638 | + | 000000000000000000000000000000000108810103000000ef03000000000000 | |
| 639 | + | 090000000000000000000000000000006e6574207469746c650000001c008101 | |
| 640 | + | 01005000f3030000000000000000000000000000000000000000000001088201 | |
| 641 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 642 | + | 7469746c650000001c00820101005000f3030000000000000000000000000000 | |
| 643 | + | 00000000000000000108830103000000ef030000000000000900000000000000 | |
| 644 | + | 00000000000000006e6574207469746c650000001c00830101005000f3030000 | |
| 645 | + | 00000000000000000000000000000000000000000108840103000000ef030000 | |
| 646 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 647 | + | 1c00840101005000f30300000000000000000000000000000000000000000000 | |
| 648 | + | 0108850103000000ef0300000000000009000000000000000000000000000000 | |
| 649 | + | 6e6574207469746c650000001c00850101005000f30300000000000000000000 | |
| 650 | + | 0000000000000000000000000108860103000000ef0300000000000009000000 | |
| 651 | + | 0000000000000000000000006e6574207469746c650000001c00860101005000 | |
| 652 | + | f303000000000000000000000000000000000000000000000108870103000000 | |
| 653 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 654 | + | 650000001c00870101005000f303000000000000000000000000000000000000 | |
| 655 | + | 000000000108880103000000ef03000000000000090000000000000000000000 | |
| 656 | + | 000000006e6574207469746c650000001c00880101005000f303000000000000 | |
| 657 | + | 000000000000000000000000000000000108890103000000ef03000000000000 | |
| 658 | + | 090000000000000000000000000000006e6574207469746c650000001c008901 | |
| 659 | + | 01005000f3030000000000000000000000000000000000000000000001088a01 | |
| 660 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 661 | + | 7469746c650000001c008a0101005000f3030000000000000000000000000000 | |
| 662 | + | 000000000000000001088b0103000000ef030000000000000900000000000000 | |
| 663 | + | 00000000000000006e6574207469746c650000001c008b0101005000f3030000 | |
| 664 | + | 000000000000000000000000000000000000000001088c0103000000ef030000 | |
| 665 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 666 | + | 1c008c0101005000f30300000000000000000000000000000000000000000000 | |
| 667 | + | 01088d0103000000ef0300000000000009000000000000000000000000000000 | |
| 668 | + | 6e6574207469746c650000001c008d0101005000f30300000000000000000000 | |
| 669 | + | 00000000000000000000000001088e0103000000ef0300000000000009000000 | |
| 670 | + | 0000000000000000000000006e6574207469746c650000001c008e0101005000 | |
| 671 | + | f3030000000000000000000000000000000000000000000001088f0103000000 | |
| 672 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 673 | + | 650000001c008f0101005000f303000000000000000000000000000000000000 | |
| 674 | + | 000000000108900103000000ef03000000000000090000000000000000000000 | |
| 675 | + | 000000006e6574207469746c650000001c00900101005000f303000000000000 | |
| 676 | + | 000000000000000000000000000000000108910103000000ef03000000000000 | |
| 677 | + | 090000000000000000000000000000006e6574207469746c650000001c009101 | |
| 678 | + | 01005000f3030000000000000000000000000000000000000000000001089201 | |
| 679 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 680 | + | 7469746c650000001c00920101005000f3030000000000000000000000000000 | |
| 681 | + | 00000000000000000108930103000000ef030000000000000900000000000000 | |
| 682 | + | 00000000000000006e6574207469746c650000001c00930101005000f3030000 | |
| 683 | + | 00000000000000000000000000000000000000000108940103000000ef030000 | |
| 684 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 685 | + | 1c00940101005000f30300000000000000000000000000000000000000000000 | |
| 686 | + | 0108950103000000ef0300000000000009000000000000000000000000000000 | |
| 687 | + | 6e6574207469746c650000001c00950101005000f30300000000000000000000 | |
| 688 | + | 0000000000000000000000000108960103000000ef0300000000000009000000 | |
| 689 | + | 0000000000000000000000006e6574207469746c650000001c00960101005000 | |
| 690 | + | f303000000000000000000000000000000000000000000000108970103000000 | |
| 691 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 692 | + | 650000001c00970101005000f303000000000000000000000000000000000000 | |
| 693 | + | 000000000108980103000000ef03000000000000090000000000000000000000 | |
| 694 | + | 000000006e6574207469746c650000001c00980101005000f303000000000000 | |
| 695 | + | 000000000000000000000000000000000108990103000000ef03000000000000 | |
| 696 | + | 090000000000000000000000000000006e6574207469746c650000001c009901 | |
| 697 | + | 01005000f3030000000000000000000000000000000000000000000001089a01 | |
| 698 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 699 | + | 7469746c650000001c009a0101005000f3030000000000000000000000000000 | |
| 700 | + | 000000000000000001089b0103000000ef030000000000000900000000000000 | |
| 701 | + | 00000000000000006e6574207469746c650000001c009b0101005000f3030000 | |
| 702 | + | 000000000000000000000000000000000000000001089c0103000000ef030000 | |
| 703 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 704 | + | 1c009c0101005000f30300000000000000000000000000000000000000000000 | |
| 705 | + | 01089d0103000000ef0300000000000009000000000000000000000000000000 | |
| 706 | + | 6e6574207469746c650000001c009d0101005000f30300000000000000000000 | |
| 707 | + | 00000000000000000000000001089e0103000000ef0300000000000009000000 | |
| 708 | + | 0000000000000000000000006e6574207469746c650000001c009e0101005000 | |
| 709 | + | f3030000000000000000000000000000000000000000000001089f0103000000 | |
| 710 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 711 | + | 650000001c009f0101005000f303000000000000000000000000000000000000 | |
| 712 | + | 000000000108a00103000000ef03000000000000090000000000000000000000 | |
| 713 | + | 000000006e6574207469746c650000001c00a00101005000f303000000000000 | |
| 714 | + | 000000000000000000000000000000000108a10103000000ef03000000000000 | |
| 715 | + | 090000000000000000000000000000006e6574207469746c650000001c00a101 | |
| 716 | + | 01005000f303000000000000000000000000000000000000000000000108a201 | |
| 717 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 718 | + | 7469746c650000001c00a20101005000f3030000000000000000000000000000 | |
| 719 | + | 00000000000000000108a30103000000ef030000000000000900000000000000 | |
| 720 | + | 00000000000000006e6574207469746c650000001c00a30101005000f3030000 | |
| 721 | + | 00000000000000000000000000000000000000000108a40103000000ef030000 | |
| 722 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 723 | + | 1c00a40101005000f30300000000000000000000000000000000000000000000 | |
| 724 | + | 0108a50103000000ef0300000000000009000000000000000000000000000000 | |
| 725 | + | 6e6574207469746c650000001c00a50101005000f30300000000000000000000 | |
| 726 | + | 0000000000000000000000000108a60103000000ef0300000000000009000000 | |
| 727 | + | 0000000000000000000000006e6574207469746c650000001c00a60101005000 | |
| 728 | + | f303000000000000000000000000000000000000000000000108a70103000000 | |
| 729 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 730 | + | 650000001c00a70101005000f303000000000000000000000000000000000000 | |
| 731 | + | 000000000108a80103000000ef03000000000000090000000000000000000000 | |
| 732 | + | 000000006e6574207469746c650000001c00a80101005000f303000000000000 | |
| 733 | + | 000000000000000000000000000000000108a90103000000ef03000000000000 | |
| 734 | + | 090000000000000000000000000000006e6574207469746c650000001c00a901 | |
| 735 | + | 01005000f303000000000000000000000000000000000000000000000108aa01 | |
| 736 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 737 | + | 7469746c650000001c00aa0101005000f3030000000000000000000000000000 | |
| 738 | + | 00000000000000000108ab0103000000ef030000000000000900000000000000 | |
| 739 | + | 00000000000000006e6574207469746c650000001c00ab0101005000f3030000 | |
| 740 | + | 00000000000000000000000000000000000000000108ac0103000000ef030000 | |
| 741 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 742 | + | 1c00ac0101005000f30300000000000000000000000000000000000000000000 | |
| 743 | + | 0108ad0103000000ef0300000000000009000000000000000000000000000000 | |
| 744 | + | 6e6574207469746c650000001c00ad0101005000f30300000000000000000000 | |
| 745 | + | 0000000000000000000000000108ae0103000000ef0300000000000009000000 | |
| 746 | + | 0000000000000000000000006e6574207469746c650000001c00ae0101005000 | |
| 747 | + | f303000000000000000000000000000000000000000000000108af0103000000 | |
| 748 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 749 | + | 650000001c00af0101005000f303000000000000000000000000000000000000 | |
| 750 | + | 000000000108b00103000000ef03000000000000090000000000000000000000 | |
| 751 | + | 000000006e6574207469746c650000001c00b00101005000f303000000000000 | |
| 752 | + | 000000000000000000000000000000000108b10103000000ef03000000000000 | |
| 753 | + | 090000000000000000000000000000006e6574207469746c650000001c00b101 | |
| 754 | + | 01005000f303000000000000000000000000000000000000000000000108b201 | |
| 755 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 756 | + | 7469746c650000001c00b20101005000f3030000000000000000000000000000 | |
| 757 | + | 00000000000000000108b30103000000ef030000000000000900000000000000 | |
| 758 | + | 00000000000000006e6574207469746c650000001c00b30101005000f3030000 | |
| 759 | + | 00000000000000000000000000000000000000000108b40103000000ef030000 | |
| 760 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 761 | + | 1c00b40101005000f30300000000000000000000000000000000000000000000 | |
| 762 | + | 0108b50103000000ef0300000000000009000000000000000000000000000000 | |
| 763 | + | 6e6574207469746c650000001c00b50101005000f30300000000000000000000 | |
| 764 | + | 0000000000000000000000000108b60103000000ef0300000000000009000000 | |
| 765 | + | 0000000000000000000000006e6574207469746c650000001c00b60101005000 | |
| 766 | + | f303000000000000000000000000000000000000000000000108b70103000000 | |
| 767 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 768 | + | 650000001c00b70101005000f303000000000000000000000000000000000000 | |
| 769 | + | 000000000108b80103000000ef03000000000000090000000000000000000000 | |
| 770 | + | 000000006e6574207469746c650000001c00b80101005000f303000000000000 | |
| 771 | + | 000000000000000000000000000000000108b90103000000ef03000000000000 | |
| 772 | + | 090000000000000000000000000000006e6574207469746c650000001c00b901 | |
| 773 | + | 01005000f303000000000000000000000000000000000000000000000108ba01 | |
| 774 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 775 | + | 7469746c650000001c00ba0101005000f3030000000000000000000000000000 | |
| 776 | + | 00000000000000000108bb0103000000ef030000000000000900000000000000 | |
| 777 | + | 00000000000000006e6574207469746c650000001c00bb0101005000f3030000 | |
| 778 | + | 00000000000000000000000000000000000000000108bc0103000000ef030000 | |
| 779 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 780 | + | 1c00bc0101005000f30300000000000000000000000000000000000000000000 | |
| 781 | + | 0108bd0103000000ef0300000000000009000000000000000000000000000000 | |
| 782 | + | 6e6574207469746c650000001c00bd0101005000f30300000000000000000000 | |
| 783 | + | 0000000000000000000000000108be0103000000ef0300000000000009000000 | |
| 784 | + | 0000000000000000000000006e6574207469746c650000001c00be0101005000 | |
| 785 | + | f303000000000000000000000000000000000000000000000108bf0103000000 | |
| 786 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 787 | + | 650000001c00bf0101005000f303000000000000000000000000000000000000 | |
| 788 | + | 000000000108c00103000000ef03000000000000090000000000000000000000 | |
| 789 | + | 000000006e6574207469746c650000001c00c00101005000f303000000000000 | |
| 790 | + | 000000000000000000000000000000000108c10103000000ef03000000000000 | |
| 791 | + | 090000000000000000000000000000006e6574207469746c650000001c00c101 | |
| 792 | + | 01005000f303000000000000000000000000000000000000000000000108c201 | |
| 793 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 794 | + | 7469746c650000001c00c20101005000f3030000000000000000000000000000 | |
| 795 | + | 00000000000000000108c30103000000ef030000000000000900000000000000 | |
| 796 | + | 00000000000000006e6574207469746c650000001c00c30101005000f3030000 | |
| 797 | + | 00000000000000000000000000000000000000000108c40103000000ef030000 | |
| 798 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 799 | + | 1c00c40101005000f30300000000000000000000000000000000000000000000 | |
| 800 | + | 0108c50103000000ef0300000000000009000000000000000000000000000000 | |
| 801 | + | 6e6574207469746c650000001c00c50101005000f30300000000000000000000 | |
| 802 | + | 0000000000000000000000000108c60103000000ef0300000000000009000000 | |
| 803 | + | 0000000000000000000000006e6574207469746c650000001c00c60101005000 | |
| 804 | + | f303000000000000000000000000000000000000000000000108c70103000000 | |
| 805 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 806 | + | 650000001c00c70101005000f303000000000000000000000000000000000000 | |
| 807 | + | 000000000108c80103000000ef03000000000000090000000000000000000000 | |
| 808 | + | 000000006e6574207469746c650000001c00c80101005000f303000000000000 | |
| 809 | + | 000000000000000000000000000000000108c90103000000ef03000000000000 | |
| 810 | + | 090000000000000000000000000000006e6574207469746c650000001c00c901 | |
| 811 | + | 01005000f303000000000000000000000000000000000000000000000108ca01 | |
| 812 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 813 | + | 7469746c650000001c00ca0101005000f3030000000000000000000000000000 | |
| 814 | + | 00000000000000000108cb0103000000ef030000000000000900000000000000 | |
| 815 | + | 00000000000000006e6574207469746c650000001c00cb0101005000f3030000 | |
| 816 | + | 00000000000000000000000000000000000000000108cc0103000000ef030000 | |
| 817 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 818 | + | 1c00cc0101005000f30300000000000000000000000000000000000000000000 | |
| 819 | + | 0108cd0103000000ef0300000000000009000000000000000000000000000000 | |
| 820 | + | 6e6574207469746c650000001c00cd0101005000f30300000000000000000000 | |
| 821 | + | 0000000000000000000000000108ce0103000000ef0300000000000009000000 | |
| 822 | + | 0000000000000000000000006e6574207469746c650000001c00ce0101005000 | |
| 823 | + | f303000000000000000000000000000000000000000000000108cf0103000000 | |
| 824 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 825 | + | 650000001c00cf0101005000f303000000000000000000000000000000000000 | |
| 826 | + | 000000000108d00103000000ef03000000000000090000000000000000000000 | |
| 827 | + | 000000006e6574207469746c650000001c00d00101005000f303000000000000 | |
| 828 | + | 000000000000000000000000000000000108d10103000000ef03000000000000 | |
| 829 | + | 090000000000000000000000000000006e6574207469746c650000001c00d101 | |
| 830 | + | 01005000f303000000000000000000000000000000000000000000000108d201 | |
| 831 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 832 | + | 7469746c650000001c00d20101005000f3030000000000000000000000000000 | |
| 833 | + | 00000000000000000108d30103000000ef030000000000000900000000000000 | |
| 834 | + | 00000000000000006e6574207469746c650000001c00d30101005000f3030000 | |
| 835 | + | 00000000000000000000000000000000000000000108d40103000000ef030000 | |
| 836 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 837 | + | 1c00d40101005000f30300000000000000000000000000000000000000000000 | |
| 838 | + | 0108d50103000000ef0300000000000009000000000000000000000000000000 | |
| 839 | + | 6e6574207469746c650000001c00d50101005000f30300000000000000000000 | |
| 840 | + | 0000000000000000000000000108d60103000000ef0300000000000009000000 | |
| 841 | + | 0000000000000000000000006e6574207469746c650000001c00d60101005000 | |
| 842 | + | f303000000000000000000000000000000000000000000000108d70103000000 | |
| 843 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 844 | + | 650000001c00d70101005000f303000000000000000000000000000000000000 | |
| 845 | + | 000000000108d80103000000ef03000000000000090000000000000000000000 | |
| 846 | + | 000000006e6574207469746c650000001c00d80101005000f303000000000000 | |
| 847 | + | 000000000000000000000000000000000108d90103000000ef03000000000000 | |
| 848 | + | 090000000000000000000000000000006e6574207469746c650000001c00d901 | |
| 849 | + | 01005000f303000000000000000000000000000000000000000000000108da01 | |
| 850 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 851 | + | 7469746c650000001c00da0101005000f3030000000000000000000000000000 | |
| 852 | + | 00000000000000000108db0103000000ef030000000000000900000000000000 | |
| 853 | + | 00000000000000006e6574207469746c650000001c00db0101005000f3030000 | |
| 854 | + | 00000000000000000000000000000000000000000108dc0103000000ef030000 | |
| 855 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 856 | + | 1c00dc0101005000f30300000000000000000000000000000000000000000000 | |
| 857 | + | 0108dd0103000000ef0300000000000009000000000000000000000000000000 | |
| 858 | + | 6e6574207469746c650000001c00dd0101005000f30300000000000000000000 | |
| 859 | + | 0000000000000000000000000108de0103000000ef0300000000000009000000 | |
| 860 | + | 0000000000000000000000006e6574207469746c650000001c00de0101005000 | |
| 861 | + | f303000000000000000000000000000000000000000000000108df0103000000 | |
| 862 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 863 | + | 650000001c00df0101005000f303000000000000000000000000000000000000 | |
| 864 | + | 000000000108e00103000000ef03000000000000090000000000000000000000 | |
| 865 | + | 000000006e6574207469746c650000001c00e00101005000f303000000000000 | |
| 866 | + | 000000000000000000000000000000000108e10103000000ef03000000000000 | |
| 867 | + | 090000000000000000000000000000006e6574207469746c650000001c00e101 | |
| 868 | + | 01005000f303000000000000000000000000000000000000000000000108e201 | |
| 869 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 870 | + | 7469746c650000001c00e20101005000f3030000000000000000000000000000 | |
| 871 | + | 00000000000000000108e30103000000ef030000000000000900000000000000 | |
| 872 | + | 00000000000000006e6574207469746c650000001c00e30101005000f3030000 | |
| 873 | + | 00000000000000000000000000000000000000000108e40103000000ef030000 | |
| 874 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 875 | + | 1c00e40101005000f30300000000000000000000000000000000000000000000 | |
| 876 | + | 0108e50103000000ef0300000000000009000000000000000000000000000000 | |
| 877 | + | 6e6574207469746c650000001c00e50101005000f30300000000000000000000 | |
| 878 | + | 0000000000000000000000000108e60103000000ef0300000000000009000000 | |
| 879 | + | 0000000000000000000000006e6574207469746c650000001c00e60101005000 | |
| 880 | + | f303000000000000000000000000000000000000000000000108e70103000000 | |
| 881 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 882 | + | 650000001c00e70101005000f303000000000000000000000000000000000000 | |
| 883 | + | 000000000108e80103000000ef03000000000000090000000000000000000000 | |
| 884 | + | 000000006e6574207469746c650000001c00e80101005000f303000000000000 | |
| 885 | + | 000000000000000000000000000000000108e90103000000ef03000000000000 | |
| 886 | + | 090000000000000000000000000000006e6574207469746c650000001c00e901 | |
| 887 | + | 01005000f303000000000000000000000000000000000000000000000108ea01 | |
| 888 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 889 | + | 7469746c650000001c00ea0101005000f3030000000000000000000000000000 | |
| 890 | + | 00000000000000000108eb0103000000ef030000000000000900000000000000 | |
| 891 | + | 00000000000000006e6574207469746c650000001c00eb0101005000f3030000 | |
| 892 | + | 00000000000000000000000000000000000000000108ec0103000000ef030000 | |
| 893 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 894 | + | 1c00ec0101005000f30300000000000000000000000000000000000000000000 | |
| 895 | + | 0108ed0103000000ef0300000000000009000000000000000000000000000000 | |
| 896 | + | 6e6574207469746c650000001c00ed0101005000f30300000000000000000000 | |
| 897 | + | 0000000000000000000000000108ee0103000000ef0300000000000009000000 | |
| 898 | + | 0000000000000000000000006e6574207469746c650000001c00ee0101005000 | |
| 899 | + | f303000000000000000000000000000000000000000000000108ef0103000000 | |
| 900 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 901 | + | 650000001c00ef0101005000f303000000000000000000000000000000000000 | |
| 902 | + | 000000000108f00103000000ef03000000000000090000000000000000000000 | |
| 903 | + | 000000006e6574207469746c650000001c00f00101005000f303000000000000 | |
| 904 | + | 000000000000000000000000000000000108f10103000000ef03000000000000 | |
| 905 | + | 090000000000000000000000000000006e6574207469746c650000001c00f101 | |
| 906 | + | 01005000f303000000000000000000000000000000000000000000000108f201 | |
| 907 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 908 | + | 7469746c650000001c00f20101005000f3030000000000000000000000000000 | |
| 909 | + | 00000000000000000108f30103000000ef030000000000000900000000000000 | |
| 910 | + | 00000000000000006e6574207469746c650000001c00f30101005000f3030000 | |
| 911 | + | 00000000000000000000000000000000000000000108f40103000000ef030000 | |
| 912 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 913 | + | 1c00f40101005000f30300000000000000000000000000000000000000000000 | |
| 914 | + | 0108f50103000000ef0300000000000009000000000000000000000000000000 | |
| 915 | + | 6e6574207469746c650000001c00f50101005000f30300000000000000000000 | |
| 916 | + | 0000000000000000000000000108f60103000000ef0300000000000009000000 | |
| 917 | + | 0000000000000000000000006e6574207469746c650000001c00f60101005000 | |
| 918 | + | f303000000000000000000000000000000000000000000000108f70103000000 | |
| 919 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 920 | + | 650000001c00f70101005000f303000000000000000000000000000000000000 | |
| 921 | + | 000000000108f80103000000ef03000000000000090000000000000000000000 | |
| 922 | + | 000000006e6574207469746c650000001c00f80101005000f303000000000000 | |
| 923 | + | 000000000000000000000000000000000108f90103000000ef03000000000000 | |
| 924 | + | 090000000000000000000000000000006e6574207469746c650000001c00f901 | |
| 925 | + | 01005000f303000000000000000000000000000000000000000000000108fa01 | |
| 926 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 927 | + | 7469746c650000001c00fa0101005000f3030000000000000000000000000000 | |
| 928 | + | 00000000000000000108fb0103000000ef030000000000000900000000000000 | |
| 929 | + | 00000000000000006e6574207469746c650000001c00fb0101005000f3030000 | |
| 930 | + | 00000000000000000000000000000000000000000108fc0103000000ef030000 | |
| 931 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 932 | + | 1c00fc0101005000f30300000000000000000000000000000000000000000000 | |
| 933 | + | 0108fd0103000000ef0300000000000009000000000000000000000000000000 | |
| 934 | + | 6e6574207469746c650000001c00fd0101005000f30300000000000000000000 | |
| 935 | + | 0000000000000000000000000108fe0103000000ef0300000000000009000000 | |
| 936 | + | 0000000000000000000000006e6574207469746c650000001c00fe0101005000 | |
| 937 | + | f303000000000000000000000000000000000000000000000108ff0103000000 | |
| 938 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 939 | + | 650000001c00ff0101005000f303000000000000000000000000000000000000 | |
| 940 | + | 000000000108000203000000ef03000000000000090000000000000000000000 | |
| 941 | + | 000000006e6574207469746c650000001c00000201005000f303000000000000 | |
| 942 | + | 000000000000000000000000000000000108010203000000ef03000000000000 | |
| 943 | + | 090000000000000000000000000000006e6574207469746c650000001c000102 | |
| 944 | + | 01005000f3030000000000000000000000000000000000000000000001080202 | |
| 945 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 946 | + | 7469746c650000001c00020201005000f3030000000000000000000000000000 | |
| 947 | + | 00000000000000000108030203000000ef030000000000000900000000000000 | |
| 948 | + | 00000000000000006e6574207469746c650000001c00030201005000f3030000 | |
| 949 | + | 00000000000000000000000000000000000000000108040203000000ef030000 | |
| 950 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
A internal/adapters/wayland/xwm/testdata/streams/steady.hex +2150 -0 large diff (2150 lines changed): collapsed
A internal/adapters/wayland/xwm/unmanaged.go +109 -0
| @@ -0,0 +1,109 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bnema.dev/xturbo" | |
| 5 | + | "bnema.dev/xturbo/xproto" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // Override-redirect windows are tracked, never managed: the XWM reads their | |
| 9 | + | // window type once at map, pairs them with their wl_surface and reports the | |
| 10 | + | // mapped rect to the host in layout coordinates. | |
| 11 | + | ||
| 12 | + | // maxTypeWords bounds the _NET_WM_WINDOW_TYPE read. | |
| 13 | + | const maxTypeWords = 32 | |
| 14 | + | ||
| 15 | + | // requestOverrideType asks for the window type of a freshly mapped override | |
| 16 | + | // window; the reply completes its readiness. | |
| 17 | + | func (x *XWM) requestOverrideType(s Slot) { | |
| 18 | + | rec := x.tab.rec(s) | |
| 19 | + | seq := xproto.GetProperty(x.conn.Buf(), false, xproto.Window(rec.xid), | |
| 20 | + | xproto.Atom(x.atoms[atomNetWMWindowType]), xproto.AtomAny, 0, maxTypeWords) | |
| 21 | + | x.expect(seq, kindOverrideType, x.tab.tag(s), 0) | |
| 22 | + | x.wake() | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | func (x *XWM) onOverrideType(s Slot, rec *record, r *xturbo.Reply) { | |
| 26 | + | rec.noInput = false | |
| 27 | + | if rep, ok := xproto.ParseGetPropertyReply(r.Raw); ok && rep.Format() == 32 { | |
| 28 | + | val := rep.Value() | |
| 29 | + | for i := 0; i+4 <= len(val); i += 4 { | |
| 30 | + | switch x.windowType(word(val, i/4)) { | |
| 31 | + | case atomNetWMWindowTypeTooltip, atomNetWMWindowTypeNotification, atomNetWMWindowTypeDND: | |
| 32 | + | rec.noInput = true | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | } | |
| 36 | + | x.overrideReady(s) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // overrideReady marks a mapped override window typed and pairs it. | |
| 40 | + | func (x *XWM) overrideReady(s Slot) { | |
| 41 | + | rec := x.tab.rec(s) | |
| 42 | + | if rec == nil || rec.kind != kindUnmanaged || !rec.mapped { | |
| 43 | + | return | |
| 44 | + | } | |
| 45 | + | rec.ready = true | |
| 46 | + | x.tryPair(s) | |
| 47 | + | x.overrideMapped(s, rec) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // overrideMapped reports the window once it is mapped, typed and paired. | |
| 51 | + | func (x *XWM) overrideMapped(s Slot, rec *record) { | |
| 52 | + | if x.host == nil || rec.ovPaired || !rec.mapped || !rec.ready || rec.surface == 0 { | |
| 53 | + | return | |
| 54 | + | } | |
| 55 | + | rec.ovPaired = true | |
| 56 | + | rec.ov = x.layoutRect(rec) | |
| 57 | + | o := rec.ov | |
| 58 | + | x.host.OverrideMapped(s, rec.xid, rec.surface, int(o.x), int(o.y), int(o.w), int(o.h), rec.noInput) | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // overrideUnmapped ends the mapped life of an override window; a remap reads | |
| 62 | + | // the type again and pairs with a new surface. | |
| 63 | + | func (x *XWM) overrideUnmapped(s Slot, rec *record) { | |
| 64 | + | rec.ready, rec.serial, rec.surface, rec.noInput = false, 0, 0, false | |
| 65 | + | rec.gen++ // a type reply still in flight names a dead tag | |
| 66 | + | if rec.gen == 0 { | |
| 67 | + | rec.gen = 1 | |
| 68 | + | } | |
| 69 | + | if rec.ovPaired { | |
| 70 | + | rec.ovPaired = false | |
| 71 | + | x.host.OverrideGone(s) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (x *XWM) layoutRect(rec *record) ovRect { | |
| 76 | + | lx, ly := x.space.ToLayout(int(rec.x), int(rec.y)) | |
| 77 | + | return ovRect{int32(lx), int32(ly), int32(rec.w), int32(rec.h)} | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // queueOverride schedules a geometry report for s; flushOverrides sends it | |
| 81 | + | // once per iteration, only if the layout rect changed. | |
| 82 | + | func (x *XWM) queueOverride(s Slot, rec *record) { | |
| 83 | + | if rec.ovQueued || s == 0 { | |
| 84 | + | return | |
| 85 | + | } | |
| 86 | + | if len(x.ovDirty) == cap(x.ovDirty) { | |
| 87 | + | x.flushOverrides() | |
| 88 | + | } | |
| 89 | + | rec.ovQueued = true | |
| 90 | + | x.ovDirty = append(x.ovDirty, s) | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | func (x *XWM) flushOverrides() { | |
| 94 | + | for _, s := range x.ovDirty { | |
| 95 | + | rec := x.tab.rec(s) | |
| 96 | + | if rec == nil || rec.kind != kindUnmanaged || !rec.ovQueued { | |
| 97 | + | continue | |
| 98 | + | } | |
| 99 | + | rec.ovQueued = false | |
| 100 | + | if !rec.ovPaired { | |
| 101 | + | continue | |
| 102 | + | } | |
| 103 | + | if o := x.layoutRect(rec); o != rec.ov { | |
| 104 | + | rec.ov = o | |
| 105 | + | x.host.OverrideMoved(s, int(o.x), int(o.y), int(o.w), int(o.h)) | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | x.ovDirty = x.ovDirty[:0] | |
| 109 | + | } | |
A internal/adapters/wayland/xwm/unmanaged_test.go +247 -0
A internal/adapters/wayland/xwm/xpeer_test.go +655 -0
A internal/adapters/wayland/xwm/xsettings_test.go +244 -0
A internal/adapters/wayland/xwm/xwm.go +509 -0
A internal/adapters/wayland/xwm/xwm_test.go +387 -0
A internal/adapters/wayland/xwm_selection_mock_test.go +143 -0
A internal/adapters/xwayland/builtin.go +216 -0
A internal/adapters/xwayland/starter_mock_test.go +110 -0
M internal/adapters/xwayland/xwayland.go +46 -18
M internal/adapters/xwayland/xwayland_test.go +207 -0
M internal/app/app.go +13 -6
M internal/app/cursor.go +18 -0
M internal/app/session.go +2 -1
A internal/app/testdata/x11probe/main.go +692 -0
A internal/app/xwayland_test.go +808 -0
M internal/core/client.go +8 -0
M internal/core/core.go +16 -5
M internal/core/core_test.go +52 -3
M internal/core/datadrag.go +1 -1
M internal/core/fullscreen_left_core_test.go +1 -6
M internal/core/fullscreen_left_test.go +29 -0
M internal/core/layout.go +7 -1
A internal/core/place_test.go +80 -0
M internal/core/rules_test.go +44 -0
M internal/core/scene.go +12 -1
M internal/core/spawn_placement.go +9 -1
M internal/core/transition.go +20 -0
A internal/core/unmanaged.go +117 -0
A internal/core/unmanaged_test.go +145 -0
M internal/ports/client_commands.go +10 -0
M internal/ports/client_events.go +37 -0
M internal/ports/config.go +35 -3
Comments0 open / 0
no comments yet.