Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New features for clipdel #100

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 142 additions & 46 deletions clipdel
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

: "${CM_DIR="${XDG_RUNTIME_DIR-"${TMPDIR-/tmp}"}"}"
CM_REAL_DELETE=0
if [[ $1 == -d ]]; then
CM_REAL_DELETE=1
shift
fi
CM_REQUIRE_PATTERN=1
CM_FUNCTION=''
CM_PATTERNS=()
CM_OLDER_THAN=''

major_version=5

Expand All @@ -16,72 +16,168 @@ cache_file_prefix=$cache_dir/line_cache
lock_file=$cache_dir/lock
lock_timeout=2

if [[ $1 == --help ]] || [[ $1 == -h ]]; then
cat << 'EOF'
clipdel deletes clipmenu entries matching a regex. By default, just lists what
it would delete, pass -d to do it for real.
line_cache_files=( "$cache_file_prefix"_* )

check_conflict() {
if [[ -z "$CM_FUNCTION" ]]; then
CM_FUNCTION="$1"
CM_REQUIRE_PATTERN=$2
if ! ((CM_REQUIRE_PATTERN)) && ((${#CM_PATTERNS[@]})); then
printf '%s\n' "Too many arguments" >&2
return 2
fi
else
printf '%s\n' "${3:-Options '$1' and '$CM_FUNCTION' conflict}" >&2
return 1
fi
}

".*" is special, it will just nuke the entire data directory, including the
line caches and all other state.
print_usage() {
cat << 'EOF'
Usage: clipdel [OPTION] [REGEX]
Delete clipmenu entries. If -d or --delete is not specified only a dry run is performed.

Arguments:

-d Delete for real.
--all Delete all entries
-c, --current Delete current entry
-d, --delete Delete for real
-t, --older-than <STRING> Delete entries older than STRING (format: N UNIT [N UNIT...])
-h, --help Print this message

Examples:

clipmenu | clipdel -d Launch clipmenu and delete selected entry
clipdel -d "bye world" Delete all entries containing "bye world"
clipdel -t "10 hours" -d Delete all entries older than 10 hours
clipdel -t "1 year 1 day" List entries older than 1 year plus 1 day

Environment variables:

- $CM_DIR: specify the base directory to store the cache dir in (default: $XDG_RUNTIME_DIR, $TMPDIR, or /tmp)
EOF
exit 0
fi

line_cache_files=( "$cache_file_prefix"_* )
}

while (($# > 0)); do
case "$1" in
--all|-c|--current) check_conflict "$1" 0 || exit 1;;
-d|--delete) CM_REAL_DELETE=1;;
-h|--help)
print_usage
exit 0
;;
-t|--older-than)
check_conflict "$1" 0 || exit 1
shift
if [[ "$1" ]]; then
date_format=$(sed "s,\([0-9]\+ [A-Za-z]\+\),\1 ago,g" <<< "$1")
CM_OLDER_THAN=$(date --date="$date_format" +%s%N 2> /dev/null) ||
{
printf '%s\n' "Unrecognized date format '$1'" >&2
exit 1
}
else
printf '%s\n' "Missing date format" >&2
exit 1
fi
;;
--|[!-]*)
if ((CM_REQUIRE_PATTERN)) && ! ((${#CM_PATTERNS[@]})); then
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this logic a bit hard to reason about. Can it be implemented with slicing and a for loop instead? :-)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so everything after a -- is read as a pattern. E.g.:

clipdel --all: will delete all entries
clipdel -- --all: will delete all entries matching --all

clipdel pattern1 -- pattern2 will fail due to excessive arguments.

Sorry, I don't understand what you mean by "be implemented with slicing and a for loop instead". Could you give me a quick draft?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'm talking about something like this: https://github.com/cdown/mpdmenu/blob/master/mpdmenu#L36-L40

if [[ "$1" == "--" ]]; then
shift
(( $# )) && CM_PATTERNS=("$*")
break
fi
CM_PATTERNS=("$1")
else
printf '%s\n' "Too many arguments" >&2
exit 1
fi
;;
-*)
printf "Unrecognized option '%s'\n" "$1" >&2
exit 1
;;
esac
shift
done

if (( ${#line_cache_files[@]} == 0 )); then
printf '%s\n' "No line cache files found, no clips exist" >&2
exit 0 # Well, this is a kind of success...
fi

# https://github.com/koalaman/shellcheck/issues/1141
# shellcheck disable=SC2124
raw_pattern=$1
esc_pattern=${raw_pattern//\#/'\#'}

if ! [[ $raw_pattern ]]; then
elif [[ -p /dev/stdin ]] || [[ -s /dev/stdin ]]; then
check_conflict "stdin" 0 "Option '$CM_FUNCTION' can't be used when reading from stdin" || exit 1
IFS=$'\n' read -d '' -r -a CM_PATTERNS < /dev/stdin
elif (( CM_REQUIRE_PATTERN )) && ! (( ${#CM_PATTERNS[@]} )); then
printf '%s\n' 'No pattern provided, see --help' >&2
exit 2
fi

exec {lock_fd}> "$lock_file"

if (( CM_REAL_DELETE )) && [[ "$raw_pattern" == ".*" ]]; then
case "$CM_FUNCTION" in
stdin) matches=("${CM_PATTERNS[@]}");;
--all)
if (( CM_REAL_DELETE )); then
flock -x -w "$lock_timeout" "$lock_fd" || exit
rm -rf -- "$cache_dir"
exit 0
else
mapfile -t matches < <(
cat "$cache_file_prefix"_* /dev/null | LC_ALL=C sort -rnk 1 | cut -d' ' -f2-
)
fi
;;
-c|--current)
mapfile -t matches < <(
cat "$cache_file_prefix"_* /dev/null | LC_ALL=C sort -rnk 1 | cut -d' ' -f2- | head -n 1
)
;;
-t|--older-than)
mapfile -t matches < <(
cat "${line_cache_files[@]}" | LC_ALL=C sort -rnk 1 | while read line; do
if [[ $(cut -d' ' -f1 <<< "$line") -lt $CM_OLDER_THAN ]]; then
cut -d' ' -f2- <<< "$line"
fi
done | tac
)
;;
*)
# https://github.com/koalaman/shellcheck/issues/1141
# shellcheck disable=SC2124
mapfile -t matches < <(
cat "${line_cache_files[@]}" | cut -d' ' -f2- | sort -u |
sed -n "\\#${CM_PATTERNS[0]//\#/'\#'}#p"
)
;;
esac

if (( CM_REAL_DELETE )); then
flock -x -w "$lock_timeout" "$lock_fd" || exit
rm -rf -- "$cache_dir"
exit 0
else
mapfile -t matches < <(
cat "${line_cache_files[@]}" | cut -d' ' -f2- | sort -u |
sed -n "\\#${esc_pattern}#p"
)

if (( CM_REAL_DELETE )); then
flock -x -w "$lock_timeout" "$lock_fd" || exit

for match in "${matches[@]}"; do
ck=$(cksum <<< "$match")
for match in "${matches[@]}"; do
ck=$(cksum <<< "$match")
if [[ -f "$cache_dir/$ck" ]]; then
rm -f -- "$cache_dir/$ck"
done
else
printf '%s\n' "Couldn't find a cache file for '$match'" >&2
fi

for file in "${line_cache_files[@]}"; do
temp=$(mktemp)
cut -d' ' -f2- < "$file" | sed "\\#${esc_pattern}#d" > "$temp"
mv -- "$temp" "$file"
safe_line=$(sed 's/[[\.*^$/]/\\&/g' <<< "$match")
sed -i "/^[0-9]\+ ${safe_line}$/d" "$file"
done
done

flock -u "$lock_fd"
else
if (( ${#matches[@]} )); then
printf '%s\n' "${matches[@]}"
fi
ck=$(cat "$cache_file_prefix"_* /dev/null | LC_ALL=C sort -rnk 1 | cut -d' ' -f2- | head -n 1 | cksum)
[[ -f "$cache_dir/$ck" ]] &&
for selection in clipboard primary; do
xsel --logfile /dev/null -i --"$selection" < "$cache_dir/$ck"
done

flock -u "$lock_fd"
else
if (( ${#matches[@]} )); then
printf '%s\n' "${matches[@]}"
fi
fi
fi
3 changes: 3 additions & 0 deletions clipmenu
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ if [[ "$CM_LAUNCHER" == rofi-script ]]; then
# shellcheck disable=SC2124
chosen_line="${@: -1}"
fi
elif [[ ! -t 1 ]]; then
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think this seems good, but it should likely be implemented by a flag rather than by checking if stdout is connected to a terminal. Otherwise I worry this feature is too hard to turn off.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean, like clipmenu --print?

Currently "all arguments are passed through to dmenu itself" (-h/--help being the exception), so it would have to be changed and maybe could get a little confusing.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think with the comment below, this can be done reasonably as it is in mpdmenu :-)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, are you suggesting we change the way arguments are handled to make it mpdmenu-like?

Pass clipmenu arguments first, followed by any dmenu arguments. They are separated by ::. For example:

clipmenu -p :: -sb '#000000'

Wouldn't it break current workflows?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should likely be implemented by a flag rather than by checking if stdout is connected to a terminal. Otherwise I worry this feature is too hard to turn off.

I'd argue that having selection passed through a pipe or redirected to a file would be the expected behavior when using *nix tools. Why would someone run something like clipmenu > test and expect it to print nothing to test (current behavior)?

The only flaw I see in this implementation is that what is printed is the content in line_cache, instead of the selection itself, so manipulating the output would need some extra work. Maybe this should be changed, but it'd require some thought on how to handle multiple selections.

list_clips | "$CM_LAUNCHER" -l "${CM_HISTLENGTH}" "$@"
exit
else
chosen_line=$(
list_clips | "$CM_LAUNCHER" -l "${CM_HISTLENGTH}" "$@"
Expand Down
5 changes: 3 additions & 2 deletions tests/test-clipmenu
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ EOF

temp=$(mktemp)

/tmp/clipmenu --foo bar > "$temp" 2>&1
# Use script so clipmenu thinks its output is going to a terminal
script -qfc "/tmp/clipmenu --foo bar 2>&1" /dev/null | tr -d '\015' > "$temp"

# Arguments are transparently passed to dmenu
grep -Fxq 'dmenu args: -l 8 --foo bar' "$temp"
Expand All @@ -87,7 +88,7 @@ grep -Fxq 'xsel args: --logfile /dev/null -i --primary' "$temp"
grep -Fxq 'xsel line 1 stdin: Selected text.' "$temp"
grep -Fxq "xsel line 2 stdin: Yes, it's selected text." "$temp"

CM_LAUNCHER=rofi /tmp/clipmenu --foo bar > "$temp" 2>&1
script -qfc "env CM_LAUNCHER=rofi /tmp/clipmenu --foo bar 2>&1" /dev/null | tr -d '\015' > "$temp"

# We have a special case to add -dmenu for rofi
grep -Fxq 'rofi args: -l 8 -dmenu --foo bar' "$temp"