Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions internal/clipboard/clipboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package clipboard

import (
"errors"
"sync"
"time"

"github.com/zyedidia/clipper"
)
Expand Down Expand Up @@ -37,6 +39,36 @@ const (

var clipboard clipper.Clipboard

var (
lastWrittenLock sync.Mutex
lastWritten = make(map[Register]string)
lastWriteTime = make(map[Register]time.Time)
writeDebounce time.Duration
)

// SetWriteDebounce sets the debounce duration for external clipboard writes.
func SetWriteDebounce(d time.Duration) {
lastWrittenLock.Lock()
defer lastWrittenLock.Unlock()
writeDebounce = d
}

// ResetCache clears the cached last written clipboard data.
func ResetCache() {
lastWrittenLock.Lock()
defer lastWrittenLock.Unlock()
lastWritten = make(map[Register]string)
lastWriteTime = make(map[Register]time.Time)
}

// LastWritten returns the last string written to a clipboard register.
func LastWritten(r Register) (string, bool) {
lastWrittenLock.Lock()
defer lastWrittenLock.Unlock()
val, ok := lastWritten[r]
return val, ok
}

// Initialize attempts to initialize the clipboard using the given method
func Initialize(m Method) error {
var err error
Expand Down Expand Up @@ -109,12 +141,25 @@ func writeMulti(text string, r Register, num int, ncursors int, m Method) error
func read(r Register, m Method) (string, error) {
switch m {
case External:
if clipboard == nil {
return internal.read(r), nil
}
switch r {
case ClipboardReg:
b, e := clipboard.ReadAll(clipper.RegClipboard)
if e == nil {
lastWrittenLock.Lock()
lastWritten[r] = string(b)
lastWrittenLock.Unlock()
}
return string(b), e
case PrimaryReg:
b, e := clipboard.ReadAll(clipper.RegPrimary)
if e == nil {
lastWrittenLock.Lock()
lastWritten[r] = string(b)
lastWrittenLock.Unlock()
}
return string(b), e
default:
return internal.read(r), nil
Expand All @@ -137,8 +182,28 @@ func read(r Register, m Method) (string, error) {
}

func write(text string, r Register, m Method) error {
lastWrittenLock.Lock()
prev, exists := lastWritten[r]
if exists && prev == text {
lastWrittenLock.Unlock()
return nil
}
if writeDebounce > 0 && m == External {
if t, ok := lastWriteTime[r]; ok && time.Since(t) < writeDebounce {
lastWrittenLock.Unlock()
return nil
}
}
lastWritten[r] = text
lastWriteTime[r] = time.Now()
lastWrittenLock.Unlock()

switch m {
case External:
if clipboard == nil {
internal.write(text, r)
return nil
}
switch r {
case ClipboardReg:
return clipboard.WriteAll(clipper.RegClipboard, []byte(text))
Expand Down
104 changes: 104 additions & 0 deletions internal/clipboard/clipboard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package clipboard

import (
"testing"
"time"
)

func TestClipboardGatingIdenticalWrites(t *testing.T) {
ResetCache()
CurrentMethod = Internal

err := Write("sample content", ClipboardReg)
if err != nil {
t.Fatalf("expected write to succeed, got %v", err)
}

last, ok := LastWritten(ClipboardReg)
if !ok || last != "sample content" {
t.Fatalf("expected last written to be 'sample content', got '%s'", last)
}

// Repeated identical write should be gated cleanly
err = Write("sample content", ClipboardReg)
if err != nil {
t.Fatalf("gated write should succeed without error, got %v", err)
}

// Writing new content updates cache
err = Write("updated content", ClipboardReg)
if err != nil {
t.Fatalf("write of updated content failed: %v", err)
}

last, ok = LastWritten(ClipboardReg)
if !ok || last != "updated content" {
t.Fatalf("expected last written to be 'updated content', got '%s'", last)
}
}

func TestClipboardPrimaryRegisterGating(t *testing.T) {
ResetCache()
CurrentMethod = Internal

err := Write("primary text", PrimaryReg)
if err != nil {
t.Fatalf("primary write failed: %v", err)
}

val, ok := LastWritten(PrimaryReg)
if !ok || val != "primary text" {
t.Fatalf("expected primary register to record 'primary text', got '%s'", val)
}

// Gated identical write
err = Write("primary text", PrimaryReg)
if err != nil {
t.Fatalf("gated primary write failed: %v", err)
}
}

func TestClipboardResetCache(t *testing.T) {
ResetCache()
CurrentMethod = Internal

_ = Write("cache test", ClipboardReg)
_, ok := LastWritten(ClipboardReg)
if !ok {
t.Fatalf("expected cached entry before reset")
}

ResetCache()
_, ok = LastWritten(ClipboardReg)
if ok {
t.Fatalf("expected cache to be empty after ResetCache()")
}
}

func TestClipboardWriteDebounce(t *testing.T) {
ResetCache()
CurrentMethod = External
SetWriteDebounce(50 * time.Millisecond)
defer SetWriteDebounce(0)

// First write
_ = write("debounce initial", ClipboardReg, External)
val, ok := LastWritten(ClipboardReg)
if !ok || val != "debounce initial" {
t.Fatalf("initial write should be recorded")
}

// Rapid subsequent write within debounce interval should be gated
_ = write("debounce rapid", ClipboardReg, External)
val, _ = LastWritten(ClipboardReg)
if val != "debounce initial" {
t.Fatalf("rapid write should have been debounced")
}

time.Sleep(60 * time.Millisecond)
_ = write("debounce after window", ClipboardReg, External)
val, _ = LastWritten(ClipboardReg)
if val != "debounce after window" {
t.Fatalf("write after debounce window should succeed, got %s", val)
}
}
Loading