| 1 | #!/bin/sh |
| 2 | # Requires: rofi, xclip, rofi-clipd running; xdotool for the paste step |
| 3 | # Clipboard history picker. Runs as a rofi script mode so each row can carry |
| 4 | # its entry's hash out of band — the visible text is a one-line preview, but |
| 5 | # what gets copied is the full original, newlines and all. |
| 6 | # Enter paste the entry into the window rofi was opened over |
| 7 | # Shift+Delete forget that entry |
| 8 | # The entry goes to the clipboard either way, so a manual Ctrl+V still works |
| 9 | # when the synthetic keystroke does not suit the app underneath. |
| 10 | # Called with no arguments it launches the rofi that then calls it back, so |
| 11 | # the keybinding stays a single path. |
| 12 | set -u |
| 13 | |
| 14 | DIR="${XDG_CACHE_HOME:-$HOME/.cache}/rofi-clipboard" |
| 15 | STORE="$DIR/store" |
| 16 | INDEX="$DIR/index" |
| 17 | |
| 18 | if [ -z "${ROFI_RETV:-}" ]; then |
| 19 | exec rofi -show clipboard -modes "clipboard:$0" |
| 20 | fi |
| 21 | |
| 22 | # Row options ride behind a NUL, with 0x1f between key and value. NUL cannot |
| 23 | # survive a shell variable, so it has to come from the format string itself. |
| 24 | US=$(printf '\037') |
| 25 | |
| 26 | list() { |
| 27 | if [ ! -s "$INDEX" ]; then |
| 28 | printf '\0message%sClipboard history is empty — is rofi-clipd running?\n' "$US" |
| 29 | return |
| 30 | fi |
| 31 | printf '\0prompt%sclipboard\n' "$US" |
| 32 | # keep-selection stops the cursor jumping back to the top after a delete. |
| 33 | printf '\0keep-selection%strue\n' "$US" |
| 34 | while IFS=' ' read -r hash preview; do |
| 35 | printf '%s\0info%s%s\n' "$preview" "$US" "$hash" |
| 36 | done < "$INDEX" |
| 37 | } |
| 38 | |
| 39 | # Type the paste keystroke into whatever the window manager focuses once rofi |
| 40 | # is gone. It cannot be sent from here directly: rofi holds a keyboard grab |
| 41 | # for as long as it is up, and XTEST events go to the grab holder, so this |
| 42 | # runs detached and waits the grab out. Its stdout goes to /dev/null so it |
| 43 | # cannot hold that pipe open either — see the note on xclip below. |
| 44 | paste_into_focus() { |
| 45 | if ! command -v xdotool >/dev/null 2>&1; then |
| 46 | notify-send "Clipboard" "Copied — install xdotool to paste automatically." \ |
| 47 | >/dev/null 2>&1 |
| 48 | return 0 |
| 49 | fi |
| 50 | ( |
| 51 | # Up to three seconds of waiting for focus to land somewhere that is |
| 52 | # not rofi; if it never does, paste nowhere rather than into rofi. |
| 53 | target="" |
| 54 | i=0 |
| 55 | while [ "$i" -lt 60 ]; do |
| 56 | class=$(xdotool getactivewindow getwindowclassname 2>/dev/null) || class="" |
| 57 | case $class in |
| 58 | ""|[Rr]ofi) ;; |
| 59 | *) target=$class; break ;; |
| 60 | esac |
| 61 | sleep 0.05 |
| 62 | i=$((i + 1)) |
| 63 | done |
| 64 | [ -n "$target" ] || exit 0 |
| 65 | |
| 66 | # Terminals keep Ctrl+V for the readline binding and put paste behind |
| 67 | # Shift; everything else takes the plain one. |
| 68 | case $target in |
| 69 | org.wezfurlong.wezterm|Alacritty|com.mitchellh.ghostty|kitty|foot|Foot) |
| 70 | key=ctrl+shift+v ;; |
| 71 | *) key=ctrl+v ;; |
| 72 | esac |
| 73 | |
| 74 | # Let the newly focused window settle before typing at it. |
| 75 | sleep 0.05 |
| 76 | xdotool key --clearmodifiers "$key" |
| 77 | ) >/dev/null 2>&1 & |
| 78 | } |
| 79 | |
| 80 | case "$ROFI_RETV" in |
| 81 | 0) list ;; |
| 82 | 1) |
| 83 | hash=${ROFI_INFO:-} |
| 84 | [ -n "$hash" ] && [ -f "$STORE/$hash" ] || exit 0 |
| 85 | # xclip forks and holds the selection until something else claims it, |
| 86 | # so it has to outlive this script — which it does on its own. Its |
| 87 | # stdout has to go, though: it inherits the pipe rofi reads this script |
| 88 | # on, and rofi waits for EOF there before closing and dropping its |
| 89 | # keyboard grab. That EOF never comes while xclip holds the pipe open, |
| 90 | # and a hung rofi still holding the grab looks like a frozen desktop. |
| 91 | xclip -selection clipboard -i "$STORE/$hash" >/dev/null 2>&1 |
| 92 | paste_into_focus |
| 93 | ;; |
| 94 | 2) |
| 95 | # Typed text with no match: copy what was typed, it is the one thing |
| 96 | # the user could have meant. |
| 97 | if [ -n "${1:-}" ]; then |
| 98 | printf '%s' "$1" | xclip -selection clipboard -i >/dev/null 2>&1 |
| 99 | paste_into_focus |
| 100 | fi |
| 101 | ;; |
| 102 | 3) |
| 103 | hash=${ROFI_INFO:-} |
| 104 | if [ -n "$hash" ]; then |
| 105 | grep -v "^$hash " "$INDEX" > "$INDEX.$$" 2>/dev/null |
| 106 | mv "$INDEX.$$" "$INDEX" |
| 107 | rm -f "$STORE/$hash" |
| 108 | fi |
| 109 | list |
| 110 | ;; |
| 111 | esac |