From 2d43f929e38c6105f9ca1476eaad7e4356ecd509 Mon Sep 17 00:00:00 2001 From: Uyanide Date: Wed, 11 Feb 2026 09:06:49 +0100 Subject: [PATCH] update fzfclip --- config/scripts/.local/scripts/fzfclip | 310 +++++++++++++++------ config/scripts/.local/scripts/fzfclip-wrap | 4 +- config/scripts/.local/scripts/playlive | 6 +- memo/oh-my-altgr.md | 4 +- scripts | 1 + 5 files changed, 236 insertions(+), 89 deletions(-) create mode 120000 scripts diff --git a/config/scripts/.local/scripts/fzfclip b/config/scripts/.local/scripts/fzfclip index 1d93a4e..99c9ec3 100755 --- a/config/scripts/.local/scripts/fzfclip +++ b/config/scripts/.local/scripts/fzfclip @@ -1,15 +1,14 @@ #!/usr/bin/env bash -# shellcheck disable=SC2016 # Description: # View and manage clipboard history using fzf, with support for # image preview in compatible terminals. # Requirements: +# - fzf # - cliphist # - wl-clipboard -# - fzf -# - sixel-query, kgp-query, iterm2-query from this repository -# - chafa (for image preview) +# - python with urllib (for URL quoting/unquoting) +# - chafa (optional, for image preview) # - ffmpegthumbnailer (optional, for video thumbnails) # Credits: # - Original idea and some code adapted from https://github.com/SHORiN-KiWATA/shorinclip @@ -38,9 +37,21 @@ set -euo pipefail +_cleanup() { + if [ -n "${CACHE_DIR:-}" ] && [ -d "$CACHE_DIR" ]; then + rm -rf "$CACHE_DIR" + fi + if [ -n "${WATCH_PID:-}" ]; then + kill "$WATCH_PID" 2>/dev/null || true + fi + if [ -n "${FZF_SOCKET:-}" ] && [ -S "$FZF_SOCKET" ]; then + rm -f "$FZF_SOCKET" + fi +} +trap _cleanup EXIT + CACHE_DIR=$(mktemp -d) export CACHE_DIR -# trap rm later export C_TERTIARY='\x1b[1;35m' export C_PRIMARY='\x1b[1;34m' @@ -49,12 +60,111 @@ export C_RESET='\x1b[0m' # Check for terminal graphics support and set environment variables accordingly +graphics-query() { + # Port of [graphics-query](https://github.com/Uyanide/dotfiles/blob/main/config/scripts/.local/scripts/graphics-query) + + # Ensure in a interactive terminal + [ ! -t 0 ] && return + + ( + # Construct query + KGP_QUERY_ID=$RANDOM + KGP_QUERY_CODE=$(printf "\033_Gi=%d,s=1,v=1,a=q,t=d,f=24;AAAA\033\\" "$KGP_QUERY_ID") + ITERM2_QUERY_CODE=$(printf "\033]1337;ReportCellSize\a") + KGP_EXPECTED_RESPONSE=$(printf "\033_Gi=%d;OK\033\\" "$KGP_QUERY_ID") + ITERM2_EXPECTED_RESPONSE=$(printf "\033]1337;") # followed by "ReportCellSize=...", but only the prefix is enough + FENCE_CODE=$(printf "\033[c") + + # Set terminal to raw mode with timeout + stty_orig=$(stty -g) + trap 'stty "$stty_orig"' EXIT + stty -echo -icanon min 1 time 0 + + printf "%s%s%s" "$ITERM2_QUERY_CODE" "$KGP_QUERY_CODE" "$FENCE_CODE" > /dev/tty + + support_kgp=0 + support_iterm2=0 + support_sixel=0 + + response="" + while true; do + IFS= read -r -N 1 -t 0.3 char || { + [ -z "$char" ] && break + } + + response+="$char" + + if [[ "$response" == *"$KGP_EXPECTED_RESPONSE"* ]]; then + support_kgp=1 + fi + + if [[ "$response" == *"$ITERM2_EXPECTED_RESPONSE"* ]]; then + support_iterm2=1 + fi + + if [[ "$response" == *$'\033['*'c' ]]; then + break + fi + + if [ ${#response} -gt 1024 ]; then + break + fi + done + + if [[ "$response" =~ $'\x1b'\[\?([0-9;]*)c ]]; then + params="${BASH_REMATCH[1]}" + + IFS=';' read -ra codes <<< "$params" + + for code in "${codes[@]}"; do + if [[ "$code" == "4" ]]; then + support_sixel=1 + break + fi + done + fi + + if [ "$support_kgp" -eq 1 ]; then + echo "kitty" + fi + + if [ "$support_iterm2" -eq 1 ]; then + echo "iterm" + fi + + if [ "$support_sixel" -eq 1 ]; then + echo "sixels" + fi + ) +} + +SUPPORT_ICAT=0 +SUPPORT_SIXEL=0 +SUPPORT_ITERM2=0 + +ENABLE_ICAT=0 +ENABLE_SIXEL=0 +ENABLE_ITERM2=0 + +_check_graphics_support() { + # type graphics-query &>/dev/null || return + local result + result=$(graphics-query) + if [[ "$result" == *"kitty"* ]]; then + SUPPORT_ICAT=1 + elif [[ "$result" == *"sixels"* ]]; then + SUPPORT_SIXEL=1 + elif [[ "$result" == *"iterm"* ]]; then + SUPPORT_ITERM2=1 + fi +} + _check_kitty_icat() { - # workaround for WezTerm + # # workaround for WezTerm if [ -n "${WEZTERM_EXECUTABLE:-}" ]; then return 1 fi - kgp-query + [[ "$SUPPORT_ICAT" -eq 1 ]] } _check_sixel() { @@ -65,36 +175,70 @@ _check_sixel() { elif [ -n "${TMUX:-}" ]; then return 1 fi - sixel-query + [[ "$SUPPORT_SIXEL" -eq 1 ]] } _check_iterm2() { - iterm2-query + [[ "$SUPPORT_ITERM2" -eq 1 ]] } -ENABLE_ICAT=0 -ENABLE_SIXEL=0 -ENABLE_ITERM2=0 - # Priority: KGP > sixel > iterm2 +_check_graphics_support if _check_kitty_icat; then - export ENABLE_ICAT=1 + ENABLE_ICAT=1 elif _check_sixel; then - export ENABLE_SIXEL=1 + ENABLE_SIXEL=1 elif _check_iterm2; then - export ENABLE_ITERM2=1 + ENABLE_ITERM2=1 fi export ENABLE_ICAT export ENABLE_SIXEL export ENABLE_ITERM2 +# URL handling (for file:// URLs) + +url_unquote() { + if type python3 &>/dev/null; then + python3 -c "import sys, urllib.parse; print(urllib.parse.unquote(sys.stdin.read().strip()), end='')" + else + cat + fi +} + +url_quote() { + if type python3 &>/dev/null; then + python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()), end='')" + else + cat + fi +} +export -f url_unquote +export -f url_quote + # Preview functions +_clear_preview() { + # chafa --clear will simply send '\x1b[H\x1b[2J' which may not work + # for images displayed with KGP, so we send the specific clear sequence + # manually in this case. + if [ "$ENABLE_ICAT" -eq 1 ]; then + printf "\x1b_Ga=d\x1b\\" + fi +} +export -f _clear_preview + _preview_image() { local file="$1" + if ! type chafa >/dev/null 2>&1; then + text="Preview not available (chafa not found)."$'\n' + text+="Image: $file" + _preview_text "$text" + return + fi + # Though chafa is able to detect which output format to use based on the terminal capabilities, + # it is yet not always reliable, so we force it based on our checks. if [ "$ENABLE_ICAT" -eq 1 ]; then - printf "\x1b_Ga=d\x1b\\" chafa -f kitty --size="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}" "$file" elif [ "$ENABLE_SIXEL" -eq 1 ]; then chafa -f sixels --size="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}" "$file" @@ -108,40 +252,61 @@ export -f _preview_image _preview_text() { local content="$1" - if [ "$ENABLE_ICAT" -eq 1 ]; then - printf "\x1b_Ga=d\x1b\\" - fi - echo "$content" | head -n 100 + printf "%s" "$content" | head -n 100 } export -f _preview_text +_preview_video() { + local video_hash thumb_file path + path="$1" + video_hash=$(echo "$path" | md5sum | cut -d" " -f1) + thumb_file="$CACHE_DIR/$video_hash.png" + if [ ! -f "$thumb_file" ]; then + if type ffmpegthumbnailer &>/dev/null; then + ffmpegthumbnailer -i "$path" -o "$thumb_file" -s 480 -t 0 >/dev/null 2>&1 + else + _preview_text "Thumbnail not available (ffmpegthumbnailer not found)."$'\n' + fi + fi + if [ -s "$thumb_file" ]; then + _preview_image "$thumb_file" + else + _preview_text "Video: $path" + fi +} +export -f _preview_video + +# Kinda buggy right now +# _preview_audio() { +# local path="$1" +# if type mpv &>/dev/null; then +# echo "Playing audio: $path" >&2 +# exec mpv --no-video --keep-open=no --loop-file=no --loop-playlist=no "$path" &>/dev/null +# else +# _preview_text "Audio: $path" +# fi +# } +# export -f _preview_audio + _preview_file() { path="$1" path_mime=$(file -b --mime-type "$path") if [[ $path_mime =~ image ]]; then _preview_image "$path" elif [[ "$path_mime" =~ video ]]; then - video_hash=$(echo "$path" | md5sum | cut -d" " -f1) - thumb_file="$CACHE_DIR/$video_hash.png" - if [ ! -f "$thumb_file" ]; then - if command -v ffmpegthumbnailer &>/dev/null; then - ffmpegthumbnailer -i "$path" -o "$thumb_file" -s 480 -t 0 >/dev/null 2>&1 - else - _preview_text "ffmpegthumbnailer not installed, cannot generate thumbnail for video." - fi - fi - if [ -s "$thumb_file" ]; then - _preview_image "$thumb_file" - else - _preview_text "Video: $path (No thumbnail)" - fi + _preview_video "$path" + # elif [[ "$path_mime" =~ audio ]]; then + # _preview_audio "$path" else _preview_text "$path" fi } export -f _preview_file + preview() { + _clear_preview + entry="$1" content=$(echo "$entry" | cut -f2-) @@ -154,31 +319,31 @@ preview() { [ -f "$cache_file" ] || echo "$entry" | cliphist decode >"$cache_file" _preview_image "$cache_file" - elif [ "$mimeType" = "text/html" ] && echo "$content" | grep -q QQ; then - qq_img_file=$(echo "$entry" | cliphist decode | grep -oP "^/dev/null 2>&1 & +wl-paste --watch bash -c "curl -s --unix-socket '$FZF_SOCKET' -X POST -d 'reload($RELOAD_CMD)' http://localhost" &>/dev/null & WATCH_PID=$! -trap 'rm -rf "$CACHE_DIR"; kill $WATCH_PID 2>/dev/null' EXIT # Ensure terminal is large enough @@ -277,9 +422,9 @@ while [[ $(tput cols) -lt 35 || $(tput lines) -lt 25 ]]; do [ "$wait_timeout" -eq 0 ] && exit 1 done -cliphist list | format_clip_list | add_num | fzf \ +$RELOAD_CMD | fzf \ --ansi \ - --listen "$FZF_PORT" \ + --listen "$FZF_SOCKET" \ --bind "ctrl-r:reload($RELOAD_CMD)" \ --bind "ctrl-x:execute-silent(bash -c 'cliphist delete <<< \"\$1\"' -- {})+reload($RELOAD_CMD)" \ --prompt="󰅍 > " \ @@ -292,4 +437,5 @@ cliphist list | format_clip_list | add_num | fzf \ --delimiter '\t' \ --preview-window=down:60%,wrap \ --preview "preview {}" \ - --bind "enter:execute-silent(bash -c 'copy_selection \"\$1\"' -- {})+accept" + --bind "enter:execute-silent(bash -c 'copy_selection \"\$1\"' -- {})+accept" \ +|| [ $? -eq 141 ] diff --git a/config/scripts/.local/scripts/fzfclip-wrap b/config/scripts/.local/scripts/fzfclip-wrap index 0cb9c29..f272421 100755 --- a/config/scripts/.local/scripts/fzfclip-wrap +++ b/config/scripts/.local/scripts/fzfclip-wrap @@ -4,8 +4,6 @@ # Wrapper for fzfclip to ensure only one instance is # running and to launch it in ghostty. -set -euo pipefail - exec {LOCK_FD}>/tmp/"$(basename "$0")".lock flock -n "$LOCK_FD" || { @@ -13,4 +11,4 @@ flock -n "$LOCK_FD" || { exit 1 } -ghostty -e fzfclip "$@" +ghostty -e fzfclip "$@" {LOCK_FD}>&- diff --git a/config/scripts/.local/scripts/playlive b/config/scripts/.local/scripts/playlive index 4f39cdc..258755c 100755 --- a/config/scripts/.local/scripts/playlive +++ b/config/scripts/.local/scripts/playlive @@ -45,13 +45,13 @@ mp4_offset=$((offset - 3)) tail -c "+$mp4_offset" "$file" > "$tmp_video" play_cmd=() -if command -v mpv >/dev/null 2>&1; then +if type mpv >/dev/null 2>&1; then play_cmd=(mpv --title="Live Photo View: $file" --keep-open=yes --loop-file=yes --loop-playlist=no --idle=yes) -elif command -v vlc >/dev/null 2>&1; then +elif type vlc >/dev/null 2>&1; then play_cmd=(vlc --meta-title="Live Photo View: $file") else echo "Error: No suitable media player found." >&2 exit 1 fi -"${play_cmd[@]}" "$tmp_video" \ No newline at end of file +"${play_cmd[@]}" "$tmp_video" diff --git a/memo/oh-my-altgr.md b/memo/oh-my-altgr.md index 5e5422a..08320d0 100644 --- a/memo/oh-my-altgr.md +++ b/memo/oh-my-altgr.md @@ -1,4 +1,4 @@ -de 布局太全能了 +de 布局太全能了: Deutsche Tastaturbelegung unter Linux @@ -23,3 +23,5 @@ shift > Y X C V B N M ; : _ altgr | » « ¢ „ “ ” µ · … – sh+al ˍ › ‹ © ‚ ‘ ’ º × ÷ — ``` + +btw, `^` 死键对数字键(上方一排和小键盘均可)也有效, 作用为打出n次幂, 例如 `^` + `9` -> `⁹`. diff --git a/scripts b/scripts new file mode 120000 index 0000000..736fee1 --- /dev/null +++ b/scripts @@ -0,0 +1 @@ +config/scripts/.local/scripts \ No newline at end of file