diff --git a/v2/claim.go b/v2/claim.go new file mode 100644 index 0000000..82fbf95 --- /dev/null +++ b/v2/claim.go @@ -0,0 +1,180 @@ +package xgbsel + +import "io" +import "github.com/jezek/xgb" +import "github.com/jezek/xgbutil" +import "github.com/jezek/xgb/xproto" +import "github.com/jezek/xgbutil/xprop" +import "github.com/jezek/xgbutil/xevent" +import "github.com/jezek/xgbutil/xwindow" + +// Claim represents a claim that a window has on a particular selection. +type Claim struct { + window *xwindow.Window + data Data + selection xproto.Atom +} + +// NewClaim claims ownership of a specified selection, and allows using data +// passed to it to fulfill requests for the selection's contents. If the claim +// happens because of a user input event, use NewClaimWithTimestamp instead of +// this function. See the documentation of NewClaimWithTimestamp for details. +func NewClaim (window *xwindow.Window, selection xproto.Atom, data Data) *Claim { + return NewClaimWithTimestamp(window, selection, data, 0) +} + +// NewClaimWithTimestamp claims ownership of a specified selection, and allows +// using data passed to it to fulfill requests for the selection's contents. The +// timestamp should be set to that of the event that triggered the claim, such +// as a Ctrl+C event, or a mouse motion that led to text being selected. +func NewClaimWithTimestamp (window *xwindow.Window, selection xproto.Atom, data Data, timestamp xproto.Timestamp) *Claim { + // Follow: + // https://tronche.com/gui/x/icccm/sec-2.html#s-2.1 + + // A client wishing to acquire ownership of a particular selection + // should call SetSelectionOwner. The client should set the specified + // selection to the atom that represents the selection, set the + // specified owner to some window that the client created, and set the + // specified time to some time between the current last-change time of + // the selection concerned and the current server time. This time value + // usually will be obtained from the timestamp of the event that + // triggers the acquisition of the selection. Clients should not set the + // time value to CurrentTime, because if they do so, they have no way of + // finding when they gained ownership of the selection. Clients must use + // a window they created so that requestors can route events to the + // owner of the selection. + err := xproto.SetSelectionOwnerChecked ( + window.X.Conn(), + window.Id, selection, timestamp).Check() + if err != nil { return nil } + + ownerReply, err := xproto.GetSelectionOwner ( + window.X.Conn(), selection).Reply() + if err != nil { return nil } + if ownerReply.Owner != window.Id { return nil } + + return &Claim { + window: window, + data: data, + selection: selection, + } +} + +func (claim *Claim) refuseSelectionRequest (request xevent.SelectionRequestEvent) { + // ... refuse the SelectionRequest by sending the requestor window a + // SelectionNotify event with the property set to None (by means of a + // SendEvent request with an empty event mask). + event := xproto.SelectionNotifyEvent { + Requestor: request.Requestor, + Selection: request.Selection, + Target: request.Target, + Property: 0, + }.Bytes() + xproto.SendEvent ( + claim.window.X.Conn(), + false, request.Requestor, 0, string(event)) +} + +func (claim *Claim) fulfillSelectionRequest ( + data []byte, + format byte, + ty xproto.Atom, + request xevent.SelectionRequestEvent, +) { + die := func () { claim.refuseSelectionRequest(request) } + + // If the specified property is not None, the owner should place the + // data resulting from converting the selection into the specified + // property on the requestor window and should set the property's type + // to some appropriate value, which need not be the same as the + // specified target. + err := xproto.ChangePropertyChecked ( + claim.window.X.Conn(), + xproto.PropModeReplace, request.Requestor, + request.Property, + ty, format, + uint32(len(data) / (int(format) / 8)), data).Check() + if err != nil { die() } + + // If the property is successfully stored, the owner should acknowledge + // the successful conversion by sending the requestor window a + // SelectionNotify event (by means of a SendEvent request with an empty + // mask). + event := xproto.SelectionNotifyEvent { + Requestor: request.Requestor, + Selection: request.Selection, + Target: request.Target, + Property: request.Property, + }.Bytes() + xproto.SendEvent ( + claim.window.X.Conn(), + false, request.Requestor, 0, string(event)) +} + +// While the selection claim is active, HandleSelectionRequest should be called +// when the owner window recieves a SelectionRequest event. This must be +// registered as an event handler manually. +func (claim *Claim) HandleSelectionRequest ( + connection *xgbutil.XUtil, + event xevent.SelectionRequestEvent, +) { + // Follow: + // https://tronche.com/gui/x/icccm/sec-2.html#s-2.2 + + die := func () { claim.refuseSelectionRequest(event) } + + // When a requestor wants the value of a selection, the owner receives a + // SelectionRequest event. The specified owner and selection will be the + // values that were specified in the SetSelectionOwner request. The + // owner should compare the timestamp with the period it has owned the + // selection and, if the time is outside, refuse the SelectionRequest. + if event.Selection != claim.selection { die(); return } + + // If the specified property is None, the requestor is an obsolete + // client. Owners are encouraged to support these clients by using the + // specified target atom as the property name to be used for the reply. + if event.Property == 0 { + event.Property = event.Target + } + + // Otherwise, the owner should use the target to decide the form into + // which the selection should be converted. Some targets may be defined + // such that requestors can pass parameters along with the request. The + // owner will find these parameters in the property named in the + // selection request. The type, format, and contents of this property + // are dependent upon the definition of the target. If the target is not + // defined to have parameters, the owner should ignore the property if + // it is present. If the selection cannot be converted into a form based + // on the target (and parameters, if any), the owner should refuse the + // SelectionRequest as previously described. + targetName, err := xprop.AtomName(claim.window.X, event.Target) + if err != nil { die(); return } + + switch targetName { + case "TARGETS": + // generate a list of supported targets + targetNames := []Target { "TARGETS" } + targetNames = append(targetNames, claim.data.Supported()...) + data := make([]byte, len(targetNames) * 4) + for index, name := range targetNames { + atom, err := xprop.Atm(claim.window.X, string(name)) + if err != nil { die(); return } + xgb.Put32(data[(index) * 4:], uint32(atom)) + } + + // send that list to the requestor + atomAtom, err := xprop.Atm(claim.window.X, "ATOM") + if err != nil { die(); return } + claim.fulfillSelectionRequest(data, 32, atomAtom, event) + + default: + // respond with data + reader, ok := claim.data.Convert(Target(targetName)) + if !ok { die(); return } + reader.Seek(0, io.SeekStart) + data, err := io.ReadAll(reader) + reader.Close() + if err != nil { die() } + claim.fulfillSelectionRequest(data, 8, event.Target, event) + } +} diff --git a/v2/data.go b/v2/data.go new file mode 100644 index 0000000..5e2dc69 --- /dev/null +++ b/v2/data.go @@ -0,0 +1,88 @@ +package xgbsel + +import "io" +import "strings" + +// Data represents X selection data. +type Data interface { + // Convert converts the data to the specified target and returns it. If + // the target is not supported, this behavior will return false for ok. + Convert (Target) (reader io.ReadSeekCloser, ok bool) + + // Supported returns a slice of targets that Convert can accept. This + // can just be the result of MimeToTargets. + Supported () []Target +} + +// Target represents an X selection target. It defines the type of data stored +// within an X selection. This data may be a MIME type, or a more specific name +// that is unique to X. A list of these names can be found here: +// https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 +type Target string + +// Confidence represents how accurate a conversion from a target to a MIME type +// is. +type Confidence int; const ( + ConfidenceNone Confidence = iota + ConfidencePartial + ConfidenceFull +) + +// ToMime converts the specified target to a MIME type. Because a single MIME +// type may correspond to several targets, a confidence value is returned +// representing how one-to-one of a match it is. If some data is represented by +// multiple targets, they can each be checked individually and the one with the +// highest confidence value can be chosen. If a target cannot be converted to a +// MIME type, ("", ConfidenceNone) is returned. +func (target Target) ToMime () (string, Confidence) { + // TODO: add other stuff. reference this table: + // https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 + // perhaps we should also have parameters for mime types so we can + // return an encoding here for things like STRING? + switch target { + case "ADOBE_PORTABLE_DOCUMENT_FORMAT": + return "application/pdf", ConfidenceFull + case "APPLE_PICT": + return "image/pict", ConfidenceFull + case + "POSTSCRIPT", + "ENCAPSULATED_POSTSCRIPT", + "ENCAPSULATED_POSTSCRIPT_INTERCHANGE": + return "application/postscript", ConfidenceFull + case "FILE_NAME": + return "text/uri-list", ConfidenceFull + case "UTF8_STRING": + return "text/plain", ConfidenceFull + case "TEXT": + return "text/plain", ConfidencePartial + case "STRING": + return "text/plain", ConfidencePartial + default: + if strings.Count(string(target), "/") == 1 { + return string(target), ConfidenceFull + } else { + return "", ConfidenceNone + } + } +} + +// MimeToTargets returns a slice of targets that correspond to a specified MIME +// type. The MIME type itself is always the first item of the slice. All targets +// returned by this function are guaranteed to convert to the given MIME type +// when ToMime is called on them. +func MimeToTargets (mime string) []Target { + targets := []Target { Target(mime) } + switch mime { + case "application/pdf": + targets = append(targets, "ADOBE_PORTABLE_DOCUMENT_FORMAT") + case "image/pict": + targets = append(targets, "APPLE_PICT") + case "application/postscript": + targets = append(targets, "POSTSCRIPT") + case "text/uri-list": + targets = append(targets, "FILE_NAME") + case "text/plain": + targets = append(targets, "UTF8_STRING", "TEXT", "STRING") + } + return targets +} diff --git a/v2/doc.go b/v2/doc.go new file mode 100644 index 0000000..bfa78e3 --- /dev/null +++ b/v2/doc.go @@ -0,0 +1,3 @@ +// Package xgbsel provides easy clipboard/selection manipulation and access with +// xgb and xgbutil. +package xgbsel diff --git a/v2/examples/copy/main.go b/v2/examples/copy/main.go new file mode 100644 index 0000000..a17e1a1 --- /dev/null +++ b/v2/examples/copy/main.go @@ -0,0 +1,73 @@ +// Example copy shows how to place text data in the CLIPBOARD selection. +package main + +import "os" +import "io" +import "log" +import "bytes" +import "github.com/jezek/xgbutil" +import "github.com/jezek/xgb/xproto" +import "github.com/jezek/xgbutil/xprop" +import "github.com/jezek/xgbutil/xevent" +import "github.com/jezek/xgbutil/xwindow" +import "git.tebibyte.media/tomo/xgbsel/v2" + +// data is a very basic implementation of xgbsel.Data that only serves data of +// one type. +type data struct { + buffer io.ReadSeekCloser + mime string +} + +func (this *data) Convert (target xgbsel.Target) (io.ReadSeekCloser, bool) { + if mime, _ := target.ToMime(); mime == this.mime { + return this.buffer, true + } else { + return nil, false + } +} + +func (this *data) Supported () []xgbsel.Target { + return xgbsel.MimeToTargets(this.mime) +} + +// nopSeekCloser is like io.NopCloser but for an io.ReadSeeker. +type nopSeekCloser struct { io.ReadSeeker } +func (nopSeekCloser) Close () error { return nil } + +func main () { + // get data from user + log.Println("enter data, ^D when done: ") + buffer, _ := io.ReadAll(os.Stdin) + data := &data { + buffer: nopSeekCloser { + ReadSeeker: bytes.NewReader(buffer), + }, + mime: "text/plain", + } + + // establish connection + X, err := xgbutil.NewConn() + if err != nil { + log.Fatal(err) + } + + // create window + window, err := xwindow.Generate(X) + if err != nil { + log.Fatalln("could not generate a new window X id:", err) + } + window.Create(X.RootWin(), 0, 0, 500, 500, xproto.CwBackPixel, 0xffffffff) + + // obtain claim on CLIPBOARD + log.Println("obtaining claim") + clipboard, _ := xprop.Atm(X, "CLIPBOARD") + claim := xgbsel.NewClaim(window, clipboard, data) + + // listen for events + window.Listen(xproto.EventMaskPropertyChange) + xevent.SelectionRequestFun(claim.HandleSelectionRequest).Connect(X, window.Id) + + log.Println("running main event loop") + xevent.Main(X) +} diff --git a/v2/examples/paste/main.go b/v2/examples/paste/main.go new file mode 100644 index 0000000..bcd181a --- /dev/null +++ b/v2/examples/paste/main.go @@ -0,0 +1,94 @@ +// Example paste shows how to read text data from the CLIPBOARD selection. +package main + +import "os" +import "io" +import "log" +import "github.com/jezek/xgbutil" +import "github.com/jezek/xgb/xproto" +import "github.com/jezek/xgbutil/xprop" +import "github.com/jezek/xgbutil/xevent" +import "github.com/jezek/xgbutil/xwindow" +import "git.tebibyte.media/tomo/xgbsel/v2" + +// requestor implements xgbsel.Requestor. It asks for text and outputs it to +// os.Stdout, and any logs to the default logging output (os.Stderr). +type requestor struct { + window *xwindow.Window +} + +func (requestor requestor) Window () *xwindow.Window { + return requestor.window +} + +func (requestor requestor) Success (target xgbsel.Target, data io.ReadCloser) { + defer data.Close() + text, _ := io.ReadAll(data) + log.Println("got clipboard text:") + os.Stdout.Write(text) + os.Exit(0) +} + +func (requestor requestor) Failure (err error) { + if err == nil { + log.Fatalln("no available clipboard data") + } else { + log.Fatalln("could not get clipboard:", err) + } + os.Exit(1) +} + +func (requestor requestor) Choose (available []xgbsel.Target) (xgbsel.Target, bool) { + log.Println("owner supports these targets:", available) + + // try to find the closest thing to text/plain by converting each target + // to a MIME type and comparing confidence values + var bestTarget xgbsel.Target + var bestConfidence xgbsel.Confidence + for _, target := range available { + mime, confidence := target.ToMime() + if mime == "text/plain" && confidence > bestConfidence { + bestConfidence = confidence + bestTarget = target + } + } + + // if we have any confidence at all, return the result we got + if bestConfidence > xgbsel.ConfidenceNone { + return bestTarget, true + } else { + return "", false + } +} + +func main () { + X, err := xgbutil.NewConn() + if err != nil { + log.Fatal(err) + } + + // create window + window, err := xwindow.Generate(X) + if err != nil { + log.Fatalln("could not generate a new window X id:", err) + } + window.Create(X.RootWin(), 0, 0, 500, 500, xproto.CwBackPixel, 0xffffffff) + + // get the atom for the clipboard, and the name of the property we want + // to recieve the selection contents on (which can be anything) + log.Println("creating request") + clipboard, _ := xprop.Atm(X, "CLIPBOARD") + property, _ := xprop.Atm(X, "DESTINATION") + request := xgbsel.NewRequest ( + requestor { window: window }, + clipboard, + property) + + // listen for events + window.Listen(xproto.EventMaskPropertyChange) + xevent.PropertyNotifyFun(request.HandlePropertyNotify).Connect(X, window.Id) + xevent.SelectionNotifyFun(request.HandleSelectionNotify).Connect(X, window.Id) + + log.Println("running main event loop") + xevent.Main(X) +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..7b518af --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,8 @@ +module git.tebibyte.media/tomo/xgbsel/v2 + +go 1.11 + +require ( + github.com/jezek/xgb v1.1.0 + github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 +) diff --git a/v2/request.go b/v2/request.go new file mode 100644 index 0000000..dcd02f0 --- /dev/null +++ b/v2/request.go @@ -0,0 +1,293 @@ +package xgbsel + +import "io" +import "bytes" +import "errors" +import "github.com/jezek/xgb" +import "github.com/jezek/xgbutil" +import "github.com/jezek/xgb/xproto" +import "github.com/jezek/xgbutil/xprop" +import "github.com/jezek/xgbutil/xevent" +import "github.com/jezek/xgbutil/xwindow" + +type selReqState int; const ( + selReqStateClosed selReqState = iota + selReqStateAwaitTargets + selReqStateAwaitValue + selReqStateAwaitChunk +) + +// Requestor provices details about the request such as the window that is +// requesting the selection data, and what targets it accepts. +type Requestor interface { + // Window returns the window that is requesting the selection data. + Window () *xwindow.Window + + // Choose picks target from a slice of available targets and returns + // (target, true). If no target was picket, it returns ("", false). + Choose (available []Target) (target Target, ok bool) + + // Success is called once the owner responds with data. The data must be + // closed once it has been read. + Success (Target, io.ReadCloser) + + // Failure is called if the transfer fails at any point, or if there + // isn't any data to begin with. In the first case, an error is given. + // In the second case, the error will be nil. + Failure (error) +} + +// Request represents an ongoing request for the selection contents. +type Request struct { + state selReqState + requestor Requestor + source xproto.Atom + destination xproto.Atom + incrBuffer []byte + incrTarget Target +} + +// NewRequest sends a new selection request. +func NewRequest (requestor Requestor, source, destination xproto.Atom) *Request { + request := &Request { + source: source, + destination: destination, + requestor: requestor, + } + + targets, err := xprop.Atm(requestor.Window().X, "TARGETS") + if err != nil { request.die(err); return nil } + request.convertSelection(targets, selReqStateAwaitTargets) + return request +} + +func (request *Request) convertSelection (target xproto.Atom, switchTo selReqState) { + // The requestor should set the property argument to the name of a + // property that the owner can use to report the value of the selection. + // Requestors should ensure that the named property does not exist on + // the window before issuing the ConvertSelection. The exception to this + // rule is when the requestor intends to pass parameters with the + // request. Some targets may be defined such that requestors can pass + // parameters along with the request. If the requestor wishes to provide + // parameters to a request, they should be placed in the specified + // property on the requestor window before the requestor issues the + // ConvertSelection request, and this property should be named in the + // request. + err := xproto.DeletePropertyChecked ( + request.requestor.Window().X.Conn(), + request.requestor.Window().Id, + request.destination).Check() + if err != nil { request.die(err); return } + + // The selection argument specifies the particular selection involved, + // and the target argument specifies the required form of the + // information. For information about the choice of suitable atoms to + // use, see section 2.6. The requestor should set the requestor argument + // to a window that it created; the owner will place the reply property + // there. The requestor should set the time argument to the timestamp on + // the event that triggered the request for the selection value. Note + // that clients should not specify CurrentTime*. + err = xproto.ConvertSelectionChecked ( + request.requestor.Window().X.Conn(), + request.requestor.Window().Id, + request.source, + target, + request.destination, + // TODO: *possibly replace this zero with an actual timestamp + // received from the server. this is non-trivial as we cannot + // rely on the timestamp of the last received event. + 0).Check() + if err != nil { request.die(err); return } + + request.state = switchTo +} + +func (request *Request) die (err error) { + request.requestor.Failure(err) + request.state = selReqStateClosed +} + +func (request *Request) finalize (target Target, reader io.ReadCloser) { + request.requestor.Success(target, reader) + request.state = selReqStateClosed +} + +func (request *Request) open () bool { + return request.state != selReqStateClosed +} + +// While the selection request is active, HandleSelectionNotify should be called +// when the requesting window recieves a SelectionNotify event. This must be +// registered as an event handler manually. +func (request *Request) HandleSelectionNotify ( + connection *xgbutil.XUtil, + event xevent.SelectionNotifyEvent, +) { + // the only valid states that we can process a SelectionNotify event in + invalidState := + request.state != selReqStateAwaitValue && + request.state != selReqStateAwaitTargets + if invalidState { return } + + // Follow: + // https://tronche.com/gui/x/icccm/sec-2.html#s-2.4 + + // If the property argument is None, the conversion has been refused. + // This can mean either that there is no owner for the selection, that + // the owner does not support the conversion implied by the target, or + // that the server did not have sufficient space to accommodate the + // data. + if event.Property == 0 { request.die(nil); return } + + // When using GetProperty to retrieve the value of a selection, the + // property argument should be set to the corresponding value in the + // SelectionNotify event. Because the requestor has no way of knowing + // beforehand what type the selection owner will use, the type argument + // should be set to AnyPropertyType. Several GetProperty requests may be + // needed to retrieve all the data in the selection; each should set the + // long-offset argument to the amount of data received so far, and the + // size argument to some reasonable buffer size (see section 2.5). If + // the returned value of bytes-after is zero, the whole property has + // been transferred. + reply, err := xproto.GetProperty ( + connection.Conn(), false, request.requestor.Window().Id, + event.Property, xproto.GetPropertyTypeAny, + 0, (1 << 32) - 1).Reply() + if err != nil { request.die(err); return } + if reply.Format == 0 { + request.die(errors.New("x: missing selection property")) + return + } + + // https://tronche.com/gui/x/icccm/sec-2.html#s-2.7.2 + // Requestors may receive a property of type INCR9 in response to any + // target that results in selection data. This indicates that the owner + // will send the actual data incrementally. The contents of the INCR + // property will be an integer, which represents a lower bound on the + // number of bytes of data in the selection. The requestor and the + // selection owner transfer the data in the selection in the following + // manner. The selection requestor starts the transfer process by + // deleting the (type==INCR) property forming the reply to the + // selection. + incr, err := xprop.Atm(request.requestor.Window().X, "INCR") + if err != nil { request.die(err); return } + if reply.Type == incr { + // reply to the INCR selection + err = xproto.DeletePropertyChecked ( + request.requestor.Window().X.Conn(), + request.requestor.Window().Id, + request.destination).Check() + if err != nil { request.die(err); return } + + // await the first chunk + request.state = selReqStateAwaitChunk + return + } + + // Once all the data in the selection has been retrieved (which may + // require getting the values of several properties &emdash; see section + // 2.7), the requestor should delete the property in the SelectionNotify + // request by using a GetProperty request with the delete argument set + // to True. As previously discussed, the owner has no way of knowing + // when the data has been transferred to the requestor unless the + // property is removed. + err = xproto.DeletePropertyChecked ( + request.requestor.Window().X.Conn(), + request.requestor.Window().Id, + request.destination).Check() + if err != nil { request.die(err); return } + + // depending on which state the selection request is in, do something + // different with the property's value + switch request.state { + case selReqStateAwaitValue: + // get the type from the property and convert that to the mime + // value to pass to the application. + targetName, err := xprop.AtomName ( + request.requestor.Window().X, reply.Type) + if err != nil { request.die(err); return } + + // we now have the full selection data in the property, so we + // finalize the request and are done. + reader := io.NopCloser(bytes.NewReader(reply.Value)) + request.finalize(Target(targetName), reader) + + case selReqStateAwaitTargets: + // make a list of the targets we got + buffer := reply.Value + atoms := make([]xproto.Atom, len(buffer) / 4) + for index := range atoms { + atoms[index] = xproto.Atom(xgb.Get32(buffer[index * 4:])) + } + targets := make([]Target, len(atoms)) + for index, atom := range atoms { + targetName, err := xprop.AtomName ( + request.requestor.Window().X, atom) + if err != nil { request.die(err); return } + targets[index] = Target(targetName) + } + + // choose the best target + var chosenAtom xproto.Atom + chosenTarget, ok := request.requestor.Choose(targets) + if ok { + for index, target := range targets { + if target == chosenTarget { + chosenAtom = atoms[index] + } + } + } + + if chosenAtom == 0 { + request.die(nil) + return + } + + // await the selection value + request.convertSelection(chosenAtom, selReqStateAwaitValue) + } +} + +// While the selection request is active, HandlePropertyNotify should be called +// when the requesting window recieves a PropertyNotify event. This must be +// registered as an event handler manually. +func (request *Request) HandlePropertyNotify ( + connection *xgbutil.XUtil, + event xevent.PropertyNotifyEvent, +) { + // the only valid state that we can process a PropertyNotify event in + if request.state != selReqStateAwaitChunk { return } + if event.State != xproto.PropertyNewValue { return } + + request.handleINCRProperty(event.Atom) +} + +func (request *Request) handleINCRProperty (property xproto.Atom) { + // Retrieving data using GetProperty with the delete argument True. + reply, err := xproto.GetProperty ( + request.requestor.Window().X.Conn(), true, + request.requestor.Window().Id, property, xproto.GetPropertyTypeAny, + 0, (1 << 32) - 1).Reply() + if err != nil { request.die(err); return } + + if len(reply.Value) == 0 { + // a zero length property means the transfer has finished. we + // finalize the request with the data we have, and don't wait + // for more. + reader := io.NopCloser(bytes.NewReader(request.incrBuffer)) + request.finalize(request.incrTarget, reader) + + // we want to be extra sure we aren't holding onto this memory + request.incrBuffer = nil + } else { + // a property with content means the transfer is still ongoing. + // we append the data we got and wait for more. + request.state = selReqStateAwaitChunk + request.incrBuffer = append(request.incrBuffer, reply.Value...) + + targetName, err := xprop.AtomName ( + request.requestor.Window().X, reply.Type) + if err != nil { request.die(err); return } + request.incrTarget = Target(targetName) + } +}