// 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 }