diff options
| author | Přemysl Janouch <p@janouch.name> | 2018-09-24 13:11:11 +0200 | 
|---|---|---|
| committer | Přemysl Janouch <p@janouch.name> | 2018-09-30 18:45:29 +0200 | 
| commit | 7d51aaa9a49d8f7461993a564b5dfbac4e4ba6de (patch) | |
| tree | a7b7f2df98c5f002cd5ed777d387e662a6d887e5 /hpcu | |
| parent | f198f9f6acfb89ac024ea8262c638ebca8a2eb68 (diff) | |
| download | haven-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.go | 351 | 
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) +		} +	} +} | 
