initial commit

This commit is contained in:
yosh 2023-12-18 23:14:55 -06:00
commit 9c0dcef048
6 changed files with 363 additions and 0 deletions

5
LICENSE Normal file
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.

22
README.md Normal file
View File

@ -0,0 +1,22 @@
# shclip
`shclip` is an X11 clipboard manager and menu implemented in POSIX shell, largely inspired by [clipmenu](https://github.com/cdown/clipmenu)
## dependencies
- A POSIX compliant shell
- An implementation of POSIX Utilities (`coreutils`, `bsdutils`, `busybox`, ...)
- An implementation of `file(1)` that supports `-b` and `-i` for mime-types (yours should)
- `ed` (mentioning this separately since many unixes/distributions have it separately)
- A newline-delimited menu program (`dmenu`, `rofi`, `bemenu`, `fzfmenu`, ...)
- (Optional, but recommended) `notify-send`
## using
put `shclip-{daemon,menu}` in your `$PATH` and start `shclip-daemon` in a way where it can read `$DISPLAY`. everything should Just Work, hopefully. to use the menu, run `shclip-menu`
`shclip` reads a config file at `$XDG_CONFIG_HOME/shclip/config` and reads filters from `$XDG_CONFIG_HOME/shclip/filter.sh`. if `config` doesn't exist, then the defaults as specified in the file are used. if `filter.sh` doesn't exist, then filtering won't happen whether it's enabled or not.
if filtering is enabled, every text clip will be sent to the stdin of `filter.sh` and be replaced by its stdout. A simple replacement filter is provided as an example
`shclip-menu` should hopefully be straightforward. you have the option to copy a previous clip, delete any individual clip (or all clips), edit any individual clip, toggle caching clips, and toggle clip filtering
## rationale
I made this because I really liked `clipmenu`, but felt as if it fell short in very specific aspects that I didn't like (mainly, I wanted image support). I decided to use posix shell because I'm more comfortable with it than including bashisms in my scripts, and also to maybe serve as being useful for users of more minimal shells or other unixes.

43
config Normal file
View File

@ -0,0 +1,43 @@
### DAEMON OPTIONS ###
# enable shclip on startup
# blank for no, anything for yes
# default: yes
SHCLIP_ENABLED="1"
# max clips to store
# default: 25
SHCLIP_MAX_CLIPS=25
# memory limit + 4 in kb
# default: 4100 (4MB)
SHCLIP_MEMORY_LIMIT=4100
# enable filtering for clips (see filter.sh)
# blank for no, anything for yes
# default: no
SHCLIP_FILTER_ENABLED=""
# enable syncing the clipboard to the primary selection
# blank for no, anything for yes
# default: yes
SHCLIP_SYNC_CLIPBOARD="1"
### SHCLIP-MENU OPTIONS ###
# text editor for editing clips
# default: xdg-open
SHCLIP_TEXT_EDITOR="xdg-open"
# image editor for editing clips
# default: gimp -n
SHCLIP_IMAGE_EDITOR="gimp -n"
# shclip menu commands
# defaults:
# MAIN: fzfmenu -p shclip
# COPY, DEL, and EDIT copy MAIN if blank
SHCLIP_MENU_MAIN_CMD="fzfmenu -p shclip"
SHCLIP_MENU_COPY_CMD=""
SHCLIP_MENU_DEL_CMD=""
SHCLIP_MENU_EDIT_CMD=""

6
filter.sh Normal file
View File

@ -0,0 +1,6 @@
# this file is run as a shell script for filtering text clips
# stdin is the current text clip, stdout is what gets put back in buffer
sed -E '
s/nitter.salastil.com/fxtwitter.com/g
'

183
shclip-daemon Executable file
View File

@ -0,0 +1,183 @@
#!/bin/sh
# shclip, by yosh
# https://git.unix.dog/yosh/shclip
set -eu
TAB=" "
BN="${0##*/}"
# config file if exists
SHCLIP_CONFIG_DIR="${SHCLIP_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/shclip}"
. "$SHCLIP_CONFIG_DIR/config"
SHCLIP_MAX_CLIPS="${SHCLIP_MAX_CLIPS:-25}"
SHCLIP_MEMORY_LIMIT="${SHCLIP_MEMORY_LIMIT:-4100}" # memory limit in kb
SHCLIP_ENABLED="${SHCLIP_ENABLED-1}"
SHCLIP_FILTER_ENABLED="${SHCLIP_FILTER_ENABLED-}"
SHCLIP_SYNC_CLIPBOARD="${SHCLIP_SYNC_CLIPBOARD-1}"
SHCLIP_TEMP_DIR="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/shclip-$USER-$DISPLAY"
errecho() { printf '%s\n' "$*" >&2; }
exists() { command -v "$@" >/dev/null 2>&1; }
fail() {
errecho "error: $BN: $*"
exists notify-send && notify-send "error: $BN: $*"
exit 1
}
cleanup() {
# TODO: better cleanup by sorting shit, etc.
trap 'exit' INT HUP QUIT TERM EXIT
set +e
rmdir "$SHCLIP_TEMP_DIR/lock"
kill "$clipnotify_pid"
exit
}
toggle() {
if [ -z "$SHCLIP_ENABLED" ]; then
SHCLIP_ENABLED=1
errecho "shclip enabled!"
exists notify-send && notify-send "shclip enabled!"
else
SHCLIP_ENABLED=""
errecho "shclip disabled!"
exists notify-send && notify-send "shclip disabled!"
fi
kill "$clipnotify_pid"
}
filter_toggle() {
if [ -z "$SHCLIP_FILTER_ENABLED" ]; then
SHCLIP_FILTER_ENABLED=1
errecho "shclip filter enabled!"
exists notify-send && notify-send "shclip filter enabled!"
else
SHCLIP_FILTER_ENABLED=""
errecho "shclip filter disabled!"
exists notify-send && notify-send "shclip filter disabled!"
fi
kill "$clipnotify_pid"
}
# setup process
setup() {
mkdir -p -m 700 "$SHCLIP_TEMP_DIR"
# test the temp/lock directory
if mkdir -m 700 "$SHCLIP_TEMP_DIR/lock"; then
errecho "successfully acquired lock"
else
fail "cannot acquire lock. is another instance running?
if not, delete the directory at $SHCLIP_TEMP_DIR/lock"
fi
trap 'cleanup' INT HUP QUIT TERM EXIT
trap 'toggle' USR1
trap 'filter_toggle' USR2
BUFFER="$SHCLIP_TEMP_DIR/buffer"
ORDER="$SHCLIP_TEMP_DIR/order"
PID="$SHCLIP_TEMP_DIR/pid"
printf '%s' $$ > "$PID"
# create ORDER file
if [ ! -f "$ORDER" ]; then
digits=${#SHCLIP_MAX_CLIPS}
i=1
while [ "$i" -le "$SHCLIP_MAX_CLIPS" ]; do
printf "%0${digits}d.clip\n" "$i"
i="$((i + 1))"
done > "$ORDER"
fi
}
# reclip top of the stack
reclip() {
n="$(head -n 1 "$ORDER")"
mime="$(file -bi "$SHCLIP_TEMP_DIR/$n")"
xclip -sel clipboard -t "${mime%%;*}" "$SHCLIP_TEMP_DIR/$n"
}
updateclip() {
# some good variables to have
firstind="$(head -n 1 "$ORDER")" # top of clip stack
lastind="$(tail -n 1 "$ORDER")" # bottom of stack
targets="$(xclip -sel clipboard -t TARGETS -o)" || { reclip && return 0; } # targets of clip, will also return if clip doesn't exist
# this covers if we've copied a file in a GUI file manager
# probably don't want that in your history
case "$targets" in
*gnome-copied-files*) return 0 ;;
*image/png*) xclip -sel clipboard -t image/png -o > "$BUFFER" ;;
*) xclip -sel clipboard -o > "$BUFFER" ;;
esac
# check if clip exceeds memory limit
sz="$(du -k "$BUFFER")"
if [ "${sz%%${TAB}*}" -gt "$SHCLIP_MEMORY_LIMIT" ]; then
errecho 'clip exceeds memory threshold! not caching...'
exists notify-send && notify-send 'clip exceeds memory threshold! not caching...'
rm -f "$BUFFER"
return 0
fi
# buffer mime matching
mime="$(file -bi "$BUFFER")"
case "${mime%%;}" in
inode/x-empty*) # empty clip
reclip
return 0
;;
text*)
# apply text filters
[ -n "$SHCLIP_FILTER_ENABLED" ] && [ -f "$SHCLIP_CONFIG_DIR/filter.sh" ] && \
sh "$SHCLIP_CONFIG_DIR/filter.sh" < "$BUFFER" | diff "$BUFFER" - | patch "$BUFFER"
xclip -sel clipboard "$BUFFER"
[ -n "$SHCLIP_SYNC_CLIPBOARD" ] && xclip -sel primary "$BUFFER"
;;
*) ;;
esac
# top of stack & buffer are identical, don't cache
if cmp "$BUFFER" "$SHCLIP_TEMP_DIR/$firstind" >/dev/null 2>&1; then
errecho 'duplicate clip! not caching...'
rm -f "$BUFFER"
return 0
fi
mv -f "$BUFFER" "$SHCLIP_TEMP_DIR/$lastind"
# edit order file in place
ed -s "$ORDER" <<-EOF
${SHCLIP_MAX_CLIPS}m0
wq
EOF
}
[ -z "${DISPLAY:-}" ] && fail 'Cannot find $DISPLAY. Is your X server running?'
# parse options
while getopts :pnf OPT; do
case $OPT in
n) SHCLIP_ENABLED="" ;;
f) SHCLIP_FILTER_ENABLED=1 ;;
*) fail "$BN: unknown option: -$OPTARG" ;;
esac
done
setup
while
if [ -z "$SHCLIP_ENABLED" ]; then
:
else
updateclip
fi
do
set +e
clipnotify -s clipboard &
clipnotify_pid=$!
wait
set -e
done

