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 type claimRequest struct { reader io.ReadSeekCloser ty xproto.Atom // must be "INCR" event xevent.SelectionRequestEvent } // Claim represents a claim that a window has on a particular selection. type Claim struct { window *xwindow.Window data Data selection xproto.Atom timestamp xproto.Timestamp active bool requests map[xproto.Atom] 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 { active: true, window: window, data: data, selection: selection, timestamp: timestamp, requests: make(map[xproto.Atom] 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 := 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, format32bit, 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, 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 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. // 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] if !ok { return } done := func () { request.reader.Close() delete(claim.requests, request.event.Property) } 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. 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() 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. format := format8bit err := xproto.ChangePropertyChecked ( claim.window.X.Conn(), xproto.PropModeAppend, request.event.Requestor, request.event.Property, request.ty, format, 0, nil).Check() 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.active = false } // 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 responding to events. func (claim *Claim) Close () error { if !claim.active { return nil } claim.active = 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 }