clipmenu-spmenu/clipmenud
Chris Down a85f8998b4 Revert "Make main work block async"
There is currently a bug where this causes a copy storm at startup. More
worryingly, more and more newlines are appended to the end, so something
is going wrong in copy logic anyway, async or not.

This reverts commit 11304d3a60.
2018-02-20 09:29:49 +00:00

200 lines
5.8 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}"
: "${CM_SELECTIONS=clipboard primary}"
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
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"
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
# We need to take ownership synchronously before we run `clipnotify` as
# otherwise we could enter an infinite loop.
if (( CM_OWN_CLIPBOARD )) && 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 need to check if the clipboard is empty to mitigate #34.
data=$(_xsel -o --clipboard; printf x)
[[ $data != x ]] && _xsel -i --clipboard <<< "${data%x}"
fi
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
printf 'ERROR: %s\n' 'Timed out waiting for lock' >&2
exit 1
fi
for selection in "${cm_selections[@]}"; do
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
first_line=$(get_first_line "$data")
debug "New clipboard entry on $selection selection: \"$first_line\""
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 (( 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"
if (( CM_ONESHOT )); then
debug 'Oneshot mode enabled, exiting'
break
fi
done