~eliasnaur/gio-patches

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
1

[PATCH gio] io,app: add ReadClipboardOp and WriteClipboardOp v2

~inkeliz
Details
Message ID
<160679747630.5621.6662469352245087419-0@git.sr.ht>
DKIM signature
missing
Download raw message
Patch: +177 -17
From: Inkeliz <inkeliz@inkeliz.com>

Previously, the only way to manipulate the clipboard (read or write) is
using the `app.Window`, that is inaccessible inside widgets, by default.

The new `clipboard.ReadClipboardOp` and `clipboard.WriteClipboardOp`
makes possible to read/write inside the widget. The old
`system.ClipboardEvent` was removed and replaced by `clipboard.Event`.

Fixes gio#183.

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
---
 app/internal/window/os_android.go |  3 +-
 app/internal/window/os_ios.go     |  3 +-
 app/internal/window/os_js.go      |  3 +-
 app/internal/window/os_macos.go   |  3 +-
 app/internal/window/os_wayland.go |  5 +-
 app/internal/window/os_windows.go |  3 +-
 app/internal/window/os_x11.go     |  3 +-
 app/window.go                     | 12 +++-
 internal/opconst/ops.go           |  8 ++-
 io/clipboard/clipboard.go         | 34 +++++++++++
 io/router/clipboard.go            | 93 +++++++++++++++++++++++++++++++
 io/router/router.go               | 17 ++++++
 io/system/system.go               |  7 ---
 13 files changed, 177 insertions(+), 17 deletions(-)
 create mode 100644 io/clipboard/clipboard.go
 create mode 100644 io/router/clipboard.go

diff --git a/app/internal/window/os_android.go b/app/internal/window/os_android.go
index fc494e5..b73ca38 100644
--- a/app/internal/window/os_android.go
+++ b/app/internal/window/os_android.go
@@ -52,6 +52,7 @@ import (
	"unsafe"

	"gioui.org/f32"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -645,7 +646,7 @@ func (w *window) ReadClipboard() {
			return
		}
		content := goString(env, C.jstring(c))
		w.callbacks.Event(system.ClipboardEvent{Text: content})
		w.callbacks.Event(clipboard.Event{Text: content})
	})
}

diff --git a/app/internal/window/os_ios.go b/app/internal/window/os_ios.go
index 1865454..d92999a 100644
--- a/app/internal/window/os_ios.go
+++ b/app/internal/window/os_ios.go
@@ -38,6 +38,7 @@ import (
	"unsafe"

	"gioui.org/f32"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -222,7 +223,7 @@ func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.C
func (w *window) ReadClipboard() {
	runOnMain(func() {
		content := nsstringToString(C.gio_readClipboard())
		w.w.Event(system.ClipboardEvent{Text: content})
		w.w.Event(clipboard.Event{Text: content})
	})
}

diff --git a/app/internal/window/os_js.go b/app/internal/window/os_js.go
index e4fb17c..3c26922 100644
--- a/app/internal/window/os_js.go
+++ b/app/internal/window/os_js.go
@@ -12,6 +12,7 @@ import (
	"unicode/utf8"

	"gioui.org/f32"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -59,7 +60,7 @@ func NewWindow(win Callbacks, opts *Options) error {
	})
	w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
		content := args[0].String()
		win.Event(system.ClipboardEvent{Text: content})
		win.Event(clipboard.Event{Text: content})
		return nil
	})
	w.addEventListeners()
