mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
clipboard: add cliphist-migrate CLI
This commit is contained in:
@@ -1,17 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
@@ -144,6 +156,30 @@ var (
|
||||
clipConfigEnabled bool
|
||||
)
|
||||
|
||||
var clipExportCmd = &cobra.Command{
|
||||
Use: "export [file]",
|
||||
Short: "Export clipboard history to JSON",
|
||||
Long: "Export clipboard history to JSON file. If no file specified, writes to stdout.",
|
||||
Run: runClipExport,
|
||||
}
|
||||
|
||||
var clipImportCmd = &cobra.Command{
|
||||
Use: "import <file>",
|
||||
Short: "Import clipboard history from JSON",
|
||||
Long: "Import clipboard history from JSON file exported by 'dms cl export'.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: runClipImport,
|
||||
}
|
||||
|
||||
var clipMigrateCmd = &cobra.Command{
|
||||
Use: "cliphist-migrate [db-path]",
|
||||
Short: "Migrate from cliphist",
|
||||
Long: "Migrate clipboard history from cliphist. Uses default cliphist path if not specified.",
|
||||
Run: runClipMigrate,
|
||||
}
|
||||
|
||||
var clipMigrateDelete bool
|
||||
|
||||
func init() {
|
||||
clipCopyCmd.Flags().BoolVarP(&clipCopyForeground, "foreground", "f", false, "Stay in foreground instead of forking")
|
||||
clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste")
|
||||
@@ -170,8 +206,10 @@ func init() {
|
||||
|
||||
clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)")
|
||||
|
||||
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
|
||||
|
||||
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
|
||||
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd)
|
||||
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
|
||||
}
|
||||
|
||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||
@@ -606,3 +644,154 @@ func runClipConfigSet(cmd *cobra.Command, args []string) {
|
||||
|
||||
fmt.Println("Config updated")
|
||||
}
|
||||
|
||||
func runClipExport(cmd *cobra.Command, args []string) {
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "clipboard.getHistory",
|
||||
}
|
||||
|
||||
resp, err := sendServerRequest(req)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get clipboard history: %v", err)
|
||||
}
|
||||
if resp.Error != "" {
|
||||
log.Fatalf("Error: %s", resp.Error)
|
||||
}
|
||||
if resp.Result == nil {
|
||||
log.Fatal("No clipboard history")
|
||||
}
|
||||
|
||||
out, err := json.MarshalIndent(resp.Result, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
fmt.Println(string(out))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(args[0], out, 0644); err != nil {
|
||||
log.Fatalf("Failed to write file: %v", err)
|
||||
}
|
||||
fmt.Printf("Exported to %s\n", args[0])
|
||||
}
|
||||
|
||||
func runClipImport(cmd *cobra.Command, args []string) {
|
||||
data, err := os.ReadFile(args[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read file: %v", err)
|
||||
}
|
||||
|
||||
var entries []map[string]any
|
||||
if err := json.Unmarshal(data, &entries); err != nil {
|
||||
log.Fatalf("Failed to parse JSON: %v", err)
|
||||
}
|
||||
|
||||
var imported int
|
||||
for _, entry := range entries {
|
||||
dataStr, ok := entry["data"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
mimeType, _ := entry["mimeType"].(string)
|
||||
if mimeType == "" {
|
||||
mimeType = "text/plain"
|
||||
}
|
||||
|
||||
var entryData []byte
|
||||
if decoded, err := base64.StdEncoding.DecodeString(dataStr); err == nil {
|
||||
entryData = decoded
|
||||
} else {
|
||||
entryData = []byte(dataStr)
|
||||
}
|
||||
|
||||
if err := clipboard.Store(entryData, mimeType); err != nil {
|
||||
log.Errorf("Failed to store entry: %v", err)
|
||||
continue
|
||||
}
|
||||
imported++
|
||||
}
|
||||
|
||||
fmt.Printf("Imported %d entries\n", imported)
|
||||
}
|
||||
|
||||
func runClipMigrate(cmd *cobra.Command, args []string) {
|
||||
dbPath := getCliphistPath()
|
||||
if len(args) > 0 {
|
||||
dbPath = args[0]
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dbPath); err != nil {
|
||||
log.Fatalf("Cliphist db not found: %s", dbPath)
|
||||
}
|
||||
|
||||
db, err := bolt.Open(dbPath, 0644, &bolt.Options{
|
||||
ReadOnly: true,
|
||||
Timeout: 1 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open cliphist db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var migrated int
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("b"))
|
||||
if b == nil {
|
||||
return fmt.Errorf("cliphist bucket not found")
|
||||
}
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
mimeType := detectMimeType(v)
|
||||
if err := clipboard.Store(v, mimeType); err != nil {
|
||||
log.Errorf("Failed to store entry %d: %v", btoi(k), err)
|
||||
continue
|
||||
}
|
||||
migrated++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Migration failed: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Migrated %d entries from cliphist\n", migrated)
|
||||
|
||||
if !clipMigrateDelete {
|
||||
return
|
||||
}
|
||||
|
||||
db.Close()
|
||||
if err := os.Remove(dbPath); err != nil {
|
||||
log.Errorf("Failed to delete cliphist db: %v", err)
|
||||
return
|
||||
}
|
||||
os.Remove(filepath.Dir(dbPath))
|
||||
fmt.Println("Deleted cliphist db")
|
||||
}
|
||||
|
||||
func getCliphistPath() string {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return filepath.Join(os.Getenv("HOME"), ".cache", "cliphist", "db")
|
||||
}
|
||||
return filepath.Join(cacheDir, "cliphist", "db")
|
||||
}
|
||||
|
||||
func detectMimeType(data []byte) string {
|
||||
if _, _, err := image.DecodeConfig(bytes.NewReader(data)); err == nil {
|
||||
return "image/png"
|
||||
}
|
||||
return "text/plain"
|
||||
}
|
||||
|
||||
func btoi(v []byte) uint64 {
|
||||
return binary.BigEndian.Uint64(v)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
||||
return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize)
|
||||
}
|
||||
|
||||
dbPath, err := getDBPath()
|
||||
dbPath, err := GetDBPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get db path: %w", err)
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
||||
})
|
||||
}
|
||||
|
||||
func getDBPath() (string, error) {
|
||||
func GetDBPath() (string, error) {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
@@ -121,12 +121,31 @@ func getDBPath() (string, error) {
|
||||
cacheDir = filepath.Join(homeDir, ".cache")
|
||||
}
|
||||
|
||||
dbDir := filepath.Join(cacheDir, "dms-clipboard")
|
||||
if err := os.MkdirAll(dbDir, 0700); err != nil {
|
||||
return "", err
|
||||
newDir := filepath.Join(cacheDir, "DankMaterialShell", "clipboard")
|
||||
newPath := filepath.Join(newDir, "db")
|
||||
|
||||
if _, err := os.Stat(newPath); err == nil {
|
||||
return newPath, nil
|
||||
}
|
||||
|
||||
return filepath.Join(dbDir, "db"), nil
|
||||
oldDir := filepath.Join(cacheDir, "dms-clipboard")
|
||||
oldPath := filepath.Join(oldDir, "db")
|
||||
|
||||
if _, err := os.Stat(oldPath); err == nil {
|
||||
if err := os.MkdirAll(newDir, 0700); 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, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return newPath, nil
|
||||
}
|
||||
|
||||
func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
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"
|
||||
@@ -37,7 +38,7 @@ var sensitiveMimeTypes = []string{
|
||||
|
||||
func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error) {
|
||||
display := wlCtx.Display()
|
||||
dbPath, err := getDBPath()
|
||||
dbPath, err := clipboardstore.GetDBPath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get db path: %w", err)
|
||||
}
|
||||
@@ -102,24 +103,6 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func getDBPath() (string, error) {
|
||||
cacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||
if cacheDir == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cacheDir = filepath.Join(homeDir, ".cache")
|
||||
}
|
||||
|
||||
dbDir := filepath.Join(cacheDir, "dms-clipboard")
|
||||
if err := os.MkdirAll(dbDir, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(dbDir, "db"), nil
|
||||
}
|
||||
|
||||
func openDB(path string) (*bolt.DB, error) {
|
||||
db, err := bolt.Open(path, 0644, &bolt.Options{
|
||||
Timeout: 1 * time.Second,
|
||||
|
||||
Reference in New Issue
Block a user