aboutsummaryrefslogtreecommitdiff
path: root/prototypes/xgb-selection.go
diff options
context:
space:
mode:
Diffstat (limited to 'prototypes/xgb-selection.go')
-rw-r--r--prototypes/xgb-selection.go199
1 files changed, 199 insertions, 0 deletions
diff --git a/prototypes/xgb-selection.go b/prototypes/xgb-selection.go
new file mode 100644
index 0000000..d81cd51
--- /dev/null
+++ b/prototypes/xgb-selection.go
@@ -0,0 +1,199 @@
+// Follow X11 selection contents as they are being changed.
+package main
+
+import (
+ "janouch.name/haven/nexgb"
+ "janouch.name/haven/nexgb/xfixes"
+ "janouch.name/haven/nexgb/xproto"
+ "log"
+)
+
+func main() {
+ X, err := nexgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if err := xfixes.Init(X); err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ // Resolve a few required atoms that are not static in the core protocol.
+ const (
+ clipboard = "CLIPBOARD"
+ utf8String = "UTF8_STRING"
+ incr = "INCR"
+ )
+
+ atomCLIPBOARD, err := xproto.InternAtom(X, false,
+ uint16(len(clipboard)), clipboard).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ atomUTF8String, err := xproto.InternAtom(X, false,
+ uint16(len(utf8String)), utf8String).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ atomINCR, err := xproto.InternAtom(X, false,
+ uint16(len(incr)), incr).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Create a window.
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateWindow(X, screen.RootDepth, wid, screen.Root, 0, 0, 1, 1,
+ 0, xproto.WindowClassInputOutput, screen.RootVisual, xproto.CwEventMask,
+ []uint32{xproto.EventMaskPropertyChange})
+
+ // Select for update events of each selection.
+ _ = xfixes.QueryVersion(X, xfixes.MajorVersion, xfixes.MinorVersion)
+
+ _ = xfixes.SelectSelectionInput(X, wid,
+ xproto.AtomPrimary, xfixes.SelectionEventMaskSetSelectionOwner|
+ xfixes.SelectionEventMaskSelectionWindowDestroy|
+ xfixes.SelectionEventMaskSelectionClientClose)
+
+ _ = xfixes.SelectSelectionInput(X, wid,
+ atomCLIPBOARD.Atom, xfixes.SelectionEventMaskSetSelectionOwner|
+ xfixes.SelectionEventMaskSelectionWindowDestroy|
+ xfixes.SelectionEventMaskSelectionClientClose)
+
+ type selectionState struct {
+ name string // name of the selection
+ inProgress xproto.Timestamp // timestamp of retrieved selection
+ buffer []byte // UTF-8 text buffer
+ incr bool // INCR running
+ incrFailed bool // INCR failure indicator
+ }
+ states := map[xproto.Atom]*selectionState{
+ xproto.AtomPrimary: {name: "PRIMARY"},
+ atomCLIPBOARD.Atom: {name: "CLIPBOARD"},
+ }
+
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ switch e := ev.(type) {
+ case xfixes.SelectionNotifyEvent:
+ state, ok := states[e.Selection]
+ if !ok {
+ break
+ }
+
+ // Not checking whether we should give up when our current retrieval
+ // attempt is interrupted--the timeout mostly solves this.
+ if e.Owner == xproto.WindowNone {
+ // This may potentially log before a request for past data
+ // finishes, if only because of INCR. Just saying.
+ log.Printf("%s: -\n", state.name)
+ break
+ }
+
+ // Don't try to process two things at once. Each request gets a few
+ // seconds to finish, then we move on, hoping that a property race
+ // doesn't commence. Ideally we'd set up a separate queue for these
+ // skipped requests and process them later.
+ if state.inProgress != 0 && e.Timestamp-state.inProgress < 5000 {
+ break
+ }
+
+ // ICCCM says we should ensure the named property doesn't exist.
+ _ = xproto.DeleteProperty(X, e.Window, e.Selection)
+
+ _ = xproto.ConvertSelection(X, e.Window, e.Selection,
+ atomUTF8String.Atom, e.Selection, e.Timestamp)
+
+ state.inProgress = e.Timestamp
+ state.incr = false
+
+ case xproto.SelectionNotifyEvent:
+ state, ok := states[e.Selection]
+ if e.Requestor != wid || !ok || e.Time != state.inProgress {
+ break
+ }
+
+ state.inProgress = 0
+ if e.Property == xproto.AtomNone {
+ break
+ }
+
+ // XXX: This is simplified and doesn't necessarily read it all.
+ // Maybe we could use setup.MaximumRequestLength.
+ // Though xorg-xserver doesn't seem to limit the length of replies
+ // or even the length of properties in the first place.
+ // It only has a huge (0xffffffff - sizeof(xChangePropertyReq))/4
+ // limit for ChangeProperty requests, which is way more than
+ // max-request-len (normally 0xffff), even though I can't
+ // XChangeProperty more than 0xffffe0 bytes at a time.
+ reply, err := xproto.GetProperty(X, false, /* delete */
+ e.Requestor, e.Property, xproto.GetPropertyTypeAny,
+ 0, 0x8000).Reply()
+ if err != nil {
+ break
+ }
+
+ state.buffer = nil
+
+ // When you select a lot of text in VIM, it starts the ICCCM
+ // INCR mechanism, from which there is no opt-out.
+ if reply.Type == atomINCR.Atom {
+ state.inProgress = e.Time
+ state.incr = true
+ state.incrFailed = false
+ } else if reply.Type == atomUTF8String.Atom && reply.Format == 8 {
+ log.Printf("%s: '%s'\n", state.name, string(reply.Value))
+ }
+
+ _ = xproto.DeleteProperty(X, e.Requestor, e.Property)
+
+ case xproto.PropertyNotifyEvent:
+ state, ok := states[e.Atom]
+ if e.Window != wid || e.State != xproto.PropertyNewValue ||
+ !ok || !state.incr {
+ break
+ }
+
+ reply, err := xproto.GetProperty(X, false, /* delete */
+ e.Window, e.Atom, xproto.GetPropertyTypeAny, 0, 0x8000).Reply()
+ if err != nil {
+ state.incrFailed = true
+ break
+ }
+
+ if reply.Type == atomUTF8String.Atom && reply.Format == 8 {
+ state.buffer = append(state.buffer, reply.Value...)
+ } else {
+ // We need to keep deleting the property.
+ state.incrFailed = true
+ }
+
+ if reply.ValueLen == 0 {
+ if !state.incrFailed {
+ log.Printf("%s: '%s'\n",
+ state.name, string(state.buffer))
+ }
+ state.inProgress = 0
+ state.incr = false
+ }
+
+ _ = xproto.DeleteProperty(X, e.Window, e.Atom)
+ }
+ }
+}