diff --git a/app/internal/window/os_macos.go b/app/internal/window/os_macos.go
index 1afec6f..339e659 100644
--- a/app/internal/window/os_macos.go
+++ b/app/internal/window/os_macos.go
@@ -14,6 +14,7 @@ import (
	"unsafe"

	"gioui.org/f32"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -107,7 +108,7 @@ func (w *window) contextView() C.CFTypeRef {
func (w *window) ReadClipboard() {
	runOnMain(func() {
		content := nsstringToString(C.gio_readClipboard())
		w.w.Event(system.ClipboardEvent{Text: content})
		w.w.Event(clipboard.Event{Text: content})
	})
}

diff --git a/app/internal/window/os_wayland.go b/app/internal/window/os_wayland.go
index 60f59b1..36837b1 100644
--- a/app/internal/window/os_wayland.go
+++ b/app/internal/window/os_wayland.go
@@ -22,6 +22,7 @@ import (
	"gioui.org/app/internal/xkb"
	"gioui.org/f32"
	"gioui.org/internal/fling"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -1099,14 +1100,14 @@ func (w *window) process() {
		r, err := w.disp.readClipboard()
		// Send empty responses on unavailable clipboards or errors.
		if r == nil || err != nil {
			w.w.Event(system.ClipboardEvent{})
			w.w.Event(clipboard.Event{})
			return
		}
		// Don't let slow clipboard transfers block event loop.
		go func() {
			defer r.Close()
			data, _ := ioutil.ReadAll(r)
			w.w.Event(system.ClipboardEvent{Text: string(data)})
			w.w.Event(clipboard.Event{Text: string(data)})
		}()
	}
	if writeClipboard != nil {
diff --git a/app/internal/window/os_windows.go b/app/internal/window/os_windows.go
index 2a38d53..041c276 100644
--- a/app/internal/window/os_windows.go
+++ b/app/internal/window/os_windows.go
@@ -22,6 +22,7 @@ import (
	"gioui.org/unit"

	"gioui.org/f32"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -508,7 +509,7 @@ func (w *window) readClipboard() error {
	hdr.Len = n
	content := string(utf16.Decode(u16))
	go func() {
		w.w.Event(system.ClipboardEvent{Text: content})
		w.w.Event(clipboard.Event{Text: content})
	}()
	return nil
}
diff --git a/app/internal/window/os_x11.go b/app/internal/window/os_x11.go
index 413a959..4317386 100644
--- a/app/internal/window/os_x11.go
+++ b/app/internal/window/os_x11.go
@@ -34,6 +34,7 @@ import (
	"unsafe"

	"gioui.org/f32"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -410,7 +411,7 @@ func (h *x11EventHandler) handleEvents() bool {
				break
			}
			str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
			w.w.Event(system.ClipboardEvent{Text: str})
			w.w.Event(clipboard.Event{Text: str})
		case C.SelectionRequest:
			cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
			if cevt.selection != w.atoms.clipboard || cevt.property == C.None {
diff --git a/app/window.go b/app/window.go
index ed46958..99fa89e 100644
--- a/app/window.go
+++ b/app/window.go
@@ -167,6 +167,16 @@ func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op.
	case router.TextInputClose:
		w.driver.ShowTextInput(false)
	}
	if w.queue.q.ReadClipboard() {
		go func() {
			w.driver.ReadClipboard()
		}()
	}
	if text := w.queue.q.WriteClipboard(); text != nil {
		go func() {
			w.driver.WriteClipboard(*text)
		}()
	}
	if w.queue.q.Profiling() {
		frameDur := time.Since(frameStart)
		frameDur = frameDur.Truncate(100 * time.Microsecond)
@@ -194,7 +204,7 @@ func (w *Window) Invalidate() {
}

// ReadClipboard initiates a read of the clipboard in the form
// of a system.ClipboardEvent. Multiple reads may be coalesced
// of a clipboard.Event. Multiple reads may be coalesced
// to a single event.
func (w *Window) ReadClipboard() {
	go w.driverDo(func() {
diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go
index dba0f54..3634ce9 100644
--- a/internal/opconst/ops.go
+++ b/internal/opconst/ops.go
@@ -20,6 +20,8 @@ const (
	TypeArea
	TypePointerInput
	TypePass
	TypeWriteClipboard
	TypeReadClipboard
	TypeKeyInput
	TypeKeyFocus
	TypeKeySoftKeyboard
@@ -43,6 +45,8 @@ const (
	TypeAreaLen            = 1 + 1 + 4*4
	TypePointerInputLen    = 1 + 1 + 1
	TypePassLen            = 1 + 1
	TypeWriteClipboardLen  = 1
	TypeReadClipboardLen   = 1
	TypeKeyInputLen        = 1
	TypeKeyFocusLen        = 1 + 1
	TypeKeySoftKeyboardLen = 1 + 1
@@ -67,6 +71,8 @@ func (t OpType) Size() int {
		TypeAreaLen,
		TypePointerInputLen,
		TypePassLen,
		TypeWriteClipboardLen,
		TypeReadClipboardLen,
		TypeKeyInputLen,
		TypeKeyFocusLen,
		TypeKeySoftKeyboardLen,
@@ -80,7 +86,7 @@ func (t OpType) Size() int {

func (t OpType) NumRefs() int {
	switch t {
	case TypeKeyInput, TypePointerInput, TypeProfile, TypeCall:
	case TypeKeyInput, TypePointerInput, TypeProfile, TypeCall, TypeReadClipboard, TypeWriteClipboard:
		return 1
	case TypeImage:
		return 2
diff --git a/io/clipboard/clipboard.go b/io/clipboard/clipboard.go
new file mode 100644
index 0000000..46d6b00
--- /dev/null
+++ b/io/clipboard/clipboard.go
@@ -0,0 +1,34 @@
package clipboard

import (
	"gioui.org/internal/opconst"
	"gioui.org/io/event"
	"gioui.org/op"
)

type ReadClipboardOp struct {
	Tag event.Tag
}

type WriteClipboardOp struct {
	Text string
}

// Event is generated when a handler request
// the ReadClipboardOp
type Event struct {
	Text string
}

func (h ReadClipboardOp) Add(o *op.Ops) {
	data := o.Write1(opconst.TypeReadClipboardLen, h.Tag)
	data[0] = byte(opconst.TypeReadClipboard)
}

func (h WriteClipboardOp) Add(o *op.Ops) {
	data := o.Write1(opconst.TypeWriteClipboardLen, &h.Text)
	data[0] = byte(opconst.TypeWriteClipboard)
}


func (Event) ImplementsEvent()  {}
\ No newline at end of file
diff --git a/io/router/clipboard.go b/io/router/clipboard.go
new file mode 100644
index 0000000..06aa768
--- /dev/null
+++ b/io/router/clipboard.go
@@ -0,0 +1,93 @@
package router

import (
	"gioui.org/internal/opconst"
	"gioui.org/internal/ops"
	"gioui.org/io/event"
	"gioui.org/op"
)

type clipboardQueue struct {
	receiver  event.Tag
	requested bool
	text      *string
	reader    ops.Reader
}

// WriteClipboard returns the last text supossed to be
// copied to clipboard as determined in Frame.
func (q *clipboardQueue) WriteClipboard() *string {
	if q.text != nil {
		t := q.text
		q.text = nil
		return t
	}
	return nil
}

// ReadClipboard returns true if there's any request
// to read the clipboard.
func (q *clipboardQueue) ReadClipboard() bool {
	if q.receiver != nil && !q.requested {
		q.requested = true
		return true
	}
	return false
}

func (q *clipboardQueue) Frame(root *op.Ops, events *handlerEvents) {
	q.reader.Reset(root)

	receiver, text := q.resolveClipboard(events)
	if text != nil {
		q.text = text
	}
	if receiver != nil {
		q.receiver = receiver
		q.requested = false
	}
}

func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) {
	if q.receiver != nil {
		events.Add(q.receiver, e)
		q.receiver = nil
	}
}

func (q *clipboardQueue) resolveClipboard(events *handlerEvents) (receiver event.Tag, text *string) {
loop:
	for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
		switch opconst.OpType(encOp.Data[0]) {
		case opconst.TypeWriteClipboard:
			text = decodeWriteClipboard(encOp.Data, encOp.Refs)
		case opconst.TypeReadClipboard:
			receiver = decodeReadClipboard(encOp.Data, encOp.Refs)
		case opconst.TypePush:
			newReceiver, newWrite := q.resolveClipboard(events)
			if newWrite != nil {
				text = newWrite
			}
			if newReceiver != nil {
				receiver = newReceiver
			}
		case opconst.TypePop:
			break loop
		}
	}
	return receiver, text
}

func decodeWriteClipboard(d []byte, refs []interface{}) *string {
	if opconst.OpType(d[0]) != opconst.TypeWriteClipboard {
		panic("invalid op")
	}
	return refs[0].(*string)
}

func decodeReadClipboard(d []byte, refs []interface{}) event.Tag {
	if opconst.OpType(d[0]) != opconst.TypeReadClipboard {
		panic("invalid op")
	}
	return refs[0].(event.Tag)
}
diff --git a/io/router/router.go b/io/router/router.go
index d24d214..7ab212d 100644
--- a/io/router/router.go
+++ b/io/router/router.go
@@ -16,6 +16,7 @@ import (

	"gioui.org/internal/opconst"
	"gioui.org/internal/ops"
	"gioui.org/io/clipboard"
	"gioui.org/io/event"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
@@ -28,6 +29,7 @@ import (
type Router struct {
	pqueue pointerQueue
	kqueue keyQueue
	cqueue clipboardQueue

	handlers handlerEvents

@@ -73,6 +75,7 @@ func (q *Router) Frame(ops *op.Ops) {

	q.pqueue.Frame(ops, &q.handlers)
	q.kqueue.Frame(ops, &q.handlers)
	q.cqueue.Frame(ops, &q.handlers)
	if q.handlers.HadEvents() {
		q.wakeup = true
		q.wakeupTime = time.Time{}
@@ -88,6 +91,8 @@ func (q *Router) Add(events ...event.Event) bool {
			q.pqueue.Push(e, &q.handlers)
		case key.EditEvent, key.Event, key.FocusEvent:
			q.kqueue.Push(e, &q.handlers)
		case clipboard.Event:
			q.cqueue.Push(e, &q.handlers)
		}
	}
	return q.handlers.HadEvents()
@@ -99,6 +104,18 @@ func (q *Router) TextInputState() TextInputState {
	return q.kqueue.InputState()
}

// WriteClipboard returns the most recent text to be copied
// to the clipboard, if any.
func (q *Router) WriteClipboard() *string {
	return q.cqueue.WriteClipboard()
}

// ReadClipboard returns true if some handler is waiting to
// retrieve the clipboard.
func (q *Router) ReadClipboard() bool {
	return q.cqueue.ReadClipboard()
}

func (q *Router) collect() {
	for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
		switch opconst.OpType(encOp.Data[0]) {
diff --git a/io/system/system.go b/io/system/system.go
index 4aa88d3..8a84051 100644
--- a/io/system/system.go
+++ b/io/system/system.go
@@ -60,12 +60,6 @@ type DestroyEvent struct {
	Err error
}

// ClipboardEvent is sent once for each request for the
// clipboard content.
type ClipboardEvent struct {
	Text string
}

// Insets is the space taken up by
// system decoration such as translucent
// system bars and software keyboards.
@@ -122,4 +116,3 @@ func (FrameEvent) ImplementsEvent()     {}
func (StageEvent) ImplementsEvent()     {}
func (*CommandEvent) ImplementsEvent()  {}
func (DestroyEvent) ImplementsEvent()   {}
func (ClipboardEvent) ImplementsEvent() {}
-- 
2.26.2
Details
Message ID
<C7IBNBB0EI5M.7NB889KTHVFO@themachine>
In-Reply-To
<160679747630.5621.6662469352245087419-0@git.sr.ht> (view parent)
DKIM signature
pass
Download raw message
If you're going to add package clipboard, please split into a separate
change the new package and adjusted references. Note that the change is
and API change in the commit message, and include an automatic gofmt fixup
if possible. See gioui.org/commit/878131189b10c2341 for an example.

On Tue Dec 1, 2020 at 04:37, ~inkeliz wrote:
> From: Inkeliz <inkeliz@inkeliz.com>
>
> Previously, the only way to manipulate the clipboard (read or write) is
> using the `app.Window`, that is inaccessible inside widgets, by default.
>
> The new `clipboard.ReadClipboardOp` and `clipboard.WriteClipboardOp`
> makes possible to read/write inside the widget. The old
> `system.ClipboardEvent` was removed and replaced by `clipboard.Event`.
>
> Fixes gio#183.
>
> Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
> diff --git a/app/window.go b/app/window.go
> index ed46958..99fa89e 100644
> --- a/app/window.go
> +++ b/app/window.go
> @@ -167,6 +167,16 @@ func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op.
>  	case router.TextInputClose:
>  		w.driver.ShowTextInput(false)
>  	}
> +	if w.queue.q.ReadClipboard() {
> +		go func() {
> +			w.driver.ReadClipboard()
> +		}()
> +	}
> +	if text := w.queue.q.WriteClipboard(); text != nil {
> +		go func() {
> +			w.driver.WriteClipboard(*text)
> +		}()
> +	}
>  	if w.queue.q.Profiling() {
>  		frameDur := time.Since(frameStart)
>  		frameDur = frameDur.Truncate(100 * time.Microsecond)
> @@ -194,7 +204,7 @@ func (w *Window) Invalidate() {
>  }
>  
>  // ReadClipboard initiates a read of the clipboard in the form
> -// of a system.ClipboardEvent. Multiple reads may be coalesced
> +// of a clipboard.Event. Multiple reads may be coalesced
>  // to a single event.
>  func (w *Window) ReadClipboard() {
>  	go w.driverDo(func() {
> diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go
> index dba0f54..3634ce9 100644
> --- a/internal/opconst/ops.go
> +++ b/internal/opconst/ops.go
> @@ -20,6 +20,8 @@ const (
>  	TypeArea
>  	TypePointerInput
>  	TypePass
> +	TypeWriteClipboard
> +	TypeReadClipboard

Follow the naming convention of key and pointer ops:

TypeClipboardWrite
TypeClipboardRead

Here and below.

>  	TypeKeyInput
>  	TypeKeyFocus
>  	TypeKeySoftKeyboard
> @@ -43,6 +45,8 @@ const (
>  	TypeAreaLen            = 1 + 1 + 4*4
>  	TypePointerInputLen    = 1 + 1 + 1
>  	TypePassLen            = 1 + 1
> +	TypeWriteClipboardLen  = 1
> +	TypeReadClipboardLen   = 1

Ditto.

>  	TypeKeyInputLen        = 1
>  	TypeKeyFocusLen        = 1 + 1
>  	TypeKeySoftKeyboardLen = 1 + 1
> @@ -67,6 +71,8 @@ func (t OpType) Size() int {
>  		TypeAreaLen,
>  		TypePointerInputLen,
>  		TypePassLen,
> +		TypeWriteClipboardLen,
> +		TypeReadClipboardLen,

Ditto.

>  		TypeKeyInputLen,
>  		TypeKeyFocusLen,
>  		TypeKeySoftKeyboardLen,
> diff --git a/io/clipboard/clipboard.go b/io/clipboard/clipboard.go
> new file mode 100644
> index 0000000..46d6b00
> --- /dev/null
> +++ b/io/clipboard/clipboard.go
> @@ -0,0 +1,34 @@
> +package clipboard
> +
> +import (
> +	"gioui.org/internal/opconst"
> +	"gioui.org/io/event"
> +	"gioui.org/op"
> +)
> +
> +type ReadClipboardOp struct {

Clipboard is redundant in package clipboard. Rename to ReadOp

> +	Tag event.Tag
> +}
> +
> +type WriteClipboardOp struct {

Ditto: WriteOp.

> +	Text string
> +}
> +
> +// Event is generated when a handler request
> +// the ReadClipboardOp
> +type Event struct {
> +	Text string
> +}
> +
> +func (h ReadClipboardOp) Add(o *op.Ops) {
> +	data := o.Write1(opconst.TypeReadClipboardLen, h.Tag)
> +	data[0] = byte(opconst.TypeReadClipboard)
> +}
> +
> +func (h WriteClipboardOp) Add(o *op.Ops) {
> +	data := o.Write1(opconst.TypeWriteClipboardLen, &h.Text)
> +	data[0] = byte(opconst.TypeWriteClipboard)
> +}
> +
> +
> +func (Event) ImplementsEvent()  {}
> \ No newline at end of file
> diff --git a/io/router/clipboard.go b/io/router/clipboard.go
> new file mode 100644
> index 0000000..06aa768
> --- /dev/null
> +++ b/io/router/clipboard.go
> @@ -0,0 +1,93 @@
> +package router
> +
> +import (
> +	"gioui.org/internal/opconst"
> +	"gioui.org/internal/ops"
> +	"gioui.org/io/event"
> +	"gioui.org/op"
> +)
> +
> +type clipboardQueue struct {
> +	receiver  event.Tag

There may be multiple receivers waiting. Make receiver a

receivers map[event.Tag]struct{}

> +	requested bool

Replace requested with len(q.receivers) > 0.

> +	text      *string
> +	reader    ops.Reader
> +}
> +
> +// WriteClipboard returns the last text supossed to be
> +// copied to clipboard as determined in Frame.

Tighten comment a bit:

// WriteClipboard returns the text from the last clipboard write
// operation, if any.

> +func (q *clipboardQueue) WriteClipboard() *string {

Return nil as a separate bool:

func (q *...) WriteClipboard() (string, bool)

> +	if q.text != nil {

Nit: indent the shortest case:

if q.text == nil {
	return "", false
}
return *q.text, true

> +		t := q.text
> +		q.text = nil
> +		return t
> +	}
> +	return nil
> +}
> +
> +// ReadClipboard returns true if there's any request
> +// to read the clipboard.

// ReadClipboard reports whether the last frame contained a clipboard
// read operation.

> +func (q *clipboardQueue) ReadClipboard() bool {
> +	if q.receiver != nil && !q.requested {

Ditto: indent the shortest case:

if q.requested {
	return false
}
q.requested = true
return true

> +		q.requested = true
> +		return true
> +	}
> +	return false
> +}
> +
> +func (q *clipboardQueue) Frame(root *op.Ops, events *handlerEvents) {
> +	q.reader.Reset(root)
> +
> +	receiver, text := q.resolveClipboard(events)
> +	if text != nil {
> +		q.text = text
> +	}
> +	if receiver != nil {
> +		q.receiver = receiver
> +		q.requested = false
> +	}
> +}
> +
> +func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) {
> +	if q.receiver != nil {
> +		events.Add(q.receiver, e)
> +		q.receiver = nil
> +	}
> +}
> +
> +func (q *clipboardQueue) resolveClipboard(events *handlerEvents) (receiver event.Tag, text *string) {
> +loop:
> +	for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {

Another run over the operation list is wasteful. Merge the decoding of
clipboard ops into Router.collect.

Perhaps the operations list should be split into multiple lists during
encoding. Another time :)

> +		switch opconst.OpType(encOp.Data[0]) {
> +		case opconst.TypeWriteClipboard:
> +			text = decodeWriteClipboard(encOp.Data, encOp.Refs)
> +		case opconst.TypeReadClipboard:
> +			receiver = decodeReadClipboard(encOp.Data, encOp.Refs)
> +		case opconst.TypePush:
> +			newReceiver, newWrite := q.resolveClipboard(events)
> +			if newWrite != nil {
> +				text = newWrite
> +			}
> +			if newReceiver != nil {
> +				receiver = newReceiver
> +			}
> +		case opconst.TypePop:
> +			break loop
> +		}
> +	}
> +	return receiver, text
> +}
> +
> diff --git a/io/router/router.go b/io/router/router.go
> index d24d214..7ab212d 100644
> --- a/io/router/router.go
> +++ b/io/router/router.go
> @@ -99,6 +104,18 @@ func (q *Router) TextInputState() TextInputState {
>  	return q.kqueue.InputState()
>  }
>  
> +// WriteClipboard returns the most recent text to be copied
> +// to the clipboard, if any.
> +func (q *Router) WriteClipboard() *string {

Change return values to (string, bool).

> +	return q.cqueue.WriteClipboard()
> +}
> +
> +// ReadClipboard returns true if some handler is waiting to
> +// retrieve the clipboard.

Nit:

// ReadClipboard reports whether ...

> +func (q *Router) ReadClipboard() bool {
> +	return q.cqueue.ReadClipboard()
> +}
> +
>  func (q *Router) collect() {
>  	for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
>  		switch opconst.OpType(encOp.Data[0]) {
Reply to thread Export thread (mbox)