Skip to content

Commit 04dc711

Browse files
authored
clipboard: make Watch variadic and tag values with their format (#89) (#124)
* clipboard: make Watch variadic and tag values with their format (#89) Watch now accepts zero or more formats and returns <-chan Data, where each value carries the Format it was detected in. A single call can observe multiple formats at once; passing no format watches all supported ones (FmtText, FmtImage). This unifies the requested watch-all ergonomics into the existing Watch rather than adding a parallel WatchAll, by treating the single-format watcher as the degenerate case of the general one. BREAKING CHANGE: the element type changes from []byte to Data. Callers migrate by reading data.Bytes (and may inspect data.Format). To ship in v0.8.0. Closes #89. * clipboard: fix multi-format watch test on Linux (clear + distinct payload) The Linux watcher emits only when the polled bytes differ from what it read at startup (clipboard_linux.go byte-equality check), unlike the macOS change-counter and Windows sequence-number paths. The test reused the same text payload that the preceding TestClipboardWatch leaves on the clipboard, so the text watcher initialized last to those bytes and never saw a change (text=false image=true on ubuntu CI). Clear the clipboard before watching and use a distinct payload.
1 parent 1f198eb commit 04dc711

4 files changed

Lines changed: 160 additions & 11 deletions

File tree

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,23 @@ clipboard data is changed, use the watcher API:
6969
ch := clipboard.Watch(context.TODO(), clipboard.FmtText)
7070
for data := range ch {
7171
// print out clipboard data whenever it is changed
72-
println(string(data))
72+
println(string(data.Bytes))
73+
}
74+
```
75+
76+
`Watch` is variadic and tags each value with the format it was detected
77+
in, so a single call can observe more than one format at once (pass no
78+
format to watch all supported ones):
79+
80+
```go
81+
ch := clipboard.Watch(context.TODO())
82+
for data := range ch {
83+
switch data.Format {
84+
case clipboard.FmtText:
85+
println("text:", string(data.Bytes))
86+
case clipboard.FmtImage:
87+
println("image bytes:", len(data.Bytes))
88+
}
7389
}
7490
```
7591

clipboard.go

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,21 @@ clipboard data is changed, use the watcher API:
5050
ch := clipboard.Watch(context.TODO(), clipboard.FmtText)
5151
for data := range ch {
5252
// print out clipboard data whenever it is changed
53-
println(string(data))
53+
println(string(data.Bytes))
54+
}
55+
56+
Watch is variadic and each value is tagged with its format, so a single
57+
call can observe more than one format at once (passing no format watches
58+
all supported ones):
59+
60+
ch := clipboard.Watch(context.TODO())
61+
for data := range ch {
62+
switch data.Format {
63+
case clipboard.FmtText:
64+
println("text:", string(data.Bytes))
65+
case clipboard.FmtImage:
66+
println("image bytes:", len(data.Bytes))
67+
}
5468
}
5569
5670
# Platform-specific caveats
@@ -173,10 +187,45 @@ func Write(t Format, buf []byte) <-chan struct{} {
173187
return changed
174188
}
175189

176-
// Watch returns a receive-only channel that received the clipboard data
177-
// whenever any change of clipboard data in the desired format happens.
190+
// Data is a single observed clipboard change: the format the change was
191+
// detected in, together with the raw bytes encoded the same way Read
192+
// returns them (UTF-8 for FmtText, PNG for FmtImage).
193+
type Data struct {
194+
Format Format
195+
Bytes []byte
196+
}
197+
198+
// Watch returns a receive-only channel that receives the clipboard data
199+
// whenever any change of clipboard data in one of the desired formats
200+
// happens. Each received value carries the format it was detected in, so a
201+
// single Watch call can observe multiple formats at once. If no format is
202+
// given, all supported formats (FmtText and FmtImage) are observed.
178203
//
179-
// The returned channel will be closed if the given context is canceled.
180-
func Watch(ctx context.Context, t Format) <-chan []byte {
181-
return watch(ctx, t)
204+
// The returned channel will be closed once the given context is canceled.
205+
func Watch(ctx context.Context, t ...Format) <-chan Data {
206+
if len(t) == 0 {
207+
t = []Format{FmtText, FmtImage}
208+
}
209+
210+
out := make(chan Data)
211+
var wg sync.WaitGroup
212+
for _, f := range t {
213+
in := watch(ctx, f)
214+
wg.Add(1)
215+
go func() {
216+
defer wg.Done()
217+
for b := range in {
218+
select {
219+
case out <- Data{Format: f, Bytes: b}:
220+
case <-ctx.Done():
221+
return
222+
}
223+
}
224+
}()
225+
}
226+
go func() {
227+
wg.Wait()
228+
close(out)
229+
}()
230+
return out
182231
}

clipboard_test.go

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,10 +285,13 @@ loop:
285285
}
286286
break loop
287287
}
288-
if !bytes.Equal(data, want) {
289-
t.Fatalf("received data from watch mismatch, want: %v, got %v", string(want), string(data))
288+
if !bytes.Equal(data.Bytes, want) {
289+
t.Fatalf("received data from watch mismatch, want: %v, got %v", string(want), string(data.Bytes))
290290
}
291-
lastRead = data
291+
if data.Format != clipboard.FmtText {
292+
t.Fatalf("received data from watch has wrong format, want: %v, got %v", clipboard.FmtText, data.Format)
293+
}
294+
lastRead = data.Bytes
292295
}
293296
}
294297
// After the context is cancelled, watch must close the channel (per the
@@ -308,6 +311,87 @@ loop:
308311
}
309312
}
310313

314+
// TestClipboardWatchMultiFormat exercises the variadic Watch: a single call
315+
// observes more than one format at once and each received value is tagged with
316+
// the format it was detected in. Watching with no format argument observes all
317+
// supported formats. This test cannot compile against the old single-format
318+
// Watch(ctx, Format) <-chan []byte signature.
319+
func TestClipboardWatchMultiFormat(t *testing.T) {
320+
if degradesWithoutCgo() {
321+
if val, ok := os.LookupEnv("CGO_ENABLED"); ok && val == "0" {
322+
t.Skip("CGO_ENABLED is set to 0")
323+
}
324+
}
325+
326+
img, err := os.ReadFile("tests/testdata/clipboard.png")
327+
if err != nil {
328+
t.Fatalf("failed to read test image: %v", err)
329+
}
330+
// Use a payload distinct from other tests and clear the clipboard first:
331+
// the Linux watcher emits only when the bytes differ from what it read at
332+
// startup, so a leftover identical string would suppress the text event.
333+
wantText := []byte("golang.design/x/clipboard#89-watch-multiformat")
334+
clipboard.Write(clipboard.FmtText, []byte(""))
335+
336+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
337+
defer cancel()
338+
339+
// Watch all supported formats through a single call.
340+
changed := clipboard.Watch(ctx)
341+
342+
// The clipboard holds only the most recently written format and the
343+
// platform watchers poll once per second, so alternate the two formats
344+
// on a tick slower than that poll interval. Writing both back-to-back
345+
// would let the second clobber the first before any watcher observes it.
346+
go func(ctx context.Context) {
347+
tk := time.NewTicker(time.Millisecond * 1300)
348+
defer tk.Stop()
349+
writeImage := false
350+
for {
351+
select {
352+
case <-ctx.Done():
353+
return
354+
case <-tk.C:
355+
if writeImage {
356+
clipboard.Write(clipboard.FmtImage, img)
357+
} else {
358+
clipboard.Write(clipboard.FmtText, wantText)
359+
}
360+
writeImage = !writeImage
361+
}
362+
}
363+
}(ctx)
364+
365+
var sawText, sawImage bool
366+
for !(sawText && sawImage) {
367+
select {
368+
case <-ctx.Done():
369+
t.Fatalf("did not observe both formats from a single Watch: text=%v image=%v", sawText, sawImage)
370+
case data, ok := <-changed:
371+
if !ok {
372+
t.Fatalf("watch channel closed before observing both formats: text=%v image=%v", sawText, sawImage)
373+
}
374+
switch data.Format {
375+
case clipboard.FmtText:
376+
if !bytes.Equal(data.Bytes, wantText) {
377+
t.Fatalf("text event payload mismatch, want %q got %q", wantText, data.Bytes)
378+
}
379+
sawText = true
380+
case clipboard.FmtImage:
381+
// Image bytes round-trip through platform conversions
382+
// (DIB/TIFF), so assert the payload is a decodable PNG
383+
// rather than byte-identical to the source.
384+
if _, err := png.Decode(bytes.NewReader(data.Bytes)); err != nil {
385+
t.Fatalf("image event payload is not a valid PNG: %v", err)
386+
}
387+
sawImage = true
388+
default:
389+
t.Fatalf("watch reported an unexpected format: %v", data.Format)
390+
}
391+
}
392+
}
393+
}
394+
311395
func BenchmarkClipboard(b *testing.B) {
312396
b.Run("text", func(b *testing.B) {
313397
data := []byte("golang.design/x/clipboard")

example_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func ExampleWatch() {
5050
go func(ctx context.Context) {
5151
clipboard.Write(clipboard.FmtText, []byte("你好,world"))
5252
}(ctx)
53-
fmt.Println(string(<-changed))
53+
fmt.Println(string((<-changed).Bytes))
5454
// Output:
5555
// 你好,world
5656
}

0 commit comments

Comments
 (0)