hpcu: add a selection unifier

So far not supporting large selections.
This commit is contained in:
Přemysl Eric Janouch 2018-09-24 13:11:11 +02:00
parent f198f9f6ac
commit 7d51aaa9a4
Signed by: p
GPG Key ID: A0420B94F92B9493
2 changed files with 368 additions and 4 deletions

21
README
View File

@ -173,6 +173,22 @@ The result of testing hid with telnet, OpenSSL s_client, OpenBSD nc, GNU nc and
Ncat is that neither of them can properly shutdown the connection. We need Ncat is that neither of them can properly shutdown the connection. We need
a good implementation with TLS support. a good implementation with TLS support.
hpcu -- PRIMARY-CLIPBOARD unifier
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An improved replacement for autocutsel in selection synchronization "mode":
- using only one OS process;
- not polling selections twice a second unnecessarily;
- calling SetSelectionOwner on change even when it already owns the selection,
so that XFIXES SelectionNotify events are delivered;
- not using cut buffers for anything.
Only UTF8_STRING-convertible selections are synchronized.
ht -- terminal emulator
~~~~~~~~~~~~~~~~~~~~~~~
Similar scope to st(1). Clever display of internal padding for better looks.
hib and hic -- IRC bouncer and client hib and hic -- IRC bouncer and client
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An IRC client is a good starting application for building a GUI toolkit, as the An IRC client is a good starting application for building a GUI toolkit, as the
@ -241,6 +257,7 @@ most basic features includes a VFS for archives. The editing widget in read-
-only mode could be used for F3. The shell is going to work very simply, -only mode could be used for F3. The shell is going to work very simply,
creating a PTY device and running things under TERM=dumb while decoding SGR, creating a PTY device and running things under TERM=dumb while decoding SGR,
or one could decide to run a new terminal emulator with a different shortcut. or one could decide to run a new terminal emulator with a different shortcut.
ht could probably also be integrated.
Eventually the number of panels should be arbitrary with proper shortcuts for Eventually the number of panels should be arbitrary with proper shortcuts for
working with them. We might also integrate a special view for picture previews, working with them. We might also integrate a special view for picture previews,
@ -255,10 +272,6 @@ Indexing and search may be based on a common database, no need to get all fancy:
http://rachbelaid.com/postgres-full-text-search-is-good-enough/ http://rachbelaid.com/postgres-full-text-search-is-good-enough/
https://www.sqlite.org/fts3.html#full_text_index_queries (FTS4 seems better) https://www.sqlite.org/fts3.html#full_text_index_queries (FTS4 seems better)
ht -- terminal emulator
~~~~~~~~~~~~~~~~~~~~~~~
Similar scope to st(1). Clever display of internal padding for better looks.
The rest The rest
~~~~~~~~ ~~~~~~~~
Currently there are no significant, specific plans about the other applications. Currently there are no significant, specific plans about the other applications.

351
hpcu/main.go Normal file
View File

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