From c9ca484b244929079e721d7b8b06fa3526aa90b5 Mon Sep 17 00:00:00 2001 From: Joe Rayhawk Date: Tue, 31 Dec 2024 23:41:59 -0800 Subject: bin/clipsync3.sh: support non-text data --- bin/clipsync3.sh | 117 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 100 insertions(+), 17 deletions(-) (limited to 'bin') diff --git a/bin/clipsync3.sh b/bin/clipsync3.sh index 2298d46..0a60a37 100755 --- a/bin/clipsync3.sh +++ b/bin/clipsync3.sh @@ -1,25 +1,63 @@ #!/bin/sh -# Scan for wlroots and x11 clipboard events and synchronize text strings. +# Scan for wlroots and x11 primary selection and clipboard events and synchronize between them # Requirements: # clipnotify https://github.com/cdown/clipnotify (xfixes clipboard events) # wl-clipboard https://github.com/bugaevc/wl-clipboard # xclip https://github.com/astrand/xclip +# sha256sum coreutils -# non-text TARGET/mimetype negotiation requires a more complicated protocol +# multi-TARGET/mimetype negotiation requires a more complicated protocol # bridge than what xclip and wl-clipboard can be expected to provide. # To see the scope of the problem for a given selection: -# xclip -t TARGETS -o -# wl-paste -l +# xclip -o -t TARGETS +# wl-paste --list-types +# Instead, we naively prioritize top-to-bottom from the following list that +# you're welcome to modify to suit your needs: + +TYPEPRIORITY=" +image/png +image/tiff +text/plain;charset=utf-8 +UTF8_STRING +text/plain +STRING +" set -e -set -x # debug -#PS4='+ $(sleep .1)' # debug - -differ() { [ x"$new" != x"$old" ]; } -wcp() { printf "%s" "$new" | wl-copy -p; } -wcc() { printf "%s" "$new" | wl-copy; } -xcp() { printf "%s" "$new" | xclip -selection primary; } -#xcc() { printf "%s" "$new" | xclip -selection clipboard; } # why is this broken? + +debug() { + prevexit="$?"; + prevcmd="$cmd"; + cmd="$BASH_COMMAND"; + if [ "$prevexit" -eq 0 ]; then + printf "\033[0;32m%i %s\033[0m\n" "$prevexit" "$prevcmd" 1>&2 + else + printf "\033[0;31m%i %s\033[0m\n" "$prevexit" "$prevcmd" 1>&2 + fi +} + +#set -T; trap 'debug' DEBUG # debug, Bash-only +set -x # debug, POSIX +#PS4='+ $(sleep .1)' # debug with aggressive serialization + +# "list targets/types" commands +wpt() { wl-paste --primary --list-types; }; +wct() { wl-paste --list-types; }; +xpt() { xclip -o -selection primary -t TARGETS || true; }; # see xclip note at bottom +xct() { xclip -o -selection clipboard -t TARGETS || true; }; + +# "paste from" commands +# note: binary data must go through STDIO; ARGV and ENV are both NUL-delimited +wpp() { wl-paste -n --primary -t "$TYPE"; } +wcp() { wl-paste -n -t "$TYPE"; } +xpp() { xclip -o -selection primary -t "$TYPE"; } +xcp() { xclip -o -selection clipboard -t "$TYPE"; } + +# "copy into" commands +wpc() { wl-copy --primary -t "$TYPE"; } +wcc() { wl-copy -t "$TYPE"; } +xpc() { xclip -selection primary -t "$TYPE"; } +#xcc() { xclip -selection clipboard -t "$TYPE"; } # why is this broken? xcc() { true; } fifo="${XDG_RUNTIME_DIR:-/tmp}/clipsync-$WAYLAND_DISPLAY-$DISPLAY.sock" @@ -31,21 +69,66 @@ trap "trap - TERM; rm $fifo; kill -- -$$;" INT TERM EXIT # rewrite trap so it do exec 3<> "$fifo" # open as fd 3 #rm "$fifo" # we don't technically use the link anymore, but it *is* useful as lockfile -wl-paste --watch echo wc >&3 & wcpid=$!; read -r spam <&3; unset spam +# Event generation commands wl-paste --primary --watch echo wp >&3 & wppid=$!; read -r spam <&3; unset spam +wl-paste --watch echo wc >&3 & wcpid=$!; read -r spam <&3; unset spam while clipnotify -s primary ; do echo xp; done >&3 & xppid=$! #while clipnotify -s clipboard; do echo xc; done >&3 & xcpid=$! # why is this broken? +# Event processing while read -r event <&3; do case "$event" in - (wc) new="$( wl-paste -n )"; differ && ( wcp; xcc; xcp; ) ;; - (wp) new="$( wl-paste -n --primary )"; differ && ( wcc; xcc; xcp; ) ;; - (xc) new="$( xclip -o -selection clipboard )"; differ && ( wcc; wcp; xcp; ) ;; - (xp) new="$( xclip -o -selection primary )"; differ && ( wcc; wcp; xcc; ) ;; + ([wx][pc]) # wayland or x11, primary or clipboard + TYPES="$( ${event}t )" && + if [ -n "$TYPES" ]; then # GIMP X11 GDK likes doing zero-length selections with no targets + for TYPE in $TYPEPRIORITY; do + if [ -z "${TYPES##*$TYPE*}" ]; then # matching type/target? + new="$( ${event}p | sha256sum )" && # new sum + [ x"$new" != x"$old" ] && # different from old sum? + for sink in wp wc xp xc; do + [ "$event" != "$sink" ] && # avoid sinking into the source + ${event}p | ${sink}c + done + break; # always break on matched type/target + fi + done; + fi;; + (*) echo "ERROR: INVALID EVENT ON FD 3: $event"; exit 1; ;; esac old="$new" done +# Notes: +# xclip returns 1 on a failure to return TARGETS (such as with -selection clipboard)... +# 000:<:000d: 24: Request(24): ConvertSelection requestor=0x00a00001 selection=0xf9("CLIPBOARD") target=0xf5("TARGETS") property=0x16a("XCLIP_OUT") time=CurrentTime(0x00000000) +# 000:>:000d: Event (generated) SelectionNotify(31) time=CurrentTime(0x00000000) requestor=0x00a00001 selection=0xf9("CLIPBOARD") target=0xf5("TARGETS") property=None(0x0) +# 000:<:000e: 20: Request(16): InternAtom only-if-exists=false(0x00) name='UTF8_STRING' +# 000:>:000e:32: Reply to InternAtom: atom=0x128("UTF8_STRING") +# Error: target TARGETS not available +# ...and also returns 1 on a failure to resolve what TARGETS were returned (such as with Twibright links2 text selections), regardless of how many successful resolutions have been printed... +# 000:<:000c: 24: Request(24): ConvertSelection requestor=0x00c00001 selection=0x1("PRIMARY") target=0xf5("TARGETS") property=0x16a("XCLIP_OUT") time=CurrentTime(0x00000000) +# 000:>:000c: Event (generated) SelectionNotify(31) time=CurrentTime(0x00000000) requestor=0x00a00001 selection=0x1("PRIMARY") target=0xf5("TARGETS") property=0x16a("XCLIP_OUT") +# 000:<:000d: 24: Request(20): GetProperty delete=false(0x00) window=0x00a00001 property=0x16a("XCLIP_OUT") type=any(0x0) long-offset=0x00000000 long-length=0x00000000 +# 000:>:000d:32: Reply to GetProperty: type=0x4("ATOM") bytes-after=0x0000000c data=; +# 000:<:000e: 24: Request(20): GetProperty delete=false(0x00) window=0x00a00001 property=0x16a("XCLIP_OUT") type=any(0x0) long-offset=0x00000000 long-length=0x0000000c +# 000:>:000e:44: Reply to GetProperty: type=0x4("ATOM") bytes-after=0x00000000 data=0xf5("TARGETS"),0x128,0x7f5ad23c; +# 000:<:000f: 12: Request(19): DeleteProperty window=0x00a00001 property=0x16a("XCLIP_OUT") +# 000:<:0010: 20: Request(16): InternAtom only-if-exists=true(0x01) name='text/html' +# 000:>:0010: Event PropertyNotify(28) window=0x00a00001 atom=0x16a("XCLIP_OUT") time=0x019bbed6 state=Deleted(0x01) +# 000:>:0010:32: Reply to InternAtom: atom=0x19a("text/html") +# TARGETS +# 000:<:0011: 8: Request(17): GetAtomName atom=0x128(unrecognized atom) +# 000:>:0011:44: Reply to GetAtomName: name='UTF8_STRING' +# UTF8_STRING +# 000:<:0012: 8: Request(17): GetAtomName atom=0x7f5ad23c(unrecognized atom) +# 000:>:0012:Error 5=Atom: major=17, minor=0, bad=0x7f5ad23c, seq=0012 +# ...which means we have to blindly try to use its output regardless of exit code. + +# Repeatedly using "paste from" on remote X11 servers is pretty horrifying, +# but finding a secure spot to cache NUL-containing data is annoyingly +# system-specific, "od | while read do printf" as a workaround, meanwhile, +# is bafflingly slow. POSIX shell is just not good at this job. + # if further process management is needed: #kill $wcpid $wppid $xppid $xcpid #disown $wcpid $wppid $xppid $xcpid -- cgit v1.2.3