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" // 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. 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 { window: window, data: data, selection: selection, }, 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, 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) } }