fa9d01a752
Fixes #67. This used to be useful in order to avoid doing multiple writes back when we didn't deduplicate in clipmenu client, but now we do and don't need this. Even more impressively, it actually breaks things! See #67 for more information.
240 lines
7.3 KiB
Bash
Executable file
240 lines
7.3 KiB
Bash
Executable file
#!/bin/bash
|
|
|
|
: "${CM_ONESHOT=0}"
|
|
: "${CM_OWN_CLIPBOARD=1}"
|
|
: "${CM_DEBUG=0}"
|
|
: "${CM_DIR="${XDG_RUNTIME_DIR-"${TMPDIR-/tmp}"}"}"
|
|
: "${CM_MAX_CLIPS=1000}"
|
|
|
|
# Shellcheck is mistaken here, this is used later as lowercase.
|
|
# shellcheck disable=SC2153
|
|
: "${CM_SELECTIONS=clipboard primary}"
|
|
|
|
major_version=5
|
|
cache_dir=$CM_DIR/clipmenu.$major_version.$USER/
|
|
cache_file_prefix=$cache_dir/line_cache
|
|
lock_file=$cache_dir/lock
|
|
lock_timeout=2
|
|
has_clipnotify=0
|
|
|
|
# This comes from the environment, so we rely on word splitting.
|
|
# shellcheck disable=SC2206
|
|
cm_selections=( $CM_SELECTIONS )
|
|
|
|
|
|
xsel_log=/dev/null
|
|
for file in /proc/self/fd/2 /dev/stderr; do
|
|
[[ -f "$file" ]] || continue
|
|
# In Linux, it's not possible to write to a socket represented by a file
|
|
# (for example, /dev/stderr or /proc/self/fd/2). See issue #54.
|
|
[[ -f "$(readlink "$file")" ]] || continue
|
|
xsel_log="$file"
|
|
break
|
|
done
|
|
|
|
_xsel() {
|
|
timeout 1 xsel --logfile "$xsel_log" "$@"
|
|
}
|
|
|
|
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() {
|
|
if (( CM_DEBUG )); then
|
|
printf '%s\n' "$@" >&2
|
|
fi
|
|
}
|
|
|
|
element_in() {
|
|
local item element
|
|
item="$1"
|
|
for element in "${@:2}"; do
|
|
if [[ "$item" == "$element" ]]; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
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)
|
|
- $CM_SELECTIONS: space separated list of the selections to manage (default: "clipboard primary")
|
|
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
|
|
declare -A last_cache_file_output
|
|
|
|
command -v clipnotify >/dev/null 2>&1 && has_clipnotify=1
|
|
|
|
if ! (( has_clipnotify )); then
|
|
echo "WARN: Consider installing clipnotify for better performance." >&2
|
|
echo "WARN: See https://github.com/cdown/clipnotify." >&2
|
|
fi
|
|
|
|
exec {lock_fd}> "$lock_file"
|
|
|
|
sleep_cmd=(sleep "${CM_SLEEP:-0.5}")
|
|
|
|
while true; do
|
|
if ! (( CM_ONESHOT )); then
|
|
if (( has_clipnotify )); then
|
|
# Fall back to polling if clipnotify fails
|
|
clipnotify || "${sleep_cmd[@]}"
|
|
else
|
|
# Use old polling method
|
|
"${sleep_cmd[@]}"
|
|
fi
|
|
fi
|
|
|
|
if ! flock -x -w "$lock_timeout" "$lock_fd"; then
|
|
if (( CM_ONESHOT )); then
|
|
printf 'ERROR: %s\n' 'Timed out waiting for lock' >&2
|
|
exit 1
|
|
else
|
|
printf 'ERROR: %s\n' \
|
|
'Timed out waiting for lock, skipping this run' >&2
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
for selection in "${cm_selections[@]}"; do
|
|
cache_file=${cache_file_prefix}_$selection
|
|
data=$(_xsel -o --"$selection"; printf x)
|
|
|
|
debug "Data before stripping: $data"
|
|
|
|
# We add and remove the x so that trailing newlines are not stripped.
|
|
# Otherwise, they would be stripped by the very nature of how POSIX
|
|
# defines command substitution.
|
|
data=${data%x}
|
|
|
|
debug "Data after stripping: $data"
|
|
|
|
if [[ $data != *[^[:space:]]* ]]; then
|
|
debug "Skipping as clipboard is only blank"
|
|
continue
|
|
fi
|
|
|
|
if [[ ${last_data[$selection]} == "$data" ]]; then
|
|
debug 'Skipping as last selection is the same as this one'
|
|
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"* ]] ||
|
|
[[ $possible_partial && $data == *"$possible_partial" ]]; then
|
|
debug "$possible_partial is a possible partial of $data"
|
|
debug "Removing ${last_filename[$selection]}"
|
|
|
|
previous_size=$(wc -c <<< "${last_cache_file_output[$selection]}")
|
|
truncate -s -"$previous_size" "$cache_file"
|
|
|
|
rm -- "${last_filename[$selection]}"
|
|
fi
|
|
|
|
last_data[$selection]=$data
|
|
last_filename[$selection]=$filename
|
|
|
|
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")"
|
|
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"
|
|
|
|
last_cache_file_output[$selection]=$cache_file_output
|
|
|
|
if (( CM_OWN_CLIPBOARD )) && [[ $selection != primary ]] &&
|
|
element_in clipboard "${cm_selections[@]}"; then
|
|
# Take ownership of the clipboard, in case the original application
|
|
# is unable to serve the clipboard request (due to being suspended,
|
|
# etc).
|
|
#
|
|
# Primary is excluded from the change of ownership as applications
|
|
# sometimes act up if clipboard focus is taken away from them --
|
|
# for example, urxvt will unhilight text, which is undesirable.
|
|
#
|
|
# 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.
|
|
_xsel -o --clipboard | _xsel -i --clipboard
|
|
fi
|
|
|
|
if (( CM_MAX_CLIPS )) && [[ -f $cache_file ]]; 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"
|
|
|
|
if (( CM_ONESHOT )); then
|
|
debug 'Oneshot mode enabled, exiting'
|
|
break
|
|
fi
|
|
done
|