clipboard: bind the first wl_seat only (#3989)

port 2.0

(cherry picked from commit df8b1d85ca)
This commit is contained in:
xibielemn authored and dux-ci[bot] committed 2026-10-28 21:55:48 +00:00
1 parent 47744d34b7
commit 3c365533c7
3 files changed
+128 -16

No files matched your search

+64 -12
View File
@@ -2,6 +2,7 @@ package clipboard
import (
"bytes"
"cmp"
"encoding/binary"
"errors"
"fmt"
@@ -249,6 +250,9 @@ func (m *Manager) setupRegistry() error {
m.dataControlMgr = dataControlMgr
log.Info("Bound ext_data_control_manager_v1")
case "wl_seat":
if m.seat != nil {
return
}
seat := wlclient.NewSeat(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
log.Errorf("Failed to bind wl_seat: %v", err)
@@ -634,18 +638,27 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
if maxHistory < 0 {
return nil
}
type row struct {
key []byte
entry Entry
}
var rows []row
c := b.Cursor()
var count int
for k, v := c.Last(); k != nil; k, v = c.Prev() {
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntryMeta(v)
if err == nil && entry.Pinned {
if err != nil || entry.Pinned {
continue
}
if count < maxHistory {
count++
continue
}
if err := b.Delete(k); err != nil {
rows = append(rows, row{slices.Clone(k), entry})
}
slices.SortFunc(rows, func(a, b row) int {
return compareEntriesNewestFirst(a.entry, b.entry)
})
for _, r := range rows[min(maxHistory, len(rows)):] {
if err := b.Delete(r.key); err != nil {
return err
}
}
@@ -663,7 +676,7 @@ func encodeEntry(e Entry) ([]byte, error) {
binary.Write(buf, binary.BigEndian, uint32(len(e.Preview)))
buf.WriteString(e.Preview)
binary.Write(buf, binary.BigEndian, int32(e.Size))
binary.Write(buf, binary.BigEndian, e.Timestamp.Unix())
binary.Write(buf, binary.BigEndian, e.Timestamp.UnixNano())
if e.IsImage {
buf.WriteByte(1)
} else {
@@ -729,7 +742,13 @@ func decodeEntryFields(data []byte, withData bool) (Entry, error) {
var timestamp int64
binary.Read(buf, binary.BigEndian, &timestamp)
e.Timestamp = time.Unix(timestamp, 0)
// Rows written before the switch to nanosecond precision hold seconds;
// no second-resolution stamp reaches 1e12 until the year 33658.
if timestamp < 1e12 {
e.Timestamp = time.Unix(timestamp, 0)
} else {
e.Timestamp = time.Unix(0, timestamp)
}
var isImage byte
binary.Read(buf, binary.BigEndian, &isImage)
@@ -1105,9 +1124,25 @@ func (m *Manager) GetHistory() []Entry {
go m.deleteStaleEntries(stale)
}
sortEntriesNewestFirst(history)
return history
}
// sortEntriesNewestFirst orders by timestamp, falling back to id: TouchEntry
// rewrites a row in place to keep its id stable, so bucket key order stops
// tracking recency.
func sortEntriesNewestFirst(entries []Entry) {
slices.SortFunc(entries, compareEntriesNewestFirst)
}
func compareEntriesNewestFirst(a, b Entry) int {
if c := b.Timestamp.Compare(a.Timestamp); c != 0 {
return c
}
return cmp.Compare(b.ID, a.ID)
}
func (m *Manager) deleteStaleEntries(ids []uint64) {
if m.db == nil {
return
@@ -1243,9 +1278,24 @@ func (m *Manager) TouchEntry(id uint64) error {
return err
}
entry.Timestamp = time.Now()
// Round(0) drops the monotonic reading so the in-memory stamp compares the
// same way as the one decoded back from the bucket.
entry.Timestamp = time.Now().Round(0)
if err := m.storeEntry(*entry); err != nil {
// storeEntry would dedupe the row against itself and hand out a fresh
// sequence number, invalidating the id the UI is still holding.
err = m.dbUpdate(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
if b == nil {
return fmt.Errorf("clipboard bucket missing")
}
encoded, err := encodeEntry(*entry)
if err != nil {
return err
}
return b.Put(itob(id), encoded)
})
if err != nil {
return err
}
@@ -2200,6 +2250,8 @@ func (m *Manager) GetPinnedEntries() []Entry {
log.Errorf("Failed to get pinned entries: %v", err)
}
sortEntriesNewestFirst(pinned)
return pinned
}
+60 -1
View File
@@ -3,6 +3,7 @@ package clipboard
import (
"bytes"
"encoding/json"
"fmt"
"image"
"image/png"
"net"
@@ -446,7 +447,9 @@ func TestUnpinEntry_KeepsTopUnpinnedDuplicate(t *testing.T) {
history = m.GetHistory()
require.Len(t, history, 3)
require.Equal(t, topHistoryDuplicate.ID, history[0].ID)
// GetHistory sorts by timestamp, so the newer row leads even though its
// id is lower.
require.Equal(t, olderHistoryDuplicate.ID, history[0].ID)
require.NoError(t, m.UnpinEntry(pinnedID))
history = m.GetHistory()
@@ -1088,3 +1091,59 @@ func TestApplyConfigChange_DisableReleasesDataDevice(t *testing.T) {
require.ErrorIs(t, m.CopyText("secret"), errSelectionUnavailable)
assert.Empty(t, m.GetHistory())
}
func TestTouchEntry_SurvivesTrimWhenHistoryFull(t *testing.T) {
m := newTestManagerWithDB(t)
m.config.MaxHistory = 3
for i := range 3 {
require.NoError(t, m.storeEntry(Entry{
Data: []byte(fmt.Sprintf("entry-%d", i)),
MimeType: "text/plain",
Timestamp: time.Now(),
}))
}
oldest := m.GetHistory()[2]
require.NoError(t, m.TouchEntry(oldest.ID))
// A new copy trims the bucket back to MaxHistory.
require.NoError(t, m.storeEntry(Entry{
Data: []byte("newcomer"),
MimeType: "text/plain",
Timestamp: time.Now(),
}))
history := m.GetHistory()
require.Len(t, history, 3)
ids := make([]uint64, len(history))
for i, e := range history {
ids[i] = e.ID
}
assert.Contains(t, ids, oldest.ID, "the touched row must survive trimming")
assert.Equal(t, oldest.ID, history[1].ID, "the touched row ranks second, behind the newer copy")
}
func TestGetHistory_SameSecondOrdersByID(t *testing.T) {
m := newTestManagerWithDB(t)
m.config.MaxHistory = 10
stamp := time.Now()
for i := range 3 {
require.NoError(t, m.storeEntry(Entry{
Data: []byte(fmt.Sprintf("same-second-%d", i)),
MimeType: "text/plain",
Timestamp: stamp,
}))
}
// Touching the oldest row rewrites it in place, so the bucket now yields
// it first even though every row shares a timestamp.
oldest := m.GetHistory()[2]
require.NoError(t, m.TouchEntry(oldest.ID))
history := m.GetHistory()
require.Len(t, history, 3)
assert.Equal(t, oldest.ID, history[0].ID, "the touched row must lead: its timestamp is newer")
assert.Greater(t, history[1].ID, history[2].ID, "rows still tied on timestamp break on newest id")
}
+4 -3
View File
@@ -119,9 +119,10 @@ Singleton {
(entry.pinned ? pinned : unpinned).push(entry);
}
const byIdDesc = (a, b) => b.id - a.id;
pinned.sort(byIdDesc);
unpinned.sort(byIdDesc);
// JS Date keeps milliseconds only, so rows sharing one get id as the tie-break.
const byTimestampDesc = (a, b) => (new Date(b.timestamp) - new Date(a.timestamp)) || (b.id - a.id);
pinned.sort(byTimestampDesc);
unpinned.sort(byTimestampDesc);
pinnedEntries = pinned;
unpinnedEntries = unpinned;