diff --git a/.travis.yml b/.travis.yml index ec0f110..6b793dd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ before_script: script: - shellcheck -s bash clipmenu clipmenud + - tests/test-clipmenu matrix: fast_finish: true diff --git a/README.md b/README.md index c1b13b4..360eb74 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,38 @@ clipmenu is a simple clipboard manager using [dmenu][] and [xsel][]. -To use it, start the `clipmenud` daemon, and then call `clipmenu` to launch -`dmenu`. Upon choosing an entry, it is copied to the clipboard. +# Usage + +Start `clipmenud`, then run `clipmenu` to select something to put on the +clipboard. + +A systemd user service for starting clipmenud is included at +[init/clipmenud.service](https://github.com/cdown/clipmenu/blob/develop/init/clipmenud.service). All args passed to clipmenu are transparently dispatched to dmenu. That is, if you usually call dmenu with args to set colours and other properties, you can -invoke clipmenu in exactly the same way to get the same effect. +invoke clipmenu in exactly the same way to get the same effect, like so: + + clipmenu -i -fn Terminus:size=8 -nb '#002b36' -nf '#839496' -sb '#073642' -sf '#93a1a1' + +# How does it work? + +The code is fairly simple and easy to follow, you may find it easier to read +there, but it basically works like this: + +## clipmenud + +1. `clipmenud` polls the clipboard every 0.5 seconds (or another interval as + configured with the `CLIPMENUD_SLEEP` environment variable). Unfortunately + there's no interface to subscribe for changes in X11, so we must poll. +2. If `clipmenud` detects changes to the clipboard contents, it writes them out + to the cache directory. + +## clipmenu + +1. `clipmenu` reads the cache directory to find all available clips. +2. `dmenu` is executed to allow the user to select a clip. +3. After selection, the clip is put onto the PRIMARY and CLIPBOARD X + selections. [dmenu]: http://tools.suckless.org/dmenu/ [xsel]: http://www.vergenet.net/~conrad/software/xsel/ diff --git a/clipmenu b/clipmenu index 05ad197..5b5f873 100755 --- a/clipmenu +++ b/clipmenu @@ -1,15 +1,17 @@ #!/bin/bash +major_version=3 + shopt -s nullglob -cache_dir=/tmp/clipmenu.$USER +cache_dir=/tmp/clipmenu.$major_version.$USER cache_file=$cache_dir/line_cache # It's okay to hardcode `-l 8` here as a sensible default without checking # whether `-l` is also in "$@", because the way that dmenu works allows a later # argument to override an earlier one. That is, if the user passes in `-l`, our # one will be ignored. -chosen_line=$(tac "$cache_file" | uniq | dmenu -l 8 "$@") +chosen_line=$(tac "$cache_file" | awk '!seen[$0]++' | dmenu -l 8 "$@") [[ $chosen_line ]] || exit 1 @@ -22,9 +24,5 @@ if ! [[ -f "$file" ]]; then fi for selection in clipboard primary; do - if type -p xsel >/dev/null 2>&1; then - xsel --logfile /dev/null -i --"$selection" < "$file" - else - xclip -sel "$selection" < "$file" - fi + xsel --logfile /dev/null -i --"$selection" < "$file" done diff --git a/clipmenud b/clipmenud index 879f18c..8eca3a3 100755 --- a/clipmenud +++ b/clipmenud @@ -1,5 +1,9 @@ #!/bin/bash +major_version=3 +cache_dir=/tmp/clipmenu.$major_version.$USER/ +cache_file=$cache_dir/line_cache + get_first_line() { # Args: # - $1, the file or data @@ -37,9 +41,6 @@ debug() { fi } -cache_dir=/tmp/clipmenu.$USER/ -cache_file=$cache_dir/line_cache - # It's ok that this only applies to the final directory. # shellcheck disable=SC2174 mkdir -p -m0700 "$cache_dir" @@ -48,15 +49,8 @@ declare -A last_data declare -A last_filename while sleep "${CLIPMENUD_SLEEP:-0.5}"; do - for selection in clipboard primary; do - if type -p xsel >/dev/null 2>&1; then - debug 'Using xsel' - data=$(xsel --logfile /dev/null -o --"$selection"; printf x) - else - debug 'Using xclip' - data=$(xclip -o -sel "$selection"; printf x) - fi + data=$(xsel --logfile /dev/null -o --"$selection"; printf x) debug "Data before stripping: $data" @@ -109,11 +103,7 @@ while sleep "${CLIPMENUD_SLEEP:-0.5}"; do # We can't colocate this with the above copying code because # https://github.com/cdown/clipmenu/issues/34 requires knowing if # we would skip first. - if type -p xsel >/dev/null 2>&1; then - xsel --logfile /dev/null -o --"$selection" | xsel -i --"$selection" - else - xclip -o -sel "$selection" | xclip -i -sel "$selection" - fi + xsel --logfile /dev/null -o --"$selection" | xsel -i --"$selection" fi done done diff --git a/init/clipmenud.service b/init/clipmenud.service new file mode 100644 index 0000000..029ad0c --- /dev/null +++ b/init/clipmenud.service @@ -0,0 +1,31 @@ +[Unit] +Description=Clipmenu daemon + +[Service] +ExecStart=/usr/bin/clipmenud +Restart=always +RestartSec=0 +Environment=DISPLAY=:0 + +SystemCallFilter=@basic-io @default @io-event @ipc @network-io @process \ + brk fadvise64 getegid geteuid getgid getgroups getpgrp \ + getpid getppid getrlimit getuid ioctl mprotect rt_sigaction \ + rt_sigprocmask setitimer setsid sysinfo umask uname wait4 + +# @file-system will handle this once v233 is released, see +# http://bit.ly/2l1r8Ah for more details. +SystemCallFilter=access chdir close faccessat fcntl fstat getcwd mkdir mmap \ + munmap open stat statfs unlink + +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +ProtectControlGroups=yes +ProtectKernelTunables=yes +RestrictAddressFamilies= +RestrictRealtime=yes + +ProtectSystem=strict +ReadWritePaths=/tmp + +[Install] +WantedBy=default.target diff --git a/tests/test-clipmenu b/tests/test-clipmenu new file mode 100755 index 0000000..3da51aa --- /dev/null +++ b/tests/test-clipmenu @@ -0,0 +1,81 @@ +#!/bin/bash + +set -x +set -e +set -o pipefail + +major_version=3 +dir=/tmp/clipmenu.$major_version.$USER +cache_file=$dir/line_cache + +if [[ $0 == /* ]]; then + location=${0%/*} +else + location=$PWD/${0#./} + location=${location%/*} +fi + +cat - "$location/../clipmenu" > /tmp/clipmenu << 'EOF' +#!/bin/bash + +shopt -s expand_aliases + +shim() { + printf '%s args:' "$1" >&2 + printf ' %q' "${@:2}" >&2 + printf '\n' >&2 + + i=0 + + while IFS= read -r line; do + let i++ + printf '%s line %d stdin: %s\n' "$1" "$i" "$line" >&2 + done + + if [[ -v SHIM_STDOUT ]]; then + printf '%s\n' "$SHIM_STDOUT" + fi +} + +alias dmenu='SHIM_STDOUT="Selected text. (2 lines)" shim dmenu' +alias xsel='shim xsel' +alias xclip='shim xclip' +EOF + +chmod a+x /tmp/clipmenu + +rm -rf "$dir" +mkdir -p "$dir" + +cat > "$cache_file" << 'EOF' +Selected text. (2 lines) +Selected text 2. (2 lines) +EOF + +cat > "$dir/$(cksum <<< 'Selected text. (2 lines)')" << 'EOF' +Selected text. +Yes, it's selected text. +EOF + +### TESTS ### + +output=$(/tmp/clipmenu --foo bar 2>&1) + +temp=$(mktemp) +trap 'rm -f -- "$temp"' EXIT + +printf '%s\n' "$output" > "$temp" + +# Arguments are transparently passed to dmenu +grep -Fxq 'dmenu args: -l 8 --foo bar' "$temp" + +# Output from cache file should get to dmenu, reversed +grep -Fxq 'dmenu line 1 stdin: Selected text 2. (2 lines)' "$temp" +grep -Fxq 'dmenu line 2 stdin: Selected text. (2 lines)' "$temp" + +# xsel should copy both to clipboard *and* primary +grep -Fxq 'xsel args: --logfile /dev/null -i --clipboard' "$temp" +grep -Fxq 'xsel args: --logfile /dev/null -i --primary' "$temp" + +grep -Fxq 'xsel line 1 stdin: Selected text.' "$temp" +grep -Fxq "xsel line 2 stdin: Yes, it's selected text." "$temp" diff --git a/test/test-perf b/tests/test-perf similarity index 97% rename from test/test-perf rename to tests/test-perf index fca39ac..a393de6 100755 --- a/test/test-perf +++ b/tests/test-perf @@ -1,10 +1,12 @@ #!/bin/bash +major_version=3 + msg() { printf '>>> %s\n' "$@" >&2 } -dir=/tmp/clipmenu.$USER +dir=/tmp/clipmenu.$major_version.$USER cache_file=$dir/line_cache log=$(mktemp) @@ -35,7 +37,6 @@ shopt -s expand_aliases alias dmenu=: alias xsel=: -alias xclip=: EOF