package xgbsel import "io" 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" 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 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 requests map[claimRequestKey] *claimRequest } // NewClaim 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. If the claim was // not triggered by an event, specify xproto.TimeCurrentTime. func NewClaim (window *xwindow.Window, selection xproto.Atom, data Data, timestamp xproto.Timestamp) (*Claim, error) { // 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, err } ownerReply, err := xproto.GetSelectionOwner ( window.X.Conn(), selection).Reply() if err != nil { return nil, err } if ownerReply.Owner != window.Id { return nil, errors.New("someone else took the selection") } return &Claim { open: true, incr: false, // TODO change to enable INCR once its done window: window, data: data, selection: selection, timestamp: timestamp, requests: make(map[claimRequestKey] *claimRequest), }, nil } 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 := claim.changePropertyChecked ( xproto.PropModeReplace, request.Requestor, request.Property, ty, format, data) 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)) } 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. func (claim *Claim) HandleSelectionRequest ( connection *xgbutil.XUtil, event xevent.SelectionRequestEvent, ) { // do not fulfill *new* selection requests after the claim has been // relinquished if !claim.open { return } // 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, 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 } 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) } } } // While the selection claim is active, HandlePropertyNotify should be called // when the requesting window recieves a PropertyNotify event. This must be // registered as an event handler manually. 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 } // if the request does not exist, the property was either modified in // error or the request has already finished. either way, we simply // exit. key := claimRequestKey { property: event.Atom, requestor: event.Window, } request, ok := claim.requests[key] if !ok { return } done := func () { request.reader.Close() delete(claim.requests, key) } die := func () { done() claim.refuseSelectionRequest(request.event) } // FIXME change the length of this if maximum-request-size is smaller buffer := [1024]byte { } size, err := request.reader.Read(buffer[:]) if err != nil { die(); return } dataRead := buffer[:size] if len(dataRead) > 0 { // there is more data to send // ... Appends the data in suitable-size chunks to the same property on // 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. err := claim.changePropertyChecked ( xproto.PropModeReplace, request.event.Requestor, request.event.Property, request.ty, format8bit, dataRead) if err != nil { die(); return } } else { // all data has been sent // ... Waits (after the entire data has been transferred to the // 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. err := claim.changePropertyChecked ( xproto.PropModeReplace, request.event.Requestor, request.event.Property, request.ty, format8bit, nil) if err != nil { die(); return } done() } } // While the selection claim is active, HandleSelectionClear should be called // when the requesting window recieves a SelectionClear event. This must be // registered as an event handler manually. func (claim *Claim) HandleSelectionClear ( connection *xgbutil.XUtil, event xevent.SelectionClearEvent, ) { // When some other client acquires a selection, the previous owner // receives a SelectionClear event. The timestamp argument is the time // at which the ownership changed hands, and the owner argument is the // window the previous owner specified in its SetSelectionOwner request. // If an owner loses ownership while it has a transfer in progress (that // is, before it receives notification that the requestor has received // all the data), it must continue to service the ongoing transfer until // it is complete. claim.open = false } // Prune checks for requests that cannot continue and removes them. If this // claim is to be held for an extended period of time, this method should be // called every so often. func (claim *Claim) Prune () { // TODO // check all active requests to see if anyone's window has closed, and // if it has, close the request } // Open returns false if the claim has been relinquished, either from recieving // a SelectionClear event or with the Close method. func (claim *Claim) Open () bool { return claim.open } // Active returns true if requests are currently being fulfilled. While this is // true, the claim should still be forwarded events regardless if it is closed // or not. It is safe to forward the same event to multiple claims. func (claim *Claim) Active () bool { return len(claim.requests) > 0 } // Close voluntarily relinquishes the selection claim. This will inform the X // server that the selection is being voluntarily given up, and cause the claim // to stop fulfilling new requests. func (claim *Claim) Close () error { if !claim.open { return nil } claim.open = false // To relinquish ownership of a selection voluntarily, a client should // execute a SetSelectionOwner request for that selection atom, with // owner specified as None and the time specified as the timestamp that // was used to acquire the selection. err := xproto.SetSelectionOwnerChecked ( claim.window.X.Conn(), 0, claim.selection, claim.timestamp).Check() if err != nil { return err } return nil }