mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 13:32:50 -05:00
clipboard: introduce native clipboard, clip-persist, clip-storage functionality
This commit is contained in:
332
core/internal/clipboard/clipboard.go
Normal file
332
core/internal/clipboard/clipboard.go
Normal file
@@ -0,0 +1,332 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
func Copy(data []byte, mimeType string) error {
|
||||
return CopyOpts(data, mimeType, false, false)
|
||||
}
|
||||
|
||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||
if !foreground {
|
||||
return copyFork(data, mimeType, pasteOnce)
|
||||
}
|
||||
return copyServe(data, mimeType, pasteOnce)
|
||||
}
|
||||
|
||||
func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
||||
if pasteOnce {
|
||||
args = append(args, "--paste-once")
|
||||
}
|
||||
args = append(args, "--type", mimeType)
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdin = nil
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdin pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
if _, err := stdin.Write(data); err != nil {
|
||||
stdin.Close()
|
||||
return fmt.Errorf("write stdin: %w", err)
|
||||
}
|
||||
stdin.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer display.Destroy()
|
||||
|
||||
ctx := display.Context()
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get registry: %w", err)
|
||||
}
|
||||
defer registry.Destroy()
|
||||
|
||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||
var seat *wlclient.Seat
|
||||
var bindErr error
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case "ext_data_control_manager_v1":
|
||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
case "wl_seat":
|
||||
if seat != nil {
|
||||
return
|
||||
}
|
||||
seat = wlclient.NewSeat(ctx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
|
||||
device, err := dataControlMgr.GetDataDevice(seat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get data device: %w", err)
|
||||
}
|
||||
defer device.Destroy()
|
||||
|
||||
source, err := dataControlMgr.CreateDataSource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create data source: %w", err)
|
||||
}
|
||||
|
||||
if err := source.Offer(mimeType); err != nil {
|
||||
return fmt.Errorf("offer mime type: %w", err)
|
||||
}
|
||||
if mimeType == "text/plain;charset=utf-8" || mimeType == "text/plain" {
|
||||
if err := source.Offer("text/plain"); err != nil {
|
||||
return fmt.Errorf("offer text/plain: %w", err)
|
||||
}
|
||||
if err := source.Offer("text/plain;charset=utf-8"); err != nil {
|
||||
return fmt.Errorf("offer text/plain;charset=utf-8: %w", err)
|
||||
}
|
||||
if err := source.Offer("UTF8_STRING"); err != nil {
|
||||
return fmt.Errorf("offer UTF8_STRING: %w", err)
|
||||
}
|
||||
if err := source.Offer("STRING"); err != nil {
|
||||
return fmt.Errorf("offer STRING: %w", err)
|
||||
}
|
||||
if err := source.Offer("TEXT"); err != nil {
|
||||
return fmt.Errorf("offer TEXT: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
cancelled := make(chan struct{})
|
||||
pasted := make(chan struct{}, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
defer syscall.Close(e.Fd)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
file.Write(data)
|
||||
select {
|
||||
case pasted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
|
||||
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
||||
close(cancelled)
|
||||
})
|
||||
|
||||
if err := device.SetSelection(source); err != nil {
|
||||
return fmt.Errorf("set selection: %w", err)
|
||||
}
|
||||
|
||||
display.Roundtrip()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-cancelled:
|
||||
return nil
|
||||
case <-pasted:
|
||||
if pasteOnce {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
if err := ctx.Dispatch(); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CopyText(text string) error {
|
||||
return Copy([]byte(text), "text/plain;charset=utf-8")
|
||||
}
|
||||
|
||||
func Paste() ([]byte, string, error) {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer display.Destroy()
|
||||
|
||||
ctx := display.Context()
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("get registry: %w", err)
|
||||
}
|
||||
defer registry.Destroy()
|
||||
|
||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||
var seat *wlclient.Seat
|
||||
var bindErr error
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case "ext_data_control_manager_v1":
|
||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
case "wl_seat":
|
||||
if seat != nil {
|
||||
return
|
||||
}
|
||||
seat = wlclient.NewSeat(ctx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
if bindErr != nil {
|
||||
return nil, "", fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
|
||||
if dataControlMgr == nil {
|
||||
return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
|
||||
if seat == nil {
|
||||
return nil, "", fmt.Errorf("no seat available")
|
||||
}
|
||||
|
||||
device, err := dataControlMgr.GetDataDevice(seat)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("get data device: %w", err)
|
||||
}
|
||||
defer device.Destroy()
|
||||
|
||||
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
|
||||
|
||||
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
||||
if e.Id == nil {
|
||||
return
|
||||
}
|
||||
offerMimeTypes[e.Id] = nil
|
||||
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
|
||||
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
|
||||
})
|
||||
})
|
||||
|
||||
var selectionOffer *ext_data_control.ExtDataControlOfferV1
|
||||
gotSelection := false
|
||||
|
||||
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
||||
selectionOffer = e.Id
|
||||
gotSelection = true
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
if !gotSelection || selectionOffer == nil {
|
||||
return nil, "", fmt.Errorf("no clipboard data")
|
||||
}
|
||||
|
||||
mimeTypes := offerMimeTypes[selectionOffer]
|
||||
selectedMime := selectPreferredMimeType(mimeTypes)
|
||||
if selectedMime == "" {
|
||||
return nil, "", fmt.Errorf("no supported mime type")
|
||||
}
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("create pipe: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
if err := selectionOffer.Receive(selectedMime, int(w.Fd())); err != nil {
|
||||
w.Close()
|
||||
return nil, "", fmt.Errorf("receive: %w", err)
|
||||
}
|
||||
w.Close()
|
||||
|
||||
display.Roundtrip()
|
||||
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("read: %w", err)
|
||||
}
|
||||
|
||||
return data, selectedMime, nil
|
||||
}
|
||||
|
||||
func PasteText() (string, error) {
|
||||
data, _, err := Paste()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func selectPreferredMimeType(mimes []string) string {
|
||||
preferred := []string{
|
||||
"text/plain;charset=utf-8",
|
||||
"text/plain",
|
||||
"UTF8_STRING",
|
||||
"STRING",
|
||||
"TEXT",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
}
|
||||
|
||||
for _, pref := range preferred {
|
||||
for _, mime := range mimes {
|
||||
if mime == pref {
|
||||
return mime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(mimes) > 0 {
|
||||
return mimes[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func IsImageMimeType(mime string) bool {
|
||||
return len(mime) > 6 && mime[:6] == "image/"
|
||||
}
|
||||
253
core/internal/clipboard/store.go
Normal file
253
core/internal/clipboard/store.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type StoreConfig struct {
|
||||
MaxHistory int
|
||||
MaxEntrySize int64
|
||||
}
|
||||
|
||||
func DefaultStoreConfig() StoreConfig {
|
||||
return StoreConfig{
|
||||
MaxHistory: 100,
|
||||
MaxEntrySize: 5 * 1024 * 1024,
|
||||
}
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
ID uint64
|
||||
Data []byte
|
||||
MimeType string
|
||||
Preview string
|
||||
Size int
|
||||
Timestamp time.Time
|
||||
IsImage bool
|
||||
}
|
||||
|
||||
func Store(data []byte, mimeType string) error {
|
||||
return StoreWithConfig(data, mimeType, DefaultStoreConfig())
|
||||
}
|
||||
|
||||
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if int64(len(data)) > cfg.MaxEntrySize {
|
||||
return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize)
|
||||
}
|
||||
|
||||
dbPath, err := getDBPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get db path: %w", err)
|
||||
}
|
||||
|
||||
db, err := bolt.Open(dbPath, 0644, &bolt.Options{Timeout: 1 * time.Second})
|
||||
if err != nil {
|
||||
return fmt.Errorf("open db: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
entry := Entry{
|
||||
Data: data,
|
||||
MimeType: mimeType,
|
||||
Size: len(data),
|
||||
Timestamp: time.Now(),
|
||||
IsImage: IsImageMimeType(mimeType),
|
||||
}
|
||||
|
||||
switch {
|
||||
case entry.IsImage:
|
||||
entry.Preview = imagePreview(data, mimeType)
|
||||
default:
|
||||
entry.Preview = textPreview(data)
|
||||
}
|
||||
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("clipboard"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deduplicateInTx(b, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry.ID = id
|
||||
|
||||
encoded, err := encodeEntry(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.Put(itob(id), encoded); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return trimLengthInTx(b, cfg.MaxHistory)
|
||||
})
|
||||
}
|
||||
|
||||
func getDBPath() (string, error) {
|
||||
cacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||
if cacheDir == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cacheDir = filepath.Join(homeDir, ".cache")
|
||||
}
|
||||
|
||||
dbDir := filepath.Join(cacheDir, "dms-clipboard")
|
||||
if err := os.MkdirAll(dbDir, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(dbDir, "db"), nil
|
||||
}
|
||||
|
||||
func deduplicateInTx(b *bolt.Bucket, data []byte) error {
|
||||
c := b.Cursor()
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(entry.Data, data) {
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func trimLengthInTx(b *bolt.Bucket, maxHistory int) error {
|
||||
c := b.Cursor()
|
||||
var count int
|
||||
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
||||
if count < maxHistory {
|
||||
count++
|
||||
continue
|
||||
}
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeEntry(e Entry) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
binary.Write(buf, binary.BigEndian, e.ID)
|
||||
binary.Write(buf, binary.BigEndian, uint32(len(e.Data)))
|
||||
buf.Write(e.Data)
|
||||
binary.Write(buf, binary.BigEndian, uint32(len(e.MimeType)))
|
||||
buf.WriteString(e.MimeType)
|
||||
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())
|
||||
if e.IsImage {
|
||||
buf.WriteByte(1)
|
||||
} else {
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func decodeEntry(data []byte) (Entry, error) {
|
||||
buf := bytes.NewReader(data)
|
||||
var e Entry
|
||||
|
||||
binary.Read(buf, binary.BigEndian, &e.ID)
|
||||
|
||||
var dataLen uint32
|
||||
binary.Read(buf, binary.BigEndian, &dataLen)
|
||||
e.Data = make([]byte, dataLen)
|
||||
buf.Read(e.Data)
|
||||
|
||||
var mimeLen uint32
|
||||
binary.Read(buf, binary.BigEndian, &mimeLen)
|
||||
mimeBytes := make([]byte, mimeLen)
|
||||
buf.Read(mimeBytes)
|
||||
e.MimeType = string(mimeBytes)
|
||||
|
||||
var prevLen uint32
|
||||
binary.Read(buf, binary.BigEndian, &prevLen)
|
||||
prevBytes := make([]byte, prevLen)
|
||||
buf.Read(prevBytes)
|
||||
e.Preview = string(prevBytes)
|
||||
|
||||
var size int32
|
||||
binary.Read(buf, binary.BigEndian, &size)
|
||||
e.Size = int(size)
|
||||
|
||||
var timestamp int64
|
||||
binary.Read(buf, binary.BigEndian, ×tamp)
|
||||
e.Timestamp = time.Unix(timestamp, 0)
|
||||
|
||||
var isImage byte
|
||||
binary.Read(buf, binary.BigEndian, &isImage)
|
||||
e.IsImage = isImage == 1
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func itob(v uint64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, v)
|
||||
return b
|
||||
}
|
||||
|
||||
func textPreview(data []byte) string {
|
||||
text := string(data)
|
||||
text = strings.TrimSpace(text)
|
||||
text = strings.Join(strings.Fields(text), " ")
|
||||
|
||||
if len(text) > 100 {
|
||||
return text[:100] + "…"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func imagePreview(data []byte, format string) string {
|
||||
config, imgFmt, err := image.DecodeConfig(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[[ image %s %s ]]", sizeStr(len(data)), format)
|
||||
}
|
||||
return fmt.Sprintf("[[ image %s %s %dx%d ]]", sizeStr(len(data)), imgFmt, config.Width, config.Height)
|
||||
}
|
||||
|
||||
func sizeStr(size int) string {
|
||||
units := []string{"B", "KiB", "MiB"}
|
||||
var i int
|
||||
fsize := float64(size)
|
||||
for fsize >= 1024 && i < len(units)-1 {
|
||||
fsize /= 1024
|
||||
i++
|
||||
}
|
||||
return fmt.Sprintf("%.0f %s", fsize, units[i])
|
||||
}
|
||||
160
core/internal/clipboard/watch.go
Normal file
160
core/internal/clipboard/watch.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
type ClipboardChange struct {
|
||||
Data []byte
|
||||
MimeType string
|
||||
}
|
||||
|
||||
func Watch(ctx context.Context, callback func(data []byte, mimeType string)) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer display.Destroy()
|
||||
|
||||
wlCtx := display.Context()
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get registry: %w", err)
|
||||
}
|
||||
defer registry.Destroy()
|
||||
|
||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||
var seat *wlclient.Seat
|
||||
var bindErr error
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case "ext_data_control_manager_v1":
|
||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(wlCtx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
case "wl_seat":
|
||||
if seat != nil {
|
||||
return
|
||||
}
|
||||
seat = wlclient.NewSeat(wlCtx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
|
||||
device, err := dataControlMgr.GetDataDevice(seat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get data device: %w", err)
|
||||
}
|
||||
defer device.Destroy()
|
||||
|
||||
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
|
||||
|
||||
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
||||
if e.Id == nil {
|
||||
return
|
||||
}
|
||||
offerMimeTypes[e.Id] = nil
|
||||
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
|
||||
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
|
||||
})
|
||||
})
|
||||
|
||||
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
||||
if e.Id == nil {
|
||||
return
|
||||
}
|
||||
|
||||
mimes := offerMimeTypes[e.Id]
|
||||
selectedMime := selectPreferredMimeType(mimes)
|
||||
if selectedMime == "" {
|
||||
return
|
||||
}
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := e.Id.Receive(selectedMime, int(w.Fd())); err != nil {
|
||||
w.Close()
|
||||
r.Close()
|
||||
return
|
||||
}
|
||||
w.Close()
|
||||
|
||||
go func() {
|
||||
defer r.Close()
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil || len(data) == 0 {
|
||||
return
|
||||
}
|
||||
callback(data, selectedMime)
|
||||
}()
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||
return fmt.Errorf("set read deadline: %w", err)
|
||||
}
|
||||
if err := wlCtx.Dispatch(); err != nil && err != os.ErrDeadlineExceeded {
|
||||
return fmt.Errorf("dispatch: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WatchChan(ctx context.Context) (<-chan ClipboardChange, <-chan error) {
|
||||
ch := make(chan ClipboardChange, 16)
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
err := Watch(ctx, func(data []byte, mimeType string) {
|
||||
select {
|
||||
case ch <- ClipboardChange{Data: data, MimeType: mimeType}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
if err != nil && err != context.Canceled {
|
||||
errCh <- err
|
||||
}
|
||||
close(errCh)
|
||||
}()
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return ch, errCh
|
||||
}
|
||||
@@ -615,10 +615,11 @@ func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalComma
|
||||
|
||||
spawnDms := `spawn-at-startup "dms" "run"`
|
||||
if !strings.Contains(config, spawnDms) {
|
||||
config = strings.Replace(config,
|
||||
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`,
|
||||
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`+"\n"+spawnDms,
|
||||
1)
|
||||
// Insert spawn-at-startup for dms after the environment block
|
||||
envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
|
||||
if loc := envBlockEnd.FindStringIndex(config); loc != nil {
|
||||
config = config[:loc[1]] + "\n" + spawnDms + config[loc[1]:]
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
@@ -12,7 +12,6 @@ monitor = , preferred,auto,auto
|
||||
# ==================
|
||||
exec-once = dbus-update-activation-environment --systemd --all
|
||||
exec-once = systemctl --user start hyprland-session.target
|
||||
exec-once = bash -c "wl-paste --watch cliphist store &"
|
||||
|
||||
# ==================
|
||||
# INPUT CONFIG
|
||||
|
||||
@@ -109,7 +109,6 @@ overview {
|
||||
// which may be more convenient to use.
|
||||
// See the binds section below for more spawn examples.
|
||||
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
|
||||
environment {
|
||||
XDG_CURRENT_DESKTOP "niri"
|
||||
}
|
||||
|
||||
@@ -139,7 +139,6 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
|
||||
@@ -188,23 +188,12 @@ func (b *BaseDistribution) detectSpecificTerminal(terminal deps.Terminal) deps.D
|
||||
func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
cliphist := deps.StatusMissing
|
||||
if b.commandExists("cliphist") {
|
||||
cliphist = deps.StatusInstalled
|
||||
}
|
||||
|
||||
wlClipboard := deps.StatusMissing
|
||||
if b.commandExists("wl-copy") && b.commandExists("wl-paste") {
|
||||
wlClipboard = deps.StatusInstalled
|
||||
}
|
||||
|
||||
dependencies = append(dependencies,
|
||||
deps.Dependency{
|
||||
Name: "cliphist",
|
||||
Status: cliphist,
|
||||
Description: "Wayland clipboard manager",
|
||||
Required: true,
|
||||
},
|
||||
deps.Dependency{
|
||||
Name: "wl-clipboard",
|
||||
Status: wlClipboard,
|
||||
|
||||
@@ -111,7 +111,6 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
}
|
||||
|
||||
@@ -549,7 +548,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
|
||||
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Rust: %w", err)
|
||||
}
|
||||
case "cliphist", "dgop":
|
||||
case "dgop":
|
||||
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Go: %w", err)
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
// COPR packages
|
||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
}
|
||||
|
||||
@@ -151,7 +151,6 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
|
||||
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
|
||||
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||
"cliphist": {Name: "app-misc/cliphist", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||
}
|
||||
|
||||
@@ -86,10 +86,6 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
|
||||
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install matugen: %w", err)
|
||||
}
|
||||
case "cliphist":
|
||||
if err := m.installCliphist(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install cliphist: %w", err)
|
||||
}
|
||||
case "xwayland-satellite":
|
||||
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
||||
@@ -803,52 +799,6 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installCliphist(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing cliphist from source...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Installing cliphist via go install...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "go install go.senan.xyz/cliphist@latest",
|
||||
}
|
||||
|
||||
installCmd := exec.CommandContext(ctx, "go", "install", "go.senan.xyz/cliphist@latest")
|
||||
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building cliphist..."); err != nil {
|
||||
return fmt.Errorf("failed to install cliphist: %w", err)
|
||||
}
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
sourcePath := filepath.Join(homeDir, "go", "bin", "cliphist")
|
||||
targetPath := "/usr/local/bin/cliphist"
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.7,
|
||||
Step: "Installing cliphist binary to system...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||
}
|
||||
|
||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := copyCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to copy cliphist to /usr/local/bin: %w", err)
|
||||
}
|
||||
|
||||
// Make it executable
|
||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to make cliphist executable: %w", err)
|
||||
}
|
||||
|
||||
m.log("cliphist installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing xwayland-satellite from source...")
|
||||
|
||||
|
||||
@@ -110,7 +110,6 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||
|
||||
// DMS packages from OBS
|
||||
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
|
||||
@@ -121,7 +121,6 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
}
|
||||
|
||||
@@ -539,8 +538,6 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
||||
buildDeps["libpam0g-dev"] = true
|
||||
case "matugen":
|
||||
buildDeps["curl"] = true
|
||||
case "cliphist":
|
||||
// Go will be installed separately with PPA
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,7 +547,7 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
||||
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Rust: %w", err)
|
||||
}
|
||||
case "cliphist", "dgop":
|
||||
case "dgop":
|
||||
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Go: %w", err)
|
||||
}
|
||||
|
||||
387
core/internal/proto/ext_data_control/data_control.go
Normal file
387
core/internal/proto/ext_data_control/data_control.go
Normal file
@@ -0,0 +1,387 @@
|
||||
package ext_data_control
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const ExtDataControlManagerV1InterfaceName = "ext_data_control_manager_v1"
|
||||
|
||||
type ExtDataControlManagerV1 struct {
|
||||
client.BaseProxy
|
||||
}
|
||||
|
||||
func NewExtDataControlManagerV1(ctx *client.Context) *ExtDataControlManagerV1 {
|
||||
m := &ExtDataControlManagerV1{}
|
||||
ctx.Register(m)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *ExtDataControlManagerV1) CreateDataSource() (*ExtDataControlSourceV1, error) {
|
||||
id := NewExtDataControlSourceV1(m.Context())
|
||||
const opcode = 0
|
||||
const reqBufLen = 8 + 4
|
||||
var reqBuf [reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(reqBuf[l:4], m.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], uint32(reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], id.ID())
|
||||
l += 4
|
||||
err := m.Context().WriteMsg(reqBuf[:], nil)
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (m *ExtDataControlManagerV1) GetDataDevice(seat *client.Seat) (*ExtDataControlDeviceV1, error) {
|
||||
id := NewExtDataControlDeviceV1(m.Context())
|
||||
const opcode = 1
|
||||
const reqBufLen = 8 + 4 + 4
|
||||
var reqBuf [reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(reqBuf[l:4], m.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], uint32(reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], id.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], seat.ID())
|
||||
l += 4
|
||||
err := m.Context().WriteMsg(reqBuf[:], nil)
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (m *ExtDataControlManagerV1) GetDataDeviceWithProxy(device *ExtDataControlDeviceV1, seat *client.Seat) error {
|
||||
const opcode = 1
|
||||
const reqBufLen = 8 + 4 + 4
|
||||
var reqBuf [reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(reqBuf[l:4], m.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], uint32(reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], device.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], seat.ID())
|
||||
l += 4
|
||||
return m.Context().WriteMsg(reqBuf[:], nil)
|
||||
}
|
||||
|
||||
func (m *ExtDataControlManagerV1) Destroy() error {
|
||||
defer m.MarkZombie()
|
||||
const opcode = 2
|
||||
const reqBufLen = 8
|
||||
var reqBuf [reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(reqBuf[l:4], m.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], uint32(reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
return m.Context().WriteMsg(reqBuf[:], nil)
|
||||
}
|
||||
|
||||
const ExtDataControlDeviceV1InterfaceName = "ext_data_control_device_v1"
|
||||
|
||||
type ExtDataControlDeviceV1 struct {
|
||||
client.BaseProxy
|
||||
dataOfferHandler ExtDataControlDeviceV1DataOfferHandlerFunc
|
||||
selectionHandler ExtDataControlDeviceV1SelectionHandlerFunc
|
||||
finishedHandler ExtDataControlDeviceV1FinishedHandlerFunc
|
||||
primarySelectionHandler ExtDataControlDeviceV1PrimarySelectionHandlerFunc
|
||||
}
|
||||
|
||||
func NewExtDataControlDeviceV1(ctx *client.Context) *ExtDataControlDeviceV1 {
|
||||
d := &ExtDataControlDeviceV1{}
|
||||
ctx.Register(d)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *ExtDataControlDeviceV1) SetSelection(source *ExtDataControlSourceV1) error {
|
||||
const opcode = 0
|
||||
const reqBufLen = 8 + 4
|
||||
var reqBuf [reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(reqBuf[l:4], d.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], uint32(reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
if source == nil {
|
||||
client.PutUint32(reqBuf[l:l+4], 0)
|
||||
} else {
|
||||
client.PutUint32(reqBuf[l:l+4], source.ID())
|
||||
}
|
||||
l += 4
|
||||
return d.Context().WriteMsg(reqBuf[:], nil)
|
||||
}
|
||||
|
||||
func (d *ExtDataControlDeviceV1) Destroy() error {
|
||||
defer d.MarkZombie()
|
||||
const opcode = 1
|
||||
const reqBufLen = 8
|
||||
var reqBuf [reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(reqBuf[l:4], d.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], uint32(reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
return d.Context().WriteMsg(reqBuf[:], nil)
|
||||
}
|
||||
|
||||
func (d *ExtDataControlDeviceV1) SetPrimarySelection(source *ExtDataControlSourceV1) error {
|
||||
const opcode = 2
|
||||
const reqBufLen = 8 + 4
|
||||
var reqBuf [reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(reqBuf[l:4], d.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], uint32(reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
if source == nil {
|
||||
client.PutUint32(reqBuf[l:l+4], 0)
|
||||
} else {
|
||||
client.PutUint32(reqBuf[l:l+4], source.ID())
|
||||
}
|
||||
l += 4
|
||||
return d.Context().WriteMsg(reqBuf[:], nil)
|
||||
}
|
||||
|
||||
type ExtDataControlDeviceV1DataOfferEvent struct {
|
||||
Id *ExtDataControlOfferV1
|
||||
}
|
||||
type ExtDataControlDeviceV1DataOfferHandlerFunc func(ExtDataControlDeviceV1DataOfferEvent)
|
||||
|
||||
func (d *ExtDataControlDeviceV1) SetDataOfferHandler(f ExtDataControlDeviceV1DataOfferHandlerFunc) {
|
||||
d.dataOfferHandler = f
|
||||
}
|
||||
|
||||
type ExtDataControlDeviceV1SelectionEvent struct {
|
||||
Id *ExtDataControlOfferV1
|
||||
OfferId uint32
|
||||
}
|
||||
type ExtDataControlDeviceV1SelectionHandlerFunc func(ExtDataControlDeviceV1SelectionEvent)
|
||||
|
||||
func (d *ExtDataControlDeviceV1) SetSelectionHandler(f ExtDataControlDeviceV1SelectionHandlerFunc) {
|
||||
d.selectionHandler = f
|
||||
}
|
||||
|
||||
type ExtDataControlDeviceV1FinishedEvent struct{}
|
||||
type ExtDataControlDeviceV1FinishedHandlerFunc func(ExtDataControlDeviceV1FinishedEvent)
|
||||
|
||||
func (d *ExtDataControlDeviceV1) SetFinishedHandler(f ExtDataControlDeviceV1FinishedHandlerFunc) {
|
||||
d.finishedHandler = f
|
||||
}
|
||||
|
||||
type ExtDataControlDeviceV1PrimarySelectionEvent struct {
|
||||
Id *ExtDataControlOfferV1
|
||||
OfferId uint32
|
||||
}
|
||||
type ExtDataControlDeviceV1PrimarySelectionHandlerFunc func(ExtDataControlDeviceV1PrimarySelectionEvent)
|
||||
|
||||
func (d *ExtDataControlDeviceV1) SetPrimarySelectionHandler(f ExtDataControlDeviceV1PrimarySelectionHandlerFunc) {
|
||||
d.primarySelectionHandler = f
|
||||
}
|
||||
|
||||
func (d *ExtDataControlDeviceV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
switch opcode {
|
||||
case 0:
|
||||
if d.dataOfferHandler == nil {
|
||||
return
|
||||
}
|
||||
l := 0
|
||||
newID := client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
ctx := d.Context()
|
||||
offer := &ExtDataControlOfferV1{}
|
||||
offer.SetContext(ctx)
|
||||
offer.SetID(newID)
|
||||
ctx.RegisterWithID(offer, newID)
|
||||
|
||||
d.dataOfferHandler(ExtDataControlDeviceV1DataOfferEvent{Id: offer})
|
||||
case 1:
|
||||
if d.selectionHandler == nil {
|
||||
return
|
||||
}
|
||||
l := 0
|
||||
objID := client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
var offer *ExtDataControlOfferV1
|
||||
if objID != 0 {
|
||||
if p := d.Context().GetProxy(objID); p != nil {
|
||||
offer = p.(*ExtDataControlOfferV1)
|
||||
}
|
||||
}
|
||||
d.selectionHandler(ExtDataControlDeviceV1SelectionEvent{Id: offer, OfferId: objID})
|
||||
case 2:
|
||||
if d.finishedHandler == nil {
|
||||
return
|
||||
}
|
||||
d.finishedHandler(ExtDataControlDeviceV1FinishedEvent{})
|
||||
case 3:
|
||||
if d.primarySelectionHandler == nil {
|
||||
return
|
||||
}
|
||||
l := 0
|
||||
objID := client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
var offer *ExtDataControlOfferV1
|
||||
if objID != 0 {
|
||||
if p := d.Context().GetProxy(objID); p != nil {
|
||||
offer = p.(*ExtDataControlOfferV1)
|
||||
}
|
||||
}
|
||||
d.primarySelectionHandler(ExtDataControlDeviceV1PrimarySelectionEvent{Id: offer, OfferId: objID})
|
||||
}
|
||||
}
|
||||
|
||||
const ExtDataControlSourceV1InterfaceName = "ext_data_control_source_v1"
|
||||
|
||||
type ExtDataControlSourceV1 struct {
|
||||
client.BaseProxy
|
||||
sendHandler ExtDataControlSourceV1SendHandlerFunc
|
||||
cancelledHandler ExtDataControlSourceV1CancelledHandlerFunc
|
||||
}
|
||||
|
||||
func NewExtDataControlSourceV1(ctx *client.Context) *ExtDataControlSourceV1 {
|
||||
s := &ExtDataControlSourceV1{}
|
||||
ctx.Register(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ExtDataControlSourceV1) Offer(mimeType string) error {
|
||||
const opcode = 0
|
||||
mimeTypeLen := client.PaddedLen(len(mimeType) + 1)
|
||||
reqBufLen := 8 + (4 + mimeTypeLen)
|
||||
reqBuf := make([]byte, reqBufLen)
|
||||
l := 0
|
||||
client.PutUint32(reqBuf[l:4], s.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], uint32(reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutString(reqBuf[l:l+(4+mimeTypeLen)], mimeType)
|
||||
l += (4 + mimeTypeLen)
|
||||
return s.Context().WriteMsg(reqBuf, nil)
|
||||
}
|
||||
|
||||
func (s *ExtDataControlSourceV1) Destroy() error {
|
||||
defer s.MarkZombie()
|
||||
const opcode = 1
|
||||
const reqBufLen = 8
|
||||
var reqBuf [reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(reqBuf[l:4], s.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], uint32(reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
return s.Context().WriteMsg(reqBuf[:], nil)
|
||||
}
|
||||
|
||||
type ExtDataControlSourceV1SendEvent struct {
|
||||
MimeType string
|
||||
Fd int
|
||||
}
|
||||
type ExtDataControlSourceV1SendHandlerFunc func(ExtDataControlSourceV1SendEvent)
|
||||
|
||||
func (s *ExtDataControlSourceV1) SetSendHandler(f ExtDataControlSourceV1SendHandlerFunc) {
|
||||
s.sendHandler = f
|
||||
}
|
||||
|
||||
type ExtDataControlSourceV1CancelledEvent struct{}
|
||||
type ExtDataControlSourceV1CancelledHandlerFunc func(ExtDataControlSourceV1CancelledEvent)
|
||||
|
||||
func (s *ExtDataControlSourceV1) SetCancelledHandler(f ExtDataControlSourceV1CancelledHandlerFunc) {
|
||||
s.cancelledHandler = f
|
||||
}
|
||||
|
||||
func (s *ExtDataControlSourceV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
switch opcode {
|
||||
case 0:
|
||||
if s.sendHandler == nil {
|
||||
if fd != -1 {
|
||||
unix.Close(fd)
|
||||
}
|
||||
return
|
||||
}
|
||||
l := 0
|
||||
mimeTypeLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
|
||||
l += 4
|
||||
mimeType := client.String(data[l : l+mimeTypeLen])
|
||||
l += mimeTypeLen
|
||||
|
||||
s.sendHandler(ExtDataControlSourceV1SendEvent{MimeType: mimeType, Fd: fd})
|
||||
case 1:
|
||||
if s.cancelledHandler == nil {
|
||||
return
|
||||
}
|
||||
s.cancelledHandler(ExtDataControlSourceV1CancelledEvent{})
|
||||
}
|
||||
}
|
||||
|
||||
const ExtDataControlOfferV1InterfaceName = "ext_data_control_offer_v1"
|
||||
|
||||
type ExtDataControlOfferV1 struct {
|
||||
client.BaseProxy
|
||||
offerHandler ExtDataControlOfferV1OfferHandlerFunc
|
||||
}
|
||||
|
||||
func NewExtDataControlOfferV1(ctx *client.Context) *ExtDataControlOfferV1 {
|
||||
o := &ExtDataControlOfferV1{}
|
||||
ctx.Register(o)
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *ExtDataControlOfferV1) Receive(mimeType string, fd int) error {
|
||||
const opcode = 0
|
||||
mimeTypeLen := client.PaddedLen(len(mimeType) + 1)
|
||||
reqBufLen := 8 + (4 + mimeTypeLen)
|
||||
reqBuf := make([]byte, reqBufLen)
|
||||
l := 0
|
||||
client.PutUint32(reqBuf[l:4], o.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], uint32(reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutString(reqBuf[l:l+(4+mimeTypeLen)], mimeType)
|
||||
l += (4 + mimeTypeLen)
|
||||
oob := unix.UnixRights(fd)
|
||||
return o.Context().WriteMsg(reqBuf, oob)
|
||||
}
|
||||
|
||||
func (o *ExtDataControlOfferV1) Destroy() error {
|
||||
defer o.MarkZombie()
|
||||
const opcode = 1
|
||||
const reqBufLen = 8
|
||||
var reqBuf [reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(reqBuf[l:4], o.ID())
|
||||
l += 4
|
||||
client.PutUint32(reqBuf[l:l+4], uint32(reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
return o.Context().WriteMsg(reqBuf[:], nil)
|
||||
}
|
||||
|
||||
type ExtDataControlOfferV1OfferEvent struct {
|
||||
MimeType string
|
||||
}
|
||||
type ExtDataControlOfferV1OfferHandlerFunc func(ExtDataControlOfferV1OfferEvent)
|
||||
|
||||
func (o *ExtDataControlOfferV1) SetOfferHandler(f ExtDataControlOfferV1OfferHandlerFunc) {
|
||||
o.offerHandler = f
|
||||
}
|
||||
|
||||
func (o *ExtDataControlOfferV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
switch opcode {
|
||||
case 0:
|
||||
if o.offerHandler == nil {
|
||||
return
|
||||
}
|
||||
l := 0
|
||||
mimeTypeLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
|
||||
l += 4
|
||||
mimeType := client.String(data[l : l+mimeTypeLen])
|
||||
l += mimeTypeLen
|
||||
|
||||
o.offerHandler(ExtDataControlOfferV1OfferEvent{MimeType: mimeType})
|
||||
}
|
||||
}
|
||||
215
core/internal/server/clipboard/handlers.go
Normal file
215
core/internal/server/clipboard/handlers.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||
)
|
||||
|
||||
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
||||
switch req.Method {
|
||||
case "clipboard.getState":
|
||||
handleGetState(conn, req, m)
|
||||
case "clipboard.getHistory":
|
||||
handleGetHistory(conn, req, m)
|
||||
case "clipboard.getEntry":
|
||||
handleGetEntry(conn, req, m)
|
||||
case "clipboard.deleteEntry":
|
||||
handleDeleteEntry(conn, req, m)
|
||||
case "clipboard.clearHistory":
|
||||
handleClearHistory(conn, req, m)
|
||||
case "clipboard.copy":
|
||||
handleCopy(conn, req, m)
|
||||
case "clipboard.paste":
|
||||
handlePaste(conn, req, m)
|
||||
case "clipboard.subscribe":
|
||||
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)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, m.GetState())
|
||||
}
|
||||
|
||||
func handleGetHistory(conn net.Conn, req models.Request, m *Manager) {
|
||||
history := m.GetHistory()
|
||||
for i := range history {
|
||||
history[i].Data = nil
|
||||
}
|
||||
models.Respond(conn, req.ID, history)
|
||||
}
|
||||
|
||||
func handleGetEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
entry, err := m.GetEntry(uint64(id))
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, entry)
|
||||
}
|
||||
|
||||
func handleDeleteEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.DeleteEntry(uint64(id)); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry deleted"})
|
||||
}
|
||||
|
||||
func handleClearHistory(conn net.Conn, req models.Request, m *Manager) {
|
||||
m.ClearHistory()
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "history cleared"})
|
||||
}
|
||||
|
||||
func handleCopy(conn net.Conn, req models.Request, m *Manager) {
|
||||
text, err := params.String(req.Params, "text")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.CopyText(text); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
|
||||
}
|
||||
|
||||
func handlePaste(conn net.Conn, req models.Request, m *Manager) {
|
||||
text, err := m.PasteText()
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, map[string]string{"text": text})
|
||||
}
|
||||
|
||||
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
|
||||
clientID := fmt.Sprintf("clipboard-%d", req.ID)
|
||||
|
||||
ch := m.Subscribe(clientID)
|
||||
defer m.Unsubscribe(clientID)
|
||||
|
||||
initialState := m.GetState()
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||
ID: req.ID,
|
||||
Result: &initialState,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for state := range ch {
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||
ID: req.ID,
|
||||
Result: &state,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleSearch(conn net.Conn, req models.Request, m *Manager) {
|
||||
p := SearchParams{
|
||||
Query: params.StringOpt(req.Params, "query", ""),
|
||||
MimeType: params.StringOpt(req.Params, "mimeType", ""),
|
||||
Limit: params.IntOpt(req.Params, "limit", 50),
|
||||
Offset: params.IntOpt(req.Params, "offset", 0),
|
||||
}
|
||||
|
||||
if img, ok := req.Params["isImage"].(bool); ok {
|
||||
p.IsImage = &img
|
||||
}
|
||||
if b, ok := req.Params["before"].(float64); ok {
|
||||
v := int64(b)
|
||||
p.Before = &v
|
||||
}
|
||||
if a, ok := req.Params["after"].(float64); ok {
|
||||
v := int64(a)
|
||||
p.After = &v
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, m.Search(p))
|
||||
}
|
||||
|
||||
func handleGetConfig(conn net.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, m.GetConfig())
|
||||
}
|
||||
|
||||
func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
|
||||
cfg := m.GetConfig()
|
||||
|
||||
if _, ok := req.Params["maxHistory"]; ok {
|
||||
cfg.MaxHistory = params.IntOpt(req.Params, "maxHistory", cfg.MaxHistory)
|
||||
}
|
||||
if _, ok := req.Params["maxEntrySize"]; ok {
|
||||
cfg.MaxEntrySize = int64(params.IntOpt(req.Params, "maxEntrySize", int(cfg.MaxEntrySize)))
|
||||
}
|
||||
if _, ok := req.Params["autoClearDays"]; ok {
|
||||
cfg.AutoClearDays = params.IntOpt(req.Params, "autoClearDays", cfg.AutoClearDays)
|
||||
}
|
||||
if v, ok := req.Params["clearAtStartup"].(bool); ok {
|
||||
cfg.ClearAtStartup = v
|
||||
}
|
||||
if v, ok := req.Params["disabled"].(bool); ok {
|
||||
cfg.Disabled = v
|
||||
}
|
||||
if v, ok := req.Params["disableHistory"].(bool); ok {
|
||||
cfg.DisableHistory = v
|
||||
}
|
||||
if v, ok := req.Params["disablePersist"].(bool); ok {
|
||||
cfg.DisablePersist = v
|
||||
}
|
||||
|
||||
if err := m.SetConfig(cfg); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "config updated"})
|
||||
}
|
||||
|
||||
func handleStore(conn net.Conn, req models.Request, m *Manager) {
|
||||
data, err := params.String(req.Params, "data")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
mimeType := params.StringOpt(req.Params, "mimeType", "text/plain;charset=utf-8")
|
||||
|
||||
if err := m.StoreData([]byte(data), mimeType); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"})
|
||||
}
|
||||
1302
core/internal/server/clipboard/manager.go
Normal file
1302
core/internal/server/clipboard/manager.go
Normal file
File diff suppressed because it is too large
Load Diff
191
core/internal/server/clipboard/types.go
Normal file
191
core/internal/server/clipboard/types.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
MaxHistory int `json:"maxHistory"`
|
||||
MaxEntrySize int64 `json:"maxEntrySize"`
|
||||
AutoClearDays int `json:"autoClearDays"`
|
||||
ClearAtStartup bool `json:"clearAtStartup"`
|
||||
|
||||
Disabled bool `json:"disabled"`
|
||||
DisableHistory bool `json:"disableHistory"`
|
||||
DisablePersist bool `json:"disablePersist"`
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
MaxHistory: 100,
|
||||
MaxEntrySize: 5 * 1024 * 1024,
|
||||
AutoClearDays: 0,
|
||||
ClearAtStartup: false,
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigPath() (string, error) {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(configDir, "DankMaterialShell", "clsettings.json"), nil
|
||||
}
|
||||
|
||||
func LoadConfig() Config {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
path, err := getConfigPath()
|
||||
if err != nil {
|
||||
return cfg
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return cfg
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return DefaultConfig()
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func SaveConfig(cfg Config) error {
|
||||
path, err := getConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
type SearchParams struct {
|
||||
Query string `json:"query"`
|
||||
MimeType string `json:"mimeType"`
|
||||
IsImage *bool `json:"isImage"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
Before *int64 `json:"before"`
|
||||
After *int64 `json:"after"`
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
Entries []Entry `json:"entries"`
|
||||
Total int `json:"total"`
|
||||
HasMore bool `json:"hasMore"`
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
ID uint64 `json:"id"`
|
||||
Data []byte `json:"data,omitempty"`
|
||||
MimeType string `json:"mimeType"`
|
||||
Preview string `json:"preview"`
|
||||
Size int `json:"size"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
IsImage bool `json:"isImage"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
History []Entry `json:"history"`
|
||||
Current *Entry `json:"current,omitempty"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
config Config
|
||||
configMutex sync.RWMutex
|
||||
configPath string
|
||||
|
||||
display *wlclient.Display
|
||||
wlCtx *wlcontext.SharedContext
|
||||
|
||||
registry *wlclient.Registry
|
||||
dataControlMgr any
|
||||
seat *wlclient.Seat
|
||||
dataDevice any
|
||||
currentOffer any
|
||||
currentSource any
|
||||
seatName uint32
|
||||
mimeTypes []string
|
||||
offerMimeTypes map[any][]string
|
||||
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
|
||||
initialized bool
|
||||
|
||||
alive bool
|
||||
stopChan chan struct{}
|
||||
|
||||
db *bolt.DB
|
||||
dbPath string
|
||||
|
||||
state *State
|
||||
stateMutex sync.RWMutex
|
||||
|
||||
subscribers map[string]chan State
|
||||
subMutex sync.RWMutex
|
||||
dirty chan struct{}
|
||||
notifierWg sync.WaitGroup
|
||||
lastState *State
|
||||
}
|
||||
|
||||
func (m *Manager) GetState() State {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
if m.state == nil {
|
||||
return State{}
|
||||
}
|
||||
return *m.state
|
||||
}
|
||||
|
||||
func (m *Manager) Subscribe(id string) chan State {
|
||||
ch := make(chan State, 64)
|
||||
m.subMutex.Lock()
|
||||
m.subscribers[id] = ch
|
||||
m.subMutex.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(id string) {
|
||||
m.subMutex.Lock()
|
||||
if ch, ok := m.subscribers[id]; ok {
|
||||
close(ch)
|
||||
delete(m.subscribers, id)
|
||||
}
|
||||
m.subMutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) notifySubscribers() {
|
||||
select {
|
||||
case m.dirty <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||
@@ -147,6 +148,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Method, "clipboard.") {
|
||||
if clipboardManager == nil {
|
||||
models.RespondError(conn, req.ID, "clipboard manager not initialized")
|
||||
return
|
||||
}
|
||||
clipboard.HandleRequest(conn, req, clipboardManager)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
case "ping":
|
||||
models.Respond(conn, req.ID, "pong")
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||
@@ -32,7 +33,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
const APIVersion = 22
|
||||
const APIVersion = 23
|
||||
|
||||
var CLIVersion = "dev"
|
||||
|
||||
@@ -63,6 +64,7 @@ var extWorkspaceManager *extworkspace.Manager
|
||||
var brightnessManager *brightness.Manager
|
||||
var wlrOutputManager *wlroutput.Manager
|
||||
var evdevManager *evdev.Manager
|
||||
var clipboardManager *clipboard.Manager
|
||||
var wlContext *wlcontext.SharedContext
|
||||
|
||||
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
||||
@@ -336,6 +338,31 @@ func InitializeEvdevManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeClipboardManager() error {
|
||||
log.Info("Attempting to initialize clipboard manager...")
|
||||
|
||||
if wlContext == nil {
|
||||
ctx, err := wlcontext.New()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create shared Wayland context: %v", err)
|
||||
return err
|
||||
}
|
||||
wlContext = ctx
|
||||
}
|
||||
|
||||
config := clipboard.LoadConfig()
|
||||
manager, err := clipboard.NewManager(wlContext, config)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to initialize clipboard manager: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
clipboardManager = manager
|
||||
|
||||
log.Info("Clipboard manager initialized successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
@@ -409,6 +436,10 @@ func getCapabilities() Capabilities {
|
||||
caps = append(caps, "evdev")
|
||||
}
|
||||
|
||||
if clipboardManager != nil {
|
||||
caps = append(caps, "clipboard")
|
||||
}
|
||||
|
||||
return Capabilities{Capabilities: caps}
|
||||
}
|
||||
|
||||
@@ -463,6 +494,10 @@ func getServerInfo() ServerInfo {
|
||||
caps = append(caps, "evdev")
|
||||
}
|
||||
|
||||
if clipboardManager != nil {
|
||||
caps = append(caps, "clipboard")
|
||||
}
|
||||
|
||||
return ServerInfo{
|
||||
APIVersion: APIVersion,
|
||||
CLIVersion: CLIVersion,
|
||||
@@ -1034,6 +1069,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
||||
}()
|
||||
}
|
||||
|
||||
if shouldSubscribe("clipboard") && clipboardManager != nil {
|
||||
wg.Add(1)
|
||||
clipboardChan := clipboardManager.Subscribe(clientID + "-clipboard")
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer clipboardManager.Unsubscribe(clientID + "-clipboard")
|
||||
|
||||
initialState := clipboardManager.GetState()
|
||||
select {
|
||||
case eventChan <- ServiceEvent{Service: "clipboard", Data: initialState}:
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case state, ok := <-clipboardChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case eventChan <- ServiceEvent{Service: "clipboard", Data: state}:
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(eventChan)
|
||||
@@ -1096,6 +1163,9 @@ func cleanupManagers() {
|
||||
if evdevManager != nil {
|
||||
evdevManager.Close()
|
||||
}
|
||||
if clipboardManager != nil {
|
||||
clipboardManager.Close()
|
||||
}
|
||||
if wlContext != nil {
|
||||
wlContext.Close()
|
||||
}
|
||||
@@ -1259,6 +1329,18 @@ func Start(printDocs bool) error {
|
||||
log.Info("Evdev:")
|
||||
log.Info(" evdev.getState - Get current evdev state (caps lock)")
|
||||
log.Info(" evdev.subscribe - Subscribe to evdev state changes (streaming)")
|
||||
log.Info("Clipboard:")
|
||||
log.Info(" clipboard.getState - Get clipboard state (enabled, history, current)")
|
||||
log.Info(" clipboard.getHistory - Get clipboard history with previews")
|
||||
log.Info(" clipboard.getEntry - Get full entry by ID (params: id)")
|
||||
log.Info(" clipboard.deleteEntry - Delete entry by ID (params: id)")
|
||||
log.Info(" clipboard.clearHistory - Clear all clipboard history")
|
||||
log.Info(" clipboard.copy - Copy text to clipboard (params: text)")
|
||||
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.subscribe - Subscribe to clipboard state changes (streaming)")
|
||||
log.Info("")
|
||||
}
|
||||
log.Info("Initializing managers...")
|
||||
@@ -1366,10 +1448,15 @@ func Start(printDocs bool) error {
|
||||
}
|
||||
}()
|
||||
|
||||
if wlContext != nil {
|
||||
wlContext.Start()
|
||||
log.Info("Wayland event dispatcher started")
|
||||
}
|
||||
go func() {
|
||||
if err := InitializeClipboardManager(); err != nil {
|
||||
log.Warnf("Clipboard manager unavailable: %v", err)
|
||||
}
|
||||
if wlContext != nil {
|
||||
wlContext.Start()
|
||||
log.Info("Wayland event dispatcher started")
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info("")
|
||||
log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package wlcontext
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
@@ -13,6 +16,7 @@ type SharedContext struct {
|
||||
display *wlclient.Display
|
||||
stopChan chan struct{}
|
||||
fatalError chan error
|
||||
cmdQueue chan func()
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
started bool
|
||||
@@ -28,6 +32,7 @@ func New() (*SharedContext, error) {
|
||||
display: display,
|
||||
stopChan: make(chan struct{}),
|
||||
fatalError: make(chan error, 1),
|
||||
cmdQueue: make(chan func(), 256),
|
||||
started: false,
|
||||
}
|
||||
|
||||
@@ -51,6 +56,13 @@ func (sc *SharedContext) Display() *wlclient.Display {
|
||||
return sc.display
|
||||
}
|
||||
|
||||
func (sc *SharedContext) Post(fn func()) {
|
||||
select {
|
||||
case sc.cmdQueue <- fn:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *SharedContext) FatalError() <-chan error {
|
||||
return sc.fatalError
|
||||
}
|
||||
@@ -74,10 +86,35 @@ func (sc *SharedContext) eventDispatcher() {
|
||||
case <-sc.stopChan:
|
||||
return
|
||||
default:
|
||||
if err := ctx.Dispatch(); err != nil {
|
||||
log.Errorf("Wayland connection error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sc.drainCmdQueue()
|
||||
|
||||
if err := ctx.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
|
||||
log.Errorf("Failed to set read deadline: %v", err)
|
||||
}
|
||||
err := ctx.Dispatch()
|
||||
if err := ctx.SetReadDeadline(time.Time{}); err != nil {
|
||||
log.Errorf("Failed to clear read deadline: %v", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
case errors.Is(err, os.ErrDeadlineExceeded):
|
||||
default:
|
||||
log.Errorf("Wayland connection error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *SharedContext) drainCmdQueue() {
|
||||
for {
|
||||
select {
|
||||
case fn := <-sc.cmdQueue:
|
||||
fn()
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user