forked from cdown/clipmenu
-
Notifications
You must be signed in to change notification settings - Fork 0
/
clipmenud
executable file
·272 lines (223 loc) · 9.23 KB
/
clipmenud
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#!/usr/bin/env bash
: "${CM_ONESHOT=0}"
: "${CM_OWN_CLIPBOARD=0}"
: "${CM_SYNC_PRIMARY_TO_CLIPBOARD=0}"
: "${CM_SYNC_CLIPBOARD_TO_PRIMARY=0}"
: "${CM_DEBUG=0}"
: "${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"
cache_dir=$(clipctl cache-dir)
cache_file=$cache_dir/line_cache
status_file=$cache_dir/status
# 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; }
sig_disable() {
if (( _CM_DISABLED )); then
info "Received disable signal but we're already disabled, so doing nothing"
return
fi
info "Received disable signal, suspending clipboard capture"
_CM_DISABLED=1
echo "disabled" > "$status_file"
}
sig_enable() {
if ! (( _CM_DISABLED )); then
info "Received enable signal but we're already enabled, so doing nothing"
return
fi
# Still store the last data so we don't end up eventually putting it in the
# clipboard if it wasn't changed
for selection in "${selections[@]}"; do
data=$(_xsel -o --"$selection"; printf x)
last_data_sel[$selection]=${data%x}
done
info "Received enable signal, resuming clipboard capture"
_CM_DISABLED=0
echo "enabled" > "$status_file"
}
kill_background_jobs() {
# While we usually _are_, there are no guarantees that we're the process
# group leader. As such, all we can do is look at the pending jobs. Bash
# avoids a subshell here, so the job list is in the right shell.
local -a bg
readarray -t bg < <(jobs -p)
# Don't log `kill' failures, since with KillMode=control-group, we're
# racing with init.
(( ${#bg[@]} )) && kill -- "${bg[@]}" 2>/dev/null
}
# Avoid clipmenu showing "no cache file yet" if we launched it before selecting
# anything
touch -- "$cache_file"
if [[ $1 == --help ]] || [[ $1 == -h ]]; then
cat << 'EOF'
clipmenud collects and caches what's on the clipboard. You can manage its
operation with clipctl.
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_SYNC_PRIMARY_TO_CLIPBOARD: sync selections from primary to clipboard immediately (default: 0)
- $CM_SYNC_CLIPBOARD_TO_PRIMARY: sync selections from clipboard to primary immediately (default: 0)
- $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
[[ $DISPLAY ]] || die 2 'The X display is unset, is your X server running?'
# It's ok that this only applies to the final directory.
# shellcheck disable=SC2174
mkdir -p -m0700 "$cache_dir"
echo "enabled" > "$status_file"
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
declare -A updated_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"
trap '_CM_TRAP=1; sig_disable' USR1
trap '_CM_TRAP=1; sig_enable' USR2
trap 'trap - INT TERM EXIT; kill_background_jobs; exit 0' INT TERM EXIT
while true; do
if ! (( CM_ONESHOT )); then
# Make sure we're interruptible for the sig_{en,dis}able traps
clipnotify &
_CM_CLIPNOTIFY_PID="$!"
if ! wait "$_CM_CLIPNOTIFY_PID" && ! (( _CM_TRAP )); then
# X server dead?
sleep 10
continue
fi
fi
# Trapping a signal breaks the `wait`
if (( _CM_TRAP )); then
# Prevent spawning another clipnotify job restarting the loop
kill_background_jobs
unset _CM_TRAP
continue
fi
if (( _CM_DISABLED )); then
info "Got a clipboard notification, but we are disabled, skipping"
continue
fi
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
updated_sel[$selection]=0
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
updated_sel[$selection]=1
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_SYNC_PRIMARY_TO_CLIPBOARD )) && (( updated_sel[primary] )); then
_xsel -o --primary | _xsel -i --clipboard
fi
if (( CM_SYNC_CLIPBOARD_TO_PRIMARY )) && (( updated_sel[clipboard] )); then
_xsel -o --clipboard | _xsel -i --primary
fi
# The cache file may not exist if this is the first run and data is skipped
if (( CM_MAX_CLIPS )) && [[ -f "$cache_file" ]] && (( "$(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