Files
clip-sync/internal/history/history.go
T

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
}