clipmenu

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

commit 09c32b025ea3e4e3d7b0d949035b4f104f7e10e8
parent 2ab5b5b502b83c0ba3239da50dd5475a5b5e9061
Author: Chris Down <chris@chrisdown.name>
Date:   Fri,  6 Jan 2017 17:36:01 +0000

Merge branch 'release/2.0.0'

Diffstat:
Mclipmenu | 48+++++++++++++-----------------------------------
Mclipmenud | 73+++++++++++++++++++++++++++++++++++++------------------------------------
Atest/test-perf | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 140 insertions(+), 71 deletions(-)

diff --git a/clipmenu b/clipmenu @@ -2,51 +2,29 @@ shopt -s nullglob -# We use this to make sure the cache files are sorted bytewise -LC_COLLATE=C - -# Some people copy/paste huge swathes of text that could slow down dmenu -line_length_limit=500 - -declare -A selections -ordered_selections=() - -files=("/tmp/clipmenu.$USER/"*) - -# We can't use `for ... in` here because we need to add files to -# ordered_selections from last to first -- that is, newest to oldest. Incoming -# clipboard entries have a ISO datetime prefixed to the front to aid in this. -for (( i=${#files[@]}-1; i>=0; i-- )); do - file=${files[$i]} - - # We look for the first line matching regex /./ here because we want the - # first line that can provide reasonable context to the user. That is, if - # you have 5 leading lines of whitespace, displaying " (6 lines)" is much - # less useful than displaying "foo (6 lines)", where "foo" is the first - # line in the entry with actionable context. - first_line=$(sed -n '/./{p;q}' "$file" | cut -c1-"$line_length_limit") - lines=$(wc -l < "$file") - - if (( lines > 1 )); then - first_line+=" ($lines lines)" - fi - - ordered_selections+=("$first_line") - selections[$first_line]=$file -done +cache_dir=/tmp/clipmenu.$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=$(printf '%s\n' "${ordered_selections[@]}" | uniq | dmenu -l 8 "$@") +chosen_line=$(tac "$cache_file" | uniq | dmenu -l 8 "$@") [[ $chosen_line ]] || exit 1 +file=$cache_dir/$(cksum <<< "$chosen_line") + +if ! [[ -f "$file" ]]; then + # We didn't find this in cache + printf 'FATAL: %s not in cache\n' "$chosen_line" >&2 + exit 2 +fi + for selection in clipboard primary; do if type -p xsel >/dev/null 2>&1; then - xsel -i --"$selection" < "${selections[$chosen_line]}" + xsel --logfile /dev/null -i --"$selection" < "$file" else - xclip -sel "$selection" < "${selections[$chosen_line]}" + xclip -sel "$selection" < "$file" fi done diff --git a/clipmenud b/clipmenud @@ -1,7 +1,34 @@ #!/bin/bash -hr_msg() { - printf -- '\n--- %s ---\n\n' "$1" >&2 +get_first_line() { + # Args: + # - $1, the file or data + # - $2, optional, the line length limit + + data=${1?} + line_length_limit=${2-300} + + # We look for the first line matching regex /./ here because we want the + # first line that can provide reasonable context to the user. That is, if + # you have 5 leading lines of whitespace, displaying " (6 lines)" is much + # less useful than displaying "foo (6 lines)", where "foo" is the first + # line in the entry with actionable context. + awk -v limit="$line_length_limit" ' + BEGIN { printed = 0; } + + printed == 0 && NF { + $0 = substr($0, 0, limit); + printf("%s", $0); + printed = 1; + } + + END { + if (NR > 1) { + print " (" NR " lines)"; + } else { + printf("\n"); + } + }' <<< "$data" } debug() { @@ -10,34 +37,8 @@ debug() { fi } -print_debug_info() { - # DEBUG comes from the environment - if ! (( DEBUG )); then - return - fi - - local msg="${1?}" - - hr_msg "$msg" - - hr_msg Environment - env | LC_ALL=C sort >&2 - - cgroup_path=/proc/$$/cgroup - - if [[ -f $cgroup_path ]]; then - hr_msg cgroup - cat "$cgroup_path" >&2 - else - hr_msg 'NO CGROUP' - fi - - hr_msg 'Finished debug info' -} - -print_debug_info 'Initialising' - 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 @@ -47,14 +48,11 @@ declare -A last_data declare -A last_filename while sleep "${CLIPMENUD_SLEEP:-0.5}"; do - print_debug_info 'About to run selection' for selection in clipboard primary; do - print_debug_info "About to do selection for '$selection'" - if type -p xsel >/dev/null 2>&1; then debug 'Using xsel' - data=$(xsel -o --"$selection"; printf x) + data=$(xsel --logfile /dev/null -o --"$selection"; printf x) else debug 'Using xclip' data=$(xclip -o -sel "$selection"; printf x) @@ -88,14 +86,17 @@ while sleep "${CLIPMENUD_SLEEP:-0.5}"; do rm -- "${last_filename[$selection]}" fi - filename="$cache_dir/$(LC_ALL=C date +%F-%T.%N)" - last_data[$selection]=$data last_filename[$selection]=$filename + first_line=$(get_first_line "$data") + 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" + if ! (( NO_OWN_CLIPBOARD )) && [[ $selection != primary ]]; then # Take ownership of the clipboard, in case the original application # is unable to serve the clipboard request (due to being suspended, @@ -109,7 +110,7 @@ while sleep "${CLIPMENUD_SLEEP:-0.5}"; do # https://github.com/cdown/clipmenu/issues/34 requires knowing if # we would skip first. if type -p xsel >/dev/null 2>&1; then - xsel -o --"$selection" | xsel -i --"$selection" + xsel --logfile /dev/null -o --"$selection" | xsel -i --"$selection" else xclip -o -sel "$selection" | xclip -i -sel "$selection" fi diff --git a/test/test-perf b/test/test-perf @@ -0,0 +1,90 @@ +#!/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