diff --git a/v2/claim.go b/v2/claim.go index 01765c4..66c26d4 100644 --- a/v2/claim.go +++ b/v2/claim.go @@ -9,25 +9,33 @@ import "github.com/jezek/xgbutil/xprop" import "github.com/jezek/xgbutil/xevent" import "github.com/jezek/xgbutil/xwindow" -const format8bit byte = 8 -const format16bit byte = 16 -const format32bit byte = 32 +const format8bit byte = 8 +const format16bit byte = 16 +const format32bit byte = 32 +const propertyNewValue byte = 0 +const propertyDelete byte = 1 type claimRequest struct { reader io.ReadSeekCloser - ty xproto.Atom // must be "INCR" + ty xproto.Atom event xevent.SelectionRequestEvent } +type claimRequestKey struct { + property xproto.Atom + requestor xproto.Window +} + // Claim represents a claim that a window has on a particular selection. type Claim struct { + open bool + incr bool window *xwindow.Window data Data selection xproto.Atom timestamp xproto.Timestamp - open bool - requests map[xproto.Atom] claimRequest + requests map[claimRequestKey] *claimRequest } // NewClaim claims ownership of a specified selection, and allows using data @@ -65,11 +73,12 @@ func NewClaim (window *xwindow.Window, selection xproto.Atom, data Data, timesta return &Claim { open: true, + incr: false, // TODO change to enable INCR window: window, data: data, selection: selection, timestamp: timestamp, - requests: make(map[xproto.Atom] claimRequest), + requests: make(map[claimRequestKey] *claimRequest), }, nil } @@ -101,12 +110,10 @@ func (claim *Claim) fulfillSelectionRequest ( // 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(), + err := claim.changePropertyChecked ( xproto.PropModeReplace, request.Requestor, request.Property, - ty, format, - uint32(len(data) / (int(format) / 8)), data).Check() + ty, format, data) if err != nil { die() } // If the property is successfully stored, the owner should acknowledge @@ -124,6 +131,19 @@ func (claim *Claim) fulfillSelectionRequest ( false, request.Requestor, 0, string(event)) } +func (claim *Claim) changePropertyChecked ( + mode byte, + window xproto.Window, + property xproto.Atom, + ty xproto.Atom, + format byte, + data []byte, +) error { + return xproto.ChangePropertyChecked ( + claim.window.X.Conn(), mode, window, property, ty, format, + uint32(len(data) / (int(format) / 8)), data).Check() +} + // 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. @@ -185,14 +205,52 @@ func (claim *Claim) HandleSelectionRequest ( claim.fulfillSelectionRequest(data, format32bit, atomAtom, event) default: + incr, err := xprop.Atm(claim.window.X, "INCR") + if err != nil { die(); return } + // 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, format8bit, event.Target, event) + + if claim.incr { + // TODO: we need to subscribe to PropertyNotify events + // on the destination window. apparently event masks as + // set by ChangeWindowAttributes are client-specific. + // we also need to clean up afterwards. + // https://tronche.com/gui/x/xlib/window/XChangeWindowAttributes.html + + request := &claimRequest { + reader: reader, + ty: event.Target, + event: event, + } + + // Requestors may receive a property of type INCR 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. + + data := [4]byte { } + xgb.Put32(data[:], 1) + err := claim.changePropertyChecked ( + xproto.PropModeReplace, event.Requestor, + event.Property, incr, format32bit, data[:]) + if err != nil { die() } + + claim.requests[claimRequestKey { + property: event.Property, + requestor: event.Requestor, + }] = request + } else { + reader.Seek(0, io.SeekStart) + data, err := io.ReadAll(reader) + reader.Close() + if err != nil { die() } + claim.fulfillSelectionRequest(data, format8bit, event.Target, event) + } } } @@ -203,21 +261,28 @@ func (claim *Claim) HandlePropertyNotify ( connection *xgbutil.XUtil, event xevent.PropertyNotifyEvent, ) { + // The selection requestor starts the transfer process by deleting the + // (type==INCR) property forming the reply to the selection. + // // The selection owner then: // ... Waits between each append for a PropertyNotify (state==Deleted) // event that shows that the requestor has read the data. The reason for // doing this is to limit the consumption of space in the server. + if event.State != propertyDelete { return } - // FIXME check for state==Deleted // if the request does not exist, the property was either modified in // error or the request has already finished. either way, we simply // exit. - request, ok := claim.requests[event.Atom] + key := claimRequestKey { + property: event.Atom, + requestor: event.Window, + } + request, ok := claim.requests[key] if !ok { return } done := func () { request.reader.Close() - delete(claim.requests, request.event.Property) + delete(claim.requests, key) } die := func () { @@ -238,13 +303,10 @@ func (claim *Claim) HandlePropertyNotify ( // the same window as the selection reply with a type corresponding to // the actual type of the converted selection. The size should be less // than the maximum-request-size in the connection handshake. - format := format8bit - err = xproto.ChangePropertyChecked ( - claim.window.X.Conn(), - xproto.PropModeAppend, request.event.Requestor, - request.event.Property, - request.ty, format, - uint32(len(dataRead) / (int(format) / 8)), dataRead).Check() + err := claim.changePropertyChecked ( + xproto.PropModeReplace, + request.event.Requestor, request.event.Property, + request.ty, format8bit, dataRead) if err != nil { die(); return } } else { @@ -254,14 +316,10 @@ func (claim *Claim) HandlePropertyNotify ( // server) until a PropertyNotify (state==Deleted) event that // shows that the data has been read by the requestor and then // writes zero-length data to the property. - - format := format8bit - err := xproto.ChangePropertyChecked ( - claim.window.X.Conn(), - xproto.PropModeAppend, request.event.Requestor, - request.event.Property, - request.ty, format, - 0, nil).Check() + err := claim.changePropertyChecked ( + xproto.PropModeReplace, + request.event.Requestor, request.event.Property, + request.ty, format8bit, nil) if err != nil { die(); return } done()