clipmenu

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

commit ef3102c5e9c8c927afc0c463f0bbe372a75a815b
parent 3ab7a6e2e7b4ebb18a432e7adee6a64bbbc825f9
Author: Chris Down <chris@chrisdown.name>
Date:   Wed, 25 Oct 2017 01:58:03 +0100

Merge branch 'release/4.0.0'

Diffstat:
MREADME.md | 5+++++
Mclipmenu | 26++++++++++++++------------
Mclipmenud | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Minit/clipmenud.service | 3---
Mtests/test-clipmenu | 6++++--
Mtests/test-perf | 6++++--
6 files changed, 80 insertions(+), 44 deletions(-)

diff --git a/README.md b/README.md @@ -29,6 +29,11 @@ there, but it basically works like this: 1. `clipmenud` polls the clipboard every 0.5 seconds (or another interval as configured with the `CM_SLEEP` environment variable). Unfortunately there's no interface to subscribe for changes in X11, so we must poll. + + Instead of polling, you can bind your copy key binding to also issue + `CM_ONESHOT=1 clipmenud`. However, there's no generic way to do this, since + any keys or mouse buttons could be bound to do this action in a number of + ways. 2. If `clipmenud` detects changes to the clipboard contents, it writes them out to the cache directory. diff --git a/clipmenu b/clipmenu @@ -1,21 +1,17 @@ #!/bin/bash -major_version=3 +: "${CM_LAUNCHER=dmenu}" +: "${CM_DIR="${XDG_RUNTIME_DIR-"${TMPDIR-/tmp}"}"}" + +major_version=4 shopt -s nullglob -cache_dir=/tmp/clipmenu.$major_version.$USER +cache_dir=$CM_DIR/clipmenu.$major_version.$USER cache_file=$cache_dir/line_cache -: "${CM_LAUNCHER=dmenu}" - -if [[ "$CM_LAUNCHER" == rofi ]]; then - # rofi supports dmenu-like arguments through the -dmenu flag - set -- -dmenu "$@" -fi - -if [[ $1 == --help ]]; then - cat << EOF +if [[ $1 == --help ]] || [[ $1 == -h ]]; then + cat << 'EOF' clipmenu is a simple clipboard manager using dmenu and xsel. Launch this when you want to select a clip. @@ -23,11 +19,17 @@ All arguments are passed through to dmenu itself. Environment variables: -- \$CM_LAUNCHER: specify a dmenu-compatible launcher (default: dmenu) +- $CM_LAUNCHER: specify a dmenu-compatible launcher (default: dmenu) +- $CM_DIR: specify the base directory to store the cache dir in (default: $XDG_RUNTIME_DIR, $TMPDIR, or /tmp) EOF exit 0 fi +if [[ "$CM_LAUNCHER" == rofi ]]; then + # rofi supports dmenu-like arguments through the -dmenu flag + set -- -dmenu "$@" +fi + # 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 diff --git a/clipmenud b/clipmenud @@ -1,14 +1,16 @@ #!/bin/bash -major_version=3 -cache_dir=/tmp/clipmenu.$major_version.$USER/ -cache_file=$cache_dir/line_cache -lock_file=$cache_dir/lock -lock_timeout=2 - : "${CM_ONESHOT=0}" : "${CM_OWN_CLIPBOARD=1}" : "${CM_DEBUG=0}" +: "${CM_DIR="${XDG_RUNTIME_DIR-"${TMPDIR-/tmp}"}"}" +: "${CM_MAX_CLIPS=1000}" + +major_version=4 +cache_dir=$CM_DIR/clipmenu.$major_version.$USER/ +cache_file=$cache_dir/line_cache +lock_file=$cache_dir/lock +lock_timeout=2 _xsel() { timeout 1 xsel --logfile /dev/stderr "$@" @@ -51,16 +53,32 @@ debug() { fi } +if [[ $1 == --help ]] || [[ $1 == -h ]]; then + cat << 'EOF' +clipmenud is the daemon that collects and caches what's on the clipboard. +when you want to select a clip. + +Environment variables: + +- $CM_ONESHOT: run once immediately, do not loop (default: 0) +- $CM_DEBUG: turn on debugging output (default: 0) +- $CM_OWN_CLIPBOARD: take ownership of the clipboard (default: 1) +- $CM_MAX_CLIPS: maximum number of clips to store, 0 for inf (default: 1000) +- $CM_DIR: specify the base directory to store the cache dir in (default: $XDG_RUNTIME_DIR, $TMPDIR, or /tmp) +EOF + exit 0 +fi + + # It's ok that this only applies to the final directory. # shellcheck disable=SC2174 mkdir -p -m0700 "$cache_dir" declare -A last_data -declare -A last_filename exec {lock_fd}> "$lock_file" -while sleep "${CM_SLEEP:-0.5}"; do +while (( CM_ONESHOT )) || sleep "${CM_SLEEP:-0.5}"; do if ! flock -x -w "$lock_timeout" "$lock_fd"; then if (( CM_ONESHOT )); then printf 'ERROR: %s\n' 'Timed out waiting for lock' >&2 @@ -94,29 +112,24 @@ while sleep "${CM_SLEEP:-0.5}"; do continue fi - # If we were in the middle of doing a selection when the previous poll - # ran, then we may have got a partial clip. - possible_partial=${last_data[$selection]} - if [[ $possible_partial && $data == "$possible_partial"* ]]; then - debug "$possible_partial is a possible partial of $data" - debug "Removing ${last_filename[$selection]}" - rm -- "${last_filename[$selection]}" - fi - last_data[$selection]=$data - last_filename[$selection]=$filename first_line=$(get_first_line "$data") - printf 'New clipboard entry on %s selection: "%s"\n' \ - "$selection" "$first_line" + debug "New clipboard entry on $selection selection: \"$first_line\"" - filename="$cache_dir/$(cksum <<< "$first_line")" - debug "Writing $data to $filename" - printf '%s' "$data" > "$filename" + # Without checking ${last_data[any]}, we often double write since both + # selections get the same content + if [[ ${last_data[any]} != "$data" ]]; then + filename="$cache_dir/$(cksum <<< "$first_line")" + debug "Writing $data to $filename" + printf '%s' "$data" > "$filename" - debug "Writing $first_line to $cache_file" - printf '%s\n' "$first_line" >> "$cache_file" + debug "Writing $first_line to $cache_file" + printf '%s\n' "$first_line" >> "$cache_file" + fi + + last_data[any]=$data if (( CM_OWN_CLIPBOARD )) && [[ $selection != primary ]]; then # Take ownership of the clipboard, in case the original application @@ -132,6 +145,21 @@ while sleep "${CM_SLEEP:-0.5}"; do # we would skip first. _xsel -o --"$selection" | _xsel -i --"$selection" fi + + if (( CM_MAX_CLIPS )); then + mapfile -t to_remove < <( + head -n -"$CM_MAX_CLIPS" "$cache_file" | + while read -r line; do cksum <<< "$line"; done + ) + num_to_remove="${#to_remove[@]}" + if (( num_to_remove )); then + debug "Removing $num_to_remove old clips" + rm -- "${to_remove[@]/#/"$cache_dir/"}" + trunc_tmp=$(mktemp) + tail -n "$CM_MAX_CLIPS" "$cache_file" | uniq > "$trunc_tmp" + mv -- "$trunc_tmp" "$cache_file" + fi + fi done flock -u "$lock_fd" diff --git a/init/clipmenud.service b/init/clipmenud.service @@ -14,8 +14,5 @@ ProtectKernelTunables=yes RestrictAddressFamilies= RestrictRealtime=yes -ProtectSystem=strict -ReadWritePaths=/tmp - [Install] WantedBy=default.target diff --git a/tests/test-clipmenu b/tests/test-clipmenu @@ -4,8 +4,10 @@ set -x set -e set -o pipefail -major_version=3 -dir=/tmp/clipmenu.$major_version.$USER +: "${CM_DIR="${XDG_RUNTIME_DIR-"${TMPDIR-/tmp}"}"}" + +major_version=4 +dir=$CM_DIR/clipmenu.$major_version.$USER cache_file=$dir/line_cache if [[ $0 == /* ]]; then diff --git a/tests/test-perf b/tests/test-perf @@ -1,12 +1,14 @@ #!/bin/bash -major_version=3 +major_version=4 msg() { printf '>>> %s\n' "$@" >&2 } -dir=/tmp/clipmenu.$major_version.$USER +: "${CM_DIR="${XDG_RUNTIME_DIR-"${TMPDIR-/tmp}"}"}" + +dir=$CM_DIR/clipmenu.$major_version.$USER cache_file=$dir/line_cache log=$(mktemp)