package main import ( "encoding/base64" "fmt" "regexp" "strings" "unicode/utf8" ) // Selections are the X selections xclip reaches. var Selections = []string{"clipboard", "primary", "secondary"} // MostCopy bounds what a caller may put on the clipboard. const MostCopy = 1 << 20 var targetName = regexp.MustCompile(`^[A-Za-z0-9_][A-Za-z0-9_.+/;=-]{0,127}$`) func checkTarget(t string) error { if t != "" && !targetName.MatchString(t) { return fmt.Errorf("%q is not a type a selection can be offered as", t) } return nil } // xclip runs xclip in the session, naming an unreachable display as such. func xclip(s Session, c Cmd) (Result, error) { c.Name, c.Env = "xclip", s.Env() r := run(c) said := r.Stderr + r.Stdout switch { case r.Error != "": return r, failure(c, r) case strings.Contains(said, "Can't open display"): return r, fmt.Errorf("the X session at %s (found by %s) refused the connection: the display is gone, or the cookie in %s is not the server's", s.Display, s.FoundBy, orNone(s.XAuthority)) case r.Status != 0: return r, failure(c, r) } return r, nil } func orNone(s string) string { if s == "" { return "(no authority file)" } return s } // CopyAnswer is what xclip_copy answers. type CopyAnswer struct { Selection string `json:"selection"` Type string `json:"type"` Bytes int `json:"bytes"` Session Session `json:"session"` Note string `json:"note"` } // Copy puts text on a selection. xclip forks a process that holds the selection until another // program takes it, so the command runs detached. func Copy(text, selection, typ string) (CopyAnswer, error) { if len(text) > MostCopy { return CopyAnswer{}, fmt.Errorf("text is %d bytes; at most %d are copied", len(text), MostCopy) } if err := checkTarget(typ); err != nil { return CopyAnswer{}, err } s, err := findSession() if err != nil { return CopyAnswer{}, err } args := []string{"-selection", selection, "-in"} if typ != "" { args = append(args, "-t", typ) } if _, err := xclip(s, Cmd{Args: args, Stdin: text, Detached: true}); err != nil { return CopyAnswer{}, err } if typ == "" { typ = "UTF8_STRING" } return CopyAnswer{Selection: selection, Type: typ, Bytes: len(text), Session: s, Note: "xclip holds the selection until another program takes it; a clipboard manager may copy it into its history"}, nil } // PasteAnswer is what xclip_paste answers. type PasteAnswer struct { Selection string `json:"selection"` Type string `json:"type"` Empty bool `json:"empty"` Text string `json:"text,omitempty"` Base64 string `json:"base64,omitempty"` Bytes int `json:"bytes"` Truncated bool `json:"truncated,omitempty"` } // Paste reads a selection. func Paste(selection, typ string) (PasteAnswer, error) { if err := checkTarget(typ); err != nil { return PasteAnswer{}, err } s, err := findSession() if err != nil { return PasteAnswer{}, err } args := []string{"-selection", selection, "-out"} if typ != "" { args = append(args, "-t", typ) } else { typ = "UTF8_STRING" } out := PasteAnswer{Selection: selection, Type: typ} r, err := xclip(s, Cmd{Args: args}) if err != nil { // "Error: target … not available": the selection is empty, or not offered as that type. if strings.Contains(r.Stderr, "not available") { out.Empty = true return out, nil } return PasteAnswer{}, err } out.Bytes, out.Truncated = len(r.Stdout), r.Truncated if utf8.ValidString(r.Stdout) { out.Text = r.Stdout } else { out.Base64 = base64.StdEncoding.EncodeToString([]byte(r.Stdout)) } out.Empty = out.Bytes == 0 return out, nil } // Targets answers the types a selection is offered as. func Targets(selection string) (map[string]any, error) { s, err := findSession() if err != nil { return nil, err } r, err := xclip(s, Cmd{Args: []string{"-selection", selection, "-out", "-t", "TARGETS"}}) if err != nil { if strings.Contains(r.Stderr, "not available") { return map[string]any{"selection": selection, "targets": []string{}, "empty": true}, nil } return nil, err } return map[string]any{"selection": selection, "targets": lines(r.Stdout), "empty": strings.TrimSpace(r.Stdout) == ""}, nil } // SessionCheck answers the session and whether its X server answers. func SessionCheck() (map[string]any, error) { s, err := findSession() if err != nil { return map[string]any{"found": false, "why": err.Error()}, nil } out := map[string]any{"found": true, "session": s} if _, err := Targets("clipboard"); err != nil { out["answers"], out["why"] = false, err.Error() } else { out["answers"] = true } return out, nil }