2019-03-22 15:13:41 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2019-09-16 19:39:53 +02:00
|
|
|
"bufio"
|
2019-03-22 15:13:41 +01:00
|
|
|
"encoding/json"
|
2019-08-02 13:51:13 +02:00
|
|
|
"fmt"
|
2019-03-22 15:13:41 +01:00
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"os"
|
2019-09-16 20:44:19 +02:00
|
|
|
"os/exec"
|
2019-09-07 19:41:41 +02:00
|
|
|
"strings"
|
2019-03-22 16:25:09 +01:00
|
|
|
|
|
|
|
"gopkg.in/alecthomas/kingpin.v2"
|
2019-03-22 15:13:41 +01:00
|
|
|
)
|
|
|
|
|
2019-10-13 14:22:09 +02:00
|
|
|
const version = "1.1.0"
|
2019-10-02 16:20:04 +02:00
|
|
|
|
2019-03-22 16:25:09 +01:00
|
|
|
var (
|
2019-09-17 09:46:30 +02:00
|
|
|
app = kingpin.New("clipman", "A clipboard manager for Wayland")
|
|
|
|
histpath = app.Flag("histpath", "Path of history file").Default("~/.local/share/clipman.json").String()
|
|
|
|
|
|
|
|
storer = app.Command("store", "Record clipboard events (run as argument to `wl-paste --watch`)")
|
|
|
|
maxDemon = storer.Flag("max-items", "history size").Default("15").Int()
|
|
|
|
noPersist = storer.Flag("no-persist", "Don't persist a copy buffer after a program exits").Short('P').Default("false").Bool()
|
|
|
|
|
2019-10-14 09:40:00 +02:00
|
|
|
picker = app.Command("pick", "Pick an item from clipboard history")
|
|
|
|
maxPicker = picker.Flag("max-items", "scrollview length").Default("15").Int()
|
|
|
|
pickTool = picker.Flag("tool", "Which selector to use: dmenu/rofi/wofi/STDOUT").Short('t').Default("dmenu").String()
|
|
|
|
pickToolArgs = picker.Flag("tool-args", "Extra arguments to pass to the --tool").Short('T').Default("").String()
|
|
|
|
|
|
|
|
clearer = app.Command("clear", "Remove item(s) from history")
|
|
|
|
maxClearer = clearer.Flag("max-items", "scrollview length").Default("15").Int()
|
|
|
|
clearTool = clearer.Flag("tool", "Which selector to use: dmenu/rofi/wofi/STDOUT").Short('t').Default("dmenu").String()
|
|
|
|
clearToolArgs = clearer.Flag("tool-args", "Extra arguments to pass to the --tool").Short('T').Default("").String()
|
|
|
|
clearAll = clearer.Flag("all", "Remove all items").Short('a').Default("false").Bool()
|
2019-10-27 09:13:44 +01:00
|
|
|
|
|
|
|
restorer = app.Command("restore", "Serve the last recorded item from history")
|
2019-03-22 16:25:09 +01:00
|
|
|
)
|
2019-03-22 15:13:41 +01:00
|
|
|
|
2019-03-22 16:25:09 +01:00
|
|
|
func main() {
|
2019-10-02 16:20:04 +02:00
|
|
|
app.Version(version)
|
2019-03-22 16:25:09 +01:00
|
|
|
app.HelpFlag.Short('h')
|
2019-10-02 16:20:04 +02:00
|
|
|
app.VersionFlag.Short('v')
|
2019-09-15 12:36:53 +02:00
|
|
|
switch kingpin.MustParse(app.Parse(os.Args[1:])) {
|
2019-09-16 19:39:53 +02:00
|
|
|
case "store":
|
2019-09-17 09:46:30 +02:00
|
|
|
histfile, history, err := getHistory(*histpath)
|
2019-09-15 12:36:53 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2019-09-16 19:39:53 +02:00
|
|
|
|
|
|
|
// read copy from stdin
|
|
|
|
var stdin []string
|
|
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
|
|
for scanner.Scan() {
|
|
|
|
stdin = append(stdin, scanner.Text())
|
|
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
log.Fatal("Error getting input from stdin.")
|
|
|
|
}
|
|
|
|
text := strings.Join(stdin, "\n")
|
|
|
|
|
2019-09-17 09:46:30 +02:00
|
|
|
persist := !*noPersist
|
|
|
|
if err := store(text, history, histfile, *maxDemon, persist); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2019-09-15 12:36:53 +02:00
|
|
|
case "pick":
|
2019-09-17 09:46:30 +02:00
|
|
|
_, history, err := getHistory(*histpath)
|
2019-09-15 12:36:53 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2019-10-14 09:40:00 +02:00
|
|
|
selection, err := selector(history, *maxPicker, *pickTool, "pick", *pickToolArgs)
|
2019-09-16 20:44:19 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2019-09-17 13:28:34 +02:00
|
|
|
if selection != "" {
|
|
|
|
// serve selection to the OS
|
2019-10-13 10:25:49 +02:00
|
|
|
if err := exec.Command("wl-copy", "--", selection).Run(); err != nil {
|
2019-09-17 13:28:34 +02:00
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2019-09-17 11:03:37 +02:00
|
|
|
}
|
2019-10-27 09:13:44 +01:00
|
|
|
case "restore":
|
|
|
|
_, history, err := getHistory(*histpath)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(history) == 0 {
|
|
|
|
log.Println("Nothing to restore")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := exec.Command("wl-copy", "--", history[len(history)-1]).Run(); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2019-09-16 20:44:19 +02:00
|
|
|
case "clear":
|
2019-09-17 09:46:30 +02:00
|
|
|
histfile, history, err := getHistory(*histpath)
|
2019-09-16 20:44:19 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2019-09-17 09:46:30 +02:00
|
|
|
// remove all history
|
2019-09-16 20:44:19 +02:00
|
|
|
if *clearAll {
|
2019-10-13 10:25:49 +02:00
|
|
|
if err := wipeAll(histfile); err != nil {
|
2019-09-16 20:44:19 +02:00
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2019-10-13 10:25:49 +02:00
|
|
|
return
|
2019-09-16 20:44:19 +02:00
|
|
|
}
|
|
|
|
|
2019-10-14 09:40:00 +02:00
|
|
|
selection, err := selector(history, *maxClearer, *clearTool, "clear", *clearToolArgs)
|
2019-09-16 20:44:19 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2019-10-13 10:25:49 +02:00
|
|
|
if selection == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(history) < 2 {
|
|
|
|
// there was only one possible item we could select, and we selected it,
|
|
|
|
// so wipe everything
|
|
|
|
if err := wipeAll(histfile); err != nil {
|
|
|
|
log.Fatal(err)
|
2019-09-17 13:55:08 +02:00
|
|
|
}
|
2019-10-13 10:25:49 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if selection == history[len(history)-1] {
|
|
|
|
// wl-copy is still serving the copy, so replace with next latest
|
|
|
|
// note: we alread exited if less than 2 items
|
|
|
|
if err := exec.Command("wl-copy", "--", history[len(history)-2]).Run(); err != nil {
|
2019-09-17 13:28:34 +02:00
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2019-09-15 12:36:53 +02:00
|
|
|
}
|
2019-10-13 10:25:49 +02:00
|
|
|
|
|
|
|
if err := write(filter(history, selection), histfile); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func wipeAll(histfile string) error {
|
|
|
|
// clear WM's clipboard
|
|
|
|
if err := exec.Command("wl-copy", "-c").Run(); err != nil {
|
|
|
|
return err
|
2019-03-22 15:13:41 +01:00
|
|
|
}
|
2019-10-13 10:25:49 +02:00
|
|
|
|
|
|
|
if err := os.Remove(histfile); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2019-09-15 12:36:53 +02:00
|
|
|
}
|
2019-03-22 15:13:41 +01:00
|
|
|
|
2019-09-17 09:46:30 +02:00
|
|
|
func getHistory(rawPath string) (string, []string, error) {
|
|
|
|
// set histfile; expand user home
|
|
|
|
histfile := rawPath
|
2019-09-07 19:41:41 +02:00
|
|
|
if strings.HasPrefix(histfile, "~") {
|
2019-08-24 21:08:38 +02:00
|
|
|
home, err := os.UserHomeDir()
|
|
|
|
if err != nil {
|
2019-09-15 12:36:53 +02:00
|
|
|
return "", nil, err
|
2019-08-24 21:08:38 +02:00
|
|
|
}
|
2019-09-07 19:41:41 +02:00
|
|
|
histfile = strings.Replace(histfile, "~", home, 1)
|
2019-03-22 15:13:41 +01:00
|
|
|
}
|
|
|
|
|
2019-09-17 09:46:30 +02:00
|
|
|
// read history if it exists
|
2019-03-25 19:13:19 +01:00
|
|
|
var history []string
|
2019-03-22 16:25:09 +01:00
|
|
|
b, err := ioutil.ReadFile(histfile)
|
2019-09-07 19:41:41 +02:00
|
|
|
if err != nil {
|
2019-09-16 12:51:33 +02:00
|
|
|
if !os.IsNotExist(err) {
|
2019-09-17 09:46:30 +02:00
|
|
|
return "", nil, fmt.Errorf("failure reading history file: %s", err)
|
2019-09-16 12:51:33 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if err := json.Unmarshal(b, &history); err != nil {
|
2019-09-17 09:46:30 +02:00
|
|
|
return "", nil, fmt.Errorf("failure parsing history: %s", err)
|
2019-09-16 12:51:33 +02:00
|
|
|
}
|
2019-03-22 15:13:41 +01:00
|
|
|
}
|
|
|
|
|
2019-09-15 12:36:53 +02:00
|
|
|
return histfile, history, nil
|
2019-03-22 15:13:41 +01:00
|
|
|
}
|