summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard
diff options
context:
space:
mode:
authorAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
committerAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
commit70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch)
treeab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard
parent5d94c0a7d44a2255b81815a52a7056a94a39842d (diff)
downloadRaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz
RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard/clipboard.go630
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard/store.go248
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard/watch.go303
3 files changed, 1181 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard/clipboard.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard/clipboard.go
new file mode 100644
index 0000000..d27d6ff
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard/clipboard.go
@@ -0,0 +1,630 @@
+package clipboard
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "syscall"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
+ wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
+)
+
+const envServe = "_DMS_CLIPBOARD_SERVE"
+const envMime = "_DMS_CLIPBOARD_MIME"
+const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
+const envCacheFile = "_DMS_CLIPBOARD_CACHE"
+
+// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
+// child. Reads source data into memory, deletes any cache file, then serves.
+func MaybeServeAndExit() {
+ if os.Getenv(envServe) == "" {
+ return
+ }
+
+ mimeType := os.Getenv(envMime)
+ pasteOnce := os.Getenv(envPasteOnce) == "1"
+ cachePath := os.Getenv(envCacheFile)
+
+ var data []byte
+ var err error
+
+ switch {
+ case cachePath != "":
+ data, err = os.ReadFile(cachePath)
+ os.Remove(cachePath)
+ default:
+ data, err = io.ReadAll(os.Stdin)
+ }
+
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err)
+ os.Exit(1)
+ }
+
+ if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
+ fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
+ os.Exit(1)
+ }
+ os.Exit(0)
+}
+
+func Copy(data []byte, mimeType string) error {
+ return copyForkCached(data, mimeType, false)
+}
+
+func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
+ if foreground {
+ return serveClipboard(data, mimeType, pasteOnce)
+ }
+ return copyForkCached(data, mimeType, pasteOnce)
+}
+
+func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
+ if foreground {
+ buf, err := io.ReadAll(data)
+ if err != nil {
+ return fmt.Errorf("read source: %w", err)
+ }
+ return serveClipboard(buf, mimeType, pasteOnce)
+ }
+ return copyFork(data, mimeType, pasteOnce)
+}
+
+func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
+ cmd := exec.Command(os.Args[0])
+ cmd.Stderr = nil
+ cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
+ cmd.Env = append(os.Environ(),
+ envServe+"=1",
+ envMime+"="+mimeType,
+ )
+ if pasteOnce {
+ cmd.Env = append(cmd.Env, envPasteOnce+"=1")
+ }
+ cmd.Env = append(cmd.Env, extra...)
+ return cmd
+}
+
+func waitReady(cmd *exec.Cmd) error {
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return fmt.Errorf("stdout pipe: %w", err)
+ }
+ if err := cmd.Start(); err != nil {
+ return fmt.Errorf("start: %w", err)
+ }
+ var buf [1]byte
+ if _, err := stdout.Read(buf[:]); err != nil {
+ return fmt.Errorf("waiting for clipboard ready: %w", err)
+ }
+ return nil
+}
+
+func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
+ cacheFile, err := createClipboardCacheFile()
+ if err != nil {
+ return fmt.Errorf("create cache file: %w", err)
+ }
+ cachePath := cacheFile.Name()
+
+ if _, err := cacheFile.Write(data); err != nil {
+ cacheFile.Close()
+ os.Remove(cachePath)
+ return fmt.Errorf("write cache file: %w", err)
+ }
+ if err := cacheFile.Close(); err != nil {
+ os.Remove(cachePath)
+ return fmt.Errorf("close cache file: %w", err)
+ }
+
+ cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
+ cmd.Stdin = nil
+ if err := waitReady(cmd); err != nil {
+ os.Remove(cachePath)
+ return err
+ }
+ return nil
+}
+
+func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
+ cmd := newForkCmd(mimeType, pasteOnce)
+
+ switch src := data.(type) {
+ case *os.File:
+ cmd.Stdin = src
+ return waitReady(cmd)
+
+ default:
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ return fmt.Errorf("stdin pipe: %w", err)
+ }
+
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return fmt.Errorf("stdout pipe: %w", err)
+ }
+
+ if err := cmd.Start(); err != nil {
+ return fmt.Errorf("start: %w", err)
+ }
+ if _, err := io.Copy(stdin, data); err != nil {
+ stdin.Close()
+ return fmt.Errorf("write stdin: %w", err)
+ }
+ if err := stdin.Close(); err != nil {
+ return fmt.Errorf("close stdin: %w", err)
+ }
+
+ var buf [1]byte
+ if _, err := stdout.Read(buf[:]); err != nil {
+ return fmt.Errorf("waiting for clipboard ready: %w", err)
+ }
+ return nil
+ }
+}
+
+func signalReady() {
+ if os.Getenv(envServe) == "" {
+ return
+ }
+ os.Stdout.Write([]byte{1})
+}
+
+func createClipboardCacheFile() (*os.File, error) {
+ preferredDirs := []string{}
+
+ if cacheDir, err := os.UserCacheDir(); err == nil {
+ preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
+ }
+ preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
+
+ for _, dir := range preferredDirs {
+ if err := os.MkdirAll(dir, 0o700); err != nil {
+ continue
+ }
+ cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
+ if err == nil {
+ return cachedData, nil
+ }
+ }
+ return os.CreateTemp("", "dms-clipboard-*")
+}
+
+func serveClipboard(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) {
+ _ = syscall.SetNonblock(e.Fd, false)
+ 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()
+ signalReady()
+
+ 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/"
+}
+
+type Offer struct {
+ MimeType string
+ Data []byte
+}
+
+func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
+ if !foreground {
+ return copyMultiFork(offers, pasteOnce)
+ }
+ return copyMultiServe(offers, pasteOnce)
+}
+
+func copyMultiFork(offers []Offer, pasteOnce bool) error {
+ args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
+ if pasteOnce {
+ args = append(args, "--paste-once")
+ }
+
+ 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)
+ }
+
+ for _, offer := range offers {
+ fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
+ if _, err := stdin.Write(offer.Data); err != nil {
+ stdin.Close()
+ return fmt.Errorf("write offer data: %w", err)
+ }
+ }
+ stdin.Close()
+
+ return nil
+}
+
+func copyMultiServe(offers []Offer, 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)
+ }
+
+ offerMap := make(map[string][]byte)
+ for _, offer := range offers {
+ if err := source.Offer(offer.MimeType); err != nil {
+ return fmt.Errorf("offer %s: %w", offer.MimeType, err)
+ }
+ offerMap[offer.MimeType] = offer.Data
+ }
+
+ cancelled := make(chan struct{})
+ pasted := make(chan struct{}, 1)
+
+ source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
+ _ = syscall.SetNonblock(e.Fd, false)
+ file := os.NewFile(uintptr(e.Fd), "pipe")
+ defer file.Close()
+
+ if data, ok := offerMap[e.MimeType]; ok {
+ _, _ = 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
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard/store.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard/store.go
new file mode 100644
index 0000000..17ad7a9
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard/store.go
@@ -0,0 +1,248 @@
+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"
+ "hash/fnv"
+
+ 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
+ Hash uint64
+}
+
+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, 0o644, &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),
+ Hash: computeHash(data),
+ }
+
+ 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, entry.Hash); 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, err := os.UserCacheDir()
+ if err != nil {
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ cacheDir = filepath.Join(homeDir, ".cache")
+ }
+
+ newDir := filepath.Join(cacheDir, "DankMaterialShell", "clipboard")
+ newPath := filepath.Join(newDir, "db")
+
+ if _, err := os.Stat(newPath); err == nil {
+ return newPath, nil
+ }
+
+ oldDir := filepath.Join(cacheDir, "dms-clipboard")
+ oldPath := filepath.Join(oldDir, "db")
+
+ if _, err := os.Stat(oldPath); err == nil {
+ if err := os.MkdirAll(newDir, 0o700); err != nil {
+ return "", err
+ }
+ if err := os.Rename(oldPath, newPath); err != nil {
+ return "", err
+ }
+ os.Remove(oldDir)
+ return newPath, nil
+ }
+
+ if err := os.MkdirAll(newDir, 0o700); err != nil {
+ return "", err
+ }
+ return newPath, nil
+}
+
+func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
+ c := b.Cursor()
+ for k, v := c.Last(); k != nil; k, v = c.Prev() {
+ if extractHash(v) != hash {
+ continue
+ }
+ 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)
+ }
+ binary.Write(buf, binary.BigEndian, e.Hash)
+
+ return buf.Bytes(), nil
+}
+
+func itob(v uint64) []byte {
+ b := make([]byte, 8)
+ binary.BigEndian.PutUint64(b, v)
+ return b
+}
+
+func computeHash(data []byte) uint64 {
+ h := fnv.New64a()
+ h.Write(data)
+ return h.Sum64()
+}
+
+func extractHash(data []byte) uint64 {
+ if len(data) < 8 {
+ return 0
+ }
+ return binary.BigEndian.Uint64(data[len(data)-8:])
+}
+
+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])
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard/watch.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard/watch.go
new file mode 100644
index 0000000..327c5c5
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/clipboard/watch.go
@@ -0,0 +1,303 @@
+package clipboard
+
+import (
+ "context"
+ "errors"
+ "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
+ MimeTypes []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 {
+ if isTimeoutError(err) {
+ continue
+ }
+ return fmt.Errorf("dispatch: %w", err)
+ }
+ }
+ }
+}
+
+func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, allMimeTypes []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
+ }
+
+ mimesCopy := make([]string, len(mimes))
+ copy(mimesCopy, mimes)
+
+ 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, mimesCopy)
+ }()
+ })
+
+ 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 {
+ if isTimeoutError(err) {
+ continue
+ }
+ return fmt.Errorf("dispatch: %w", err)
+ }
+ }
+ }
+}
+
+func isTimeoutError(err error) bool {
+ if err == nil {
+ return false
+ }
+ if errors.Is(err, os.ErrDeadlineExceeded) {
+ return true
+ }
+ if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
+ return true
+ }
+ return false
+}
+
+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
+}