mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-26 13:07:49 -04:00
fix(clipboard): bind & release the data device when tracking is toggled
Port 2.0
This commit is contained in:
1 parent
52d6a46d89
commit
7e0f110fb8
6 files changed
+145
-92
No files matched your search
@@ -40,10 +40,6 @@ func HandleRequest(conn *ipc.ConnWriter, req ipc.Request, m *Manager) {
|
||||
handleSubscribe(conn, req, m)
|
||||
case "clipboard.search":
|
||||
handleSearch(conn, req, m)
|
||||
case "clipboard.getConfig":
|
||||
handleGetConfig(conn, req, m)
|
||||
case "clipboard.setConfig":
|
||||
handleSetConfig(conn, req, m)
|
||||
case "clipboard.store":
|
||||
handleStore(conn, req, m)
|
||||
case "clipboard.pinEntry":
|
||||
@@ -320,40 +316,6 @@ func handleSearch(conn *ipc.ConnWriter, req ipc.Request, m *Manager) {
|
||||
conn.Respond(req.ID, m.Search(p))
|
||||
}
|
||||
|
||||
func handleGetConfig(conn *ipc.ConnWriter, req ipc.Request, m *Manager) {
|
||||
conn.Respond(req.ID, m.GetConfig())
|
||||
}
|
||||
|
||||
func handleSetConfig(conn *ipc.ConnWriter, req ipc.Request, m *Manager) {
|
||||
cfg := m.GetConfig()
|
||||
|
||||
if v, ok := req.Get[float64]("maxHistory"); ok {
|
||||
cfg.MaxHistory = int(v)
|
||||
}
|
||||
if v, ok := req.Get[float64]("maxEntrySize"); ok {
|
||||
cfg.MaxEntrySize = int64(v)
|
||||
}
|
||||
if v, ok := req.Get[float64]("autoClearDays"); ok {
|
||||
cfg.AutoClearDays = int(v)
|
||||
}
|
||||
if v, ok := req.Get[bool]("clearAtStartup"); ok {
|
||||
cfg.ClearAtStartup = v
|
||||
}
|
||||
if v, ok := req.Get[bool]("disabled"); ok {
|
||||
cfg.Disabled = v
|
||||
}
|
||||
if v, ok := req.Get[float64]("maxPinned"); ok {
|
||||
cfg.MaxPinned = int(v)
|
||||
}
|
||||
|
||||
if err := m.SetConfig(cfg); err != nil {
|
||||
conn.RespondError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
conn.Respond(req.ID, models.SuccessResult{Success: true, Message: "config updated"})
|
||||
}
|
||||
|
||||
func handleStore(conn *ipc.ConnWriter, req ipc.Request, m *Manager) {
|
||||
data, err := params.String(req.Params, "data")
|
||||
if err != nil {
|
||||
|
||||
@@ -277,17 +277,65 @@ func (m *Manager) setupRegistry() error {
|
||||
}
|
||||
|
||||
func (m *Manager) setupDataDeviceSync() {
|
||||
if m.dataControlMgr == nil || m.seat == nil {
|
||||
if !m.bindDataDevice() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := m.display.Context()
|
||||
if err := m.display.Context().Dispatch(); err != nil {
|
||||
log.Errorf("Failed to dispatch initial events: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Data device setup complete")
|
||||
}
|
||||
|
||||
// Runs on the dispatcher goroutine; a disabled manager must hold no device so it receives no offers.
|
||||
func (m *Manager) syncTracking() {
|
||||
if m.getConfig().Disabled {
|
||||
m.releaseDataDevice()
|
||||
return
|
||||
}
|
||||
if m.dataDevice != nil {
|
||||
return
|
||||
}
|
||||
if m.registry == nil {
|
||||
if err := m.setupRegistry(); err != nil {
|
||||
log.Errorf("Failed to enable clipboard tracking: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if m.bindDataDevice() {
|
||||
log.Info("Data device setup complete")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) releaseDataDevice() {
|
||||
m.deviceBound.Store(false)
|
||||
|
||||
if m.currentOffer != nil {
|
||||
m.releaseOffer(m.currentOffer)
|
||||
m.currentOffer = nil
|
||||
}
|
||||
|
||||
if m.dataDevice == nil {
|
||||
return
|
||||
}
|
||||
m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1).Destroy()
|
||||
m.dataDevice = nil
|
||||
m.mimeTypes = nil
|
||||
}
|
||||
|
||||
func (m *Manager) bindDataDevice() bool {
|
||||
if m.dataControlMgr == nil || m.seat == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
|
||||
|
||||
dataDevice, err := dataMgr.GetDataDevice(m.seat)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to send get_data_device request: %v", err)
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
dataDevice.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
||||
@@ -307,8 +355,12 @@ func (m *Manager) setupDataDeviceSync() {
|
||||
})
|
||||
|
||||
dataDevice.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
||||
// The first event replays the selection that existed before binding.
|
||||
if !m.initialized {
|
||||
m.initialized = true
|
||||
if e.Id != nil {
|
||||
m.releaseOffer(e.Id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -336,6 +388,11 @@ func (m *Manager) setupDataDeviceSync() {
|
||||
return
|
||||
}
|
||||
|
||||
// An event can land between a runtime disable and the posted release.
|
||||
if m.getConfig().Disabled {
|
||||
return
|
||||
}
|
||||
|
||||
m.offerMutex.RLock()
|
||||
mimes := m.offerMimeTypes[offer]
|
||||
m.offerMutex.RUnlock()
|
||||
@@ -394,14 +451,10 @@ func (m *Manager) setupDataDeviceSync() {
|
||||
go m.readAndStore(r, preferredMime, altR, altMime)
|
||||
})
|
||||
|
||||
m.initialized = false
|
||||
m.dataDevice = dataDevice
|
||||
|
||||
if err := ctx.Dispatch(); err != nil {
|
||||
log.Errorf("Failed to dispatch initial events: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Data device setup complete")
|
||||
m.deviceBound.Store(true)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Manager) releaseOffer(offer any) {
|
||||
@@ -520,6 +573,9 @@ func (m *Manager) storeClipboardEntry(data []byte, mimeType string, altData []by
|
||||
}
|
||||
|
||||
func (m *Manager) storeEntry(entry Entry) error {
|
||||
if m.getConfig().Disabled {
|
||||
return errTrackingDisabled
|
||||
}
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
@@ -574,7 +630,8 @@ func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
||||
}
|
||||
|
||||
func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
||||
if m.config.MaxHistory < 0 {
|
||||
maxHistory := m.getConfig().MaxHistory
|
||||
if maxHistory < 0 {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
@@ -584,7 +641,7 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
||||
if err == nil && entry.Pinned {
|
||||
continue
|
||||
}
|
||||
if count < m.config.MaxHistory {
|
||||
if count < maxHistory {
|
||||
count++
|
||||
continue
|
||||
}
|
||||
@@ -1341,19 +1398,22 @@ func (m *Manager) reopenDB() {
|
||||
m.db = db
|
||||
}
|
||||
|
||||
var errSelectionUnavailable = errors.New("clipboard selection unavailable: tracking disabled or no data control protocol")
|
||||
var (
|
||||
errSelectionUnavailable = errors.New("clipboard selection unavailable: tracking disabled or no data control protocol")
|
||||
errTrackingDisabled = errors.New("clipboard tracking disabled")
|
||||
)
|
||||
|
||||
// Both are bound once in NewManager; a disabled manager never binds them but
|
||||
// still serves copy requests, so callers must fail before touching history.
|
||||
// A disabled manager still serves copy requests; failing here lets callers
|
||||
// such as dms screenshot fall back to their own copy.
|
||||
func (m *Manager) canTakeSelection() bool {
|
||||
return m.dataControlMgr != nil && m.dataDevice != nil
|
||||
return m.deviceBound.Load() && !m.getConfig().Disabled
|
||||
}
|
||||
|
||||
func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
||||
if !m.canTakeSelection() {
|
||||
return errSelectionUnavailable
|
||||
}
|
||||
if int64(len(data)) > m.config.MaxEntrySize {
|
||||
if int64(len(data)) > m.getConfig().MaxEntrySize {
|
||||
return fmt.Errorf("data too large")
|
||||
}
|
||||
|
||||
@@ -1371,7 +1431,7 @@ func (m *Manager) SetClipboardEntry(entry *Entry) error {
|
||||
if !m.canTakeSelection() {
|
||||
return errSelectionUnavailable
|
||||
}
|
||||
if int64(len(entry.Data)) > m.config.MaxEntrySize {
|
||||
if int64(len(entry.Data)) > m.getConfig().MaxEntrySize {
|
||||
return fmt.Errorf("data too large")
|
||||
}
|
||||
|
||||
@@ -1520,16 +1580,7 @@ func (m *Manager) Close() {
|
||||
m.subMutex.Unlock()
|
||||
|
||||
m.releaseCurrentSource()
|
||||
|
||||
if m.currentOffer != nil {
|
||||
m.releaseOffer(m.currentOffer)
|
||||
m.currentOffer = nil
|
||||
}
|
||||
|
||||
if m.dataDevice != nil {
|
||||
device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
|
||||
device.Destroy()
|
||||
}
|
||||
m.releaseDataDevice()
|
||||
|
||||
if m.dataControlMgr != nil {
|
||||
mgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
|
||||
@@ -1729,19 +1780,12 @@ func (m *Manager) Search(params SearchParams) SearchResult {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetConfig() Config {
|
||||
return m.config
|
||||
}
|
||||
|
||||
func (m *Manager) SetConfig(cfg Config) error {
|
||||
m.configMutex.Lock()
|
||||
m.config = cfg
|
||||
m.configMutex.Unlock()
|
||||
|
||||
m.updateState()
|
||||
m.notifySubscribers()
|
||||
|
||||
return SaveConfig(cfg)
|
||||
if err := SaveConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
m.applyConfigChange(cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) getConfig() Config {
|
||||
@@ -1782,7 +1826,12 @@ func (m *Manager) watchConfig() {
|
||||
if event.Op&(fsnotify.Write|fsnotify.Create) == 0 {
|
||||
continue
|
||||
}
|
||||
newCfg := LoadConfig()
|
||||
// os.WriteFile truncates first; parsing that empty read would
|
||||
// briefly reset every setting to its default.
|
||||
newCfg, err := readConfig()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
m.applyConfigChange(newCfg)
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
@@ -1799,11 +1848,9 @@ func (m *Manager) applyConfigChange(newCfg Config) {
|
||||
m.config = newCfg
|
||||
m.configMutex.Unlock()
|
||||
|
||||
switch {
|
||||
case newCfg.Disabled && !oldCfg.Disabled:
|
||||
log.Info("Clipboard tracking disabled")
|
||||
case !newCfg.Disabled && oldCfg.Disabled:
|
||||
log.Info("Clipboard tracking enabled")
|
||||
if newCfg.Disabled != oldCfg.Disabled {
|
||||
log.Infof("Clipboard tracking disabled=%v", newCfg.Disabled)
|
||||
m.post(m.syncTracking)
|
||||
}
|
||||
|
||||
m.updateState()
|
||||
@@ -1814,7 +1861,7 @@ func (m *Manager) StoreData(data []byte, mimeType string) error {
|
||||
cfg := m.getConfig()
|
||||
|
||||
if cfg.Disabled {
|
||||
return fmt.Errorf("clipboard tracking disabled")
|
||||
return errTrackingDisabled
|
||||
}
|
||||
|
||||
if m.db == nil {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
||||
"github.com/duxlabs-dev/dux-go/ipc"
|
||||
wlclient "github.com/duxlabs-dev/dux-go/wayland/client"
|
||||
"github.com/duxlabs-dev/dux-go/wayland/ext_data_control"
|
||||
mocks_wlcontext "github.com/duxlabs-dev/dux-shell/core/internal/mocks/wlcontext"
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/server/models"
|
||||
@@ -51,13 +52,15 @@ func newTestManagerWithDB(t *testing.T) *Manager {
|
||||
mockCtx := mocks_wlcontext.NewMockWaylandContext(t)
|
||||
mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Return().Maybe()
|
||||
|
||||
return &Manager{
|
||||
m := &Manager{
|
||||
config: DefaultConfig(),
|
||||
db: db,
|
||||
wlCtx: mockCtx,
|
||||
dataControlMgr: &ext_data_control.ExtDataControlManagerV1{},
|
||||
dataDevice: &ext_data_control.ExtDataControlDeviceV1{},
|
||||
}
|
||||
m.deviceBound.Store(true)
|
||||
return m
|
||||
}
|
||||
|
||||
func TestEncodeDecodeEntry_Roundtrip(t *testing.T) {
|
||||
@@ -1058,3 +1061,30 @@ func TestCopyFile_DisabledTrackingRefusesCopy(t *testing.T) {
|
||||
require.ErrorIs(t, m.CopyFile(path), errSelectionUnavailable)
|
||||
assert.Empty(t, m.GetHistory())
|
||||
}
|
||||
|
||||
func TestApplyConfigChange_DisableReleasesDataDevice(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "wayland-test")
|
||||
ln, err := net.Listen("unix", sock)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
|
||||
display, err := wlclient.Connect(sock)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { display.Context().Close() })
|
||||
|
||||
m := newTestManagerWithDB(t)
|
||||
mockCtx := mocks_wlcontext.NewMockWaylandContext(t)
|
||||
mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) { fn() })
|
||||
m.wlCtx = mockCtx
|
||||
device := ext_data_control.NewExtDataControlDeviceV1(display.Context())
|
||||
m.dataDevice = device
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.Disabled = true
|
||||
m.applyConfigChange(cfg)
|
||||
|
||||
assert.True(t, device.IsZombie())
|
||||
assert.Nil(t, m.dataDevice)
|
||||
require.ErrorIs(t, m.CopyText("secret"), errSelectionUnavailable)
|
||||
assert.Empty(t, m.GetHistory())
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
@@ -44,22 +45,30 @@ func getConfigPath() (string, error) {
|
||||
}
|
||||
|
||||
func LoadConfig() Config {
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
return DefaultConfig()
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func readConfig() (Config, error) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
path, err := getConfigPath()
|
||||
if err != nil {
|
||||
return cfg
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return cfg
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return DefaultConfig()
|
||||
return DefaultConfig(), err
|
||||
}
|
||||
return cfg
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func SaveConfig(cfg Config) error {
|
||||
@@ -140,6 +149,7 @@ type Manager struct {
|
||||
pasteSupported bool
|
||||
|
||||
initialized bool
|
||||
deviceBound atomic.Bool
|
||||
|
||||
alive bool
|
||||
stopChan chan struct{}
|
||||
|
||||
@@ -279,7 +279,11 @@ func handleClipboardSetConfig(conn *ipc.ConnWriter, req ipc.Request) {
|
||||
cfg.MaxPinned = int(v)
|
||||
}
|
||||
|
||||
if err := clipboard.SaveConfig(cfg); err != nil {
|
||||
save := clipboard.SaveConfig
|
||||
if clipboardManager != nil {
|
||||
save = clipboardManager.SetConfig
|
||||
}
|
||||
if err := save(cfg); err != nil {
|
||||
conn.RespondError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1126,7 +1126,7 @@ func (s *Server) Serve(printDocs bool) error {
|
||||
log.Info(" clipboard.paste - Get current clipboard text")
|
||||
log.Info(" clipboard.search - Search history (params: query?, mimeType?, isImage?, limit?, offset?, before?, after?)")
|
||||
log.Info(" clipboard.getConfig - Get clipboard configuration")
|
||||
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)")
|
||||
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?, disabled?, maxPinned?)")
|
||||
log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)")
|
||||
log.Info("Notify:")
|
||||
log.Info(" notify.watchAction - Open a file when a notification action fires (params: id, path)")
|
||||
|
||||
Reference in new issue
Block a user