// 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) } } }