refactor demon and selector into a single go binary
This commit is contained in:
parent
63a247000b
commit
ad5f124288
7 changed files with 191 additions and 123 deletions
|
@ -10,8 +10,8 @@ A basic clipboard manager for Wayland.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
The demon that collects history is written in Go. Install it in your path, then run it in your Sway session by adding `exec clipman` at the beginning of your config.
|
Install the binary in your path, then run it in your Sway session by adding `exec clipman -d` at the beginning of your config.
|
||||||
|
|
||||||
You can configure how many history items to preserve (default: 15) by editing directly the source.
|
You can configure how many unique history items to preserve (default: 15) by editing directly the source.
|
||||||
|
|
||||||
To query the history and select items, run the provided python script (`clipman.py`). You can assign it to a keybinding.
|
To query the history and select items, run the binary as `clipman -s`. You can assign it to a keybinding: `bindsym $mod+h exec clipman -s`.
|
||||||
|
|
45
clipman.py
45
clipman.py
|
@ -1,45 +0,0 @@
|
||||||
#!/usr/bin/python
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
MAX = 15
|
|
||||||
|
|
||||||
|
|
||||||
def selector(input_, max_):
|
|
||||||
"""Send list to dmenu for selection. Display max items."""
|
|
||||||
cmd = [
|
|
||||||
"dmenu",
|
|
||||||
"-b",
|
|
||||||
"-fn",
|
|
||||||
"-misc-dejavu sans mono-medium-r-normal--17-120-100-100-m-0-iso8859-16",
|
|
||||||
"-l",
|
|
||||||
str(max_),
|
|
||||||
]
|
|
||||||
chosen = (
|
|
||||||
subprocess.run(
|
|
||||||
cmd, input="\n".join(input_).encode("utf-8"), capture_output=True
|
|
||||||
)
|
|
||||||
.stdout.decode()
|
|
||||||
.strip()
|
|
||||||
)
|
|
||||||
return chosen
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
with open(os.path.expanduser("~/.local/share/clipman.json")) as f:
|
|
||||||
history = json.load(f)
|
|
||||||
except FileNotFoundError:
|
|
||||||
sys.exit("No history available")
|
|
||||||
|
|
||||||
history = [repr(x) for x in reversed(history)] # don't expand newlines
|
|
||||||
|
|
||||||
selected = selector(history, MAX)
|
|
||||||
|
|
||||||
subprocess.run(["wl-copy", selected])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
93
demon.go
Normal file
93
demon.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const sleep = 1 * time.Second
|
||||||
|
|
||||||
|
func write(history []string, histfile string) error {
|
||||||
|
histlog, err := json.Marshal(history)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(histfile, histlog, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter(history []string, text string) []string {
|
||||||
|
var (
|
||||||
|
found bool
|
||||||
|
idx int
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, el := range history {
|
||||||
|
if el == text {
|
||||||
|
found = true
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if found {
|
||||||
|
// we know that idx can't be the last element, because
|
||||||
|
// we never get to call this function if that's the case
|
||||||
|
history = append(history[:idx], history[idx+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return history
|
||||||
|
}
|
||||||
|
|
||||||
|
func listen(history []string, histfile string) error {
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
t, err := exec.Command("wl-paste", []string{"-n", "-t", "text"}...).Output()
|
||||||
|
text := string(t)
|
||||||
|
if err != nil || text == "" {
|
||||||
|
// there's nothing to select, so we sleep.
|
||||||
|
time.Sleep(sleep)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
l := len(history)
|
||||||
|
|
||||||
|
if l > 0 {
|
||||||
|
|
||||||
|
// wl-paste will always give back the last copied text
|
||||||
|
// (as long as the place we copied from is still running)
|
||||||
|
if history[l-1] == text {
|
||||||
|
time.Sleep(sleep)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if l == max {
|
||||||
|
// we know that at any given time len(history) cannot be bigger than max,
|
||||||
|
// so it's enough to drop the first element
|
||||||
|
history = history[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove duplicates
|
||||||
|
// consider doing this in the frontend, for sparing resources
|
||||||
|
history = filter(history, text)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
history = append(history, text)
|
||||||
|
|
||||||
|
// dump history to file so that other apps can query it
|
||||||
|
err = write(history, histfile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(sleep)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
6
go.mod
6
go.mod
|
@ -1,3 +1,9 @@
|
||||||
module clipman
|
module clipman
|
||||||
|
|
||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||||
|
)
|
||||||
|
|
6
go.sum
Normal file
6
go.sum
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
96
main.go
96
main.go
|
@ -5,101 +5,51 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path"
|
"path"
|
||||||
"time"
|
|
||||||
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const max = 15
|
const max = 15
|
||||||
const sleep = 1 * time.Second
|
|
||||||
|
|
||||||
func write(history []string, histf string) error {
|
|
||||||
histlog, err := json.Marshal(history)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = ioutil.WriteFile(histf, histlog, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func filter(history []string, text string) []string {
|
|
||||||
var (
|
var (
|
||||||
found bool
|
app = kingpin.New("clipman", "A clipboard manager for Wayland")
|
||||||
idx int
|
asDemon = app.Flag("demon", "Run as a demon to record clipboard events").Short('d').Default("false").Bool()
|
||||||
|
asSelector = app.Flag("select", "Select an item from clipboard history").Short('s').Default("false").Bool()
|
||||||
)
|
)
|
||||||
|
|
||||||
for i, el := range history {
|
var (
|
||||||
if el == text {
|
histfile string
|
||||||
found = true
|
history []string
|
||||||
idx = i
|
)
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if found {
|
|
||||||
// we know that idx can't be the last element, because
|
|
||||||
// we never get to call this function if that's the case
|
|
||||||
history = append(history[:idx], history[idx+1:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return history
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
app.HelpFlag.Short('h')
|
||||||
|
kingpin.MustParse(app.Parse(os.Args[1:]))
|
||||||
|
if (*asDemon && *asSelector) || (!*asDemon && !*asSelector) {
|
||||||
|
log.Fatal("Missing or incompatible options. See -h/--help for info")
|
||||||
|
}
|
||||||
|
|
||||||
h, err := os.UserHomeDir()
|
h, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
histf := path.Join(h, ".local/share/clipman.json")
|
histfile = path.Join(h, ".local/share/clipman.json")
|
||||||
|
|
||||||
var history []string
|
b, err := ioutil.ReadFile(histfile)
|
||||||
b, err := ioutil.ReadFile(histf)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if err := json.Unmarshal(b, &history); err != nil {
|
if err := json.Unmarshal(b, &history); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
if *asDemon {
|
||||||
t, err := exec.Command("wl-paste", []string{"-n", "-t", "text"}...).Output()
|
if err := listen(history, histfile); err != nil {
|
||||||
text := string(t)
|
log.Fatal(err)
|
||||||
if err != nil || text == "" {
|
}
|
||||||
// there's nothing to select, so we sleep.
|
} else if *asSelector {
|
||||||
time.Sleep(sleep)
|
if err := selector(history); err != nil {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
l := len(history)
|
|
||||||
|
|
||||||
if l > 0 {
|
|
||||||
// wl-paste will always give back the last copied text
|
|
||||||
// (as long as the place we copied from is still running)
|
|
||||||
if history[l-1] == text {
|
|
||||||
time.Sleep(sleep)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if l == max {
|
|
||||||
// we know that at any given time len(history) cannot be bigger than max,
|
|
||||||
// so it's enough to drop the first element
|
|
||||||
history = history[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove duplicates
|
|
||||||
history = filter(history, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
history = append(history, text)
|
|
||||||
|
|
||||||
// dump history to file so that other apps can query it
|
|
||||||
err = write(history, histf)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(sleep)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
58
selector.go
Normal file
58
selector.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func selector(history []string) error {
|
||||||
|
|
||||||
|
// reverse the history
|
||||||
|
for i, j := 0, len(history)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
history[i], history[j] = history[j], history[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
selected, err := dmenu(history, max)
|
||||||
|
if err != nil {
|
||||||
|
// dmenu exits with error when no selection done
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := exec.Command("wl-copy", selected).Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dmenu(list []string, max int) (string, error) {
|
||||||
|
args := []string{"dmenu", "-b",
|
||||||
|
"-fn",
|
||||||
|
"-misc-dejavu sans mono-medium-r-normal--17-120-100-100-m-0-iso8859-16",
|
||||||
|
"-l",
|
||||||
|
strconv.Itoa(max)}
|
||||||
|
|
||||||
|
// dmenu will break if items contain newlines, so we must pass them as literals.
|
||||||
|
// however, when it sends them back, we need a way to restore them to non literals
|
||||||
|
guide := make(map[string]int)
|
||||||
|
reprList := []string{}
|
||||||
|
for i, l := range list {
|
||||||
|
repr := fmt.Sprintf("%#v", l)
|
||||||
|
guide[repr] = i
|
||||||
|
reprList = append(reprList, repr)
|
||||||
|
}
|
||||||
|
|
||||||
|
input := strings.NewReader(strings.Join(reprList, "\n"))
|
||||||
|
|
||||||
|
cmd := exec.Cmd{Path: "/usr/bin/dmenu", Args: args, Stdin: input}
|
||||||
|
selected, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
unescaped := list[guide[string(selected)]]
|
||||||
|
|
||||||
|
return unescaped, nil
|
||||||
|
}
|
Loading…
Reference in a new issue