d24b57c9db
If we detect a duplicate for this selection, but another selection has already been written, we will truncate the wrong length in the line.
184 lines
6.3 KiB
Bash
Executable file
184 lines
6.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
: "${CM_ONESHOT=0}"
|
|
: "${CM_OWN_CLIPBOARD=0}"
|
|
: "${CM_DEBUG=0}"
|
|
: "${CM_DIR="${XDG_RUNTIME_DIR-"${TMPDIR-/tmp}"}"}"
|
|
|
|
: "${CM_MAX_CLIPS=1000}"
|
|
# Buffer to batch to avoid calling too much. Only used if CM_MAX_CLIPS >0.
|
|
CM_MAX_CLIPS_THRESH=$(( CM_MAX_CLIPS + 10 ))
|
|
|
|
: "${CM_SELECTIONS=clipboard primary}"
|
|
read -r -a selections <<< "$CM_SELECTIONS"
|
|
|
|
major_version=6
|
|
cache_dir=$CM_DIR/clipmenu.$major_version.$USER/
|
|
cache_file=$cache_dir/line_cache
|
|
|
|
# lock_file: lock for *one* iteration of clipboard capture/propagation
|
|
# session_lock_file: lock to prevent multiple clipmenud daemons
|
|
lock_file=$cache_dir/lock
|
|
session_lock_file=$cache_dir/session_lock
|
|
lock_timeout=2
|
|
has_xdotool=0
|
|
|
|
_xsel() { timeout 1 xsel --logfile /dev/null "$@"; }
|
|
|
|
error() { printf 'ERROR: %s\n' "${1?}" >&2; }
|
|
info() { printf 'INFO: %s\n' "${1?}"; }
|
|
die() {
|
|
error "${2?}"
|
|
exit "${1?}"
|
|
}
|
|
|
|
make_line_cksums() { while read -r line; do cksum <<< "${line#* }"; done; }
|
|
|
|
get_first_line() {
|
|
data=${1?}
|
|
|
|
# We look for the first line matching regex /./ here because we want the
|
|
# first line that can provide reasonable context to the user.
|
|
awk -v limit=300 '
|
|
BEGIN { printed = 0; }
|
|
printed == 0 && NF {
|
|
$0 = substr($0, 0, limit);
|
|
printf("%s", $0);
|
|
printed = 1;
|
|
}
|
|
END {
|
|
if (NR > 1)
|
|
printf(" (%d lines)", NR);
|
|
printf("\n");
|
|
}' <<< "$data"
|
|
}
|
|
|
|
debug() { (( CM_DEBUG )) && printf '%s\n' "$@" >&2; }
|
|
|
|
if [[ $1 == --help ]] || [[ $1 == -h ]]; then
|
|
cat << 'EOF'
|
|
clipmenud collects and caches what's on the clipboard.
|
|
|
|
Environment variables:
|
|
|
|
- $CM_DEBUG: turn on debugging output (default: 0)
|
|
- $CM_DIR: specify the base directory to store the cache dir in (default: $XDG_RUNTIME_DIR, $TMPDIR, or /tmp)
|
|
- $CM_MAX_CLIPS: soft maximum number of clips to store, 0 for inf. At $CM_MAX_CLIPS + 10, the number of clips is reduced to $CM_MAX_CLIPS (default: 1000)
|
|
- $CM_ONESHOT: run once immediately, do not loop (default: 0)
|
|
- $CM_OWN_CLIPBOARD: take ownership of the clipboard. Note: this may cause missed copies if some other application also handles the clipboard directly (default: 0)
|
|
- $CM_SELECTIONS: space separated list of the selections to manage (default: "clipboard primary")
|
|
- $CM_IGNORE_WINDOW: disable recording the clipboard in windows where the windowname matches the given regex (e.g. a password manager), do not ignore any windows if unset or empty (default: unset)
|
|
EOF
|
|
exit 0
|
|
fi
|
|
|
|
|
|
# It's ok that this only applies to the final directory.
|
|
# shellcheck disable=SC2174
|
|
mkdir -p -m0700 "$cache_dir"
|
|
|
|
exec {session_lock_fd}> "$session_lock_file"
|
|
flock -x -n "$session_lock_fd" ||
|
|
die 2 "Can't lock session file -- is another clipmenud running?"
|
|
|
|
declare -A last_data_sel
|
|
|
|
command -v clipnotify >/dev/null 2>&1 || die 2 "clipnotify not in PATH"
|
|
command -v xdotool >/dev/null 2>&1 && has_xdotool=1
|
|
|
|
if [[ $CM_IGNORE_WINDOW ]] && ! (( has_xdotool )); then
|
|
echo "WARN: CM_IGNORE_WINDOW does not work without xdotool, which is not installed" >&2
|
|
fi
|
|
|
|
exec {lock_fd}> "$lock_file"
|
|
|
|
while true; do
|
|
(( CM_ONESHOT )) || clipnotify
|
|
|
|
if [[ $CM_IGNORE_WINDOW ]] && (( has_xdotool )); then
|
|
windowname="$(xdotool getactivewindow getwindowname)"
|
|
if [[ "$windowname" =~ $CM_IGNORE_WINDOW ]]; then
|
|
debug "ignoring clipboard because windowname \"$windowname\" matches \"${CM_IGNORE_WINDOW}\""
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
if ! flock -x -w "$lock_timeout" "$lock_fd"; then
|
|
if (( CM_ONESHOT )); then
|
|
die 1 "Timed out waiting for lock"
|
|
else
|
|
error "Timed out waiting for lock, skipping this iteration"
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
for selection in "${selections[@]}"; do
|
|
data=$(_xsel -o --"$selection"; printf x)
|
|
data=${data%x} # avoid trailing newlines being stripped
|
|
|
|
[[ $data == *[^[:space:]]* ]] || continue
|
|
[[ $last_data == "$data" ]] && continue
|
|
[[ ${last_data_sel[$selection]} == "$data" ]] && continue
|
|
|
|
if [[ $last_data && $data == "$last_data"* ]] ||
|
|
[[ $last_data && $data == *"$last_data" ]]; then
|
|
# Don't actually remove the file yet, because it might be
|
|
# referenced by an older entry. These will be dealt with at vacuum.
|
|
debug "$selection: $last_data is a possible partial of $data"
|
|
previous_size=$(wc -c <<< "$last_cache_file_output")
|
|
truncate -s -"$previous_size" "$cache_file"
|
|
fi
|
|
|
|
first_line=$(get_first_line "$data")
|
|
debug "New clipboard entry on $selection selection: \"$first_line\""
|
|
|
|
cache_file_output="$(date +%s%N) $first_line"
|
|
filename="$cache_dir/$(cksum <<< "$first_line")"
|
|
last_cache_file_output=$cache_file_output
|
|
last_data=$data
|
|
last_data_sel[$selection]=$data
|
|
|
|
debug "Writing $data to $filename"
|
|
printf '%s' "$data" > "$filename"
|
|
debug "Writing $cache_file_output to $cache_file"
|
|
printf '%s\n' "$cache_file_output" >> "$cache_file"
|
|
|
|
if (( CM_OWN_CLIPBOARD )) && [[ $selection == clipboard ]]; then
|
|
# Only clipboard, since apps like urxvt will unhilight for PRIMARY
|
|
_xsel -o --clipboard | _xsel -i --clipboard
|
|
fi
|
|
done
|
|
|
|
if (( CM_MAX_CLIPS )) && (( "$(wc -l < "$cache_file")" > CM_MAX_CLIPS_THRESH )); then
|
|
info "Trimming clip cache to CM_MAX_CLIPS ($CM_MAX_CLIPS)"
|
|
trunc_tmp=$(mktemp)
|
|
tail -n "$CM_MAX_CLIPS" "$cache_file" | uniq > "$trunc_tmp"
|
|
mv -- "$trunc_tmp" "$cache_file"
|
|
|
|
# Vacuum up unreferenced clips. They may either have been
|
|
# unreferenced by the above CM_MAX_CLIPS code, or they may be old
|
|
# possible partials.
|
|
declare -A cksums
|
|
while IFS= read -r line; do
|
|
cksum=$(cksum <<< "$line")
|
|
cksums["$cksum"]="$line"
|
|
done < <(cut -d' ' -f2- < "$cache_file")
|
|
|
|
num_vacuumed=0
|
|
for file in "$cache_dir"/[012346789]*; do
|
|
cksum=${file##*/}
|
|
if [[ ${cksums["$cksum"]-_missing_} == _missing_ ]]; then
|
|
debug "Vacuuming due to lack of reference: $file"
|
|
(( ++num_vacuumed ))
|
|
rm -- "$file"
|
|
fi
|
|
done
|
|
unset cksums
|
|
info "Vacuumed $num_vacuumed clip files."
|
|
fi
|
|
|
|
flock -u "$lock_fd"
|
|
|
|
(( CM_ONESHOT )) && break
|
|
done
|