Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c48264a220 | |||
| 1db7d2d582 | |||
| dab360de75 | |||
| d1ba6eac9a | |||
| c8b7059976 | |||
| 08ed977234 | |||
| 49eff984ab | |||
| 3d98edbff5 |
@@ -1,3 +1,5 @@
|
|||||||
# xgbsel
|
# xgbsel
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/git.tebibyte.media/tomo/xgbsel)
|
||||||
|
|
||||||
Easy clipboard/selection manipulation and access with xgb and xgbutil.
|
Easy clipboard/selection manipulation and access with xgb and xgbutil.
|
||||||
|
|||||||
17
claim.go
17
claim.go
@@ -16,8 +16,18 @@ type Claim struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewClaim claims ownership of a specified selection, and allows using data
|
// NewClaim claims ownership of a specified selection, and allows using data
|
||||||
// passed to it to fulfill requests for the selection's contents.
|
// 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 {
|
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:
|
// Follow:
|
||||||
// https://tronche.com/gui/x/icccm/sec-2.html#s-2.1
|
// https://tronche.com/gui/x/icccm/sec-2.html#s-2.1
|
||||||
|
|
||||||
@@ -35,7 +45,7 @@ func NewClaim (window *xwindow.Window, selection xproto.Atom, data Data) *Claim
|
|||||||
// owner of the selection.
|
// owner of the selection.
|
||||||
err := xproto.SetSelectionOwnerChecked (
|
err := xproto.SetSelectionOwnerChecked (
|
||||||
window.X.Conn(),
|
window.X.Conn(),
|
||||||
window.Id, selection, 0).Check() // FIXME: should not be zero
|
window.Id, selection, timestamp).Check()
|
||||||
if err != nil { return nil }
|
if err != nil { return nil }
|
||||||
|
|
||||||
ownerReply, err := xproto.GetSelectionOwner (
|
ownerReply, err := xproto.GetSelectionOwner (
|
||||||
@@ -102,7 +112,8 @@ func (claim *Claim) fulfillSelectionRequest (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// While the selection claim is active, HandleSelectionRequest should be called
|
// While the selection claim is active, HandleSelectionRequest should be called
|
||||||
// when the owner window recieves a SelectionRequest event.
|
// when the owner window recieves a SelectionRequest event. This must be
|
||||||
|
// registered as an event handler manually.
|
||||||
func (claim *Claim) HandleSelectionRequest (
|
func (claim *Claim) HandleSelectionRequest (
|
||||||
connection *xgbutil.XUtil,
|
connection *xgbutil.XUtil,
|
||||||
event xevent.SelectionRequestEvent,
|
event xevent.SelectionRequestEvent,
|
||||||
|
|||||||
26
data.go
26
data.go
@@ -3,18 +3,25 @@ package xgbsel
|
|||||||
import "io"
|
import "io"
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
// Data represents a polymorphic data structure
|
// Data represents X selection data.
|
||||||
type Data interface {
|
type Data interface {
|
||||||
Convert (Target) (reader io.ReadSeekCloser, ok bool)
|
// 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
|
Supported () []Target
|
||||||
}
|
}
|
||||||
|
|
||||||
// Target represents an X selection target. It defines the type of data stored
|
// 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
|
// 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:
|
// 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
|
// https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2
|
||||||
type Target string
|
type Target string
|
||||||
|
|
||||||
|
// Confidence represents how accurate a conversion from a target to a MIME type
|
||||||
|
// is.
|
||||||
type Confidence int; const (
|
type Confidence int; const (
|
||||||
ConfidenceNone Confidence = iota
|
ConfidenceNone Confidence = iota
|
||||||
ConfidencePartial
|
ConfidencePartial
|
||||||
@@ -23,9 +30,10 @@ type Confidence int; const (
|
|||||||
|
|
||||||
// ToMime converts the specified target to a MIME type. Because a single MIME
|
// ToMime converts the specified target to a MIME type. Because a single MIME
|
||||||
// type may correspond to several targets, a confidence value is returned
|
// type may correspond to several targets, a confidence value is returned
|
||||||
// representing how good of a match it is. If data is represented by multiple
|
// representing how one-to-one of a match it is. If some data is represented by
|
||||||
// targets, they can be checked one after the other and the one with the highest
|
// multiple targets, they can each be checked individually and the one with the
|
||||||
// confidence value chosen.
|
// 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) {
|
func (target Target) ToMime () (string, Confidence) {
|
||||||
// TODO: add other stuff. reference this table:
|
// TODO: add other stuff. reference this table:
|
||||||
// https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2
|
// https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2
|
||||||
@@ -58,8 +66,10 @@ func (target Target) ToMime () (string, Confidence) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MimeToTargets returns a list of targets that correspond to a specified MIME
|
// MimeToTargets returns a slice of targets that correspond to a specified MIME
|
||||||
// type.
|
// 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 {
|
func MimeToTargets (mime string) []Target {
|
||||||
targets := []Target { Target(mime) }
|
targets := []Target { Target(mime) }
|
||||||
switch mime {
|
switch mime {
|
||||||
|
|||||||
73
examples/copy/main.go
Normal file
73
examples/copy/main.go
Normal file
@@ -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 "git.tebibyte.media/tomo/xgbsel"
|
||||||
|
import "github.com/jezek/xgbutil/xprop"
|
||||||
|
import "github.com/jezek/xgbutil/xevent"
|
||||||
|
import "github.com/jezek/xgbutil/xwindow"
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
@@ -6,11 +6,13 @@ import "io"
|
|||||||
import "log"
|
import "log"
|
||||||
import "github.com/jezek/xgbutil"
|
import "github.com/jezek/xgbutil"
|
||||||
import "github.com/jezek/xgb/xproto"
|
import "github.com/jezek/xgb/xproto"
|
||||||
|
import "git.tebibyte.media/tomo/xgbsel"
|
||||||
import "github.com/jezek/xgbutil/xprop"
|
import "github.com/jezek/xgbutil/xprop"
|
||||||
import "github.com/jezek/xgbutil/xevent"
|
import "github.com/jezek/xgbutil/xevent"
|
||||||
import "github.com/jezek/xgbutil/xwindow"
|
import "github.com/jezek/xgbutil/xwindow"
|
||||||
import "git.tebibyte.media/sashakoshka/xgbsel"
|
|
||||||
|
|
||||||
|
// 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 {
|
type requestor struct {
|
||||||
window *xwindow.Window
|
window *xwindow.Window
|
||||||
}
|
}
|
||||||
@@ -22,23 +24,41 @@ func (requestor requestor) Window () *xwindow.Window {
|
|||||||
func (requestor requestor) Success (target xgbsel.Target, data io.ReadCloser) {
|
func (requestor requestor) Success (target xgbsel.Target, data io.ReadCloser) {
|
||||||
defer data.Close()
|
defer data.Close()
|
||||||
text, _ := io.ReadAll(data)
|
text, _ := io.ReadAll(data)
|
||||||
log.Println("Clipboard text:", string(text))
|
log.Println("got clipboard text:")
|
||||||
|
os.Stdout.Write(text)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (requestor requestor) Failure (err error) {
|
func (requestor requestor) Failure (err error) {
|
||||||
log.Fatalln("could not get clipboard:", err)
|
if err == nil {
|
||||||
|
log.Fatalln("no available clipboard data")
|
||||||
|
} else {
|
||||||
|
log.Fatalln("could not get clipboard:", err)
|
||||||
|
}
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (requestor requestor) Choose (from []xgbsel.Target) (xgbsel.Target, bool) {
|
func (requestor requestor) Choose (available []xgbsel.Target) (xgbsel.Target, bool) {
|
||||||
for _, target := range from {
|
log.Println("owner supports these targets:", available)
|
||||||
if target == "TEXT" {
|
|
||||||
return target, true
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", false
|
// if we have any confidence at all, return the result we got
|
||||||
|
if bestConfidence > xgbsel.ConfidenceNone {
|
||||||
|
return bestTarget, true
|
||||||
|
} else {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module git.tebibyte.media/sashakoshka/xgbsel
|
module git.tebibyte.media/tomo/xgbsel
|
||||||
|
|
||||||
go 1.11
|
go 1.11
|
||||||
|
|
||||||
|
|||||||
21
request.go
21
request.go
@@ -20,9 +20,20 @@ type selReqState int; const (
|
|||||||
// Requestor provices details about the request such as the window that is
|
// Requestor provices details about the request such as the window that is
|
||||||
// requesting the selection data, and what targets it accepts.
|
// requesting the selection data, and what targets it accepts.
|
||||||
type Requestor interface {
|
type Requestor interface {
|
||||||
Window () *xwindow.Window
|
// Window returns the window that is requesting the selection data.
|
||||||
Choose (from []Target) (chosen Target, ok bool)
|
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)
|
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)
|
Failure (error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +117,8 @@ func (request *Request) open () bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// While the selection request is active, HandleSelectionNotify should be called
|
// While the selection request is active, HandleSelectionNotify should be called
|
||||||
// when the requesting window recieves a SelectionNotify event.
|
// when the requesting window recieves a SelectionNotify event. This must be
|
||||||
|
// registered as an event handler manually.
|
||||||
func (request *Request) HandleSelectionNotify (
|
func (request *Request) HandleSelectionNotify (
|
||||||
connection *xgbutil.XUtil,
|
connection *xgbutil.XUtil,
|
||||||
event xevent.SelectionNotifyEvent,
|
event xevent.SelectionNotifyEvent,
|
||||||
@@ -237,7 +249,8 @@ func (request *Request) HandleSelectionNotify (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// While the selection request is active, HandlePropertyNotify should be called
|
// While the selection request is active, HandlePropertyNotify should be called
|
||||||
// when the requesting window recieves a PropertyNotify event.
|
// when the requesting window recieves a PropertyNotify event. This must be
|
||||||
|
// registered as an event handler manually.
|
||||||
func (request *Request) HandlePropertyNotify (
|
func (request *Request) HandlePropertyNotify (
|
||||||
connection *xgbutil.XUtil,
|
connection *xgbutil.XUtil,
|
||||||
event xevent.PropertyNotifyEvent,
|
event xevent.PropertyNotifyEvent,
|
||||||
|
|||||||
Reference in New Issue
Block a user