From f198f9f6acfb89ac024ea8262c638ebca8a2eb68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Sun, 23 Sep 2018 16:59:18 +0200 Subject: xgb-selection: add a demo to track X11 selections --- prototypes/xgb-selection.go | 199 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 prototypes/xgb-selection.go 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) + } + } +} -- cgit v1.2.3