detached 8fb84bf

refactor(xwm): replace addFD/after function seams with a mockery-generated loop interface

commit
8fb84bf573f4a4c0f53ab2ffa1555c7a152973ea
author
brice · · 2026-10-22 16:51 UTC
committer
brice · · 2026-10-22 17:55 UTC
parent
07dab5d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +253 -22
M .mockery.yml +5 -0
@@ -212,3 +212,8 @@ packages:
212212 dir: internal/adapters/wayland/xwm
213213 pkgname: xwm
214214 filename: fdsource_mock_test.go
215+ loop:
216+ config:
217+ dir: internal/adapters/wayland/xwm
218+ pkgname: xwm
219+ filename: loop_mock_test.go
@@ -212,3 +212,8 @@ packages:
212 dir: internal/adapters/wayland/xwm212 dir: internal/adapters/wayland/xwm
213 pkgname: xwm213 pkgname: xwm
214 filename: fdsource_mock_test.go214 filename: fdsource_mock_test.go
215+ loop:
216+ config:
217+ dir: internal/adapters/wayland/xwm
218+ pkgname: xwm
219+ filename: loop_mock_test.go
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+}
@@ -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+}
M internal/adapters/wayland/xwm/selection.go +6 -6
@@ -489,13 +489,13 @@ func (x *XWM) inDrain(i int) {
489489 func (x *XWM) inWatch(i int, on bool) {
490490 t := &x.sel.in[i]
491491 if t.src == nil {
492- if !on || x.addFD == nil {
492+ if !on || x.lp == nil {
493493 if on {
494494 x.inAbort(i, true)
495495 }
496496 return
497497 }
498- src, err := x.addFD(t.fd, t.onR, t.onW)
498+ src, err := x.lp.AddFD(t.fd, t.onR, t.onW)
499499 if err != nil {
500500 x.inAbort(i, true)
501501 return
@@ -573,11 +573,11 @@ func (x *XWM) onSelError(e *xturbo.XError) {
573573
574574 func (x *XWM) selEnsureTimer() {
575575 b := &x.sel
576- if b.timerOn || x.after == nil {
576+ if b.timerOn || x.lp == nil {
577577 return
578578 }
579579 b.timerOn = true
580- b.cancel = x.after(tickEvery, b.tickFn)
580+ b.cancel = x.lp.After(tickEvery, b.tickFn)
581581 }
582582
583583 // selTick ages every transfer by one second and aborts the stalled ones.
@@ -855,11 +855,11 @@ func (x *XWM) outWatch(i int) {
855855 if o.src != nil {
856856 return
857857 }
858- if x.addFD == nil {
858+ if x.lp == nil {
859859 x.outAbort(i)
860860 return
861861 }
862- src, err := x.addFD(o.fd, o.onR, nil)
862+ src, err := x.lp.AddFD(o.fd, o.onR, nil)
863863 if err != nil {
864864 x.outAbort(i)
865865 return
@@ -489,13 +489,13 @@ func (x *XWM) inDrain(i int) {
489 func (x *XWM) inWatch(i int, on bool) {489 func (x *XWM) inWatch(i int, on bool) {
490 t := &x.sel.in[i]490 t := &x.sel.in[i]
491 if t.src == nil {491 if t.src == nil {
492- if !on || x.addFD == nil {492+ if !on || x.lp == nil {
493 if on {493 if on {
494 x.inAbort(i, true)494 x.inAbort(i, true)
495 }495 }
496 return496 return
497 }497 }
498- src, err := x.addFD(t.fd, t.onR, t.onW)498+ src, err := x.lp.AddFD(t.fd, t.onR, t.onW)
499 if err != nil {499 if err != nil {
500 x.inAbort(i, true)500 x.inAbort(i, true)
501 return501 return
@@ -573,11 +573,11 @@ func (x *XWM) onSelError(e *xturbo.XError) {
573 573
574 func (x *XWM) selEnsureTimer() {574 func (x *XWM) selEnsureTimer() {
575 b := &x.sel575 b := &x.sel
576- if b.timerOn || x.after == nil {576+ if b.timerOn || x.lp == nil {
577 return577 return
578 }578 }
579 b.timerOn = true579 b.timerOn = true
580- b.cancel = x.after(tickEvery, b.tickFn)580+ b.cancel = x.lp.After(tickEvery, b.tickFn)
581 }581 }
582 582
583 // selTick ages every transfer by one second and aborts the stalled ones.583 // selTick ages every transfer by one second and aborts the stalled ones.
@@ -855,11 +855,11 @@ func (x *XWM) outWatch(i int) {
855 if o.src != nil {855 if o.src != nil {
856 return856 return
857 }857 }
858- if x.addFD == nil {858+ if x.lp == nil {
859 x.outAbort(i)859 x.outAbort(i)
860 return860 return
861 }861 }
862- src, err := x.addFD(o.fd, o.onR, nil)862+ src, err := x.lp.AddFD(o.fd, o.onR, nil)
863 if err != nil {863 if err != nil {
864 x.outAbort(i)864 x.outAbort(i)
865 return865 return
M internal/adapters/wayland/xwm/selection_test.go +33 -6
@@ -252,7 +252,7 @@ func TestSelectionXToWaylandBackpressure(t *testing.T) {
252252 r := ownedRig(t)
253253 src := newMockfdSource(t)
254254 var onW func()
255- r.x.addFD = func(_ int, _, w func()) (fdSource, error) { onW = w; return src, nil }
255+ r.x.lp = newFDLoop(t, r, src, nil, &onW)
256256 src.EXPECT().SetWritable(true).Return(nil).Once()
257257 src.EXPECT().SetWritable(false).Return(nil).Once()
258258 src.EXPECT().Remove().Return().Once()
@@ -337,7 +337,7 @@ func TestSelectionPoolExhausted(t *testing.T) {
337337 func allocRig(t *testing.T) *selRig {
338338 r := ownedRig(t)
339339 sinkPeer(t, r.x, r.p)
340- r.x.after = func(time.Duration, func()) func() { return func() {} }
340+ r.x.lp = newTimerLoop(t)
341341 r.x.sel.timerOn = true // no timer re-arming in the measured loop
342342 return r
343343 }
@@ -515,7 +515,7 @@ func TestSelectionWaylandToXINCR(t *testing.T) {
515515 src := newMockfdSource(t)
516516 var onR func()
517517 removes := 0
518- r.x.addFD = func(_ int, rd, _ func()) (fdSource, error) { onR = rd; return src, nil }
518+ r.x.lp = newFDLoop(t, r, src, &onR, nil)
519519 src.EXPECT().Remove().Run(func() { removes++ }).Return().Maybe()
520520 const total = 1<<20 + 123
521521 data := pattern(total)
@@ -591,7 +591,7 @@ func TestSelectionWaylandToXTimeoutAndClear(t *testing.T) {
591591 r := ownedByWayland(t, "text/plain;charset=utf-8")
592592 src := newMockfdSource(t)
593593 src.EXPECT().Remove().Return().Maybe()
594- r.x.addFD = func(_ int, _, _ func()) (fdSource, error) { return src, nil }
594+ r.x.lp = newFDLoop(t, r, src, nil, nil)
595595 w := r.serve(t, r.utf8)
596596 _, err := w.Write(pattern(chunk)) // a full chunk: INCR
597597 require.NoError(t, err)
@@ -622,7 +622,7 @@ func TestSelectionClearAndRace(t *testing.T) {
622622 r := ownedByWayland(t, "text/plain;charset=utf-8")
623623 src := newMockfdSource(t)
624624 src.EXPECT().Remove().Return().Maybe()
625- r.x.addFD = func(_ int, _, _ func()) (fdSource, error) { return src, nil }
625+ r.x.lp = newFDLoop(t, r, src, nil, nil)
626626 w := r.serve(t, r.utf8)
627627 _, err := w.Write(pattern(chunk))
628628 require.NoError(t, err)
@@ -640,7 +640,7 @@ func TestSelectionCopyAllocationsWaylandToX(t *testing.T) {
640640 r := ownedByWayland(t, "text/plain;charset=utf-8")
641641 x := r.x
642642 sinkPeer(t, x, r.p)
643- x.after = func(time.Duration, func()) func() { return func() {} }
643+ x.lp = newTimerLoop(t)
644644 x.sel.timerOn = true
645645 data := pattern(4096)
646646 var ev xturbo.Event
@@ -708,3 +708,30 @@ func TestSelectionStaleTargetsAfterWaylandOwner(t *testing.T) {
708708 require.Zero(t, getProps(p, 0, x.selWin), "the stale TARGETS reply is not fetched")
709709 _ = h
710710 }
711+
712+// newFDLoop is a loop whose AddFD hands out src and captures the callbacks;
713+// timers are inert.
714+func newFDLoop(t *testing.T, rig *selRig, src fdSource, onR, onW *func()) *mockloop {
715+ l := newMockloop(t)
716+ l.EXPECT().AddFD(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(
717+ func(_ int, r, w func()) (fdSource, error) {
718+ if onR != nil {
719+ *onR = r
720+ }
721+ if onW != nil {
722+ *onW = w
723+ }
724+ return src, nil
725+ }).Maybe()
726+ l.EXPECT().After(mock.Anything, mock.Anything).RunAndReturn(
727+ func(_ time.Duration, fn func()) func() { rig.timer = fn; return func() {} }).Maybe()
728+ return l
729+}
730+
731+// newTimerLoop is a loop with inert timers.
732+func newTimerLoop(t *testing.T) *mockloop {
733+ l := newMockloop(t)
734+ l.EXPECT().After(mock.Anything, mock.Anything).RunAndReturn(
735+ func(time.Duration, func()) func() { return func() {} }).Maybe()
736+ return l
737+}
@@ -252,7 +252,7 @@ func TestSelectionXToWaylandBackpressure(t *testing.T) {
252 r := ownedRig(t)252 r := ownedRig(t)
253 src := newMockfdSource(t)253 src := newMockfdSource(t)
254 var onW func()254 var onW func()
255- r.x.addFD = func(_ int, _, w func()) (fdSource, error) { onW = w; return src, nil }255+ r.x.lp = newFDLoop(t, r, src, nil, &onW)
256 src.EXPECT().SetWritable(true).Return(nil).Once()256 src.EXPECT().SetWritable(true).Return(nil).Once()
257 src.EXPECT().SetWritable(false).Return(nil).Once()257 src.EXPECT().SetWritable(false).Return(nil).Once()
258 src.EXPECT().Remove().Return().Once()258 src.EXPECT().Remove().Return().Once()
@@ -337,7 +337,7 @@ func TestSelectionPoolExhausted(t *testing.T) {
337 func allocRig(t *testing.T) *selRig {337 func allocRig(t *testing.T) *selRig {
338 r := ownedRig(t)338 r := ownedRig(t)
339 sinkPeer(t, r.x, r.p)339 sinkPeer(t, r.x, r.p)
340- r.x.after = func(time.Duration, func()) func() { return func() {} }340+ r.x.lp = newTimerLoop(t)
341 r.x.sel.timerOn = true // no timer re-arming in the measured loop341 r.x.sel.timerOn = true // no timer re-arming in the measured loop
342 return r342 return r
343 }343 }
@@ -515,7 +515,7 @@ func TestSelectionWaylandToXINCR(t *testing.T) {
515 src := newMockfdSource(t)515 src := newMockfdSource(t)
516 var onR func()516 var onR func()
517 removes := 0517 removes := 0
518- r.x.addFD = func(_ int, rd, _ func()) (fdSource, error) { onR = rd; return src, nil }518+ r.x.lp = newFDLoop(t, r, src, &onR, nil)
519 src.EXPECT().Remove().Run(func() { removes++ }).Return().Maybe()519 src.EXPECT().Remove().Run(func() { removes++ }).Return().Maybe()
520 const total = 1<<20 + 123520 const total = 1<<20 + 123
521 data := pattern(total)521 data := pattern(total)
@@ -591,7 +591,7 @@ func TestSelectionWaylandToXTimeoutAndClear(t *testing.T) {
591 r := ownedByWayland(t, "text/plain;charset=utf-8")591 r := ownedByWayland(t, "text/plain;charset=utf-8")
592 src := newMockfdSource(t)592 src := newMockfdSource(t)
593 src.EXPECT().Remove().Return().Maybe()593 src.EXPECT().Remove().Return().Maybe()
594- r.x.addFD = func(_ int, _, _ func()) (fdSource, error) { return src, nil }594+ r.x.lp = newFDLoop(t, r, src, nil, nil)
595 w := r.serve(t, r.utf8)595 w := r.serve(t, r.utf8)
596 _, err := w.Write(pattern(chunk)) // a full chunk: INCR596 _, err := w.Write(pattern(chunk)) // a full chunk: INCR
597 require.NoError(t, err)597 require.NoError(t, err)
@@ -622,7 +622,7 @@ func TestSelectionClearAndRace(t *testing.T) {
622 r := ownedByWayland(t, "text/plain;charset=utf-8")622 r := ownedByWayland(t, "text/plain;charset=utf-8")
623 src := newMockfdSource(t)623 src := newMockfdSource(t)
624 src.EXPECT().Remove().Return().Maybe()624 src.EXPECT().Remove().Return().Maybe()
625- r.x.addFD = func(_ int, _, _ func()) (fdSource, error) { return src, nil }625+ r.x.lp = newFDLoop(t, r, src, nil, nil)
626 w := r.serve(t, r.utf8)626 w := r.serve(t, r.utf8)
627 _, err := w.Write(pattern(chunk))627 _, err := w.Write(pattern(chunk))
628 require.NoError(t, err)628 require.NoError(t, err)
@@ -640,7 +640,7 @@ func TestSelectionCopyAllocationsWaylandToX(t *testing.T) {
640 r := ownedByWayland(t, "text/plain;charset=utf-8")640 r := ownedByWayland(t, "text/plain;charset=utf-8")
641 x := r.x641 x := r.x
642 sinkPeer(t, x, r.p)642 sinkPeer(t, x, r.p)
643- x.after = func(time.Duration, func()) func() { return func() {} }643+ x.lp = newTimerLoop(t)
644 x.sel.timerOn = true644 x.sel.timerOn = true
645 data := pattern(4096)645 data := pattern(4096)
646 var ev xturbo.Event646 var ev xturbo.Event
@@ -708,3 +708,30 @@ func TestSelectionStaleTargetsAfterWaylandOwner(t *testing.T) {
708 require.Zero(t, getProps(p, 0, x.selWin), "the stale TARGETS reply is not fetched")708 require.Zero(t, getProps(p, 0, x.selWin), "the stale TARGETS reply is not fetched")
709 _ = h709 _ = h
710 }710 }
711+
712+// newFDLoop is a loop whose AddFD hands out src and captures the callbacks;
713+// timers are inert.
714+func newFDLoop(t *testing.T, rig *selRig, src fdSource, onR, onW *func()) *mockloop {
715+ l := newMockloop(t)
716+ l.EXPECT().AddFD(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(
717+ func(_ int, r, w func()) (fdSource, error) {
718+ if onR != nil {
719+ *onR = r
720+ }
721+ if onW != nil {
722+ *onW = w
723+ }
724+ return src, nil
725+ }).Maybe()
726+ l.EXPECT().After(mock.Anything, mock.Anything).RunAndReturn(
727+ func(_ time.Duration, fn func()) func() { rig.timer = fn; return func() {} }).Maybe()
728+ return l
729+}
730+
731+// newTimerLoop is a loop with inert timers.
732+func newTimerLoop(t *testing.T) *mockloop {
733+ l := newMockloop(t)
734+ l.EXPECT().After(mock.Anything, mock.Anything).RunAndReturn(
735+ func(time.Duration, func()) func() { return func() {} }).Maybe()
736+ return l
737+}
M internal/adapters/wayland/xwm/xwm.go +29 -10
@@ -66,8 +66,7 @@ type XWM struct {
6666 // attachment
6767 host host
6868 src fdSource
69- addFD func(fd int, onReadable, onWritable func()) (fdSource, error) // display fd source seam
70- after func(d time.Duration, fn func()) (cancel func())
69+ lp loop // display fd sources and timers
7170 post func(func())
7271 flushFn func()
7372
@@ -331,25 +330,45 @@ func (x *XWM) boot() error {
331330 // Attach registers the X socket with the display and hands the windows found
332331 // at boot to h. Display goroutine only.
333332 func (x *XWM) Attach(d *server.Display, h host) error {
334- x.addFD = func(fd int, r, w func()) (fdSource, error) {
335- s, err := d.AddFDRW(fd, r, w)
336- if err != nil {
337- return nil, err
338- }
339- return s, nil
340- }
341333 src, err := d.AddFDRW(x.conn.Fd(), x.onReadable, x.onWritable)
342334 if err != nil {
343335 return fmt.Errorf("xwm: watch X socket: %w", err)
344336 }
345337 x.attach(src, h, d.Post)
338+ x.lp.(*displayLoop).d = d
346339 return nil
347340 }
348341
342+// loop is what the XWM needs from the display beyond the X socket source:
343+// fd sources for transfer pipes and one-shot timers.
344+type loop interface {
345+ AddFD(fd int, onReadable, onWritable func()) (fdSource, error)
346+ After(d time.Duration, fn func()) (cancel func())
347+}
348+
349+// displayLoop is the production loop: the display's fd sources, the host's timers.
350+type displayLoop struct {
351+ d *server.Display
352+ h host
353+}
354+
355+func (l *displayLoop) AddFD(fd int, r, w func()) (fdSource, error) {
356+ if l.d == nil {
357+ return nil, errors.New("xwm: no display")
358+ }
359+ s, err := l.d.AddFDRW(fd, r, w)
360+ if err != nil {
361+ return nil, err
362+ }
363+ return s, nil
364+}
365+
366+func (l *displayLoop) After(d time.Duration, fn func()) func() { return l.h.After(d, fn) }
367+
349368 // attach is Attach over its seams: the event source and the display's Post.
350369 func (x *XWM) attach(src fdSource, h host, post func(func())) {
351370 x.src, x.host, x.post = src, h, post
352- x.after = h.After
371+ x.lp = &displayLoop{h: h}
353372 x.flushFn = func() {
354373 x.posted = false
355374 x.flush()
@@ -66,8 +66,7 @@ type XWM struct {
66 // attachment66 // attachment
67 host host67 host host
68 src fdSource68 src fdSource
69- addFD func(fd int, onReadable, onWritable func()) (fdSource, error) // display fd source seam69+ lp loop // display fd sources and timers
70- after func(d time.Duration, fn func()) (cancel func())
71 post func(func())70 post func(func())
72 flushFn func()71 flushFn func()
73 72
@@ -331,25 +330,45 @@ func (x *XWM) boot() error {
331 // Attach registers the X socket with the display and hands the windows found330 // Attach registers the X socket with the display and hands the windows found
332 // at boot to h. Display goroutine only.331 // at boot to h. Display goroutine only.
333 func (x *XWM) Attach(d *server.Display, h host) error {332 func (x *XWM) Attach(d *server.Display, h host) error {
334- x.addFD = func(fd int, r, w func()) (fdSource, error) {
335- s, err := d.AddFDRW(fd, r, w)
336- if err != nil {
337- return nil, err
338- }
339- return s, nil
340- }
341 src, err := d.AddFDRW(x.conn.Fd(), x.onReadable, x.onWritable)333 src, err := d.AddFDRW(x.conn.Fd(), x.onReadable, x.onWritable)
342 if err != nil {334 if err != nil {
343 return fmt.Errorf("xwm: watch X socket: %w", err)335 return fmt.Errorf("xwm: watch X socket: %w", err)
344 }336 }
345 x.attach(src, h, d.Post)337 x.attach(src, h, d.Post)
338+ x.lp.(*displayLoop).d = d
346 return nil339 return nil
347 }340 }
348 341
342+// loop is what the XWM needs from the display beyond the X socket source:
343+// fd sources for transfer pipes and one-shot timers.
344+type loop interface {
345+ AddFD(fd int, onReadable, onWritable func()) (fdSource, error)
346+ After(d time.Duration, fn func()) (cancel func())
347+}
348+
349+// displayLoop is the production loop: the display's fd sources, the host's timers.
350+type displayLoop struct {
351+ d *server.Display
352+ h host
353+}
354+
355+func (l *displayLoop) AddFD(fd int, r, w func()) (fdSource, error) {
356+ if l.d == nil {
357+ return nil, errors.New("xwm: no display")
358+ }
359+ s, err := l.d.AddFDRW(fd, r, w)
360+ if err != nil {
361+ return nil, err
362+ }
363+ return s, nil
364+}
365+
366+func (l *displayLoop) After(d time.Duration, fn func()) func() { return l.h.After(d, fn) }
367+
349 // attach is Attach over its seams: the event source and the display's Post.368 // attach is Attach over its seams: the event source and the display's Post.
350 func (x *XWM) attach(src fdSource, h host, post func(func())) {369 func (x *XWM) attach(src fdSource, h host, post func(func())) {
351 x.src, x.host, x.post = src, h, post370 x.src, x.host, x.post = src, h, post
352- x.after = h.After371+ x.lp = &displayLoop{h: h}
353 x.flushFn = func() {372 x.flushFn = func() {
354 x.posted = false373 x.posted = false
355 x.flush()374 x.flush()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close