1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 03:28:28 -04:00

clipboard: implement virtual-keyboard-unstable-v1 to replace wtype for

pasting entries
This commit is contained in:
bbedward
2026-07-04 16:23:50 -04:00
parent 81a4d3b4e0
commit fd99558ce5
24 changed files with 1383 additions and 715 deletions
@@ -6,6 +6,7 @@ import (
"fmt"
"net"
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
@@ -28,6 +29,10 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
handleCopyEntry(conn, req, m)
case "clipboard.paste":
handlePaste(conn, req, m)
case "clipboard.sendPaste":
handleSendPaste(conn, req)
case "clipboard.pasteSupported":
models.Respond(conn, req.ID, map[string]bool{"supported": m.pasteSupported})
case "clipboard.subscribe":
handleSubscribe(conn, req, m)
case "clipboard.search":
@@ -176,6 +181,17 @@ func handlePaste(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, map[string]string{"text": text})
}
func handleSendPaste(conn net.Conn, req models.Request) {
shift, _ := models.Get[bool](req, "shift")
if err := clipboardstore.SendPasteKeystroke(shift); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paste sent"})
}
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
clientID := fmt.Sprintf("clipboard-%d", req.ID)
+31 -76
View File
@@ -31,6 +31,7 @@ import (
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
@@ -164,6 +165,8 @@ func (m *Manager) setupRegistry() error {
m.seat = seat
m.seatName = e.Name
log.Info("Bound wl_seat")
case virtual_keyboard.ZwpVirtualKeyboardManagerV1InterfaceName:
m.pasteSupported = true
}
})
@@ -1054,6 +1057,13 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
dataCopy := make([]byte, len(data))
copy(dataCopy, data)
m.takeSelection(clipboardstore.ExpandOffers(dataCopy, mimeType))
return nil
}
// takeSelection makes the daemon the selection owner, serving the given
// offers until another client claims the clipboard.
func (m *Manager) takeSelection(offers []clipboardstore.Offer) {
m.post(func() {
if m.dataControlMgr == nil || m.dataDevice == nil {
log.Error("Data control manager or device not initialized")
@@ -1068,9 +1078,13 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
return
}
if err := source.Offer(mimeType); err != nil {
log.Errorf("Failed to offer mime type: %v", err)
return
offerData := make(map[string][]byte, len(offers))
for _, offer := range offers {
if err := source.Offer(offer.MimeType); err != nil {
log.Errorf("Failed to offer %s: %v", offer.MimeType, err)
return
}
offerData[offer.MimeType] = offer.Data
}
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
@@ -1080,7 +1094,11 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
file := os.NewFile(uintptr(fd), "clipboard-pipe")
defer file.Close()
if _, err := file.Write(dataCopy); err != nil {
data, ok := offerData[e.MimeType]
if !ok {
return
}
if _, err := file.Write(data); err != nil {
log.Errorf("Failed to write clipboard data: %v", err)
}
})
@@ -1093,9 +1111,6 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
m.releaseCurrentSource()
m.currentSource = source
m.sourceMutex.Lock()
m.sourceMimeTypes = []string{mimeType}
m.sourceMutex.Unlock()
m.ownerLock.Lock()
m.isOwner = true
@@ -1106,8 +1121,6 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
log.Errorf("Failed to set selection: %v", err)
}
})
return nil
}
func (m *Manager) CopyText(text string) error {
@@ -1779,74 +1792,16 @@ func (m *Manager) CopyFile(filePath string) error {
m.updateState()
m.notifySubscribers()
_, imgMime, imgErr := image.DecodeConfig(bytes.NewReader(fileData))
m.post(func() {
if m.dataControlMgr == nil || m.dataDevice == nil {
log.Error("Data control manager or device not initialized")
return
}
dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
source, err := dataMgr.CreateDataSource()
if err != nil {
log.Errorf("Failed to create data source: %v", err)
return
}
type offer struct {
mime string
data []byte
}
offers := []offer{
{"x-special/gnome-copied-files", []byte("copy\n" + fileURI)},
{"text/uri-list", []byte(fileURI + "\r\n")},
{"text/plain", []byte(filePath)},
}
if imgErr == nil {
imgMimeType := "image/" + imgMime
offers = append(offers, offer{imgMimeType, fileData})
}
offerData := make(map[string][]byte)
for _, o := range offers {
if err := source.Offer(o.mime); err != nil {
log.Errorf("Failed to offer %s: %v", o.mime, err)
return
}
offerData[o.mime] = o.data
}
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
fd := e.Fd
defer syscall.Close(fd)
file := os.NewFile(uintptr(fd), "clipboard-pipe")
defer file.Close()
if data, ok := offerData[e.MimeType]; ok {
file.Write(data)
}
})
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
m.ownerLock.Lock()
m.isOwner = false
m.ownerLock.Unlock()
})
m.releaseCurrentSource()
m.currentSource = source
m.ownerLock.Lock()
m.isOwner = true
m.ownerLock.Unlock()
device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
if err := device.SetSelection(source); err != nil {
log.Errorf("Failed to set selection: %v", err)
}
})
offers := []clipboardstore.Offer{
{MimeType: "x-special/gnome-copied-files", Data: []byte("copy\n" + fileURI)},
{MimeType: "text/uri-list", Data: []byte(fileURI + "\r\n")},
{MimeType: "text/plain", Data: []byte(filePath)},
}
if _, imgMime, err := image.DecodeConfig(bytes.NewReader(fileData)); err == nil {
offers = append(offers, clipboardstore.Offer{MimeType: "image/" + imgMime, Data: fileData})
}
m.takeSelection(offers)
return nil
}
@@ -531,47 +531,6 @@ func TestManager_ConcurrentOfferAccess(t *testing.T) {
wg.Wait()
}
func TestManager_ConcurrentPersistAccess(t *testing.T) {
m := &Manager{
persistData: make(map[string][]byte),
persistMimeTypes: []string{},
}
var wg sync.WaitGroup
const goroutines = 20
const iterations = 50
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.persistMutex.RLock()
_ = m.persistData
_ = m.persistMimeTypes
m.persistMutex.RUnlock()
}
}()
}
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.persistMutex.Lock()
m.persistMimeTypes = []string{"text/plain", "text/html"}
m.persistData = map[string][]byte{
"text/plain": []byte("test"),
}
m.persistMutex.Unlock()
}
}(i)
}
wg.Wait()
}
func TestManager_ConcurrentOwnerAccess(t *testing.T) {
m := &Manager{}
+3 -9
View File
@@ -132,15 +132,9 @@ type Manager struct {
offerMutex sync.RWMutex
offerRegistry map[uint32]any
sourceMimeTypes []string
sourceMutex sync.RWMutex
persistData map[string][]byte
persistMimeTypes []string
persistMutex sync.RWMutex
isOwner bool
ownerLock sync.Mutex
isOwner bool
ownerLock sync.Mutex
pasteSupported bool
initialized bool