summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/handlers.go325
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager.go1911
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager_test.go531
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/types.go196
4 files changed, 0 insertions, 2963 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/handlers.go
deleted file mode 100644
index 1ccc710..0000000
--- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/handlers.go
+++ /dev/null
@@ -1,325 +0,0 @@
-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.copyEntry":
- handleCopyEntry(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)
- case "clipboard.pinEntry":
- handlePinEntry(conn, req, m)
- case "clipboard.unpinEntry":
- handleUnpinEntry(conn, req, m)
- case "clipboard.getPinnedEntries":
- handleGetPinnedEntries(conn, req, m)
- case "clipboard.getPinnedCount":
- handleGetPinnedCount(conn, req, m)
- case "clipboard.copyFile":
- handleCopyFile(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 handleCopyEntry(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
- }
-
- filePath := m.EntryToFile(entry)
- if filePath != "" {
- if err := m.CopyFile(filePath); err != nil {
- models.RespondError(conn, req.ID, err.Error())
- return
- }
- models.Respond(conn, req.ID, map[string]any{
- "success": true,
- "filePath": filePath,
- })
- return
- }
-
- if err := m.SetClipboard(entry.Data, entry.MimeType); err != nil {
- models.RespondError(conn, req.ID, err.Error())
- return
- }
-
- if entry.Pinned {
- if err := m.CreateHistoryEntryFromPinned(entry); err != nil {
- models.RespondError(conn, req.ID, err.Error())
- return
- }
- } else {
- if err := m.TouchEntry(uint64(id)); 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 := models.Get[bool](req, "isImage"); ok {
- p.IsImage = &img
- }
- if b, ok := models.Get[float64](req, "before"); ok {
- v := int64(b)
- p.Before = &v
- }
- if a, ok := models.Get[float64](req, "after"); 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 v, ok := models.Get[float64](req, "maxHistory"); ok {
- cfg.MaxHistory = int(v)
- }
- if v, ok := models.Get[float64](req, "maxEntrySize"); ok {
- cfg.MaxEntrySize = int64(v)
- }
- if v, ok := models.Get[float64](req, "autoClearDays"); ok {
- cfg.AutoClearDays = int(v)
- }
- if v, ok := models.Get[bool](req, "clearAtStartup"); ok {
- cfg.ClearAtStartup = v
- }
- if v, ok := models.Get[bool](req, "disabled"); ok {
- cfg.Disabled = v
- }
- if v, ok := models.Get[float64](req, "maxPinned"); ok {
- cfg.MaxPinned = int(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"})
-}
-
-func handlePinEntry(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.PinEntry(uint64(id)); err != nil {
- models.RespondError(conn, req.ID, err.Error())
- return
- }
-
- models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry pinned"})
-}
-
-func handleUnpinEntry(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.UnpinEntry(uint64(id)); err != nil {
- models.RespondError(conn, req.ID, err.Error())
- return
- }
-
- models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry unpinned"})
-}
-
-func handleGetPinnedEntries(conn net.Conn, req models.Request, m *Manager) {
- pinned := m.GetPinnedEntries()
- models.Respond(conn, req.ID, pinned)
-}
-
-func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) {
- count := m.GetPinnedCount()
- models.Respond(conn, req.ID, map[string]int{"count": count})
-}
-
-func handleCopyFile(conn net.Conn, req models.Request, m *Manager) {
- filePath, err := params.String(req.Params, "filePath")
- if err != nil {
- models.RespondError(conn, req.ID, err.Error())
- return
- }
-
- if err := m.CopyFile(filePath); err != nil {
- models.RespondError(conn, req.ID, err.Error())
- return
- }
-
- models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied"})
-}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager.go
deleted file mode 100644
index f5f528b..0000000
--- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager.go
+++ /dev/null
@@ -1,1911 +0,0 @@
-package clipboard
-
-import (
- "bytes"
- "encoding/binary"
- "fmt"
- "image"
- _ "image/gif"
- _ "image/jpeg"
- _ "image/png"
- "io"
- "os"
- "os/exec"
- "path/filepath"
- "slices"
- "strings"
- "syscall"
- "time"
-
- "hash/fnv"
-
- "github.com/fsnotify/fsnotify"
- "github.com/godbus/dbus/v5"
- _ "golang.org/x/image/bmp"
- _ "golang.org/x/image/tiff"
- _ "golang.org/x/image/webp"
-
- bolt "go.etcd.io/bbolt"
-
- clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
- "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
- "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
- "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
- wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
-)
-
-// These mime types won't be stored in history
-var sensitiveMimeTypes = []string{
- "x-kde-passwordManagerHint",
-}
-
-func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error) {
- display := wlCtx.Display()
- dbPath, err := clipboardstore.GetDBPath()
- if err != nil {
- return nil, fmt.Errorf("failed to get db path: %w", err)
- }
-
- configPath, _ := getConfigPath()
-
- m := &Manager{
- config: config,
- configPath: configPath,
- display: display,
- wlCtx: wlCtx,
- stopChan: make(chan struct{}),
- subscribers: make(map[string]chan State),
- dirty: make(chan struct{}, 1),
- offerMimeTypes: make(map[any][]string),
- offerRegistry: make(map[uint32]any),
- dbPath: dbPath,
- }
-
- if !config.Disabled {
- if err := m.setupRegistry(); err != nil {
- return nil, err
- }
- }
-
- m.notifierWg.Add(1)
- go m.notifier()
-
- go m.watchConfig()
-
- db, err := openDB(dbPath)
- if err != nil {
- return nil, fmt.Errorf("failed to open db: %w", err)
- }
- m.db = db
-
- if err := m.migrateHashes(); err != nil {
- log.Errorf("Failed to migrate hashes: %v", err)
- }
-
- if !config.Disabled {
- if config.ClearAtStartup {
- if err := m.clearHistoryInternal(); err != nil {
- log.Errorf("Failed to clear history at startup: %v", err)
- }
- }
-
- if config.AutoClearDays > 0 {
- if err := m.clearOldEntries(config.AutoClearDays); err != nil {
- log.Errorf("Failed to clear old entries: %v", err)
- }
- }
- }
-
- m.alive = true
- m.updateState()
-
- if !config.Disabled && m.dataControlMgr != nil && m.seat != nil {
- m.setupDataDeviceSync()
- }
-
- return m, nil
-}
-
-func openDB(path string) (*bolt.DB, error) {
- db, err := bolt.Open(path, 0o644, &bolt.Options{
- Timeout: 1 * time.Second,
- })
- if err != nil {
- return nil, err
- }
-
- err = db.Update(func(tx *bolt.Tx) error {
- _, err := tx.CreateBucketIfNotExists([]byte("clipboard"))
- return err
- })
- if err != nil {
- db.Close()
- return nil, err
- }
-
- return db, nil
-}
-
-func (m *Manager) post(fn func()) {
- m.wlCtx.Post(fn)
-}
-
-func (m *Manager) setupRegistry() error {
- ctx := m.display.Context()
-
- registry, err := m.display.GetRegistry()
- if err != nil {
- return fmt.Errorf("failed to get registry: %w", err)
- }
- m.registry = registry
-
- registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
- switch e.Interface {
- case "ext_data_control_manager_v1":
- if e.Version < 1 {
- return
- }
- dataControlMgr := ext_data_control.NewExtDataControlManagerV1(ctx)
- if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
- log.Errorf("Failed to bind ext_data_control_manager_v1: %v", err)
- return
- }
- m.dataControlMgr = dataControlMgr
- log.Info("Bound ext_data_control_manager_v1")
- case "wl_seat":
- seat := wlclient.NewSeat(ctx)
- if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
- log.Errorf("Failed to bind wl_seat: %v", err)
- return
- }
- m.seat = seat
- m.seatName = e.Name
- log.Info("Bound wl_seat")
- }
- })
-
- m.display.Roundtrip()
- m.display.Roundtrip()
-
- if m.dataControlMgr == nil {
- return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
- }
-
- if m.seat == nil {
- return fmt.Errorf("no seat available")
- }
-
- return nil
-}
-
-func (m *Manager) setupDataDeviceSync() {
- if m.dataControlMgr == nil || m.seat == nil {
- return
- }
-
- ctx := m.display.Context()
- dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
-
- dataDevice := ext_data_control.NewExtDataControlDeviceV1(ctx)
-
- dataDevice.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
- if e.Id == nil {
- return
- }
-
- m.offerMutex.Lock()
- m.offerRegistry[e.Id.ID()] = e.Id
- m.offerMimeTypes[e.Id] = make([]string, 0)
- m.offerMutex.Unlock()
-
- e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
- m.offerMutex.Lock()
- m.offerMimeTypes[e.Id] = append(m.offerMimeTypes[e.Id], me.MimeType)
- m.offerMutex.Unlock()
- })
- })
-
- dataDevice.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
- if !m.initialized {
- m.initialized = true
- return
- }
-
- var offer any
- if e.Id != nil {
- offer = e.Id
- } else if e.OfferId != 0 {
- m.offerMutex.RLock()
- offer = m.offerRegistry[e.OfferId]
- m.offerMutex.RUnlock()
- }
-
- m.ownerLock.Lock()
- wasOwner := m.isOwner
- m.ownerLock.Unlock()
-
- if offer == nil {
- return
- }
-
- if wasOwner {
- return
- }
-
- prevOffer := m.currentOffer
- m.currentOffer = offer
-
- if prevOffer != nil && prevOffer != offer {
- m.offerMutex.Lock()
- delete(m.offerMimeTypes, prevOffer)
- m.offerMutex.Unlock()
- }
-
- m.offerMutex.RLock()
- mimes := m.offerMimeTypes[offer]
- m.offerMutex.RUnlock()
-
- m.mimeTypes = mimes
-
- if len(mimes) == 0 {
- return
- }
-
- if m.hasSensitiveMimeType(mimes) {
- return
- }
-
- preferredMime := m.selectMimeType(mimes)
- if preferredMime == "" {
- return
- }
-
- typedOffer := offer.(*ext_data_control.ExtDataControlOfferV1)
-
- r, w, err := os.Pipe()
- if err != nil {
- return
- }
-
- if err := typedOffer.Receive(preferredMime, int(w.Fd())); err != nil {
- r.Close()
- w.Close()
- return
- }
- w.Close()
-
- go m.readAndStore(r, preferredMime)
- })
-
- if err := dataMgr.GetDataDeviceWithProxy(dataDevice, m.seat); err != nil {
- log.Errorf("Failed to send get_data_device request: %v", err)
- return
- }
-
- m.dataDevice = dataDevice
-
- if err := ctx.Dispatch(); err != nil {
- log.Errorf("Failed to dispatch initial events: %v", err)
- return
- }
-
- log.Info("Data device setup complete")
-}
-
-func (m *Manager) readAndStore(r *os.File, mimeType string) {
- defer r.Close()
-
- cfg := m.getConfig()
-
- done := make(chan []byte, 1)
- go func() {
- data, _ := io.ReadAll(r)
- done <- data
- }()
-
- var data []byte
- select {
- case data = <-done:
- case <-time.After(500 * time.Millisecond):
- return
- }
-
- if len(data) == 0 || int64(len(data)) > cfg.MaxEntrySize {
- return
- }
- if len(bytes.TrimSpace(data)) == 0 {
- return
- }
-
- if !cfg.Disabled && m.db != nil {
- m.storeClipboardEntry(data, mimeType)
- }
-
- m.updateState()
- m.notifySubscribers()
-}
-
-func (m *Manager) storeClipboardEntry(data []byte, mimeType string) {
- if mimeType == "text/uri-list" {
- if imgData, imgMime, ok := m.tryReadImageFromURI(data); ok {
- data = imgData
- mimeType = imgMime
- }
- }
-
- entry := Entry{
- Data: data,
- MimeType: mimeType,
- Size: len(data),
- Timestamp: time.Now(),
- IsImage: m.isImageMimeType(mimeType),
- }
-
- switch {
- case entry.IsImage:
- entry.Preview = m.imagePreview(data, mimeType)
- case mimeType == "text/uri-list":
- entry.Preview, entry.IsImage = m.uriListPreview(data)
- default:
- entry.Preview = m.textPreview(data)
- }
-
- if err := m.storeEntry(entry); err != nil {
- log.Errorf("Failed to store clipboard entry: %v", err)
- }
-}
-
-func (m *Manager) storeEntry(entry Entry) error {
- if m.db == nil {
- return fmt.Errorf("database not available")
- }
-
- entry.Hash = computeHash(entry.Data)
-
- return m.db.Update(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
-
- if err := m.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 m.trimLengthInTx(b)
- })
-}
-
-func (m *Manager) 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
- }
- entry, err := decodeEntry(v)
- if err == nil && entry.Pinned {
- continue
- }
- if err := b.Delete(k); err != nil {
- return err
- }
- }
- return nil
-}
-
-func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
- if m.config.MaxHistory < 0 {
- return nil
- }
- c := b.Cursor()
- var count int
- for k, v := c.Last(); k != nil; k, v = c.Prev() {
- entry, err := decodeEntry(v)
- if err == nil && entry.Pinned {
- continue
- }
- if count < m.config.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)
- if e.Pinned {
- 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, &timestamp)
- e.Timestamp = time.Unix(timestamp, 0)
-
- var isImage byte
- binary.Read(buf, binary.BigEndian, &isImage)
- e.IsImage = isImage == 1
-
- if buf.Len() >= 8 {
- binary.Read(buf, binary.BigEndian, &e.Hash)
- }
-
- if buf.Len() >= 1 {
- var pinnedByte byte
- binary.Read(buf, binary.BigEndian, &pinnedByte)
- e.Pinned = pinnedByte == 1
- }
-
- return e, 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) < 9 {
- return 0
- }
- return binary.BigEndian.Uint64(data[len(data)-9 : len(data)-1])
-}
-
-func (m *Manager) hasSensitiveMimeType(mimes []string) bool {
- return slices.ContainsFunc(mimes, func(mime string) bool {
- return slices.Contains(sensitiveMimeTypes, mime)
- })
-}
-
-func (m *Manager) selectMimeType(mimes []string) string {
- preferredTypes := []string{
- "text/uri-list",
- "text/plain;charset=utf-8",
- "text/plain",
- "UTF8_STRING",
- "STRING",
- "TEXT",
- "image/png",
- "image/jpeg",
- "image/gif",
- "image/bmp",
- "image/tiff",
- }
-
- for _, pref := range preferredTypes {
- for _, mime := range mimes {
- if mime == pref {
- return mime
- }
- }
- }
-
- // Skip useless MIME types when falling back
- for _, mime := range mimes {
- switch mime {
- case "application/vnd.portal.filetransfer":
- continue
- default:
- return mime
- }
- }
-
- return ""
-}
-
-func (m *Manager) isImageMimeType(mime string) bool {
- return strings.HasPrefix(mime, "image/")
-}
-
-func (m *Manager) 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 (m *Manager) 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 (m *Manager) uriListPreview(data []byte) (string, bool) {
- text := strings.TrimSpace(string(data))
- uris := strings.Split(text, "\r\n")
- if len(uris) == 0 {
- uris = strings.Split(text, "\n")
- }
-
- if len(uris) > 1 {
- return fmt.Sprintf("[[ %d files ]]", len(uris)), false
- }
-
- if len(uris) == 1 && strings.HasPrefix(uris[0], "file://") {
- filePath := strings.TrimPrefix(uris[0], "file://")
- info, err := os.Stat(filePath)
- if err != nil || info.IsDir() {
- return m.textPreview(data), false
- }
-
- cfg := m.getConfig()
- if info.Size() <= cfg.MaxEntrySize {
- if imgData, err := os.ReadFile(filePath); err == nil {
- if config, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData)); err == nil {
- return fmt.Sprintf("[[ file %s %s %dx%d ]]", filepath.Base(filePath), imgFmt, config.Width, config.Height), true
- }
- }
- }
- return fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), false
- }
-
- return m.textPreview(data), false
-}
-
-func (m *Manager) tryReadImageFromURI(data []byte) ([]byte, string, bool) {
- text := strings.TrimSpace(string(data))
- uris := strings.Split(text, "\r\n")
- if len(uris) == 0 {
- uris = strings.Split(text, "\n")
- }
-
- if len(uris) != 1 || !strings.HasPrefix(uris[0], "file://") {
- return nil, "", false
- }
-
- filePath := strings.TrimPrefix(uris[0], "file://")
- info, err := os.Stat(filePath)
- if err != nil || info.IsDir() {
- return nil, "", false
- }
-
- cfg := m.getConfig()
- if info.Size() > cfg.MaxEntrySize {
- return nil, "", false
- }
-
- imgData, err := os.ReadFile(filePath)
- if err != nil {
- return nil, "", false
- }
-
- _, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData))
- if err != nil {
- return nil, "", false
- }
-
- return imgData, "image/" + imgFmt, true
-}
-
-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])
-}
-
-func (m *Manager) updateState() {
- history := m.GetHistory()
-
- for i := range history {
- history[i].Data = nil
- }
-
- var current *Entry
- if len(history) > 0 {
- c := history[0]
- c.Data = nil
- current = &c
- }
-
- newState := &State{
- Enabled: m.alive,
- History: history,
- Current: current,
- }
-
- m.stateMutex.Lock()
- m.state = newState
- m.stateMutex.Unlock()
-}
-
-func (m *Manager) notifier() {
- defer m.notifierWg.Done()
-
- for range m.dirty {
- state := m.GetState()
-
- if m.lastState != nil && stateEqual(m.lastState, &state) {
- continue
- }
-
- m.lastState = &state
-
- m.subMutex.RLock()
- subs := make([]chan State, 0, len(m.subscribers))
- for _, ch := range m.subscribers {
- subs = append(subs, ch)
- }
- m.subMutex.RUnlock()
-
- for _, ch := range subs {
- select {
- case ch <- state:
- default:
- }
- }
- }
-}
-
-func stateEqual(a, b *State) bool {
- if a == nil || b == nil {
- return false
- }
- if a.Enabled != b.Enabled {
- return false
- }
- if len(a.History) != len(b.History) {
- return false
- }
- return true
-}
-
-func (m *Manager) GetHistory() []Entry {
- if m.db == nil {
- return nil
- }
-
- cfg := m.getConfig()
- var cutoff time.Time
- if cfg.AutoClearDays > 0 {
- cutoff = time.Now().AddDate(0, 0, -cfg.AutoClearDays)
- }
-
- var history []Entry
- var stale []uint64
-
- if err := m.db.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- c := b.Cursor()
-
- for k, v := c.Last(); k != nil; k, v = c.Prev() {
- entry, err := decodeEntry(v)
- if err != nil {
- continue
- }
- if !cutoff.IsZero() && entry.Timestamp.Before(cutoff) {
- stale = append(stale, entry.ID)
- continue
- }
- history = append(history, entry)
- }
- return nil
- }); err != nil {
- log.Errorf("Failed to read clipboard history: %v", err)
- }
-
- if len(stale) > 0 {
- go m.deleteStaleEntries(stale)
- }
-
- return history
-}
-
-func (m *Manager) deleteStaleEntries(ids []uint64) {
- if m.db == nil {
- return
- }
-
- if err := m.db.Update(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- for _, id := range ids {
- if err := b.Delete(itob(id)); err != nil {
- log.Errorf("Failed to delete stale entry %d: %v", id, err)
- }
- }
- return nil
- }); err != nil {
- log.Errorf("Failed to delete stale entries: %v", err)
- }
-}
-
-func (m *Manager) GetEntry(id uint64) (*Entry, error) {
- if m.db == nil {
- return nil, fmt.Errorf("database not available")
- }
-
- var entry Entry
- var found bool
-
- err := m.db.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- v := b.Get(itob(id))
- if v == nil {
- return nil
- }
-
- var err error
- entry, err = decodeEntry(v)
- if err != nil {
- return err
- }
- found = true
- return nil
- })
-
- if err != nil {
- return nil, err
- }
- if !found {
- return nil, fmt.Errorf("entry not found")
- }
-
- return &entry, nil
-}
-
-func (m *Manager) DeleteEntry(id uint64) error {
- if m.db == nil {
- return fmt.Errorf("database not available")
- }
-
- err := m.db.Update(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- return b.Delete(itob(id))
- })
-
- if err == nil {
- m.updateState()
- m.notifySubscribers()
- }
-
- return err
-}
-
-func (m *Manager) TouchEntry(id uint64) error {
- if m.db == nil {
- return fmt.Errorf("database not available")
- }
-
- entry, err := m.GetEntry(id)
- if err != nil {
- return err
- }
-
- entry.Timestamp = time.Now()
-
- if err := m.storeEntry(*entry); err != nil {
- return err
- }
-
- m.updateState()
- m.notifySubscribers()
-
- return nil
-}
-
-func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
- if m.db == nil {
- return fmt.Errorf("database not available")
- }
-
- // Create a new unpinned entry with the same data
- newEntry := Entry{
- Data: pinnedEntry.Data,
- MimeType: pinnedEntry.MimeType,
- Size: pinnedEntry.Size,
- Timestamp: time.Now(),
- IsImage: pinnedEntry.IsImage,
- Preview: pinnedEntry.Preview,
- Pinned: false,
- }
-
- if err := m.storeEntryWithoutDedup(newEntry); err != nil {
- return err
- }
-
- m.updateState()
- m.notifySubscribers()
-
- return nil
-}
-
-func (m *Manager) storeEntryWithoutDedup(entry Entry) error {
- if m.db == nil {
- return fmt.Errorf("database not available")
- }
-
- entry.Hash = computeHash(entry.Data)
-
- return m.db.Update(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
-
- 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 m.trimLengthInTx(b)
- })
-}
-
-func (m *Manager) ClearHistory() {
- if m.db == nil {
- return
- }
-
- // Delete only non-pinned entries
- if err := m.db.Update(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- if b == nil {
- return nil
- }
-
- var toDelete [][]byte
- c := b.Cursor()
- for k, v := c.First(); k != nil; k, v = c.Next() {
- entry, err := decodeEntry(v)
- if err != nil || !entry.Pinned {
- toDelete = append(toDelete, k)
- }
- }
-
- for _, k := range toDelete {
- if err := b.Delete(k); err != nil {
- return err
- }
- }
- return nil
- }); err != nil {
- log.Errorf("Failed to clear clipboard history: %v", err)
- return
- }
-
- pinnedCount := 0
- if err := m.db.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- if b != nil {
- c := b.Cursor()
- for k, v := c.First(); k != nil; k, v = c.Next() {
- entry, _ := decodeEntry(v)
- if entry.Pinned {
- pinnedCount++
- }
- }
- }
- return nil
- }); err != nil {
- log.Errorf("Failed to count pinned entries: %v", err)
- }
-
- if pinnedCount == 0 {
- if err := m.compactDB(); err != nil {
- log.Errorf("Failed to compact database: %v", err)
- }
- }
-
- m.updateState()
- m.notifySubscribers()
-}
-
-func (m *Manager) compactDB() error {
- m.db.Close()
-
- tmpPath := m.dbPath + ".compact"
- defer os.Remove(tmpPath)
-
- srcDB, err := bolt.Open(m.dbPath, 0o644, &bolt.Options{ReadOnly: true, Timeout: time.Second})
- if err != nil {
- m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
- return fmt.Errorf("open source: %w", err)
- }
-
- dstDB, err := bolt.Open(tmpPath, 0o644, &bolt.Options{Timeout: time.Second})
- if err != nil {
- srcDB.Close()
- m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
- return fmt.Errorf("open destination: %w", err)
- }
-
- if err := bolt.Compact(dstDB, srcDB, 0); err != nil {
- srcDB.Close()
- dstDB.Close()
- m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
- return fmt.Errorf("compact: %w", err)
- }
-
- srcDB.Close()
- dstDB.Close()
-
- if err := os.Rename(tmpPath, m.dbPath); err != nil {
- m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
- return fmt.Errorf("rename: %w", err)
- }
-
- m.db, err = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
- if err != nil {
- return fmt.Errorf("reopen: %w", err)
- }
-
- return nil
-}
-
-func (m *Manager) SetClipboard(data []byte, mimeType string) error {
- if int64(len(data)) > m.config.MaxEntrySize {
- return fmt.Errorf("data too large")
- }
-
- dataCopy := make([]byte, len(data))
- copy(dataCopy, data)
-
- m.post(func() {
- if m.dataControlMgr == nil || m.dataDevice == nil {
- log.Error("Data control manager or device not initialized")
- return
- }
-
- dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
-
- source, err := dataMgr.CreateDataSource()
- if err != nil {
- log.Errorf("Failed to create data source: %v", err)
- return
- }
-
- if err := source.Offer(mimeType); err != nil {
- log.Errorf("Failed to offer mime type: %v", err)
- return
- }
-
- source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
- fd := e.Fd
- defer syscall.Close(fd)
-
- file := os.NewFile(uintptr(fd), "clipboard-pipe")
- defer file.Close()
-
- if _, err := file.Write(dataCopy); err != nil {
- log.Errorf("Failed to write clipboard data: %v", err)
- }
- })
-
- source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
- m.ownerLock.Lock()
- m.isOwner = false
- m.ownerLock.Unlock()
- })
-
- m.currentSource = source
- m.sourceMutex.Lock()
- m.sourceMimeTypes = []string{mimeType}
- m.sourceMutex.Unlock()
-
- m.ownerLock.Lock()
- m.isOwner = true
- m.ownerLock.Unlock()
-
- device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
- if err := device.SetSelection(source); err != nil {
- log.Errorf("Failed to set selection: %v", err)
- }
- })
-
- return nil
-}
-
-func (m *Manager) CopyText(text string) error {
- if err := m.SetClipboard([]byte(text), "text/plain;charset=utf-8"); err != nil {
- return err
- }
-
- entry := Entry{
- Data: []byte(text),
- MimeType: "text/plain;charset=utf-8",
- Size: len(text),
- Timestamp: time.Now(),
- IsImage: false,
- Preview: m.textPreview([]byte(text)),
- }
-
- if err := m.storeEntry(entry); err != nil {
- log.Errorf("Failed to store clipboard entry: %v", err)
- }
-
- m.updateState()
- m.notifySubscribers()
-
- return nil
-}
-
-func (m *Manager) PasteText() (string, error) {
- history := m.GetHistory()
- if len(history) == 0 {
- return "", fmt.Errorf("no clipboard data available")
- }
-
- entry := history[0]
- if entry.IsImage {
- return "", fmt.Errorf("clipboard contains image, not text")
- }
-
- fullEntry, err := m.GetEntry(entry.ID)
- if err != nil {
- return "", err
- }
-
- return string(fullEntry.Data), nil
-}
-
-func (m *Manager) Close() {
- if !m.alive {
- return
- }
-
- m.alive = false
- close(m.stopChan)
-
- close(m.dirty)
- m.notifierWg.Wait()
-
- m.subMutex.Lock()
- for _, ch := range m.subscribers {
- close(ch)
- }
- m.subscribers = make(map[string]chan State)
- m.subMutex.Unlock()
-
- if m.currentSource != nil {
- source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
- source.Destroy()
- }
-
- if m.dataDevice != nil {
- device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
- device.Destroy()
- }
-
- if m.dataControlMgr != nil {
- mgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
- mgr.Destroy()
- }
-
- if m.registry != nil {
- m.registry.Destroy()
- }
-
- if m.db != nil {
- m.db.Close()
- }
-}
-
-func (m *Manager) clearHistoryInternal() error {
- return m.db.Update(func(tx *bolt.Tx) error {
- if err := tx.DeleteBucket([]byte("clipboard")); err != nil {
- return err
- }
- _, err := tx.CreateBucket([]byte("clipboard"))
- return err
- })
-}
-
-func (m *Manager) clearOldEntries(days int) error {
- cutoff := time.Now().AddDate(0, 0, -days)
-
- return m.db.Update(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- if b == nil {
- return nil
- }
-
- var toDelete [][]byte
- c := b.Cursor()
- for k, v := c.First(); k != nil; k, v = c.Next() {
- entry, err := decodeEntry(v)
- if err != nil {
- continue
- }
- // Skip pinned entries
- if entry.Pinned {
- continue
- }
- if entry.Timestamp.Before(cutoff) {
- toDelete = append(toDelete, k)
- }
- }
-
- for _, k := range toDelete {
- if err := b.Delete(k); err != nil {
- return err
- }
- }
- return nil
- })
-}
-
-func (m *Manager) migrateHashes() error {
- if m.db == nil {
- return nil
- }
-
- var needsMigration bool
- if err := m.db.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- if b == nil {
- return nil
- }
- c := b.Cursor()
- for k, v := c.First(); k != nil; k, v = c.Next() {
- if extractHash(v) == 0 {
- needsMigration = true
- return nil
- }
- }
- return nil
- }); err != nil {
- return err
- }
-
- if !needsMigration {
- return nil
- }
-
- log.Info("Migrating clipboard entries to add hashes...")
-
- return m.db.Update(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- if b == nil {
- return nil
- }
-
- var updates []struct {
- key []byte
- entry Entry
- }
-
- c := b.Cursor()
- for k, v := c.First(); k != nil; k, v = c.Next() {
- entry, err := decodeEntry(v)
- if err != nil {
- continue
- }
- if entry.Hash != 0 {
- continue
- }
- entry.Hash = computeHash(entry.Data)
- keyCopy := make([]byte, len(k))
- copy(keyCopy, k)
- updates = append(updates, struct {
- key []byte
- entry Entry
- }{keyCopy, entry})
- }
-
- for _, u := range updates {
- encoded, err := encodeEntry(u.entry)
- if err != nil {
- continue
- }
- if err := b.Put(u.key, encoded); err != nil {
- return err
- }
- }
-
- log.Infof("Migrated %d clipboard entries", len(updates))
- return nil
- })
-}
-
-func (m *Manager) Search(params SearchParams) SearchResult {
- if m.db == nil {
- return SearchResult{}
- }
-
- if params.Limit <= 0 {
- params.Limit = 50
- }
- if params.Limit > 500 {
- params.Limit = 500
- }
-
- query := strings.ToLower(params.Query)
- mimeFilter := strings.ToLower(params.MimeType)
-
- var all []Entry
- if err := m.db.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- if b == nil {
- return nil
- }
-
- c := b.Cursor()
- for k, v := c.Last(); k != nil; k, v = c.Prev() {
- entry, err := decodeEntry(v)
- if err != nil {
- continue
- }
-
- if params.IsImage != nil && entry.IsImage != *params.IsImage {
- continue
- }
-
- if mimeFilter != "" && !strings.Contains(strings.ToLower(entry.MimeType), mimeFilter) {
- continue
- }
-
- if params.Before != nil && entry.Timestamp.Unix() >= *params.Before {
- continue
- }
-
- if params.After != nil && entry.Timestamp.Unix() <= *params.After {
- continue
- }
-
- if query != "" && !strings.Contains(strings.ToLower(entry.Preview), query) {
- continue
- }
-
- entry.Data = nil
- all = append(all, entry)
- }
- return nil
- }); err != nil {
- log.Errorf("Search failed: %v", err)
- }
-
- total := len(all)
-
- start := params.Offset
- if start > total {
- start = total
- }
- end := start + params.Limit
- if end > total {
- end = total
- }
-
- return SearchResult{
- Entries: all[start:end],
- Total: total,
- HasMore: end < total,
- }
-}
-
-func (m *Manager) GetConfig() Config {
- return m.config
-}
-
-func (m *Manager) SetConfig(cfg Config) error {
- m.configMutex.Lock()
- m.config = cfg
- m.configMutex.Unlock()
-
- m.updateState()
- m.notifySubscribers()
-
- return SaveConfig(cfg)
-}
-
-func (m *Manager) getConfig() Config {
- m.configMutex.RLock()
- defer m.configMutex.RUnlock()
- return m.config
-}
-
-func (m *Manager) watchConfig() {
- if m.configPath == "" {
- return
- }
-
- watcher, err := fsnotify.NewWatcher()
- if err != nil {
- log.Warnf("Failed to create config watcher: %v", err)
- return
- }
- defer watcher.Close()
-
- configDir := filepath.Dir(m.configPath)
- if err := watcher.Add(configDir); err != nil {
- log.Warnf("Failed to watch config directory: %v", err)
- return
- }
-
- for {
- select {
- case <-m.stopChan:
- return
- case event, ok := <-watcher.Events:
- if !ok {
- return
- }
- if event.Name != m.configPath {
- continue
- }
- if event.Op&(fsnotify.Write|fsnotify.Create) == 0 {
- continue
- }
- newCfg := LoadConfig()
- m.applyConfigChange(newCfg)
- case err, ok := <-watcher.Errors:
- if !ok {
- return
- }
- log.Warnf("Config watcher error: %v", err)
- }
- }
-}
-
-func (m *Manager) applyConfigChange(newCfg Config) {
- m.configMutex.Lock()
- oldCfg := m.config
- m.config = newCfg
- m.configMutex.Unlock()
-
- switch {
- case newCfg.Disabled && !oldCfg.Disabled:
- log.Info("Clipboard tracking disabled")
- case !newCfg.Disabled && oldCfg.Disabled:
- log.Info("Clipboard tracking enabled")
- }
-
- m.updateState()
- m.notifySubscribers()
-}
-
-func (m *Manager) StoreData(data []byte, mimeType string) error {
- cfg := m.getConfig()
-
- if cfg.Disabled {
- return fmt.Errorf("clipboard tracking disabled")
- }
-
- if m.db == nil {
- return fmt.Errorf("database not available")
- }
-
- if len(data) == 0 {
- return nil
- }
-
- if int64(len(data)) > cfg.MaxEntrySize {
- return fmt.Errorf("data too large")
- }
-
- if len(bytes.TrimSpace(data)) == 0 {
- return nil
- }
-
- entry := Entry{
- Data: data,
- MimeType: mimeType,
- Size: len(data),
- Timestamp: time.Now(),
- IsImage: m.isImageMimeType(mimeType),
- }
-
- switch {
- case entry.IsImage:
- entry.Preview = m.imagePreview(data, mimeType)
- case mimeType == "text/uri-list":
- entry.Preview, entry.IsImage = m.uriListPreview(data)
- default:
- entry.Preview = m.textPreview(data)
- }
-
- if err := m.storeEntry(entry); err != nil {
- return err
- }
-
- m.updateState()
- m.notifySubscribers()
-
- return nil
-}
-
-func (m *Manager) PinEntry(id uint64) error {
- if m.db == nil {
- return fmt.Errorf("database not available")
- }
-
- entryToPin, err := m.GetEntry(id)
- if err != nil {
- return err
- }
-
- var hashExists bool
- if err := m.db.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- if b == nil {
- return nil
- }
- c := b.Cursor()
- for k, v := c.First(); k != nil; k, v = c.Next() {
- entry, err := decodeEntry(v)
- if err != nil || !entry.Pinned {
- continue
- }
- if entry.Hash == entryToPin.Hash {
- hashExists = true
- return nil
- }
- }
- return nil
- }); err != nil {
- return err
- }
-
- if hashExists {
- return nil
- }
-
- // Check pinned count
- cfg := m.getConfig()
- pinnedCount := 0
- if err := m.db.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- if b == nil {
- return nil
- }
- c := b.Cursor()
- for k, v := c.First(); k != nil; k, v = c.Next() {
- entry, err := decodeEntry(v)
- if err == nil && entry.Pinned {
- pinnedCount++
- }
- }
- return nil
- }); err != nil {
- log.Errorf("Failed to count pinned entries: %v", err)
- }
-
- if pinnedCount >= cfg.MaxPinned {
- return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned)
- }
-
- err = m.db.Update(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- v := b.Get(itob(id))
- if v == nil {
- return fmt.Errorf("entry not found")
- }
-
- entry, err := decodeEntry(v)
- if err != nil {
- return err
- }
-
- entry.Pinned = true
- encoded, err := encodeEntry(entry)
- if err != nil {
- return err
- }
-
- return b.Put(itob(id), encoded)
- })
-
- if err == nil {
- m.updateState()
- m.notifySubscribers()
- }
-
- return err
-}
-
-func (m *Manager) UnpinEntry(id uint64) error {
- if m.db == nil {
- return fmt.Errorf("database not available")
- }
-
- err := m.db.Update(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- v := b.Get(itob(id))
- if v == nil {
- return fmt.Errorf("entry not found")
- }
-
- entry, err := decodeEntry(v)
- if err != nil {
- return err
- }
-
- entry.Pinned = false
- encoded, err := encodeEntry(entry)
- if err != nil {
- return err
- }
-
- return b.Put(itob(id), encoded)
- })
-
- if err == nil {
- m.updateState()
- m.notifySubscribers()
- }
-
- return err
-}
-
-func (m *Manager) GetPinnedEntries() []Entry {
- if m.db == nil {
- return nil
- }
-
- var pinned []Entry
- if err := m.db.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- if b == nil {
- return nil
- }
-
- c := b.Cursor()
- for k, v := c.Last(); k != nil; k, v = c.Prev() {
- entry, err := decodeEntry(v)
- if err != nil {
- continue
- }
- if entry.Pinned {
- entry.Data = nil
- pinned = append(pinned, entry)
- }
- }
- return nil
- }); err != nil {
- log.Errorf("Failed to get pinned entries: %v", err)
- }
-
- return pinned
-}
-
-func (m *Manager) GetPinnedCount() int {
- if m.db == nil {
- return 0
- }
-
- count := 0
- if err := m.db.View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte("clipboard"))
- if b == nil {
- return nil
- }
-
- c := b.Cursor()
- for k, v := c.First(); k != nil; k, v = c.Next() {
- entry, err := decodeEntry(v)
- if err == nil && entry.Pinned {
- count++
- }
- }
- return nil
- }); err != nil {
- log.Errorf("Failed to count pinned entries: %v", err)
- }
-
- return count
-}
-
-func (m *Manager) CopyFile(filePath string) error {
- fileInfo, err := os.Stat(filePath)
- if err != nil {
- return fmt.Errorf("file not found: %w", err)
- }
-
- cfg := m.getConfig()
- if fileInfo.Size() > cfg.MaxEntrySize {
- return fmt.Errorf("file too large: %d > %d", fileInfo.Size(), cfg.MaxEntrySize)
- }
-
- fileData, err := os.ReadFile(filePath)
- if err != nil {
- return fmt.Errorf("read file: %w", err)
- }
-
- exportedPath, err := m.ExportFileForFlatpak(filePath)
- if err != nil {
- exportedPath = filePath
- }
- fileURI := "file://" + exportedPath
-
- if imgData, imgMime, ok := m.tryReadImageFromURI([]byte("file://" + filePath)); ok {
- entry := Entry{
- Data: imgData,
- MimeType: imgMime,
- Size: len(imgData),
- Timestamp: time.Now(),
- IsImage: true,
- Preview: m.imagePreview(imgData, imgMime),
- }
- if err := m.storeEntry(entry); err != nil {
- log.Errorf("Failed to store file entry: %v", err)
- }
- } else {
- entry := Entry{
- Data: fileData,
- MimeType: "text/uri-list",
- Size: len(fileData),
- Timestamp: time.Now(),
- IsImage: false,
- Preview: fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)),
- }
- if err := m.storeEntry(entry); err != nil {
- log.Errorf("Failed to store file entry: %v", err)
- }
- }
-
- m.updateState()
- m.notifySubscribers()
-
- _, imgMime, imgErr := image.DecodeConfig(bytes.NewReader(fileData))
-
- m.post(func() {
- if m.dataControlMgr == nil || m.dataDevice == nil {
- log.Error("Data control manager or device not initialized")
- return
- }
-
- dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
- source, err := dataMgr.CreateDataSource()
- if err != nil {
- log.Errorf("Failed to create data source: %v", err)
- return
- }
-
- type offer struct {
- mime string
- data []byte
- }
- offers := []offer{
- {"x-special/gnome-copied-files", []byte("copy\n" + fileURI)},
- {"text/uri-list", []byte(fileURI + "\r\n")},
- {"text/plain", []byte(filePath)},
- }
-
- if imgErr == nil {
- imgMimeType := "image/" + imgMime
- offers = append(offers, offer{imgMimeType, fileData})
- }
-
- offerData := make(map[string][]byte)
- for _, o := range offers {
- if err := source.Offer(o.mime); err != nil {
- log.Errorf("Failed to offer %s: %v", o.mime, err)
- return
- }
- offerData[o.mime] = o.data
- }
-
- source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
- fd := e.Fd
- defer syscall.Close(fd)
- file := os.NewFile(uintptr(fd), "clipboard-pipe")
- defer file.Close()
- if data, ok := offerData[e.MimeType]; ok {
- file.Write(data)
- }
- })
-
- source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
- m.ownerLock.Lock()
- m.isOwner = false
- m.ownerLock.Unlock()
- })
-
- m.currentSource = source
-
- m.ownerLock.Lock()
- m.isOwner = true
- m.ownerLock.Unlock()
-
- device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
- if err := device.SetSelection(source); err != nil {
- log.Errorf("Failed to set selection: %v", err)
- }
- })
-
- return nil
-}
-
-func (m *Manager) EntryToFile(entry *Entry) string {
- switch {
- case entry.MimeType == "text/uri-list":
- data := strings.TrimSpace(string(entry.Data))
- lines := strings.Split(data, "\n")
- if len(lines) == 0 {
- return ""
- }
- uri := strings.TrimSuffix(strings.TrimSpace(lines[0]), "\r")
- if path, ok := strings.CutPrefix(uri, "file://"); ok {
- return path
- }
- case entry.IsImage:
- ext := ".png"
- if suffix, ok := strings.CutPrefix(entry.MimeType, "image/"); ok {
- ext = "." + suffix
- }
- cacheDir, err := os.UserCacheDir()
- if err != nil {
- return ""
- }
- clipDir := filepath.Join(cacheDir, "dms", "clipboard")
- if err := os.MkdirAll(clipDir, 0o755); err != nil {
- return ""
- }
- filePath := filepath.Join(clipDir, fmt.Sprintf("%d%s", time.Now().UnixNano(), ext))
- if os.WriteFile(filePath, entry.Data, 0o644) != nil {
- return ""
- }
- return filePath
- }
- return ""
-}
-
-func (m *Manager) ExportFileForFlatpak(filePath string) (string, error) {
- if _, err := os.Stat(filePath); err != nil {
- return "", fmt.Errorf("file not found: %w", err)
- }
-
- if m.dbusConn == nil {
- conn, err := dbus.ConnectSessionBus()
- if err != nil {
- return "", fmt.Errorf("connect session bus: %w", err)
- }
- if !conn.SupportsUnixFDs() {
- conn.Close()
- return "", fmt.Errorf("D-Bus connection does not support Unix FD passing")
- }
- m.dbusConn = conn
- }
-
- file, err := os.Open(filePath)
- if err != nil {
- return "", fmt.Errorf("open file: %w", err)
- }
- fd := int(file.Fd())
-
- portal := m.dbusConn.Object("org.freedesktop.portal.Documents", "/org/freedesktop/portal/documents")
-
- var docIds []string
- var extra map[string]dbus.Variant
- flags := uint32(0)
-
- err = portal.Call(
- "org.freedesktop.portal.Documents.AddFull",
- 0,
- []dbus.UnixFD{dbus.UnixFD(fd)},
- flags,
- "",
- []string{},
- ).Store(&docIds, &extra)
-
- file.Close()
-
- if err != nil {
- return "", fmt.Errorf("AddFull: %w", err)
- }
-
- if len(docIds) == 0 {
- return "", fmt.Errorf("no doc IDs returned")
- }
-
- docId := docIds[0]
-
- for _, app := range getInstalledFlatpaks() {
- _ = portal.Call(
- "org.freedesktop.portal.Documents.GrantPermissions",
- 0,
- docId,
- app,
- []string{"read"},
- ).Err
- }
-
- uid := os.Getuid()
- basename := filepath.Base(filePath)
- exportedPath := fmt.Sprintf("/run/user/%d/doc/%s/%s", uid, docId, basename)
-
- return exportedPath, nil
-}
-
-func getInstalledFlatpaks() []string {
- out, err := exec.Command("flatpak", "list", "--app", "--columns=application").Output()
- if err != nil {
- return nil
- }
-
- var apps []string
- for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
- if app := strings.TrimSpace(line); app != "" {
- apps = append(apps, app)
- }
- }
- return apps
-}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager_test.go
deleted file mode 100644
index db45bca..0000000
--- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/manager_test.go
+++ /dev/null
@@ -1,531 +0,0 @@
-package clipboard
-
-import (
- "sync"
- "sync/atomic"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
-
- mocks_wlcontext "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlcontext"
-)
-
-func TestEncodeDecodeEntry_Roundtrip(t *testing.T) {
- original := Entry{
- ID: 12345,
- Data: []byte("hello world"),
- MimeType: "text/plain;charset=utf-8",
- Preview: "hello world",
- Size: 11,
- Timestamp: time.Now().Truncate(time.Second),
- IsImage: false,
- }
-
- encoded, err := encodeEntry(original)
- assert.NoError(t, err)
-
- decoded, err := decodeEntry(encoded)
- assert.NoError(t, err)
-
- assert.Equal(t, original.ID, decoded.ID)
- assert.Equal(t, original.Data, decoded.Data)
- assert.Equal(t, original.MimeType, decoded.MimeType)
- assert.Equal(t, original.Preview, decoded.Preview)
- assert.Equal(t, original.Size, decoded.Size)
- assert.Equal(t, original.Timestamp.Unix(), decoded.Timestamp.Unix())
- assert.Equal(t, original.IsImage, decoded.IsImage)
-}
-
-func TestEncodeDecodeEntry_Image(t *testing.T) {
- original := Entry{
- ID: 99999,
- Data: []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A},
- MimeType: "image/png",
- Preview: "[[ image 8 B png 100x100 ]]",
- Size: 8,
- Timestamp: time.Now().Truncate(time.Second),
- IsImage: true,
- }
-
- encoded, err := encodeEntry(original)
- assert.NoError(t, err)
-
- decoded, err := decodeEntry(encoded)
- assert.NoError(t, err)
-
- assert.Equal(t, original.ID, decoded.ID)
- assert.Equal(t, original.Data, decoded.Data)
- assert.True(t, decoded.IsImage)
- assert.Equal(t, original.Preview, decoded.Preview)
-}
-
-func TestEncodeDecodeEntry_EmptyData(t *testing.T) {
- original := Entry{
- ID: 1,
- Data: []byte{},
- MimeType: "text/plain",
- Preview: "",
- Size: 0,
- Timestamp: time.Now().Truncate(time.Second),
- IsImage: false,
- }
-
- encoded, err := encodeEntry(original)
- assert.NoError(t, err)
-
- decoded, err := decodeEntry(encoded)
- assert.NoError(t, err)
-
- assert.Equal(t, original.ID, decoded.ID)
- assert.Empty(t, decoded.Data)
-}
-
-func TestEncodeDecodeEntry_LargeData(t *testing.T) {
- largeData := make([]byte, 100000)
- for i := range largeData {
- largeData[i] = byte(i % 256)
- }
-
- original := Entry{
- ID: 777,
- Data: largeData,
- MimeType: "application/octet-stream",
- Preview: "binary data...",
- Size: len(largeData),
- Timestamp: time.Now().Truncate(time.Second),
- IsImage: false,
- }
-
- encoded, err := encodeEntry(original)
- assert.NoError(t, err)
-
- decoded, err := decodeEntry(encoded)
- assert.NoError(t, err)
-
- assert.Equal(t, original.Data, decoded.Data)
- assert.Equal(t, original.Size, decoded.Size)
-}
-
-func TestStateEqual_BothNil(t *testing.T) {
- assert.False(t, stateEqual(nil, nil))
-}
-
-func TestStateEqual_OneNil(t *testing.T) {
- s := &State{Enabled: true}
- assert.False(t, stateEqual(s, nil))
- assert.False(t, stateEqual(nil, s))
-}
-
-func TestStateEqual_EnabledDiffers(t *testing.T) {
- a := &State{Enabled: true, History: []Entry{}}
- b := &State{Enabled: false, History: []Entry{}}
- assert.False(t, stateEqual(a, b))
-}
-
-func TestStateEqual_HistoryLengthDiffers(t *testing.T) {
- a := &State{Enabled: true, History: []Entry{{ID: 1}}}
- b := &State{Enabled: true, History: []Entry{}}
- assert.False(t, stateEqual(a, b))
-}
-
-func TestStateEqual_BothEqual(t *testing.T) {
- a := &State{Enabled: true, History: []Entry{{ID: 1}, {ID: 2}}}
- b := &State{Enabled: true, History: []Entry{{ID: 3}, {ID: 4}}}
- assert.True(t, stateEqual(a, b))
-}
-
-func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
- m := &Manager{
- subscribers: make(map[string]chan State),
- dirty: make(chan struct{}, 1),
- }
-
- var wg sync.WaitGroup
- const goroutines = 20
-
- for i := 0; i < goroutines; i++ {
- wg.Add(1)
- go func(id int) {
- defer wg.Done()
- subID := string(rune('a' + id))
- ch := m.Subscribe(subID)
- assert.NotNil(t, ch)
- time.Sleep(time.Millisecond)
- m.Unsubscribe(subID)
- }(i)
- }
-
- wg.Wait()
-}
-
-func TestManager_ConcurrentGetState(t *testing.T) {
- m := &Manager{
- state: &State{
- Enabled: true,
- History: []Entry{{ID: 1}, {ID: 2}},
- },
- }
-
- var wg sync.WaitGroup
- const goroutines = 50
- const iterations = 100
-
- for i := 0; i < goroutines/2; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- for j := 0; j < iterations; j++ {
- s := m.GetState()
- _ = s.Enabled
- _ = len(s.History)
- }
- }()
- }
-
- for i := 0; i < goroutines/2; i++ {
- wg.Add(1)
- go func(i int) {
- defer wg.Done()
- for j := 0; j < iterations; j++ {
- m.stateMutex.Lock()
- m.state = &State{
- Enabled: j%2 == 0,
- History: []Entry{{ID: uint64(j)}},
- }
- m.stateMutex.Unlock()
- }
- }(i)
- }
-
- wg.Wait()
-}
-
-func TestManager_ConcurrentConfigAccess(t *testing.T) {
- m := &Manager{
- config: DefaultConfig(),
- }
-
- var wg sync.WaitGroup
- const goroutines = 30
- const iterations = 100
-
- for i := 0; i < goroutines/2; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- for j := 0; j < iterations; j++ {
- cfg := m.getConfig()
- _ = cfg.MaxHistory
- _ = cfg.MaxEntrySize
- }
- }()
- }
-
- for i := 0; i < goroutines/2; i++ {
- wg.Add(1)
- go func(i int) {
- defer wg.Done()
- for j := 0; j < iterations; j++ {
- m.configMutex.Lock()
- m.config.MaxHistory = 50 + j
- m.config.MaxEntrySize = int64(1024 * j)
- m.configMutex.Unlock()
- }
- }(i)
- }
-
- wg.Wait()
-}
-
-func TestManager_NotifySubscribersNonBlocking(t *testing.T) {
- m := &Manager{
- dirty: make(chan struct{}, 1),
- }
-
- for i := 0; i < 10; i++ {
- m.notifySubscribers()
- }
-
- assert.Len(t, m.dirty, 1)
-}
-
-func TestManager_ConcurrentOfferAccess(t *testing.T) {
- m := &Manager{
- offerMimeTypes: make(map[any][]string),
- offerRegistry: make(map[uint32]any),
- }
-
- var wg sync.WaitGroup
- const goroutines = 20
- const iterations = 50
-
- for i := 0; i < goroutines; i++ {
- wg.Add(1)
- go func(id int) {
- defer wg.Done()
- key := uint32(id)
-
- for j := 0; j < iterations; j++ {
- m.offerMutex.Lock()
- m.offerRegistry[key] = struct{}{}
- m.offerMimeTypes[key] = []string{"text/plain"}
- m.offerMutex.Unlock()
-
- m.offerMutex.RLock()
- _ = m.offerRegistry[key]
- _ = m.offerMimeTypes[key]
- m.offerMutex.RUnlock()
-
- m.offerMutex.Lock()
- delete(m.offerRegistry, key)
- delete(m.offerMimeTypes, key)
- m.offerMutex.Unlock()
- }
- }(i)
- }
-
- wg.Wait()
-}
-
-func TestManager_ConcurrentPersistAccess(t *testing.T) {
- m := &Manager{
- persistData: make(map[string][]byte),
- persistMimeTypes: []string{},
- }
-
- var wg sync.WaitGroup
- const goroutines = 20
- const iterations = 50
-
- for i := 0; i < goroutines/2; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- for j := 0; j < iterations; j++ {
- m.persistMutex.RLock()
- _ = m.persistData
- _ = m.persistMimeTypes
- m.persistMutex.RUnlock()
- }
- }()
- }
-
- for i := 0; i < goroutines/2; i++ {
- wg.Add(1)
- go func(id int) {
- defer wg.Done()
- for j := 0; j < iterations; j++ {
- m.persistMutex.Lock()
- m.persistMimeTypes = []string{"text/plain", "text/html"}
- m.persistData = map[string][]byte{
- "text/plain": []byte("test"),
- }
- m.persistMutex.Unlock()
- }
- }(i)
- }
-
- wg.Wait()
-}
-
-func TestManager_ConcurrentOwnerAccess(t *testing.T) {
- m := &Manager{}
-
- var wg sync.WaitGroup
- const goroutines = 30
- const iterations = 100
-
- for i := 0; i < goroutines/2; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- for j := 0; j < iterations; j++ {
- m.ownerLock.Lock()
- _ = m.isOwner
- m.ownerLock.Unlock()
- }
- }()
- }
-
- for i := 0; i < goroutines/2; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- for j := 0; j < iterations; j++ {
- m.ownerLock.Lock()
- m.isOwner = j%2 == 0
- m.ownerLock.Unlock()
- }
- }()
- }
-
- wg.Wait()
-}
-
-func TestItob(t *testing.T) {
- tests := []struct {
- input uint64
- expected []byte
- }{
- {0, []byte{0, 0, 0, 0, 0, 0, 0, 0}},
- {1, []byte{0, 0, 0, 0, 0, 0, 0, 1}},
- {256, []byte{0, 0, 0, 0, 0, 0, 1, 0}},
- {0xFFFFFFFFFFFFFFFF, []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}},
- }
-
- for _, tt := range tests {
- result := itob(tt.input)
- assert.Equal(t, tt.expected, result)
- }
-}
-
-func TestSizeStr(t *testing.T) {
- tests := []struct {
- input int
- expected string
- }{
- {0, "0 B"},
- {100, "100 B"},
- {1024, "1 KiB"},
- {2048, "2 KiB"},
- {1048576, "1 MiB"},
- {5242880, "5 MiB"},
- }
-
- for _, tt := range tests {
- result := sizeStr(tt.input)
- assert.Equal(t, tt.expected, result)
- }
-}
-
-func TestSelectMimeType(t *testing.T) {
- m := &Manager{}
-
- tests := []struct {
- mimes []string
- expected string
- }{
- {[]string{"text/plain;charset=utf-8", "text/html"}, "text/plain;charset=utf-8"},
- {[]string{"text/html", "text/plain"}, "text/plain"},
- {[]string{"text/html", "image/png"}, "image/png"},
- {[]string{"image/png", "image/jpeg"}, "image/png"},
- {[]string{"image/png"}, "image/png"},
- {[]string{"application/octet-stream"}, "application/octet-stream"},
- {[]string{}, ""},
- }
-
- for _, tt := range tests {
- result := m.selectMimeType(tt.mimes)
- assert.Equal(t, tt.expected, result)
- }
-}
-
-func TestIsImageMimeType(t *testing.T) {
- m := &Manager{}
-
- assert.True(t, m.isImageMimeType("image/png"))
- assert.True(t, m.isImageMimeType("image/jpeg"))
- assert.True(t, m.isImageMimeType("image/gif"))
- assert.False(t, m.isImageMimeType("text/plain"))
- assert.False(t, m.isImageMimeType("application/json"))
-}
-
-func TestTextPreview(t *testing.T) {
- m := &Manager{}
-
- short := m.textPreview([]byte("hello world"))
- assert.Equal(t, "hello world", short)
-
- withWhitespace := m.textPreview([]byte(" hello world "))
- assert.Equal(t, "hello world", withWhitespace)
-
- longText := make([]byte, 200)
- for i := range longText {
- longText[i] = 'a'
- }
- preview := m.textPreview(longText)
- assert.True(t, len(preview) > 100)
- assert.Contains(t, preview, "…")
-}
-
-func TestDefaultConfig(t *testing.T) {
- cfg := DefaultConfig()
- assert.Equal(t, 100, cfg.MaxHistory)
- assert.Equal(t, int64(5*1024*1024), cfg.MaxEntrySize)
- assert.Equal(t, 0, cfg.AutoClearDays)
- assert.False(t, cfg.ClearAtStartup)
- assert.False(t, cfg.Disabled)
-}
-
-func TestManager_PostDelegatesToWlContext(t *testing.T) {
- mockCtx := mocks_wlcontext.NewMockWaylandContext(t)
-
- var called atomic.Bool
- mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) {
- called.Store(true)
- fn()
- }).Once()
-
- m := &Manager{
- wlCtx: mockCtx,
- }
-
- executed := false
- m.post(func() {
- executed = true
- })
-
- assert.True(t, called.Load())
- assert.True(t, executed)
-}
-
-func TestManager_PostExecutesFunctionViaContext(t *testing.T) {
- mockCtx := mocks_wlcontext.NewMockWaylandContext(t)
-
- var capturedFn func()
- mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) {
- capturedFn = fn
- }).Times(3)
-
- m := &Manager{
- wlCtx: mockCtx,
- }
-
- counter := 0
- m.post(func() { counter++ })
- m.post(func() { counter += 10 })
- m.post(func() { counter += 100 })
-
- assert.NotNil(t, capturedFn)
- capturedFn()
- assert.Equal(t, 100, counter)
-}
-
-func TestManager_ConcurrentPostWithMock(t *testing.T) {
- mockCtx := mocks_wlcontext.NewMockWaylandContext(t)
-
- var postCount atomic.Int32
- mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) {
- postCount.Add(1)
- }).Times(100)
-
- m := &Manager{
- wlCtx: mockCtx,
- }
-
- var wg sync.WaitGroup
- for i := 0; i < 10; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- for j := 0; j < 10; j++ {
- m.post(func() {})
- }
- }()
- }
-
- wg.Wait()
- assert.Equal(t, int32(100), postCount.Load())
-}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/types.go
deleted file mode 100644
index c754561..0000000
--- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/clipboard/types.go
+++ /dev/null
@@ -1,196 +0,0 @@
-package clipboard
-
-import (
- "encoding/json"
- "os"
- "path/filepath"
- "sync"
- "time"
-
- "github.com/godbus/dbus/v5"
- 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"`
- MaxPinned int `json:"maxPinned"`
-}
-
-func DefaultConfig() Config {
- return Config{
- MaxHistory: 100,
- MaxEntrySize: 5 * 1024 * 1024,
- AutoClearDays: 0,
- ClearAtStartup: false,
- MaxPinned: 25,
- }
-}
-
-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), 0o755); err != nil {
- return err
- }
-
- data, err := json.MarshalIndent(cfg, "", " ")
- if err != nil {
- return err
- }
-
- return os.WriteFile(path, data, 0o644)
-}
-
-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"`
- Hash uint64 `json:"hash,omitempty"`
- Pinned bool `json:"pinned"`
-}
-
-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.WaylandDisplay
- wlCtx wlcontext.WaylandContext
-
- 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
-
- dbusConn *dbus.Conn
-}
-
-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:
- }
-}