200 lines
5.5 KiB
Go
200 lines
5.5 KiB
Go
|
// 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)
|
||
|
}
|
||
|
}
|
||
|
}
|