1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-14 09:42:10 -04:00

clipboard: introduce native clipboard, clip-persist, clip-storage functionality

This commit is contained in:
bbedward
2025-12-11 09:41:07 -05:00
parent 7c88865d67
commit 6d62229b5f
41 changed files with 4372 additions and 547 deletions

View File

@@ -0,0 +1,215 @@
package clipboard
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
switch req.Method {
case "clipboard.getState":
handleGetState(conn, req, m)
case "clipboard.getHistory":
handleGetHistory(conn, req, m)
case "clipboard.getEntry":
handleGetEntry(conn, req, m)
case "clipboard.deleteEntry":
handleDeleteEntry(conn, req, m)
case "clipboard.clearHistory":
handleClearHistory(conn, req, m)
case "clipboard.copy":
handleCopy(conn, req, m)
case "clipboard.paste":
handlePaste(conn, req, m)
case "clipboard.subscribe":
handleSubscribe(conn, req, m)
case "clipboard.search":
handleSearch(conn, req, m)
case "clipboard.getConfig":
handleGetConfig(conn, req, m)
case "clipboard.setConfig":
handleSetConfig(conn, req, m)
case "clipboard.store":
handleStore(conn, req, m)
default:
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
}
}
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
func handleGetHistory(conn net.Conn, req models.Request, m *Manager) {
history := m.GetHistory()
for i := range history {
history[i].Data = nil
}
models.Respond(conn, req.ID, history)
}
func handleGetEntry(conn net.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
entry, err := m.GetEntry(uint64(id))
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, entry)
}
func handleDeleteEntry(conn net.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if err := m.DeleteEntry(uint64(id)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry deleted"})
}
func handleClearHistory(conn net.Conn, req models.Request, m *Manager) {
m.ClearHistory()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "history cleared"})
}
func handleCopy(conn net.Conn, req models.Request, m *Manager) {
text, err := params.String(req.Params, "text")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if err := m.CopyText(text); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
}
func handlePaste(conn net.Conn, req models.Request, m *Manager) {
text, err := m.PasteText()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, map[string]string{"text": text})
}
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
clientID := fmt.Sprintf("clipboard-%d", req.ID)
ch := m.Subscribe(clientID)
defer m.Unsubscribe(clientID)
initialState := m.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
Result: &initialState,
}); err != nil {
return
}
for state := range ch {
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
Result: &state,
}); err != nil {
return
}
}
}
func handleSearch(conn net.Conn, req models.Request, m *Manager) {
p := SearchParams{
Query: params.StringOpt(req.Params, "query", ""),
MimeType: params.StringOpt(req.Params, "mimeType", ""),
Limit: params.IntOpt(req.Params, "limit", 50),
Offset: params.IntOpt(req.Params, "offset", 0),
}
if img, ok := req.Params["isImage"].(bool); ok {
p.IsImage = &img
}
if b, ok := req.Params["before"].(float64); ok {
v := int64(b)
p.Before = &v
}
if a, ok := req.Params["after"].(float64); ok {
v := int64(a)
p.After = &v
}
models.Respond(conn, req.ID, m.Search(p))
}
func handleGetConfig(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetConfig())
}
func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
cfg := m.GetConfig()
if _, ok := req.Params["maxHistory"]; ok {
cfg.MaxHistory = params.IntOpt(req.Params, "maxHistory", cfg.MaxHistory)
}
if _, ok := req.Params["maxEntrySize"]; ok {
cfg.MaxEntrySize = int64(params.IntOpt(req.Params, "maxEntrySize", int(cfg.MaxEntrySize)))
}
if _, ok := req.Params["autoClearDays"]; ok {
cfg.AutoClearDays = params.IntOpt(req.Params, "autoClearDays", cfg.AutoClearDays)
}
if v, ok := req.Params["clearAtStartup"].(bool); ok {
cfg.ClearAtStartup = v
}
if v, ok := req.Params["disabled"].(bool); ok {
cfg.Disabled = v
}
if v, ok := req.Params["disableHistory"].(bool); ok {
cfg.DisableHistory = v
}
if v, ok := req.Params["disablePersist"].(bool); ok {
cfg.DisablePersist = v
}
if err := m.SetConfig(cfg); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "config updated"})
}
func handleStore(conn net.Conn, req models.Request, m *Manager) {
data, err := params.String(req.Params, "data")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
mimeType := params.StringOpt(req.Params, "mimeType", "text/plain;charset=utf-8")
if err := m.StoreData([]byte(data), mimeType); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"})
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,191 @@
package clipboard
import (
"encoding/json"
"os"
"path/filepath"
"sync"
"time"
bolt "go.etcd.io/bbolt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type Config struct {
MaxHistory int `json:"maxHistory"`
MaxEntrySize int64 `json:"maxEntrySize"`
AutoClearDays int `json:"autoClearDays"`
ClearAtStartup bool `json:"clearAtStartup"`
Disabled bool `json:"disabled"`
DisableHistory bool `json:"disableHistory"`
DisablePersist bool `json:"disablePersist"`
}
func DefaultConfig() Config {
return Config{
MaxHistory: 100,
MaxEntrySize: 5 * 1024 * 1024,
AutoClearDays: 0,
ClearAtStartup: false,
}
}
func getConfigPath() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(configDir, "DankMaterialShell", "clsettings.json"), nil
}
func LoadConfig() Config {
cfg := DefaultConfig()
path, err := getConfigPath()
if err != nil {
return cfg
}
data, err := os.ReadFile(path)
if err != nil {
return cfg
}
if err := json.Unmarshal(data, &cfg); err != nil {
return DefaultConfig()
}
return cfg
}
func SaveConfig(cfg Config) error {
path, err := getConfigPath()
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
type SearchParams struct {
Query string `json:"query"`
MimeType string `json:"mimeType"`
IsImage *bool `json:"isImage"`
Limit int `json:"limit"`
Offset int `json:"offset"`
Before *int64 `json:"before"`
After *int64 `json:"after"`
}
type SearchResult struct {
Entries []Entry `json:"entries"`
Total int `json:"total"`
HasMore bool `json:"hasMore"`
}
type Entry struct {
ID uint64 `json:"id"`
Data []byte `json:"data,omitempty"`
MimeType string `json:"mimeType"`
Preview string `json:"preview"`
Size int `json:"size"`
Timestamp time.Time `json:"timestamp"`
IsImage bool `json:"isImage"`
}
type State struct {
Enabled bool `json:"enabled"`
History []Entry `json:"history"`
Current *Entry `json:"current,omitempty"`
}
type Manager struct {
config Config
configMutex sync.RWMutex
configPath string
display *wlclient.Display
wlCtx *wlcontext.SharedContext
registry *wlclient.Registry
dataControlMgr any
seat *wlclient.Seat
dataDevice any
currentOffer any
currentSource any
seatName uint32
mimeTypes []string
offerMimeTypes map[any][]string
offerMutex sync.RWMutex
offerRegistry map[uint32]any
sourceMimeTypes []string
sourceMutex sync.RWMutex
persistData map[string][]byte
persistMimeTypes []string
persistMutex sync.RWMutex
isOwner bool
ownerLock sync.Mutex
initialized bool
alive bool
stopChan chan struct{}
db *bolt.DB
dbPath string
state *State
stateMutex sync.RWMutex
subscribers map[string]chan State
subMutex sync.RWMutex
dirty chan struct{}
notifierWg sync.WaitGroup
lastState *State
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
if m.state == nil {
return State{}
}
return *m.state
}
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch
}
func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok {
close(ch)
delete(m.subscribers, id)
}
m.subMutex.Unlock()
}
func (m *Manager) notifySubscribers() {
select {
case m.dirty <- struct{}{}:
default:
}
}