This commit is contained in:
tavo-wasd 2024-03-05 21:01:35 -06:00
parent c0d99b3017
commit 9f4bd60033
13 changed files with 671 additions and 4 deletions

View file

@ -7,4 +7,4 @@ name="$(sed '/^Watch\slater:\s/d;/^\s*$/d;/^#/d;s/-.*$//g' "$BOOKMARKS" | menu "
[ -z "$name" ] && exit
grep "$name" $BOOKMARKS | sed -z 's/^.*-//g;s/\n//g' |
wl-copy && notify-send "󰃀 Bookmarks" "'$name' copied to clipboard"
xsel -ib && notify-send "󰃀 Bookmarks" "'$name' copied to clipboard"

52
scripts/menu/xclipmenu/clipctl Executable file
View file

@ -0,0 +1,52 @@
#!/usr/bin/env bash
: "${CM_DIR:="${XDG_RUNTIME_DIR-"${TMPDIR-/tmp}"}"}"
if [[ -z $1 ]] || [[ $1 == --help ]] || [[ $1 == -h ]]; then
cat << 'EOF'
clipctl provides controls for the clipmenud daemon.
Commands:
enable: enable clip collection
disable: disable clip collection
status: returns "enabled" or "disabled"
toggle: toggles clip collection
version: returns major version
cache-dir: returns the directory used for caching
EOF
exit 0
fi
clipmenud_pid=$(pgrep -u "$(id -u)" -nf 'clipmenud$')
case $1 in
enable|disable|toggle|status)
if [[ -z "$clipmenud_pid" ]]; then
echo "clipmenud is not running" >&2
exit 2
fi
;;
esac
major_version=6
cache_dir=$CM_DIR/clipmenu.$major_version.$USER
status_file=$cache_dir/status
case $1 in
enable) kill -USR2 "$clipmenud_pid" ;;
disable) kill -USR1 "$clipmenud_pid" ;;
status) cat "$status_file" ;;
toggle)
if [[ $(clipctl status) == "enabled" ]]; then
clipctl disable
else
clipctl enable
fi
;;
version) echo "$major_version" ;;
cache-dir) echo "$cache_dir" ;;
*)
printf 'Unknown command: %s\n' "$1" >&2
exit 1
;;
esac

95
scripts/menu/xclipmenu/clipdel Executable file
View file

