clipmenu

Simple clipboard management using dmenu
git clone https://git.sinitax.com/cdown/clipmenu
Log | Files | Refs | README | LICENSE | sfeed.txt

commit d938354148163f549dcdd92759832d686d276d88
parent 09c32b025ea3e4e3d7b0d949035b4f104f7e10e8
Author: Chris Down <chris@chrisdown.name>
Date:   Fri, 17 Feb 2017 12:07:05 -0500

Merge branch 'release/3.0.0'

Diffstat:
M.travis.yml | 1+
MREADME.md | 33++++++++++++++++++++++++++++++---
Mclipmenu | 12+++++-------
Mclipmenud | 22++++++----------------
Ainit/clipmenud.service | 31+++++++++++++++++++++++++++++++
Dtest/test-perf | 90-------------------------------------------------------------------------------
Atests/test-clipmenu | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/test-perf | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 245 insertions(+), 116 deletions(-)

diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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/test/test-perf b/test/test-perf @@ -1,90 +0,0 @@ -#!/bin/bash - -msg() { - printf '>>> %s\n' "$@" >&2 -} - -dir=/tmp/clipmenu.$USER -cache_file=$dir/line_cache - -log=$(mktemp) -tim=$(mktemp) -clipmenu_shim=$(mktemp) -num_files=1500 - -trap 'rm -f -- "$log" "$tim" "$clipmenu_shim"' EXIT - -if [[ $0 == /* ]]; then - location=${0%/*} -else - location=$PWD/${0#./} - location=${location%/*} -fi - -msg 'Setting up edited clipmenu' - -cat - "$location/../clipmenu" > /tmp/clipmenu << EOF -#!/bin/bash - -exec 3>&2 2> >(tee "$log" | - sed -u 's/^.*$/now/' | - date -f - +%s.%N > "$tim") -set -x - -shopt -s expand_aliases - -alias dmenu=: -alias xsel=: -alias xclip=: - -EOF - -chmod a+x /tmp/clipmenu - -if ! (( NO_RECREATE )); then - rm -rf "$dir" - mkdir -p "$dir" - - msg "Writing $num_files clipboard files" - - for (( i = 0; i <= num_files; i++ )); do - (( i % 100 )) || printf '%s... ' "$i" - - line_len=$(( (RANDOM % 10000) + 1 )) - num_lines=$(( (RANDOM % 10) + 1 )) - data=$( - tr -dc 'a-zA-Z0-9' < /dev/urandom | - fold -w "$line_len" | - head -"$num_lines" - ) - read -r first_line_raw <<< "$data" - printf -v first_line '%s (%s lines)\n' "$first_line_raw" "$num_lines" - printf '%s' "$first_line" >> "$cache_file" - fn=$dir/$(cksum <<< "$first_line") - printf '%s' "$data" > "$fn" - done - - printf 'done\n' -else - msg 'Not nuking/creating new clipmenu files' -fi - -msg 'Running modified clipmenu' - -time /tmp/clipmenu - -(( TIME_ONLY )) && exit 0 - -msg 'Displaying perf data' - -# modified from http://stackoverflow.com/a/20855353/945780 -paste <( - while read -r tim ;do - [ -z "$last" ] && last=${tim//.} && first=${tim//.} - crt=000000000$((${tim//.}-10#0$last)) - ctot=000000000$((${tim//.}-10#0$first)) - printf "%12.9f %12.9f\n" ${crt:0:${#crt}-9}.${crt:${#crt}-9} \ - ${ctot:0:${#ctot}-9}.${ctot:${#ctot}-9} - last=${tim//.} - done < "$tim" -) "$log" | less diff --git a/tests/test-clipmenu 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/tests/test-perf b/tests/test-perf @@ -0,0 +1,91 @@ +#!/bin/bash + +major_version=3 + +msg() { + printf '>>> %s\n' "$@" >&2 +} + +dir=/tmp/clipmenu.$major_version.$USER +cache_file=$dir/line_cache + +log=$(mktemp) +tim=$(mktemp) +clipmenu_shim=$(mktemp) +num_files=1500 + +trap 'rm -f -- "$log" "$tim" "$clipmenu_shim"' EXIT + +if [[ $0 == /* ]]; then + location=${0%/*} +else + location=$PWD/${0#./} + location=${location%/*} +fi + +msg 'Setting up edited clipmenu' + +cat - "$location/../clipmenu" > /tmp/clipmenu << EOF +#!/bin/bash + +exec 3>&2 2> >(tee "$log" | + sed -u 's/^.*$/now/' | + date -f - +%s.%N > "$tim") +set -x + +shopt -s expand_aliases + +alias dmenu=: +alias xsel=: + +EOF + +chmod a+x /tmp/clipmenu + +if ! (( NO_RECREATE )); then + rm -rf "$dir" + mkdir -p "$dir" + + msg "Writing $num_files clipboard files" + + for (( i = 0; i <= num_files; i++ )); do + (( i % 100 )) || printf '%s... ' "$i" + + line_len=$(( (RANDOM % 10000) + 1 )) + num_lines=$(( (RANDOM % 10) + 1 )) + data=$( + tr -dc 'a-zA-Z0-9' < /dev/urandom | + fold -w "$line_len" | + head -"$num_lines" + ) + read -r first_line_raw <<< "$data" + printf -v first_line '%s (%s lines)\n' "$first_line_raw" "$num_lines" + printf '%s' "$first_line" >> "$cache_file" + fn=$dir/$(cksum <<< "$first_line") + printf '%s' "$data" > "$fn" + done + + printf 'done\n' +else + msg 'Not nuking/creating new clipmenu files' +fi + +msg 'Running modified clipmenu' + +time /tmp/clipmenu + +(( TIME_ONLY )) && exit 0 + +msg 'Displaying perf data' + +# modified from http://stackoverflow.com/a/20855353/945780 +paste <( + while read -r tim ;do + [ -z "$last" ] && last=${tim//.} && first=${tim//.} + crt=000000000$((${tim//.}-10#0$last)) + ctot=000000000$((${tim//.}-10#0$first)) + printf "%12.9f %12.9f\n" ${crt:0:${#crt}-9}.${crt:${#crt}-9} \ + ${ctot:0:${#ctot}-9}.${ctot:${#ctot}-9} + last=${tim//.} + done < "$tim" +) "$log" | less