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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 36 | 36 | return | |
| 37 | 37 | } | |
| 38 | 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 | 45 | case xproto.KindCreateNotify: | |
| 40 | 46 | x.onCreateNotify(ev) | |
| 41 | 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 | + | } | |
| @@ -36,6 +36,12 @@ func (x *XWM) onEvent(ev *xturbo.Event) { | |||||
| 36 | return | 36 | 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 ( | |||
| 30 | 30 | ||
| 31 | 31 | // selState is one selection. | |
| 32 | 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 | - | 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) | |
| 38 | 39 | // wl is what the Wayland side offers while ours: raw text availability. | |
| 39 | 40 | wlUTF8, wlPlain bool | |
| 40 | 41 | wlMime [len(textMimes)]string // offered text MIMEs, in textMimes order, "" absent | |
| @@ -719,6 +720,7 @@ func (x *XWM) WaylandSelection(kind int, mimes []string) { | |||
| 719 | 720 | return | |
| 720 | 721 | } | |
| 721 | 722 | st.ours = true | |
| 723 | + | st.ownTime = x.lastTime | |
| 722 | 724 | st.gen++ | |
| 723 | 725 | st.owner = 0 | |
| 724 | 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 | 773 | st := &x.sel.st[k] | |
| 772 | 774 | switch target { | |
| 773 | 775 | case x.atoms[atomTargets]: | |
| 774 | - | var list [5]uint32 | |
| 776 | + | var list [6]uint32 | |
| 775 | 777 | n := 0 | |
| 776 | 778 | list[n], n = x.atoms[atomTargets], n+1 | |
| 779 | + | list[n], n = x.atoms[atomTimestamp], n+1 | |
| 777 | 780 | if st.wlUTF8 { | |
| 778 | 781 | list[n], n = x.atoms[atomUTF8String], n+1 | |
| 779 | 782 | list[n], n = x.atoms[atomText], n+1 | |
| @@ -783,6 +786,11 @@ func (x *XWM) onSelectionRequest(ev *xturbo.Event) { | |||
| 783 | 786 | xproto.AtomAtom, 32, uint32(n), x.encode32(list[:n])) | |
| 784 | 787 | x.notify(req, sel, target, prop, time) | |
| 785 | 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 | |
| 786 | 794 | case x.atoms[atomUTF8String], x.atoms[atomText], x.atoms[atomString]: | |
| 787 | 795 | default: | |
| 788 | 796 | 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 uint32 | 33 | + | atom uint32 |
| 34 | - | gen uint32 // bumped on every ownership change; stales TARGETS probes | 34 | + | gen uint32 // bumped on every ownership change; stales TARGETS probes |
| 35 | - | probe uint32 // gen when the TARGETS ConvertSelection was issued | 35 | + | probe uint32 // gen when the TARGETS ConvertSelection was issued |
| 36 | - | owner uint32 // X owner window other than selWin, 0 none | 36 | + | 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 bool | 40 | wlUTF8, wlPlain bool | ||
| 40 | wlMime [len(textMimes)]string // offered text MIMEs, in textMimes order, "" absent | 41 | wlMime [len(textMimes)]string // offered text MIMEs, in textMimes order, "" absent | ||
| @@ -719,6 +720,7 @@ func (x *XWM) WaylandSelection(kind int, mimes []string) { | |||||
| 719 | return | 720 | return | ||
| 720 | } | 721 | } | ||
| 721 | st.ours = true | 722 | st.ours = true | ||
| 723 | + | st.ownTime = x.lastTime | |||
| 722 | st.gen++ | 724 | st.gen++ | ||
| 723 | st.owner = 0 | 725 | 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]uint32 | 776 | + | var list [6]uint32 |
| 775 | n := 0 | 777 | n := 0 | ||
| 776 | list[n], n = x.atoms[atomTargets], n+1 | 778 | 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+1 | 781 | list[n], n = x.atoms[atomUTF8String], n+1 | ||
| 779 | list[n], n = x.atoms[atomText], n+1 | 782 | 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 | return | 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 | |||
| 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) { | |||
| 487 | 487 | require.Equal(t, r.data, sentNotify(t, r)) | |
| 488 | 488 | v, ok := r.p.prop(reqWin, "_NEFERWL_DATA") | |
| 489 | 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 | 491 | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("image/png"), r.data) | |
| 492 | 492 | require.Zero(t, sentNotify(t, r), "unknown target refused") | |
| 493 | 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 | 784 | require.Equal(t, 1, cs) | |
| 785 | 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 | + | } | |
| @@ -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 | |||
| 52 | 52 | ||
| 53 | 53 | // XWM manages the windows of one Xwayland. Display goroutine only, after Boot. | |
| 54 | 54 | 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 | |
| 65 | 66 | ||
| 66 | 67 | // attachment | |
| 67 | 68 | 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.Conn | 55 | + | conn *xturbo.Conn |
| 56 | - | log zerowrap.Logger | 56 | + | log zerowrap.Logger |
| 57 | - | root uint32 | 57 | + | root uint32 |
| 58 | - | wmWin uint32 | 58 | + | wmWin uint32 |
| 59 | - | selWin uint32 // selection bridge window (selection.go) | 59 | + | selWin uint32 // selection bridge window (selection.go) |
| 60 | - | sel selBridge | 60 | + | lastTime uint32 // server time of the latest timestamped X event |
| 61 | - | atoms [atomCount]uint32 | 61 | + | sel selBridge |
| 62 | - | tab *table | 62 | + | atoms [atomCount]uint32 |
| 63 | - | h xturbo.Handler | 63 | + | tab *table |
| 64 | - | stats Stats | 64 | + | h xturbo.Handler |
| 65 | + | stats Stats | |||
| 65 | 66 | ||||
| 66 | // attachment | 67 | // attachment | ||
| 67 | host host | 68 | host host | ||