@ -0,0 +1,95 @@
#!/usr/bin/env bash
CM_REAL_DELETE=0
if [[ $1 == -d ]]; then
CM_REAL_DELETE=1
shift
fi
shopt -s nullglob
cache_dir=$(clipctl cache-dir)
cache_file=$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. If no pattern is passed as an argument,
it will try to read one from standard input.
".*" is special, it will just nuke the entire data directory, including the
line caches and all other state.
Arguments:
-d Delete for real.
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
if ! [[ -f $cache_file ]]; then
printf '%s\n' "No line cache file found, no clips exist" >&2
exit 0 # Well, this is a kind of success...
fi
if [[ -n $1 ]]; then
raw_pattern=$1
elif ! [[ -t 0 ]]; then
IFS= read -r raw_pattern
fi
esc_pattern=${raw_pattern//\#/'\#'}
# We use 2 separate sed commands so "esc_pattern" matches only the 'clip' text
# without the timestamp (e.g. $> clipdel '^delete_exact_match$')
sed_common_command="s#^[0-9]\+ ##;\\#${esc_pattern}#"
if ! [[ $raw_pattern ]]; then
printf '%s\n' 'No pattern provided, see --help' >&2
exit 2
elif [[ "$raw_pattern" == ".*" ]]; then
delete_cache_dir=1
else
mapfile -t matches < <(
sed -n "${sed_common_command}p" "$cache_file" |
sort -u
)
fi
exec {lock_fd}> "$lock_file"
if (( CM_REAL_DELETE )); then
if (( delete_cache_dir )); then
flock -x -w "$lock_timeout" "$lock_fd" || exit
rm -rf -- "$cache_dir"
mkdir -p -- "$cache_dir"
exit 0
else
flock -x -w "$lock_timeout" "$lock_fd" || exit
for match in "${matches[@]}"; do
ck=$(cksum <<< "$match")
rm -f -- "$cache_dir/$ck"
done
temp=$(mktemp)
# sed 'h' and 'g' here means save and restore the line, so
# timestamps are not removed from non-deleted lines. 'd' deletes the
# line and restarts, skipping 'g'/restore.
# https://www.gnu.org/software/sed/manual/html_node/Other-Commands.html#Other-Commands
sed "h;${sed_common_command}d;g" "$cache_file" > "$temp"
mv -- "$temp" "$cache_file"
flock -u "$lock_fd"
fi
elif (( delete_cache_dir )); then
printf 'delete cache dir: %s\n' "$cache_dir"
elif (( ${#matches[@]} )); then
printf '%s\n' "${matches[@]}"
fi

28
scripts/menu/xclipmenu/clipfsck Executable file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
shopt -s nullglob
cache_dir=$(clipctl cache-dir)
cache_file=$cache_dir/line_cache
declare -A cksums
while IFS= read -r line; do
cksum=$(cksum <<< "$line")
cksums["$cksum"]="$line"
# Are all cache entries represented by a file?
full_file=$cache_dir/$cksum
if ! [[ -f $full_file ]]; then
printf 'cache entry without file: %s -> %s\n' "$line" "$full_file" >&2
fi
done < <(cut -d' ' -f2- < "$cache_file")
# Are all files represented by a cache entry?
for file in "$cache_dir"/[012346789]*; do
cksum=${file##*/}
line=${cksums["$cksum"]-_missing_}
if [[ $line == _missing_ ]]; then
printf 'file without cache entry: %s\n' "$file"
fi
done

72
scripts/menu/xclipmenu/clipmenu Executable file
View file

@ -0,0 +1,72 @@
#!/usr/bin/env bash
: "${CM_LAUNCHER=dmenu}"
: "${CM_HISTLENGTH=8}"
shopt -s nullglob
cache_dir=$(clipctl cache-dir)
cache_file=$cache_dir/line_cache
# Not -h, see #142
if [[ $1 == --help ]]; then
cat << 'EOF'
clipmenu is a simple clipboard manager using dmenu and xsel. Launch this
when you want to select a clip.
All arguments are passed through to dmenu itself.
Environment variables:
- $CM_DIR: specify the base directory to store the cache dir in (default: $XDG_RUNTIME_DIR, $TMPDIR, or /tmp)
- $CM_HISTLENGTH: specify the number of lines to show in dmenu/rofi (default: 8)
- $CM_LAUNCHER: specify a dmenu-compatible launcher (default: dmenu)
- $CM_OUTPUT_CLIP: if set, output clip selection to stdout
EOF
exit 0
fi
if ! [[ -f "$cache_file" ]]; then
printf '%s\n' 'No cache file yet, did you run clipmenud?'
exit 2
fi
# Blacklist of non-dmenu launchers
launcher_args=(-l "${CM_HISTLENGTH}")
if [[ "$CM_LAUNCHER" == fzf ]]; then
launcher_args=()
fi
# rofi supports dmenu-like arguments through the -dmenu flag. -p wastes space
# in real dmenu, but rofi shows "dmenu:" anyway, so pass it here only.
[[ "$CM_LAUNCHER" == rofi ]] && set -- -dmenu -p clipmenu "$@"
list_clips() {
LC_ALL=C sort -rnk 1 < "$cache_file" | cut -d' ' -f2- | awk '!seen[$0]++'
}
if [[ "$CM_LAUNCHER" == rofi-script ]]; then
if (( $# )); then
chosen_line="${!#}"
else
list_clips
exit
fi
else
chosen_line=$(list_clips | "$CM_LAUNCHER" "${launcher_args[@]}" "$@")
launcher_exit=$?
fi
[[ $chosen_line ]] || exit 1
file=$cache_dir/$(cksum <<< "$chosen_line")
[[ -f "$file" ]] || exit 2
for selection in clipboard primary; do
xsel --logfile /dev/null -i --"$selection" < "$file"
done
if (( CM_OUTPUT_CLIP )); then
cat "$file"
fi
exit "${launcher_exit:-"$?"}"

272
scripts/menu/xclipmenu/clipmenud Executable file
View file

@ -0,0 +1,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

View file

@ -0,0 +1,5 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.

View file

@ -0,0 +1,17 @@
PREFIX ?= /usr/local
x11_bsd_flags = -I/usr/X11R6/include -L/usr/X11R6/lib
all:
${CC} ${CFLAGS} ${LDFLAGS} clipnotify.c -o clipnotify $(x11_bsd_flags) -lX11 -lXfixes
install: all
mkdir -p ${DESTDIR}${PREFIX}/bin
cp -f clipnotify ${DESTDIR}${PREFIX}/bin
chmod 755 ${DESTDIR}${PREFIX}/bin/clipnotify
uninstall:
rm -f ${DESTDIR}${PREFIX}/bin/clipnotify
clean:
rm -f *.o *~ clipnotify

View file

@ -0,0 +1,27 @@
clipnotify is a simple program that, using the
[XFIXES](https://cgit.freedesktop.org/xorg/proto/fixesproto/plain/fixesproto.txt)
extension to X11, waits until a new selection is available and then exits.
It was primarily designed for [clipmenu](https://github.com/cdown/clipmenu), to
avoid polling for new selections.
Here's how it's intended to be used:
while read; do
[an event happened, do something with the selection]
done < <(clipnotify -l)
Or:
while clipnotify; do
[an event happened, do something with the selection]
done
clipnotify doesn't try to print anything about the contents of the selection,
it just exits when it changes. This is intentional -- X11's selection API is
verging on the insane, and there are plenty of others who have already lost
their sanity to bring us xclip/xsel/etc. Use one of those tools to complement
clipnotify.
You can choose a particular selection with `-s`, and loop instead of exiting
with `-l`. See `clipmenu -h` for more information.

View file

@ -0,0 +1,96 @@
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static enum selections {
NONE = 0,
SELECTION_CLIPBOARD = (1 << 0),
SELECTION_PRIMARY = (1 << 1),
SELECTION_SECONDARY = (1 << 2)
} selections = NONE;
static int loop;
int main(int argc, char *argv[]) {
static const char *usage =
"%s: Notify by exiting on clipboard events.\n\n"
" -l Instead of exiting, print a newline when a new selection is available.\n"
" -s The selection to use. Available selections:\n"
" clipboard, primary, secondary\n"
" The default is to monitor clipboard and primary.\n";
Display *disp;
Window root;
Atom clip;
XEvent evt;
int opt;
while ((opt = getopt(argc, argv, "hs:l")) != -1) {
switch (opt) {
case 'h':
printf(usage, argv[0]);
return EXIT_SUCCESS;
case 'l':
loop = 1;
break;
case 's': {
char *token = strtok(optarg, ",");
while (token != NULL) {
if (strcmp(token, "clipboard") == 0) {
selections |= SELECTION_CLIPBOARD;
} else if (strcmp(token, "primary") == 0) {
selections |= SELECTION_PRIMARY;
} else if (strcmp(token, "secondary") == 0) {
selections |= SELECTION_SECONDARY;
} else {
fprintf(stderr, "Unknown selection '%s'\n", token);
return EXIT_FAILURE;
}
token = strtok(NULL, ",");
}
break;
}
default:
fprintf(stderr, usage, argv[0]);
return EXIT_FAILURE;
}
}
disp = XOpenDisplay(NULL);
if (!disp) {
fprintf(stderr, "Can't open X display\n");
return EXIT_FAILURE;
}
root = DefaultRootWindow(disp);
clip = XInternAtom(disp, "CLIPBOARD", False);
/* <= 1.0.2 backwards compatibility */
if (!selections)
selections = SELECTION_CLIPBOARD | SELECTION_PRIMARY;
if (selections & SELECTION_CLIPBOARD)
XFixesSelectSelectionInput(disp, root, clip,
XFixesSetSelectionOwnerNotifyMask);
if (selections & SELECTION_PRIMARY)
XFixesSelectSelectionInput(disp, root, XA_PRIMARY,
XFixesSetSelectionOwnerNotifyMask);
if (selections & SELECTION_SECONDARY)
XFixesSelectSelectionInput(disp, root, XA_SECONDARY,
XFixesSetSelectionOwnerNotifyMask);
if (loop) {
(void)setvbuf(stdout, NULL, _IONBF, 0);
do {
XNextEvent(disp, &evt);
printf("\n");
} while (1);
} else {
XNextEvent(disp, &evt);
}
XCloseDisplay(disp);
}

View file

@ -1,6 +1,5 @@
#!/bin/bash
bind '"\C-e":"dragon -x \* 2>/dev/null\C-m"'
bind '"\C-y":"copy_history\C-m"'
bind '"\C-h":"copy_history\C-m"'
bind '"\C-f":"fzf_nav\C-m"'
bind '"\C-n":"fzf_nav\C-m"'

View file

@ -122,7 +122,6 @@ Plug 'junegunn/goyo.vim'
Plug 'mhinz/vim-signify'
Plug 'ibhagwan/fzf-lua'
Plug 'morhetz/gruvbox'
Plug 'jalvesaq/Nvim-R'
Plug 'instant-markdown/vim-instant-markdown'
call plug#end()

5
wrappers/rstudio Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env sh
HOME=$HOME/.local/share/Rstudio
mkdir -p "$HOME"
exec /usr/bin/rstudio "$@"