104 lines
2.3 KiB
Go
104 lines
2.3 KiB
Go
// Package history provides a persistent, size-bounded clipboard history store.
|
|
//
|
|
// Entries are kept in memory as a ring buffer and flushed to a JSON file so the
|
|
// `clip-sync history` command can read them even when the daemon is not running.
|
|
package history
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
)
|
|
|
|
// Entry is a single clipboard history record.
|
|
type Entry struct {
|
|
Text string `json:"text"`
|
|
Origin string `json:"origin"`
|
|
Ts int64 `json:"ts"` // nanosecond timestamp
|
|
}
|
|
|
|
// Store is a thread-safe, persistent history store.
|
|
type Store struct {
|
|
mu sync.Mutex
|
|
path string
|
|
max int
|
|
entries []Entry
|
|
}
|
|
|
|
// New creates a history store that persists to the given file path, keeping at
|
|
// most max entries. If max <= 0, a sensible default is used.
|
|
func New(path string, max int) *Store {
|
|
if max <= 0 {
|
|
max = 50
|
|
}
|
|
s := &Store{path: path, max: max}
|
|
s.load()
|
|
return s
|
|
}
|
|
|
|
// Append adds an entry, trimming the store to max entries and persisting it.
|
|
func (s *Store) Append(e Entry) {
|
|
if e.Text == "" {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
// Skip exact duplicates of the most recent entry.
|
|
if len(s.entries) > 0 && s.entries[len(s.entries)-1].Text == e.Text {
|
|
return
|
|
}
|
|
|
|
s.entries = append(s.entries, e)
|
|
if len(s.entries) > s.max {
|
|
s.entries = s.entries[len(s.entries)-s.max:]
|
|
}
|
|
s.save()
|
|
}
|
|
|
|
// List returns a copy of the history, oldest first.
|
|
func (s *Store) List() []Entry {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
out := make([]Entry, len(s.entries))
|
|
copy(out, s.entries)
|
|
return out
|
|
}
|
|
|
|
func (s *Store) load() {
|
|
data, err := os.ReadFile(s.path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = json.Unmarshal(data, &s.entries)
|
|
if len(s.entries) > s.max {
|
|
s.entries = s.entries[len(s.entries)-s.max:]
|
|
}
|
|
}
|
|
|
|
func (s *Store) save() {
|
|
data, err := json.MarshalIndent(s.entries, "", " ")
|
|
if err != nil {
|
|
return
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil {
|
|
return
|
|
}
|
|
tmp := s.path + ".tmp"
|
|
if err := os.WriteFile(tmp, data, 0644); err != nil {
|
|
return
|
|
}
|
|
_ = os.Rename(tmp, s.path)
|
|
}
|
|
|
|
// DefaultPath returns the default history file location.
|
|
func DefaultPath() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("history: cannot find home directory: %w", err)
|
|
}
|
|
return filepath.Join(home, ".config", "clip-sync", "history.json"), nil
|
|
}
|