hpcu: add a selection unifier
So far not supporting large selections.
This commit is contained in:
parent
f198f9f6ac
commit
7d51aaa9a4
21
README
21
README
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue