diff options
| author | Přemysl Janouch <p@janouch.name> | 2018-09-23 16:59:18 +0200 | 
|---|---|---|
| committer | Přemysl Janouch <p@janouch.name> | 2018-09-30 18:45:29 +0200 | 
| commit | f198f9f6acfb89ac024ea8262c638ebca8a2eb68 (patch) | |
| tree | ba73dbef603e167e3bbd15c90948a56cda735094 /prototypes | |
| parent | 106e9b82b829ba23c8fec609a09433ce66f6dfda (diff) | |
| download | haven-f198f9f6acfb89ac024ea8262c638ebca8a2eb68.tar.gz haven-f198f9f6acfb89ac024ea8262c638ebca8a2eb68.tar.xz haven-f198f9f6acfb89ac024ea8262c638ebca8a2eb68.zip | |
xgb-selection: add a demo to track X11 selections
Diffstat (limited to 'prototypes')
| -rw-r--r-- | prototypes/xgb-selection.go | 199 | 
1 files changed, 199 insertions, 0 deletions
| 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) +		} +	} +} | 