104
shclip-menu Executable file
View File

@ -0,0 +1,104 @@
#!/bin/sh
# shclip, by yosh
# https://git.unix.dog/yosh/shclip
set -eu
# config file if exists
SHCLIP_CONFIG_DIR="${SHCLIP_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/shclip}"
. "$SHCLIP_CONFIG_DIR/config"
SHCLIP_MENU_MAIN_CMD=${SHCLIP_MENU_MAIN_CMD:-fzfmenu -p shclip}
SHCLIP_MENU_DEL_CMD=${SHCLIP_MENU_DEL_CMD:-${SHCLIP_MENU_MAIN_CMD}}
SHCLIP_MENU_COPY_CMD=${SHCLIP_MENU_COPY_CMD:-${SHCLIP_MENU_MAIN_CMD}}
SHCLIP_MENU_EDIT_CMD=${SHCLIP_MENU_EDIT_CMD:-${SHCLIP_MENU_MAIN_CMD}}
SHCLIP_TEXT_EDITOR=${SHCLIP_TEXT_EDITOR:-xdg-open}
SHCLIP_IMAGE_EDITOR=${SHCLIP_IMAGE_EDITOR:-gimp -n}
SHCLIP_TEMP_DIR="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/shclip-$USER-$DISPLAY"
showmenu() {
choice="$(printf 'copy\ndelete\nedit\nshclip toggle\nfilter toggle\nexit' | $SHCLIP_MENU_MAIN_CMD)"
case $choice in
delete) delmenu ;;
copy) copymenu ;;
edit) editmenu ;;
shclip*) [ -d "$SHCLIP_TEMP_DIR/lock" ] && kill -s USR1 "$(cat "$SHCLIP_TEMP_DIR/pid")" ;;
filter*) [ -d "$SHCLIP_TEMP_DIR/lock" ] && kill -s USR2 "$(cat "$SHCLIP_TEMP_DIR/pid")" ;;
*) exit ;;
esac
}
menu_gen() {
while read -r ind; do
file="$SHCLIP_TEMP_DIR/$ind"
[ ! -f "$file" ] && continue
# build menu
mime="$(file -bi "$file")"
if [ "${mime%%/*}" = "text" ]; then
preview="${ind%%.*} | $(dd if="$file" bs=128 count=1 2>/dev/null | tr '\n' ' ')"
else
preview="${ind%%.*} * --- ${mime%%;*} --- $(du -k "$file" | cut -f 1)KiB"
fi
printf '%s\n' "${preview}"
done < "$SHCLIP_TEMP_DIR/order"
}
delmenu() {
choice="$({ echo "ALL"; menu_gen; } | $SHCLIP_MENU_DEL_CMD | cut -d ' ' -f 1)"
case $choice in
ALL)
set +f
rm -f "$SHCLIP_TEMP_DIR"/*.clip
set -f
;;
[0-9]*) rm -f "$SHCLIP_TEMP_DIR/$choice.clip" ;;
*) exit 1 ;;
esac
}
copymenu() {
choice="$(menu_gen | $SHCLIP_MENU_COPY_CMD | cut -d ' ' -f 1)"
case $choice in
[!0-9]*) exit 1 ;;
*) ;;
esac
mime="$(file -bi "$SHCLIP_TEMP_DIR/$choice.clip")"
ln="$(grep -n "$choice" "$SHCLIP_TEMP_DIR/order")"
ed -s "$SHCLIP_TEMP_DIR/order" - <<-EOF
${ln%%:*}m0
wq
EOF
case $mime in
text*) xclip -sel clipboard "$SHCLIP_TEMP_DIR/$choice.clip" ;;
*) xclip -sel clipboard -t "${mime%%;*}" "$SHCLIP_TEMP_DIR/$choice.clip" ;;
esac
exit
}
editmenu() {
choice="$(menu_gen | $SHCLIP_MENU_EDIT_CMD | cut -d ' ' -f 1)"
case $choice in
[!0-9]*) exit 1 ;;
*) ;;
esac
mime="$(file -bi "$SHCLIP_TEMP_DIR/$choice.clip")"
case $mime in
image/*)
mv "$SHCLIP_TEMP_DIR/$choice.clip" "$SHCLIP_TEMP_DIR/$choice.png"
$SHCLIP_IMAGE_EDITOR "$SHCLIP_TEMP_DIR/$choice.png"
mv "$SHCLIP_TEMP_DIR/$choice.png" "$SHCLIP_TEMP_DIR/$choice.clip"
;;
*) $SHCLIP_TEXT_EDITOR "$SHCLIP_TEMP_DIR/$choice.clip" ;;
esac
exit
}
while true; do
showmenu
done