update cliphist viewer

This commit is contained in:
2026-02-04 16:09:27 +01:00
parent 3837b42437
commit 58fd5c4d50
3 changed files with 297 additions and 5 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ binds {
Alt+Space { spawn-sh "pkill -x rofi || rofi -show drun"; }
// Actions
Mod+V { spawn-sh "pkill -x rofi || rofi-cliphist"; }
Mod+V { spawn-sh "pkill -x shorin-cliphist || ghostty -e shorin-cliphist"; }
Mod+Period { spawn-sh "pkill -x rofi || rofi-emoji"; }
Ctrl+Alt+Delete { spawn-sh "pkill -x wlogout || wlogout -p layer-shell"; }
Print { spawn "niri" "msg" "action" "screenshot-screen"; }
+27 -4
View File
@@ -1,7 +1,30 @@
#!/bin/sh
#!/usr/bin/env bash
# Description:
# Quick snippet for cliphist + rofi + wl-copy
# ~~Quick~~ snippet for cliphist + rofi + wl-copy
cliphist list | rofi -dmenu -config ~/.config/rofi/dmenu.rasi -display-columns 2 -i | \
cliphist decode | wl-copy
tmp_dir="/tmp/cliphist"
trap 'rm -rf "$tmp_dir"' EXIT
mkdir -p "$tmp_dir"
read -r -d '' prog <<EOF
/^[0-9]+\s<meta http-equiv=/ { next }
match(\$0, /^([0-9]+)\s(\[\[\s)?binary.*(jpg|jpeg|png|bmp)/, grp) {
system("echo " grp[1] "\\\\\t | cliphist decode >$tmp_dir/"grp[1]"."grp[3])
print \$0"\0icon\x1f$tmp_dir/"grp[1]"."grp[3]
next
}
1
EOF
# Pipeline logic:
# 1. cliphist list: gives "ID <tab> Content"
# 2. gawk: adds icon paths
# 3. rofi -dmenu: shows list, hides column 1 (ID), returns "ID <tab> Content" on select
# 4. cliphist decode: reads ID from line, gets original content
result=$(cliphist list | gawk "$prog" | rofi -dmenu -display-columns 2 -config "$HOME/.config/rofi/dmenu.rasi" -show-icons -p "Clipboard")
if [[ -n "$result" ]]; then
echo "$result" | cliphist decode | wl-copy
fi
+269
View File
@@ -0,0 +1,269 @@
#!/usr/bin/env bash
# shellcheck disable=SC2016
# Credit: https://github.com/SHORiN-KiWATA/shorinclip
#
# MIT License
#
# Copyright (c) 2026 shorinkiwata
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
set -euo pipefail
CACHE_DIR=$(mktemp -d)
export CACHE_DIR
trap 'rm -rf "$CACHE_DIR"' EXIT
# 颜色变量
export C_TERTIARY='\x1b[1;35m'
export C_PRIMARY='\x1b[1;34m'
export C_CYAN='\x1b[1;36m'
export C_RESET='\x1b[0m'
# 过滤无法复制粘贴的信息
filter_clip_list() {
grep -vP "(?s)(?i)\t.*<html.*\[表情\]"
}
export -f filter_clip_list
# 美化列表
format_clip_list() {
sed -E \
-e "s/(\t).*\.(mp4|mkv|webm|avi|mov|flv|wmv)$/\1${C_TERTIARY}[VIDEO]File.\2${C_RESET}/" \
-e "s/(\t)file:\/\/.*\.(mp4|mkv|webm|avi|mov|flv|wmv)$/\1${C_TERTIARY}[VIDEO]Url.\2${C_RESET}/" \
-e "s/(\t).*src=\"file:\/\/.*[qQ][qQ].*/\1${C_PRIMARY}[IMG_HTML]QQ${C_RESET}/" \
-e "s/(\t)file:\/\/.*xwechat.*temp.*/\1${C_PRIMARY}[IMG]WeChat${C_RESET}/" \
-e "s/(\t)file:\/\/.*\.gif$/\1${C_PRIMARY}[IMG]Url.gif${C_RESET}/" \
-e "s/(\t)file:\/\/.*\.(png|jpg|jpeg|webp|bmp)$/\1${C_TERTIARY}[IMG]Url.\2${C_RESET}/" \
-e "s/(\t)file:\/\/.*/\1${C_CYAN}[URL]File${C_RESET}/" \
-e "s/(\t)\/.*\.gif$/\1${C_PRIMARY}[IMG]Path.gif${C_RESET}/" \
-e "s/(\t)\/.*\.(png|jpg|jpeg|webp|bmp)$/\1${C_TERTIARY}[IMG]Path.\2${C_RESET}/" \
-e "s/\[\[ binary data .* (png|jpg|jpeg|gif|webp) .*\]\]/${C_TERTIARY}[IMG]Bin.\1${C_RESET}/" \
-e "s/\[\[ binary data .* \]\]/${C_CYAN}[BINARY]${C_RESET}/"
}
export -f format_clip_list
# 导入fzf前的处理,添加序号
add_num() {
awk -F '\t' '{printf "%s\t\x1b[90m%-2d \x1b[0m%s\n", $1, NR, $2}'
}
export -f add_num
# 复制前的处理,对不同数据采取不同的处理方式
copy_selection() {
#获取fzf选择的项目
local input="$1"
# 获取decode内容
local decoded
decoded=$(echo "$input" | cliphist decode)
# 获取mime类型
local mime
mime=$(echo "$input" | cliphist decode | file -b --mime-type -)
# Python 工具:用于 URL 编码 (处理路径中的中文和空格)
url_encode() {
python -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))" "$1"
}
# 1. 二进制图片 -> 直接复制图片数据
if [[ "$mime" =~ image ]]; then
# 直接复制
echo "$input" | cliphist decode | wl-copy
# 2. QQ HTML -> 提取路径 -> 存入缓存 -> 复制 file:// 缓存路径
elif [ "$mime" = "text/html" ]; then
local qq_src
qq_src=$(echo "$decoded" | grep -oP "^<img src=\"file://\K[^\"]+")
if [ -f "$qq_src" ]; then
# url编码处理
local encoded_path
encoded_path=$(url_encode "$qq_src")
# 拼接头并复制
echo "file://$encoded_path" | wl-copy --type text/uri-list
fi
# 4. file:// 协议路径 -> 直接复制为文件链接
elif [[ "$decoded" == file://* ]]; then
echo "$decoded" | wl-copy --type text/uri-list
# 3. 绝对路径 -> 转 file:// -> 复制为URL链接
elif [[ "$decoded" == /* ]] && [ -e "$decoded" ]; then
local encoded_path
encoded_path=$(url_encode "$decoded")
echo "file://$encoded_path" | wl-copy --type text/uri-list
# 5. 其他 -> 普通文本复制
else
echo "$input" | cliphist decode | wl-copy
fi
}
export -f copy_selection
# 自动刷新机制
FZF_PORT=$(shuf -i 10000-60000 -n 1)
RELOAD_CMD="cliphist list | format_clip_list | add_num"
wl-paste --watch bash -c "curl -s -X POST -d 'reload($RELOAD_CMD)' http://localhost:$FZF_PORT" >/dev/null 2>&1 &
WATCH_PID=$!
trap 'kill $WATCH_PID 2>/dev/null' EXIT
# 读取窗口大小避免窗口过小的时候就打开fzf导致菜单错位
# 50次循环超时
wait_timeout=50
# 循环检测终端的长和宽
while [[ $(tput cols) -lt 35 || $(tput lines) -lt 25 ]]; do
echo "Waiting for terminal resize to at least 35x25..."
sleep 1
# 每循环一次减少超时时间
((wait_timeout--))
[ "$wait_timeout" -eq 0 ] && {
echo "Timeout waiting for terminal resize. Exiting."
exit 1
}
done
#=== FZF 主程序 ===
cliphist list | format_clip_list | add_num | fzf \
--ansi \
--listen "$FZF_PORT" \
--bind "ctrl-r:reload($RELOAD_CMD)" \
--bind "ctrl-x:execute-silent(bash -c 'cliphist delete <<< \"\$1\"' -- {})+reload($RELOAD_CMD)" \
--prompt="󰅍 > " \
--header='CTRL-X: Delete | CTRL-R: Reload | ENTER: Copy' \
--color='header:italic:yellow,prompt:blue,pointer:blue' \
--info=hidden \
--no-sort \
--layout=reverse \
--with-nth 2.. \
--delimiter '\t' \
--preview-window=down:50% \
--preview '
# 获取当期剪贴版项目的id
id=$(echo {} | cut -f1)
content=$(echo {} | cut -f2-)
# 通过decode数据获取mimetype
mimeType=$(echo {} | cliphist decode | file -b --mime-type -)
# 获取文件后缀名
ext=$(echo $mimeType | awk -F"/" "{print \$2}")
# 通过mimetype判断数据/类型
# 如果是二进制图片
if [[ $mimeType =~ image ]]; then
# 生成缓存文件
img_hash=$(echo {} | cliphist decode | md5sum | cut -d" " -f1)
cache_file="$CACHE_DIR/$img_hash.$ext"
echo {} | cliphist decode > "$cache_file"
kitty +kitten icat --clear --image-id=10 --transfer-mode=file \
--place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0" "$cache_file" </dev/tty
# 如果是qq复制出来的html链接
elif [ "$mimeType" = "text/html" ] && echo "$content" | grep -q QQ; then
# 获取html数据中的图片路径
qq_img_file=$(echo {} | cliphist decode | grep -oP "^<img src=\"file://\K[^\"]+")
# 生成缓存文件
#qq_ext="${qq_img_file##*.}"
#qq_img_cache_file=$CACHE_DIR/$id.$qq_ext
#cp $qq_img_file $qq_img_cache_file
# 把路径传给kitty +kitten icat预览
if [ -f "$qq_img_file" ]; then
kitty +kitten icat --transfer-mode=file --clear --image-id=10 \
--place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0" "$qq_img_file" </dev/tty
else
echo "$qq_img_file does not exsist."
fi
# 如果是复制文件获取的绝对路径
elif path=$(echo {} | cliphist decode) && [[ "$path" == /* ]]; then
# 获取路径代表的文件的mimetype
path_mime=$(file -b --mime-type "$path")
# 如果是图片的话
if [[ $path_mime =~ image ]]; then
kitty +kitten icat --transfer-mode=file --clear --image-id=10 \
--place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0" "$path" </dev/tty
# 如果是视频的话
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
echo "ffmpegthumbnailer not installed."
fi
fi
# 如果缩略图文件存在且大小不为零
if [ -s "$thumb_file" ]; then
kitty +kitten icat --transfer-mode=file --image-id=10 \
--place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@${FZF_PREVIEW_LEFT}x${FZF_PREVIEW_TOP}" \
"$thumb_file" </dev/tty
else
echo "Video: $path (No thumbnail)"
fi
else
echo "$path does not exsist."
fi
# 如果是file:///开头的协议路径
elif decoded=$(echo {} | cliphist decode) && [[ "$decoded" == file://* ]]; then
# 获取文件路径
raw_path="${decoded#file://}"
raw_path=$(echo "$raw_path" | python -c "import sys, urllib.parse; print(urllib.parse.unquote(sys.stdin.read().strip()))")
mime_raw_path=$(file -b --mime-type "$raw_path")
# 如果是图片的话
if [[ $mime_raw_path =~ image ]]; then
kitty +kitten icat --transfer-mode=file --clear --image-id=10 \
--place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0" "$raw_path" </dev/tty
# 如果是视频的话
elif [[ $mime_raw_path =~ video ]]; then
video_hash=$(echo "$raw_path" | md5sum | cut -d" " -f1)
raw_thumb_file="$CACHE_DIR/$video_hash.png"
# 如果文件不存在的话
if [ ! -f "$raw_thumb_file" ] ; then
#检测是否安装了ffmpeg缩略图软件
if command -v ffmpegthumbnailer >/dev/null 2>&1; then
ffmpegthumbnailer -i "$raw_path" -o "$raw_thumb_file" -s 480 -t 0 >/dev/null 2>&1
else
echo "ffmpegthumbnailer not installed"
fi
fi
# 如果缩略图文件存在且大小不为零
if [ -s "$raw_thumb_file" ]; then
kitty +kitten icat --transfer-mode=file --image-id=10 \
--place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@${FZF_PREVIEW_LEFT}x${FZF_PREVIEW_TOP}" \
"$raw_thumb_file" </dev/tty
else
echo "Video: $raw_path (No thumbnail)"
fi
fi
# 其他数据
else
kitty +kitten icat --clear --silent --transfer-mode=file > /dev/tty 2>/dev/null
# 直接预览
echo "[$mimeType]"
echo ---
echo {} | cliphist decode
fi
' \
--bind "enter:execute-silent(bash -c 'copy_selection \"\$1\"' -- {})+accept"
# 粘贴已经绑定到了fzf的enter键脚本会自动退出必须写在fzf的按键绑定否则wl-copy会导致浮动终端卡住niri下