detached ad55fe8

feat(xwm): host timer seam After for selection timeouts

commit
ad55fe80087e319683c0ab307ae2f182f145c3cb
author
brice · · 2026-10-22 16:15 UTC
committer
brice · · 2026-10-22 17:55 UTC
parent
41edcc9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +79 -1
M internal/adapters/wayland/x11window.go +7 -0
@@ -1,6 +1,8 @@
11 package wayland
22
33 import (
4+ "time"
5+
46 "github.com/bnema/go-wayland-bindings/server/wayland"
57 "github.com/bnema/go-wayland-bindings/server/xwaylandshell"
68 "github.com/bnema/neferwl/internal/adapters/wayland/xwm"
@@ -377,6 +379,11 @@ func (h hostImpl) SelectionOwned(kind int, mimes []string) {
377379 h.s.log.Debug().Int("kind", kind).Int("mimes", len(mimes)).Msg("X selection owner changed")
378380 }
379381
382+func (h hostImpl) After(d time.Duration, fn func()) func() {
383+ t := h.s.clock.AfterFunc(d, func() { h.s.display.Post(fn) })
384+ return func() { t.Stop() }
385+}
386+
380387 func (h hostImpl) OverrideGone(slot xwm.Slot) {
381388 s := h.s
382389 win := s.x11Slots[slot]
@@ -1,6 +1,8 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "time"
5+
4 "github.com/bnema/go-wayland-bindings/server/wayland"6 "github.com/bnema/go-wayland-bindings/server/wayland"
5 "github.com/bnema/go-wayland-bindings/server/xwaylandshell"7 "github.com/bnema/go-wayland-bindings/server/xwaylandshell"
6 "github.com/bnema/neferwl/internal/adapters/wayland/xwm"8 "github.com/bnema/neferwl/internal/adapters/wayland/xwm"
@@ -377,6 +379,11 @@ func (h hostImpl) SelectionOwned(kind int, mimes []string) {
377 h.s.log.Debug().Int("kind", kind).Int("mimes", len(mimes)).Msg("X selection owner changed")379 h.s.log.Debug().Int("kind", kind).Int("mimes", len(mimes)).Msg("X selection owner changed")
378 }380 }
379 381
382+func (h hostImpl) After(d time.Duration, fn func()) func() {
383+ t := h.s.clock.AfterFunc(d, func() { h.s.display.Post(fn) })
384+ return func() { t.Stop() }
385+}
386+
380 func (h hostImpl) OverrideGone(slot xwm.Slot) {387 func (h hostImpl) OverrideGone(slot xwm.Slot) {
381 s := h.s388 s := h.s
382 win := s.x11Slots[slot]389 win := s.x11Slots[slot]
M internal/adapters/wayland/xwm/host.go +11 -1
@@ -1,6 +1,10 @@
11 package xwm
22
3-import "github.com/bnema/neferwl/internal/ports"
3+import (
4+ "time"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
48
59 // Slot is the XWM's window slot: table index + 1, stable until the window is
610 // destroyed. 0 means none.
@@ -58,6 +62,12 @@ type host interface {
5862 // offers these MIME types, or none does any more (mimes nil). mimes is
5963 // scratch: the host copies it.
6064 SelectionOwned(kind int, mimes []string)
65+
66+ // After runs fn on the display goroutine once d has elapsed, unless the
67+ // returned cancel ran first. cancel is idempotent and display goroutine
68+ // only; fn may still run once after cancel when it was already queued,
69+ // so callers guard with their own generation.
70+ After(d time.Duration, fn func()) (cancel func())
6171 }
6272
6373 // fdSource is the display's event source for the X socket (*server.Source).
@@ -1,6 +1,10 @@
1 package xwm1 package xwm
2 2
3-import "github.com/bnema/neferwl/internal/ports"3+import (
4+ "time"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
4 8
5 // Slot is the XWM's window slot: table index + 1, stable until the window is9 // Slot is the XWM's window slot: table index + 1, stable until the window is
6 // destroyed. 0 means none.10 // destroyed. 0 means none.
@@ -58,6 +62,12 @@ type host interface {
58 // offers these MIME types, or none does any more (mimes nil). mimes is62 // offers these MIME types, or none does any more (mimes nil). mimes is
59 // scratch: the host copies it.63 // scratch: the host copies it.
60 SelectionOwned(kind int, mimes []string)64 SelectionOwned(kind int, mimes []string)
65+
66+ // After runs fn on the display goroutine once d has elapsed, unless the
67+ // returned cancel ran first. cancel is idempotent and display goroutine
68+ // only; fn may still run once after cancel when it was already queued,
69+ // so callers guard with their own generation.
70+ After(d time.Duration, fn func()) (cancel func())
61 }71 }
62 72
63 // fdSource is the display's event source for the X socket (*server.Source).73 // fdSource is the display's event source for the X socket (*server.Source).
M internal/adapters/wayland/xwm/host_mock_test.go +61 -0
@@ -5,6 +5,8 @@
55 package xwm
66
77 import (
8+ "time"
9+
810 "github.com/bnema/neferwl/internal/ports"
911 mock "github.com/stretchr/testify/mock"
1012 )
@@ -45,6 +47,65 @@ func (_m *mockhost) EXPECT() *mockhost_Expecter {
4547 return &mockhost_Expecter{mock: &_m.Mock}
4648 }
4749
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+
48109 // AppID provides a mock function for the type mockhost
49110 func (_mock *mockhost) AppID(slot Slot, appID string) {
50111 _mock.Called(slot, appID)
@@ -5,6 +5,8 @@
5 package xwm5 package xwm
6 6
7 import (7 import (
8+ "time"
9+
8 "github.com/bnema/neferwl/internal/ports"10 "github.com/bnema/neferwl/internal/ports"
9 mock "github.com/stretchr/testify/mock"11 mock "github.com/stretchr/testify/mock"
10 )12 )
@@ -45,6 +47,65 @@ func (_m *mockhost) EXPECT() *mockhost_Expecter {
45 return &mockhost_Expecter{mock: &_m.Mock}47 return &mockhost_Expecter{mock: &_m.Mock}
46 }48 }
47 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+
48 // AppID provides a mock function for the type mockhost109 // AppID provides a mock function for the type mockhost
49 func (_mock *mockhost) AppID(slot Slot, appID string) {110 func (_mock *mockhost) AppID(slot Slot, appID string) {
50 _mock.Called(slot, appID)111 _mock.Called(slot, appID)

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