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
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]) {