detached 04b38e5

feat(xwm): answer TIMESTAMP and list it in TARGETS

commit
04b38e5774888de3a789d77e50cc6a27167e64a7
author
brice · · 2026-10-22 16:56 UTC
committer
brice · · 2026-10-22 17:55 UTC
parent
6168668
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +63 -17
M internal/adapters/wayland/xwm/events.go +14 -0
@@ -36,6 +36,12 @@ func (x *XWM) onEvent(ev *xturbo.Event) {
3636 return
3737 }
3838 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 {
3945 case xproto.KindCreateNotify:
4046 x.onCreateNotify(ev)
4147 case xproto.KindDestroyNotify:
@@ -287,3 +293,11 @@ func (x *XWM) onVersionReply(r *xturbo.Reply) {
287293 }
288294 }
289295 }
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+}
@@ -36,6 +36,12 @@ func (x *XWM) onEvent(ev *xturbo.Event) {
36 return36 return
37 }37 }
38 switch ev.Kind {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 {
39 case xproto.KindCreateNotify:45 case xproto.KindCreateNotify:
40 x.onCreateNotify(ev)46 x.onCreateNotify(ev)
41 case xproto.KindDestroyNotify:47 case xproto.KindDestroyNotify:
@@ -287,3 +293,11 @@ func (x *XWM) onVersionReply(r *xturbo.Reply) {
287 }293 }
288 }294 }
289 }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+}
M internal/adapters/wayland/xwm/selection.go +14 -6
@@ -30,11 +30,12 @@ var (
3030
3131 // selState is one selection.
3232 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- ours bool // selWin owns the selection (Wayland -> X)
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)
3839 // wl is what the Wayland side offers while ours: raw text availability.
3940 wlUTF8, wlPlain bool
4041 wlMime [len(textMimes)]string // offered text MIMEs, in textMimes order, "" absent
@@ -719,6 +720,7 @@ func (x *XWM) WaylandSelection(kind int, mimes []string) {
719720 return
720721 }
721722 st.ours = true
723+ st.ownTime = x.lastTime
722724 st.gen++
723725 st.owner = 0
724726 xproto.SetSelectionOwner(b, xproto.Window(x.selWin), xproto.Atom(st.atom), 0)
@@ -771,9 +773,10 @@ func (x *XWM) onSelectionRequest(ev *xturbo.Event) {
771773 st := &x.sel.st[k]
772774 switch target {
773775 case x.atoms[atomTargets]:
774- var list [5]uint32
776+ var list [6]uint32
775777 n := 0
776778 list[n], n = x.atoms[atomTargets], n+1
779+ list[n], n = x.atoms[atomTimestamp], n+1
777780 if st.wlUTF8 {
778781 list[n], n = x.atoms[atomUTF8String], n+1
779782 list[n], n = x.atoms[atomText], n+1
@@ -783,6 +786,11 @@ func (x *XWM) onSelectionRequest(ev *xturbo.Event) {
783786 xproto.AtomAtom, 32, uint32(n), x.encode32(list[:n]))
784787 x.notify(req, sel, target, prop, time)
785788 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
786794 case x.atoms[atomUTF8String], x.atoms[atomText], x.atoms[atomString]:
787795 default:
788796 x.notify(req, sel, target, 0, time)
@@ -30,11 +30,12 @@ var (
30 30
31 // selState is one selection.31 // selState is one selection.
32 type selState struct {32 type selState struct {
33- atom uint3233+ atom uint32
34- gen uint32 // bumped on every ownership change; stales TARGETS probes34+ gen uint32 // bumped on every ownership change; stales TARGETS probes
35- probe uint32 // gen when the TARGETS ConvertSelection was issued35+ probe uint32 // gen when the TARGETS ConvertSelection was issued
36- owner uint32 // X owner window other than selWin, 0 none36+ owner uint32 // X owner window other than selWin, 0 none
37- ours bool // selWin owns the selection (Wayland -> X)37+ ownTime uint32 // lastTime when selWin took ownership, answered for TIMESTAMP
38+ ours bool // selWin owns the selection (Wayland -> X)
38 // wl is what the Wayland side offers while ours: raw text availability.39 // wl is what the Wayland side offers while ours: raw text availability.
39 wlUTF8, wlPlain bool40 wlUTF8, wlPlain bool
40 wlMime [len(textMimes)]string // offered text MIMEs, in textMimes order, "" absent41 wlMime [len(textMimes)]string // offered text MIMEs, in textMimes order, "" absent
@@ -719,6 +720,7 @@ func (x *XWM) WaylandSelection(kind int, mimes []string) {
719 return720 return
720 }721 }
721 st.ours = true722 st.ours = true
723+ st.ownTime = x.lastTime
722 st.gen++724 st.gen++
723 st.owner = 0725 st.owner = 0
724 xproto.SetSelectionOwner(b, xproto.Window(x.selWin), xproto.Atom(st.atom), 0)726 xproto.SetSelectionOwner(b, xproto.Window(x.selWin), xproto.Atom(st.atom), 0)
@@ -771,9 +773,10 @@ func (x *XWM) onSelectionRequest(ev *xturbo.Event) {
771 st := &x.sel.st[k]773 st := &x.sel.st[k]
772 switch target {774 switch target {
773 case x.atoms[atomTargets]:775 case x.atoms[atomTargets]:
774- var list [5]uint32776+ var list [6]uint32
775 n := 0777 n := 0
776 list[n], n = x.atoms[atomTargets], n+1778 list[n], n = x.atoms[atomTargets], n+1
779+ list[n], n = x.atoms[atomTimestamp], n+1
777 if st.wlUTF8 {780 if st.wlUTF8 {
778 list[n], n = x.atoms[atomUTF8String], n+1781 list[n], n = x.atoms[atomUTF8String], n+1
779 list[n], n = x.atoms[atomText], n+1782 list[n], n = x.atoms[atomText], n+1
@@ -783,6 +786,11 @@ func (x *XWM) onSelectionRequest(ev *xturbo.Event) {
783 xproto.AtomAtom, 32, uint32(n), x.encode32(list[:n]))786 xproto.AtomAtom, 32, uint32(n), x.encode32(list[:n]))
784 x.notify(req, sel, target, prop, time)787 x.notify(req, sel, target, prop, time)
785 return788 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
786 case x.atoms[atomUTF8String], x.atoms[atomText], x.atoms[atomString]:794 case x.atoms[atomUTF8String], x.atoms[atomText], x.atoms[atomString]:
787 default:795 default:
788 x.notify(req, sel, target, 0, time)796 x.notify(req, sel, target, 0, time)
M internal/adapters/wayland/xwm/selection_test.go +24 -1
@@ -487,7 +487,7 @@ func TestSelectionWaylandToXTargets(t *testing.T) {
487487 require.Equal(t, r.data, sentNotify(t, r))
488488 v, ok := r.p.prop(reqWin, "_NEFERWL_DATA")
489489 require.True(t, ok)
490- require.Equal(t, []uint32{r.p.atom("TARGETS"), r.p.atom("STRING")}, atomsOf(v), "no UTF8 target for text/plain only")
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")
491491 r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("image/png"), r.data)
492492 require.Zero(t, sentNotify(t, r), "unknown target refused")
493493 // UTF8_STRING is not offered by a text/plain-only source, but STRING reads it.
@@ -784,3 +784,26 @@ func TestSelectionCloseDoesNotStartQueued(t *testing.T) {
784784 require.Equal(t, 1, cs)
785785 require.False(t, r.x.sel.timerOn)
786786 }
787+
788+func TestSelectionWaylandToXTimestamp(t *testing.T) {
789+ r := ownedByWayland(t, "text/plain")
790+ r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("TIMESTAMP"), r.data)
791+ require.Equal(t, r.data, sentNotify(t, r))
792+ v, ok := r.p.prop(reqWin, "_NEFERWL_DATA")
793+ require.True(t, ok)
794+ require.EqualValues(t, 32, v.format)
795+ require.EqualValues(t, 19, v.typ, "INTEGER")
796+ require.Len(t, v.data, 4)
797+}
798+
799+func TestSelectionOwnershipTimeFromLastEvent(t *testing.T) {
800+ x, p, _, _, _ := attached(t)
801+ p.event(28, func(b *[32]byte) { // PropertyNotify
802+ binary.LittleEndian.PutUint32(b[4:], x.selWin)
803+ binary.LittleEndian.PutUint32(b[8:], 1)
804+ binary.LittleEndian.PutUint32(b[12:], 4242)
805+ })
806+ readUntil(t, x, func() bool { return x.lastTime == 4242 })
807+ x.WaylandSelection(SelClipboard, []string{"text/plain"})
808+ require.EqualValues(t, 4242, x.sel.st[SelClipboard].ownTime)
809+}
@@ -487,7 +487,7 @@ func TestSelectionWaylandToXTargets(t *testing.T) {
487 require.Equal(t, r.data, sentNotify(t, r))487 require.Equal(t, r.data, sentNotify(t, r))
488 v, ok := r.p.prop(reqWin, "_NEFERWL_DATA")488 v, ok := r.p.prop(reqWin, "_NEFERWL_DATA")
489 require.True(t, ok)489 require.True(t, ok)
490- require.Equal(t, []uint32{r.p.atom("TARGETS"), r.p.atom("STRING")}, atomsOf(v), "no UTF8 target for text/plain only")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)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")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.493 // UTF8_STRING is not offered by a text/plain-only source, but STRING reads it.
@@ -784,3 +784,26 @@ func TestSelectionCloseDoesNotStartQueued(t *testing.T) {
784 require.Equal(t, 1, cs)784 require.Equal(t, 1, cs)
785 require.False(t, r.x.sel.timerOn)785 require.False(t, r.x.sel.timerOn)
786 }786 }
787+
788+func TestSelectionWaylandToXTimestamp(t *testing.T) {
789+ r := ownedByWayland(t, "text/plain")
790+ r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("TIMESTAMP"), r.data)
791+ require.Equal(t, r.data, sentNotify(t, r))
792+ v, ok := r.p.prop(reqWin, "_NEFERWL_DATA")
793+ require.True(t, ok)
794+ require.EqualValues(t, 32, v.format)
795+ require.EqualValues(t, 19, v.typ, "INTEGER")
796+ require.Len(t, v.data, 4)
797+}
798+
799+func TestSelectionOwnershipTimeFromLastEvent(t *testing.T) {
800+ x, p, _, _, _ := attached(t)
801+ p.event(28, func(b *[32]byte) { // PropertyNotify
802+ binary.LittleEndian.PutUint32(b[4:], x.selWin)
803+ binary.LittleEndian.PutUint32(b[8:], 1)
804+ binary.LittleEndian.PutUint32(b[12:], 4242)
805+ })
806+ readUntil(t, x, func() bool { return x.lastTime == 4242 })
807+ x.WaylandSelection(SelClipboard, []string{"text/plain"})
808+ require.EqualValues(t, 4242, x.sel.st[SelClipboard].ownTime)
809+}
M internal/adapters/wayland/xwm/xwm.go +11 -10
@@ -52,16 +52,17 @@ const unpairedCap = 64
5252
5353 // XWM manages the windows of one Xwayland. Display goroutine only, after Boot.
5454 type XWM struct {
55- conn *xturbo.Conn
56- log zerowrap.Logger
57- root uint32
58- wmWin uint32
59- selWin uint32 // selection bridge window (selection.go)
60- sel selBridge
61- atoms [atomCount]uint32
62- tab *table
63- h xturbo.Handler
64- stats Stats
55+ conn *xturbo.Conn
56+ log zerowrap.Logger
57+ root uint32
58+ wmWin uint32
59+ selWin uint32 // selection bridge window (selection.go)
60+ lastTime uint32 // server time of the latest timestamped X event
61+ sel selBridge
62+ atoms [atomCount]uint32
63+ tab *table
64+ h xturbo.Handler
65+ stats Stats
6566
6667 // attachment
6768 host host
@@ -52,16 +52,17 @@ const unpairedCap = 64
52 52
53 // XWM manages the windows of one Xwayland. Display goroutine only, after Boot.53 // XWM manages the windows of one Xwayland. Display goroutine only, after Boot.
54 type XWM struct {54 type XWM struct {
55- conn *xturbo.Conn55+ conn *xturbo.Conn
56- log zerowrap.Logger56+ log zerowrap.Logger
57- root uint3257+ root uint32
58- wmWin uint3258+ wmWin uint32
59- selWin uint32 // selection bridge window (selection.go)59+ selWin uint32 // selection bridge window (selection.go)
60- sel selBridge60+ lastTime uint32 // server time of the latest timestamped X event
61- atoms [atomCount]uint3261+ sel selBridge
62- tab *table62+ atoms [atomCount]uint32
63- h xturbo.Handler63+ tab *table
64- stats Stats64+ h xturbo.Handler
65+ stats Stats
65 66
66 // attachment67 // attachment
67 host host68 host host

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