aboutsummaryrefslogtreecommitdiff
path: root/hpcu
diff options
context:
space:
mode:
authorPřemysl Janouch <p@janouch.name>2018-09-24 13:11:11 +0200
committerPřemysl Janouch <p@janouch.name>2018-09-30 18:45:29 +0200
commit7d51aaa9a49d8f7461993a564b5dfbac4e4ba6de (patch)
treea7b7f2df98c5f002cd5ed777d387e662a6d887e5 /hpcu
parentf198f9f6acfb89ac024ea8262c638ebca8a2eb68 (diff)
downloadhaven-7d51aaa9a49d8f7461993a564b5dfbac4e4ba6de.tar.gz
haven-7d51aaa9a49d8f7461993a564b5dfbac4e4ba6de.tar.xz
haven-7d51aaa9a49d8f7461993a564b5dfbac4e4ba6de.zip
hpcu: add a selection unifier
So far not supporting large selections.
Diffstat (limited to 'hpcu')
-rw-r--r--hpcu/main.go351
1 files changed, 351 insertions, 0 deletions
diff --git a/hpcu/main.go b/hpcu/main.go
new file mode 100644
index 0000000..6240ef1
--- /dev/null
+++ b/hpcu/main.go
@@ -0,0 +1,351 @@
+// hpcu unifies the PRIMARY and CLIPBOARD X11 selections for text contents.
+package main
+
+import (
+ "errors"
+ "janouch.name/haven/nexgb"
+ "janouch.name/haven/nexgb/xfixes"
+ "janouch.name/haven/nexgb/xproto"
+ "log"
+)
+
+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
+ owning xproto.Timestamp // since when we own the selection
+}
+
+var (
+ X *nexgb.Conn
+ setup *xproto.SetupInfo
+ screen *xproto.ScreenInfo
+
+ atomCLIPBOARD xproto.Atom // X11 atom for CLIPBOARD
+ atomUTF8String xproto.Atom // X11 atom for UTF8_STRING
+ atomINCR xproto.Atom // X11 atom for INCR
+ atomTARGETS xproto.Atom // X11 atom for TARGETS
+ atomTIMESTAMP xproto.Atom // X11 atom for TIMESTAMP
+
+ wid xproto.Window // auxiliary window
+ selections map[xproto.Atom]*selectionState
+ contents string // current shared selection contents
+)
+
+// resolveAtoms resolves a few required atoms that are not in the core protocol.
+func resolveAtoms() error {
+ for _, i := range []struct {
+ placement *xproto.Atom
+ name string
+ }{
+ {&atomCLIPBOARD, "CLIPBOARD"},
+ {&atomUTF8String, "UTF8_STRING"},
+ {&atomINCR, "INCR"},
+ {&atomTARGETS, "TARGETS"},
+ {&atomTIMESTAMP, "TIMESTAMP"},
+ } {
+ if reply, err := xproto.InternAtom(X,
+ false, uint16(len(i.name)), i.name).Reply(); err != nil {
+ return err
+ } else {
+ *i.placement = reply.Atom
+ }
+ }
+ return nil
+}
+
+// setupAuxiliaryWindow creates a window that receives notifications about
+// changed selection contents, and serves
+func setupAuxiliaryWindow() error {
+ var err error
+ if wid, err = xproto.NewWindowId(X); err != nil {
+ return err
+ }
+
+ _ = xproto.CreateWindow(X, screen.RootDepth, wid, screen.Root, 0, 0, 1, 1,
+ 0, xproto.WindowClassInputOutput, screen.RootVisual, xproto.CwEventMask,
+ []uint32{xproto.EventMaskPropertyChange})
+
+ for _, selection := range []xproto.Atom{xproto.AtomPrimary, atomCLIPBOARD} {
+ _ = xfixes.SelectSelectionInput(X, wid, selection,
+ xfixes.SelectionEventMaskSetSelectionOwner|
+ xfixes.SelectionEventMaskSelectionWindowDestroy|
+ xfixes.SelectionEventMaskSelectionClientClose)
+ }
+ return nil
+}
+
+// getProperty reads a window property in a memory-efficient manner.
+func getProperty(window xproto.Window, property xproto.Atom) (
+ *xproto.GetPropertyReply, error) {
+ // 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, even though I can't XChangeProperty more than 0xffffe0
+ // bytes at a time.
+ //
+ // Since the XGB API doesn't let us provide our own buffer for
+ // value data, let us avoid multiplying the amount of consumed memory in
+ // pathological cases where properties are several gigabytes in size by
+ // chunking the requests. This has a cost of losing atomicity, although
+ // it shouldn't pose a problem except for timeout-caused INCR races.
+ var result xproto.GetPropertyReply
+ for result.Length == 0 || result.BytesAfter > 0 {
+ reply, err := xproto.GetProperty(X, false, /* delete */
+ window, property, xproto.GetPropertyTypeAny,
+ uint32(len(result.Value))/4,
+ uint32(setup.MaximumRequestLength)).Reply()
+ if err != nil {
+ return nil, err
+ }
+ if result.Length != 0 &&
+ (reply.Format != result.Format || reply.Type != result.Type) {
+ return nil, errors.New("property type changed during read")
+ }
+
+ reply.Value = append(result.Value, reply.Value...)
+ reply.ValueLen += result.ValueLen
+ result = *reply
+ }
+ return &result, nil
+}
+
+// appendText tries to append UTF-8 text to the selection state buffer.
+func appendText(state *selectionState, prop *xproto.GetPropertyReply) bool {
+ if prop.Type == atomUTF8String && prop.Format == 8 {
+ state.buffer = append(state.buffer, prop.Value...)
+ return true
+ }
+ return false
+}
+
+func requestOwnership(origin *selectionState, time xproto.Timestamp) {
+ contents = string(origin.buffer)
+ for selection, state := range selections {
+ // We might want to replace the originator as well but it might have
+ // undesirable effects, mainly with PRIMARY.
+ if state != origin {
+ // No need to GetSelectionOwner, XFIXES is more reliable.
+ _ = xproto.SetSelectionOwner(X, wid, selection, time)
+ }
+ }
+}
+
+func handleEvent(ev nexgb.Event) {
+ switch e := ev.(type) {
+ case xfixes.SelectionNotifyEvent:
+ state, ok := selections[e.Selection]
+ if !ok {
+ break
+ }
+
+ // Ownership request has been granted, don't ask ourselves for data.
+ if e.Owner == wid {
+ state.owning = e.SelectionTimestamp
+ break
+ }
+
+ // This should always be true.
+ if state.owning < e.SelectionTimestamp {
+ state.owning = 0
+ }
+
+ // Not checking whether we should give up when our current retrieval
+ // attempt is interrupted--the timeout mostly solves this.
+ if e.Owner == xproto.WindowNone {
+ 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, e.Selection, e.Timestamp)
+
+ state.inProgress = e.Timestamp
+ state.incr = false
+
+ case xproto.SelectionNotifyEvent:
+ state, ok := selections[e.Selection]
+ if e.Requestor != wid || !ok || e.Time != state.inProgress {
+ break
+ }
+
+ state.inProgress = 0
+ if e.Property == xproto.AtomNone {
+ break
+ }
+
+ state.buffer = nil
+ reply, err := getProperty(e.Requestor, e.Property)
+ if err != nil {
+ break
+ }
+
+ // 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 {
+ state.inProgress = e.Time
+ state.incr = true
+ state.incrFailed = false
+ } else if appendText(state, reply) {
+ requestOwnership(state, e.Time)
+ }
+
+ _ = xproto.DeleteProperty(X, e.Requestor, e.Property)
+
+ case xproto.PropertyNotifyEvent:
+ state, ok := selections[e.Atom]
+ if e.Window != wid || e.State != xproto.PropertyNewValue ||
+ !ok || !state.incr {
+ break
+ }
+
+ reply, err := getProperty(e.Window, e.Atom)
+ if err != nil {
+ state.incrFailed = true
+ break
+ }
+
+ if !appendText(state, reply) {
+ // We need to keep deleting the property.
+ state.incrFailed = true
+ }
+
+ if reply.ValueLen == 0 {
+ if !state.incrFailed {
+ requestOwnership(state, e.Time)
+ }
+ state.inProgress = 0
+ state.incr = false
+ }
+
+ _ = xproto.DeleteProperty(X, e.Window, e.Atom)
+
+ case xproto.SelectionRequestEvent:
+ property := e.Property
+ if property == xproto.AtomNone {
+ property = e.Target
+ }
+
+ state, ok := selections[e.Selection]
+ if e.Owner != wid || !ok {
+ break
+ }
+
+ var (
+ typ xproto.Atom
+ format byte
+ data []byte
+ )
+
+ // XXX: We should also support the MULTIPLE target but it seems to be
+ // unimportant and largely abandoned today.
+ targets := []xproto.Atom{atomTARGETS, atomTIMESTAMP, atomUTF8String}
+
+ switch e.Target {
+ case atomTARGETS:
+ typ = xproto.AtomAtom
+ format = 32
+
+ data = make([]byte, len(targets)*4)
+ for i, atom := range targets {
+ nexgb.Put32(data[i*4:], uint32(atom))
+ }
+
+ case atomTIMESTAMP:
+ typ = xproto.AtomInteger
+ format = 32
+
+ data = make([]byte, 4)
+ nexgb.Put32(data, uint32(state.owning))
+
+ case atomUTF8String:
+ typ = atomUTF8String
+ format = 8
+
+ data = []byte(contents)
+ }
+
+ response := xproto.SelectionNotifyEvent{
+ Time: e.Time,
+ Requestor: e.Requestor,
+ Selection: e.Selection,
+ Target: e.Target,
+ Property: xproto.AtomNone,
+ }
+
+ if typ == 0 || len(data) > int(setup.MaximumRequestLength)*4-64 ||
+ state.owning == 0 || e.Time < state.owning {
+ // TODO: Use the INCR mechanism for large data transfers instead
+ // of refusing the request, or at least use PropModeAppend.
+ //
+ // According to the ICCCM we need to set up a queue for concurrent
+ // (requestor, selection, target, timestamp) requests that differ
+ // only in the target property, and process them in order. The ICCCM
+ // has a nice rationale. It seems to only concern INCR. The queue
+ // might be a map[(who, what, how, when)][](where, data, offset).
+ //
+ // NOTE: Even with BigRequests support, it may technically be
+ // missing on the particular X server, and XGB copies buffers to yet
+ // another buffer, making very large transfers a very bad idea.
+ } else if xproto.ChangePropertyChecked(X, xproto.PropModeReplace,
+ e.Requestor, property, typ, format,
+ uint32(len(data)/int(format/8)), data).Check() == nil {
+ response.Property = property
+ }
+
+ _ = xproto.SendEvent(X, false /* propagate */, e.Requestor,
+ 0 /* event mask */, string(response.Bytes()))
+ }
+}
+
+func main() {
+ var err error
+ if X, err = nexgb.NewConn(); err != nil {
+ log.Fatalln(err)
+ }
+ if err = xfixes.Init(X); err != nil {
+ log.Fatalln(err)
+ }
+
+ // Enable the extension.
+ _ = xfixes.QueryVersion(X, xfixes.MajorVersion, xfixes.MinorVersion)
+
+ setup = xproto.Setup(X)
+ screen = setup.DefaultScreen(X)
+
+ if err = resolveAtoms(); err != nil {
+ log.Fatalln(err)
+ }
+ if err = setupAuxiliaryWindow(); err != nil {
+ log.Fatalln(err)
+ }
+
+ // Now that we have our atoms, we can initialize state.
+ selections = map[xproto.Atom]*selectionState{
+ xproto.AtomPrimary: {name: "PRIMARY"},
+ atomCLIPBOARD: {name: "CLIPBOARD"},
+ }
+
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev != nil {
+ handleEvent(ev)
+ }
+ }
+}