clipmenud (7978B)
1 #!/usr/bin/env bash 2 3 : "${CM_ONESHOT=0}" 4 : "${CM_OWN_CLIPBOARD=0}" 5 : "${CM_DEBUG=0}" 6 : "${CM_DIR:="${XDG_RUNTIME_DIR-"${TMPDIR-/tmp}"}"}" 7 8 : "${CM_MAX_CLIPS:=1000}" 9 # Buffer to batch to avoid calling too much. Only used if CM_MAX_CLIPS >0. 10 CM_MAX_CLIPS_THRESH=$(( CM_MAX_CLIPS + 10 )) 11 12 : "${CM_SELECTIONS:=clipboard primary}" 13 read -r -a selections <<< "$CM_SELECTIONS" 14 15 major_version=6 16 cache_dir=$CM_DIR/clipmenu.$major_version.$USER/ 17 cache_file=$cache_dir/line_cache 18 19 # lock_file: lock for *one* iteration of clipboard capture/propagation 20 # session_lock_file: lock to prevent multiple clipmenud daemons 21 lock_file=$cache_dir/lock 22 session_lock_file=$cache_dir/session_lock 23 lock_timeout=2 24 has_xdotool=0 25 26 _xsel() { timeout 1 xsel --logfile /dev/null "$@"; } 27 28 error() { printf 'ERROR: %s\n' "${1?}" >&2; } 29 info() { printf 'INFO: %s\n' "${1?}"; } 30 die() { 31 error "${2?}" 32 exit "${1?}" 33 } 34 35 make_line_cksums() { while read -r line; do cksum <<< "${line#* }"; done; } 36 37 get_first_line() { 38 data=${1?} 39 40 # We look for the first line matching regex /./ here because we want the 41 # first line that can provide reasonable context to the user. 42 awk -v limit=300 ' 43 BEGIN { printed = 0; } 44 printed == 0 && NF { 45 $0 = substr($0, 0, limit); 46 printf("%s", $0); 47 printed = 1; 48 } 49 END { 50 if (NR > 1) 51 printf(" (%d lines)", NR); 52 printf("\n"); 53 }' <<< "$data" 54 } 55 56 debug() { (( CM_DEBUG )) && printf '%s\n' "$@" >&2; } 57 58 sig_disable() { 59 info "Received disable signal, suspending clipboard capture" 60 _CM_DISABLED=1 61 _CM_FIRST_DISABLE=1 62 [[ -v _CM_CLIPNOTIFY_PID ]] && kill "$_CM_CLIPNOTIFY_PID" 63 } 64 65 sig_enable() { 66 if ! (( _CM_DISABLED )); then 67 info "Received enable signal but we're not disabled, so doing nothing" 68 return 69 fi 70 71 # Still store the last data so we don't end up eventually putting it in the 72 # clipboard if it wasn't changed 73 for selection in "${selections[@]}"; do 74 data=$(_xsel -o --"$selection"; printf x) 75 last_data_sel[$selection]=${data%x} 76 done 77 78 info "Received enable signal, resuming clipboard capture" 79 _CM_DISABLED=0 80 } 81 82 if [[ $1 == --help ]] || [[ $1 == -h ]]; then 83 cat << 'EOF' 84 clipmenud collects and caches what's on the clipboard. You can manage its 85 operation with clipctl. 86 87 Environment variables: 88 89 - $CM_DEBUG: turn on debugging output (default: 0) 90 - $CM_DIR: specify the base directory to store the cache dir in (default: $XDG_RUNTIME_DIR, $TMPDIR, or /tmp) 91 - $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) 92 - $CM_ONESHOT: run once immediately, do not loop (default: 0) 93 - $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) 94 - $CM_SELECTIONS: space separated list of the selections to manage (default: "clipboard primary") 95 - $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) 96 EOF 97 exit 0 98 fi 99 100 [[ $DISPLAY ]] || die 2 'The X display is unset, is your X server running?' 101 102 # It's ok that this only applies to the final directory. 103 # shellcheck disable=SC2174 104 mkdir -p -m0700 "$cache_dir" 105 106 exec {session_lock_fd}> "$session_lock_file" 107 flock -x -n "$session_lock_fd" || 108 die 2 "Can't lock session file -- is another clipmenud running?" 109 110 declare -A last_data_sel 111 112 command -v clipnotify >/dev/null 2>&1 || die 2 "clipnotify not in PATH" 113 command -v xdotool >/dev/null 2>&1 && has_xdotool=1 114 115 if [[ $CM_IGNORE_WINDOW ]] && ! (( has_xdotool )); then 116 echo "WARN: CM_IGNORE_WINDOW does not work without xdotool, which is not installed" >&2 117 fi 118 119 exec {lock_fd}> "$lock_file" 120 121 trap sig_disable USR1 122 trap sig_enable USR2 123 124 # Kill all background processes on exit 125 trap 'trap - TERM; kill -- -$$' INT TERM EXIT 126 127 while true; do 128 if ! (( CM_ONESHOT )); then 129 # Make sure we're interruptible for the sig_{en,dis}able traps 130 clipnotify & 131 _CM_CLIPNOTIFY_PID="$!" 132 wait "$_CM_CLIPNOTIFY_PID" 133 fi 134 135 if (( _CM_DISABLED )); then 136 # The first one will just be from interrupting `wait`, so don't print 137 if (( _CM_FIRST_DISABLE )); then 138 unset _CM_FIRST_DISABLE 139 else 140 info "Got a clipboard notification, but we are disabled, skipping" 141 fi 142 continue 143 fi 144 145 if [[ $CM_IGNORE_WINDOW ]] && (( has_xdotool )); then 146 windowname="$(xdotool getactivewindow getwindowname)" 147 if [[ "$windowname" =~ $CM_IGNORE_WINDOW ]]; then 148 debug "ignoring clipboard because windowname \"$windowname\" matches \"${CM_IGNORE_WINDOW}\"" 149 continue 150 fi 151 fi 152 153 if ! flock -x -w "$lock_timeout" "$lock_fd"; then 154 if (( CM_ONESHOT )); then 155 die 1 "Timed out waiting for lock" 156 else 157 error "Timed out waiting for lock, skipping this iteration" 158 continue 159 fi 160 fi 161 162 for selection in "${selections[@]}"; do 163 data=$(_xsel -o --"$selection"; printf x) 164 data=${data%x} # avoid trailing newlines being stripped 165 166 [[ $data == *[^[:space:]]* ]] || continue 167 [[ $last_data == "$data" ]] && continue 168 [[ ${last_data_sel[$selection]} == "$data" ]] && continue 169 170 if [[ $last_data && $data == "$last_data"* ]] || 171 [[ $last_data && $data == *"$last_data" ]]; then 172 # Don't actually remove the file yet, because it might be 173 # referenced by an older entry. These will be dealt with at vacuum. 174 debug "$selection: $last_data is a possible partial of $data" 175 previous_size=$(wc -c <<< "$last_cache_file_output") 176 truncate -s -"$previous_size" "$cache_file" 177 fi 178 179 first_line=$(get_first_line "$data") 180 debug "New clipboard entry on $selection selection: \"$first_line\"" 181 182 cache_file_output="$(date +%s%N) $first_line" 183 filename="$cache_dir/$(cksum <<< "$first_line")" 184 last_cache_file_output=$cache_file_output 185 last_data=$data 186 last_data_sel[$selection]=$data 187 188 debug "Writing $data to $filename" 189 printf '%s' "$data" > "$filename" 190 debug "Writing $cache_file_output to $cache_file" 191 printf '%s\n' "$cache_file_output" >> "$cache_file" 192 193 if (( CM_OWN_CLIPBOARD )) && [[ $selection == clipboard ]]; then 194 # Only clipboard, since apps like urxvt will unhilight for PRIMARY 195 _xsel -o --clipboard | _xsel -i --clipboard 196 fi 197 done 198 199 # The cache file may not exist if this is the first run and data is skipped 200 if (( CM_MAX_CLIPS )) && [[ -f "$cache_file" ]] && (( "$(wc -l < "$cache_file")" > CM_MAX_CLIPS_THRESH )); then 201 info "Trimming clip cache to CM_MAX_CLIPS ($CM_MAX_CLIPS)" 202 trunc_tmp=$(mktemp) 203 tail -n "$CM_MAX_CLIPS" "$cache_file" | uniq > "$trunc_tmp" 204 mv -- "$trunc_tmp" "$cache_file" 205 206 # Vacuum up unreferenced clips. They may either have been 207 # unreferenced by the above CM_MAX_CLIPS code, or they may be old 208 # possible partials. 209 declare -A cksums 210 while IFS= read -r line; do 211 cksum=$(cksum <<< "$line") 212 cksums["$cksum"]="$line" 213 done < <(cut -d' ' -f2- < "$cache_file") 214 215 num_vacuumed=0 216 for file in "$cache_dir"/[012346789]*; do 217 cksum=${file##*/} 218 if [[ ${cksums["$cksum"]-_missing_} == _missing_ ]]; then 219 debug "Vacuuming due to lack of reference: $file" 220 (( ++num_vacuumed )) 221 rm -- "$file" 222 fi 223 done 224 unset cksums 225 info "Vacuumed $num_vacuumed clip files." 226 fi 227 228 flock -u "$lock_fd" 229 230 (( CM_ONESHOT )) && break 231 done