// 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.Sequence == 0 && 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 handleXfixesSelectionNotify(e xfixes.SelectionNotifyEvent) { state, ok := selections[e.Selection] if !ok { return } // Ownership request has been granted, don't ask ourselves for data. if e.Owner == wid { state.owning = e.SelectionTimestamp return } // 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 { return } // 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 { return } // 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 } func handleSelectionNotify(e xproto.SelectionNotifyEvent) { state, ok := selections[e.Selection] if e.Requestor != wid || !ok || e.Time != state.inProgress { return } state.inProgress = 0 if e.Property == xproto.AtomNone { return } state.buffer = nil reply, err := getProperty(e.Requestor, e.Property) if err != nil { return } // 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) } func handlePropertyNotify(e xproto.PropertyNotifyEvent) { state, ok := selections[e.Atom] if e.Window != wid || e.State != xproto.PropertyNewValue || !ok || !state.incr { return } reply, err := getProperty(e.Window, e.Atom) if err != nil { state.incrFailed = true return } 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) } func handleSelectionRequest(e xproto.SelectionRequestEvent) { property := e.Property if property == xproto.AtomNone { property = e.Target } state, ok := selections[e.Selection] if e.Owner != wid || !ok { return } 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 handleXEvent(ev nexgb.Event) { switch e := ev.(type) { case xfixes.SelectionNotifyEvent: handleXfixesSelectionNotify(e) case xproto.SelectionNotifyEvent: handleSelectionNotify(e) case xproto.PropertyNotifyEvent: handlePropertyNotify(e) case xproto.SelectionRequestEvent: handleSelectionRequest(e) } } 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 { handleXEvent(ev) } } }