mirror of
https://github.com/zhenyan121/dotfiles.git
synced 2026-04-10 14:34:09 +08:00
653 lines
16 KiB
Bash
Executable File
653 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
########################
|
||
# 配置区域(直接改这里)
|
||
########################
|
||
|
||
NIRI_CONFIG="$HOME/.config/niri/config.kdl" # niri 配置文件
|
||
|
||
SHOTEDITOR_DEFAULT="satty" # 默认截图编辑器:swappy 或 satty
|
||
COPY_CMD="wl-copy" # 复制到剪贴板的命令
|
||
|
||
# 菜单程序,按你实际使用的启动器改
|
||
# wofi 示例: MENU_CMD='wofi -d'
|
||
# rofi 示例: MENU_CMD='rofi -dmenu'
|
||
MENU_CMD='fuzzel --dmenu'
|
||
|
||
# 图片目录
|
||
PICTURES_DIR="$(xdg-user-dir PICTURES 2>/dev/null || echo "$HOME/Pictures")"
|
||
SCREEN_DIR="$PICTURES_DIR/Screenshots"
|
||
|
||
########################
|
||
# 本地化(中/英)
|
||
########################
|
||
|
||
LOCALE="${LC_MESSAGES:-${LANG:-en}}"
|
||
if [[ "$LOCALE" == zh* ]]; then
|
||
# 通用
|
||
LABEL_CANCEL="取消"
|
||
LABEL_SETTINGS="设置"
|
||
LABEL_EDIT_YES="编辑"
|
||
LABEL_EDIT_NO="不编辑"
|
||
|
||
# Niri 模式
|
||
LABEL_NIRI_FULL="全屏"
|
||
LABEL_NIRI_WINDOW="窗口"
|
||
LABEL_NIRI_REGION="选取区域"
|
||
|
||
# Grim 模式
|
||
LABEL_GRIM_FULL="全屏"
|
||
LABEL_GRIM_REGION="选取区域"
|
||
|
||
# 设置菜单
|
||
LABEL_SETTINGS_EDITOR="截图工具"
|
||
LABEL_SETTINGS_BACKEND="后端模式"
|
||
LABEL_BACKEND_AUTO="自动(检测 Niri)"
|
||
LABEL_BACKEND_GRIM="仅 Grim+slurp"
|
||
LABEL_BACK="返回"
|
||
|
||
# 编辑开关显示
|
||
LABEL_EDIT_STATE_ON="编辑:开启"
|
||
LABEL_EDIT_STATE_OFF="编辑:关闭"
|
||
|
||
# 提示文字
|
||
PROMPT_MAIN="请选择截图模式"
|
||
PROMPT_SETTINGS="设置 / 更改选项"
|
||
PROMPT_EDITOR="请选择截图编辑工具"
|
||
PROMPT_BACKEND="请选择后端模式"
|
||
else
|
||
LABEL_CANCEL="Cancel"
|
||
LABEL_SETTINGS="Settings"
|
||
LABEL_EDIT_YES="Edit"
|
||
LABEL_EDIT_NO="No edit"
|
||
|
||
LABEL_NIRI_FULL="Fullscreen"
|
||
LABEL_NIRI_WINDOW="Window"
|
||
LABEL_NIRI_REGION="Region"
|
||
|
||
LABEL_GRIM_FULL="Fullscreen"
|
||
LABEL_GRIM_REGION="Select area"
|
||
|
||
LABEL_SETTINGS_EDITOR="Screenshot tool"
|
||
LABEL_SETTINGS_BACKEND="Backend mode"
|
||
LABEL_BACKEND_AUTO="Auto (detect Niri)"
|
||
LABEL_BACKEND_GRIM="Grim+slurp only"
|
||
LABEL_BACK="Back"
|
||
|
||
LABEL_EDIT_STATE_ON="Edit: ON"
|
||
LABEL_EDIT_STATE_OFF="Edit: OFF"
|
||
|
||
PROMPT_MAIN="Choose screenshot mode"
|
||
PROMPT_SETTINGS="Settings / Options"
|
||
PROMPT_EDITOR="Choose screenshot editor"
|
||
PROMPT_BACKEND="Choose backend mode"
|
||
fi
|
||
|
||
########################
|
||
# 持久化配置路径 (已修改为 .cache 目录)
|
||
########################
|
||
|
||
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
|
||
# 旧路径: CONFIG_DIR="$HOME/.config/waybar/waybar-shot"
|
||
CONFIG_DIR="$XDG_CACHE_HOME/waybar-power-screenshot-sh"
|
||
|
||
BACKEND_FILE="$CONFIG_DIR/backend"
|
||
EDITOR_FILE="$CONFIG_DIR/editor"
|
||
EDIT_MODE_FILE="$CONFIG_DIR/edit_mode" # yes / no
|
||
|
||
# 确保新的缓存目录存在
|
||
mkdir -p "$CONFIG_DIR"
|
||
|
||
########################
|
||
# 通用工具函数
|
||
########################
|
||
|
||
menu() {
|
||
printf '%s\n' "$@" | eval "$MENU_CMD" 2>/dev/null || true
|
||
}
|
||
|
||
menu_prompt() {
|
||
local prompt="$1"
|
||
shift
|
||
local esc_prompt="${prompt//\"/\\\"}"
|
||
printf '%s\n' "$@" | eval "$MENU_CMD --prompt \"$esc_prompt\"" 2>/dev/null || true
|
||
}
|
||
|
||
load_backend_mode() {
|
||
local mode
|
||
if [[ -n "${SHOT_BACKEND:-}" ]]; then
|
||
mode="$SHOT_BACKEND"
|
||
elif [[ -f "$BACKEND_FILE" ]]; then
|
||
mode="$(<"$BACKEND_FILE")"
|
||
else
|
||
mode="auto"
|
||
fi
|
||
case "$mode" in
|
||
auto|grim|niri) ;;
|
||
*) mode="auto" ;;
|
||
esac
|
||
printf '%s\n' "$mode"
|
||
}
|
||
|
||
save_backend_mode() {
|
||
local mode="$1"
|
||
# 由于脚本开始时已创建,这里只需要保证文件写入成功
|
||
printf '%s\n' "$mode" >"$BACKEND_FILE"
|
||
}
|
||
|
||
load_editor() {
|
||
local ed
|
||
if [[ -n "${SHOTEDITOR:-}" ]]; then
|
||
ed="$SHOTEDITOR"
|
||
elif [[ -f "$EDITOR_FILE" ]]; then
|
||
ed="$(<"$EDITOR_FILE")"
|
||
else
|
||
ed="$SHOTEDITOR_DEFAULT"
|
||
fi
|
||
|
||
ed="${ed,,}"
|
||
case "$ed" in
|
||
swappy|satty) ;;
|
||
*) ed="$SHOTEDITOR_DEFAULT" ;;
|
||
esac
|
||
printf '%s\n' "$ed"
|
||
}
|
||
|
||
save_editor() {
|
||
local ed="$1"
|
||
# 由于脚本开始时已创建,这里只需要保证文件写入成功
|
||
printf '%s\n' "$ed" >"$EDITOR_FILE"
|
||
}
|
||
|
||
load_edit_mode() {
|
||
local v="yes"
|
||
if [[ -f "$EDIT_MODE_FILE" ]]; then
|
||
v="$(<"$EDIT_MODE_FILE")"
|
||
fi
|
||
case "$v" in
|
||
yes|no) ;;
|
||
*) v="yes" ;; # 默认:编辑开启
|
||
esac
|
||
printf '%s\n' "$v"
|
||
}
|
||
|
||
save_edit_mode() {
|
||
local v="$1"
|
||
# 由于脚本开始时已创建,这里只需要保证文件写入成功
|
||
printf '%s\n' "$v" >"$EDIT_MODE_FILE"
|
||
}
|
||
|
||
detect_backend() {
|
||
case "$BACKEND_MODE" in
|
||
niri) echo "niri" ;;
|
||
grim) echo "grim" ;;
|
||
auto|*)
|
||
if command -v niri >/dev/null 2>&1 && pgrep -x niri >/dev/null 2>&1; then
|
||
echo "niri"
|
||
else
|
||
echo "grim"
|
||
fi
|
||
;;
|
||
esac
|
||
}
|
||
|
||
choose_editor() {
|
||
local choice
|
||
choice="$(menu_prompt "$PROMPT_EDITOR" "swappy" "satty" "$LABEL_BACK")"
|
||
case "$choice" in
|
||
swappy|Swappy)
|
||
SHOTEDITOR="swappy"
|
||
save_editor "$SHOTEDITOR"
|
||
;;
|
||
satty|Satty)
|
||
SHOTEDITOR="satty"
|
||
save_editor "$SHOTEDITOR"
|
||
;;
|
||
*) : ;;
|
||
esac
|
||
}
|
||
|
||
choose_backend_mode() {
|
||
local choice
|
||
choice="$(menu_prompt "$PROMPT_BACKEND" "$LABEL_BACKEND_AUTO" "$LABEL_BACKEND_GRIM" "$LABEL_BACK")"
|
||
case "$choice" in
|
||
"$LABEL_BACKEND_AUTO")
|
||
BACKEND_MODE="auto"
|
||
save_backend_mode "$BACKEND_MODE"
|
||
;;
|
||
"$LABEL_BACKEND_GRIM")
|
||
BACKEND_MODE="grim"
|
||
save_backend_mode "$BACKEND_MODE"
|
||
;;
|
||
*) : ;;
|
||
esac
|
||
}
|
||
|
||
settings_menu() {
|
||
while :; do
|
||
local backend_desc editor_line backend_line choice
|
||
|
||
if [[ -n "${SHOT_BACKEND:-}" ]]; then
|
||
backend_desc="${BACKEND_MODE} (env)"
|
||
else
|
||
if [[ "$BACKEND_MODE" == "grim" ]]; then
|
||
backend_desc="$LABEL_BACKEND_GRIM"
|
||
elif [[ "$BACKEND_MODE" == "niri" ]]; then
|
||
backend_desc="niri"
|
||
else
|
||
backend_desc="$LABEL_BACKEND_AUTO"
|
||
fi
|
||
fi
|
||
|
||
editor_line="$LABEL_SETTINGS_EDITOR: $SHOTEDITOR"
|
||
backend_line="$LABEL_SETTINGS_BACKEND: $backend_desc"
|
||
|
||
choice="$(menu_prompt "$PROMPT_SETTINGS" "$editor_line" "$backend_line" "$LABEL_BACK")"
|
||
case "$choice" in
|
||
"$editor_line") choose_editor ;;
|
||
"$backend_line")
|
||
if [[ -n "${SHOT_BACKEND:-}" ]]; then
|
||
: # 环境变量强制时不改持久化
|
||
else
|
||
choose_backend_mode
|
||
fi
|
||
;;
|
||
*) return ;; # 返回上一层
|
||
esac
|
||
done
|
||
}
|
||
|
||
latest_in_dir() {
|
||
local dir="$1"
|
||
find "$dir" -maxdepth 1 -type f -printf '%T@ %p\n' 2>/dev/null \
|
||
| sort -n | tail -1 | cut -d' ' -f2-
|
||
}
|
||
|
||
########################
|
||
# 剪贴板相关(Niri 编辑用)
|
||
########################
|
||
|
||
clip_hash() {
|
||
wl-paste -t image/png 2>/dev/null \
|
||
| sha1sum 2>/dev/null \
|
||
| cut -d' ' -f1 2>/dev/null \
|
||
|| echo ""
|
||
}
|
||
|
||
wait_clipboard_change() {
|
||
local old new i
|
||
old="$(clip_hash)"
|
||
for i in {1..200}; do # 最多等 ~10 秒
|
||
new="$(clip_hash)"
|
||
if [[ -n "$new" && "$new" != "$old" ]]; then
|
||
return 0
|
||
fi
|
||
sleep 0.05
|
||
done
|
||
return 1
|
||
}
|
||
|
||
########################
|
||
# Niri 相关
|
||
########################
|
||
|
||
get_niri_shot_dir() {
|
||
[[ -f "$NIRI_CONFIG" ]] || { echo "Config not found: $NIRI_CONFIG" >&2; return 1; }
|
||
|
||
local line tpl dir
|
||
line="$(
|
||
grep -E '^[[:space:]]*screenshot-path[[:space:]]' "$NIRI_CONFIG" \
|
||
| grep -v '^[[:space:]]*//' \
|
||
| tail -n 1 || true
|
||
)"
|
||
[[ -n "$line" ]] || { echo "No screenshot-path in config" >&2; return 1; }
|
||
|
||
tpl="$(sed -E 's/.*screenshot-path[[:space:]]+"([^"]+)".*/\1/' <<<"$line")"
|
||
[[ -n "$tpl" ]] || { echo "Failed to parse screenshot-path: $line" >&2; return 1; }
|
||
|
||
tpl="${tpl/#\~/$HOME}"
|
||
dir="${tpl%/*}"
|
||
|
||
printf '%s\n' "$dir"
|
||
}
|
||
|
||
# Grim 用:从文件编辑
|
||
edit_file_image() {
|
||
local src="$1"
|
||
local backend="$2" # "niri" 或 "grim"
|
||
|
||
local dir ts dst
|
||
|
||
if [[ "$backend" == "niri" ]]; then
|
||
dir="$NIRI_EDIT_DIR"
|
||
else
|
||
dir="$SCREEN_DIR"
|
||
fi
|
||
|
||
mkdir -p "$dir"
|
||
ts="$(date +'%Y-%m-%d_%H-%M-%S')"
|
||
dst="$dir/$SHOTEDITOR-$ts.png"
|
||
|
||
case "$SHOTEDITOR" in
|
||
satty)
|
||
satty --filename "$src" --output-filename "$dst"
|
||
;;
|
||
swappy)
|
||
swappy -f "$src" -o "$dst"
|
||
;;
|
||
*)
|
||
echo "Unknown SHOTEDITOR: $SHOTEDITOR (use satty or swappy)" >&2
|
||
return 0
|
||
;;
|
||
esac
|
||
|
||
if [[ -f "$dst" ]]; then
|
||
if [[ "$backend" == "grim" && "$src" == /tmp/* ]]; then
|
||
rm -f "$src"
|
||
fi
|
||
"$COPY_CMD" < "$dst"
|
||
fi
|
||
}
|
||
|
||
# Niri 用:从剪贴板编辑
|
||
edit_from_clipboard() {
|
||
local backend="$1" # 目前只会传 "niri"
|
||
|
||
local dir ts dst
|
||
if [[ "$backend" == "niri" ]]; then
|
||
dir="$NIRI_EDIT_DIR"
|
||
else
|
||
dir="$SCREEN_DIR"
|
||
fi
|
||
|
||
mkdir -p "$dir"
|
||
ts="$(date +'%Y-%m-%d_%H-%M-%S')"
|
||
dst="$dir/$SHOTEDITOR-$ts.png"
|
||
|
||
case "$SHOTEDITOR" in
|
||
satty)
|
||
wl-paste -t image/png 2>/dev/null | satty -f - --output-filename "$dst"
|
||
;;
|
||
swappy)
|
||
wl-paste -t image/png 2>/dev/null | swappy -f - -o "$dst"
|
||
;;
|
||
*)
|
||
echo "Unknown SHOTEDITOR: $SHOTEDITOR (use satty or swappy)" >&2
|
||
return 0
|
||
;;
|
||
esac
|
||
|
||
if [[ -f "$dst" ]]; then
|
||
"$COPY_CMD" < "$dst"
|
||
fi
|
||
}
|
||
|
||
niri_capture_and_maybe_edit() {
|
||
local mode="$1" # fullscreen / window / region
|
||
local need_edit="$2" # yes / no
|
||
|
||
local action
|
||
case "$mode" in
|
||
fullscreen) action="screenshot-screen" ;;
|
||
window) action="screenshot-window" ;;
|
||
region) action="screenshot" ;;
|
||
*) return 0 ;;
|
||
esac
|
||
|
||
# 不编辑:用目录里的最新文件判断 screenshot 完成
|
||
if [[ "$need_edit" != "yes" ]]; then
|
||
local before shot
|
||
before="$(latest_in_dir "$NIRI_SHOT_DIR" || true)"
|
||
|
||
niri msg action "$action"
|
||
|
||
while :; do
|
||
shot="$(latest_in_dir "$NIRI_SHOT_DIR" || true)"
|
||
if [[ -z "$before" && -n "$shot" ]] || \
|
||
[[ -n "$before" && -n "$shot" && "$shot" != "$before" ]]; then
|
||
break
|
||
fi
|
||
sleep 0.05
|
||
done
|
||
return 0
|
||
fi
|
||
|
||
# 编辑:基于剪贴板
|
||
niri msg action "$action"
|
||
|
||
if ! wait_clipboard_change; then
|
||
echo "等待剪贴板中的截图超时" >&2
|
||
return 0
|
||
fi
|
||
|
||
edit_from_clipboard "niri"
|
||
return 0
|
||
}
|
||
|
||
run_niri_flow() {
|
||
NIRI_SHOT_DIR="$(get_niri_shot_dir)" || return 0
|
||
NIRI_EDIT_DIR="$NIRI_SHOT_DIR/Edited"
|
||
mkdir -p "$NIRI_SHOT_DIR" "$NIRI_EDIT_DIR"
|
||
|
||
while :; do
|
||
local choice mode edit_mode edit_label
|
||
|
||
edit_mode="$(load_edit_mode)"
|
||
if [[ "$edit_mode" == "yes" ]]; then
|
||
edit_label="$LABEL_EDIT_STATE_ON"
|
||
else
|
||
edit_label="$LABEL_EDIT_STATE_OFF"
|
||
fi
|
||
|
||
choice="$(menu_prompt "$PROMPT_MAIN" \
|
||
"$LABEL_NIRI_FULL" \
|
||
"$LABEL_NIRI_WINDOW" \
|
||
"$LABEL_NIRI_REGION" \
|
||
"$edit_label" \
|
||
"$LABEL_SETTINGS" \
|
||
"$LABEL_CANCEL"
|
||
)"
|
||
|
||
[[ -z "$choice" || "$choice" == "$LABEL_CANCEL" ]] && return 2
|
||
|
||
case "$choice" in
|
||
"$LABEL_NIRI_FULL") mode="fullscreen" ;;
|
||
"$LABEL_NIRI_WINDOW") mode="window" ;;
|
||
"$LABEL_NIRI_REGION") mode="region" ;;
|
||
"$edit_label")
|
||
if [[ "$edit_mode" == "yes" ]]; then
|
||
save_edit_mode "no"
|
||
else
|
||
save_edit_mode "yes"
|
||
fi
|
||
continue
|
||
;;
|
||
"$LABEL_SETTINGS")
|
||
# 进入设置菜单(可以在里面改 editor / backend / edit-mode)
|
||
settings_menu
|
||
|
||
# 立即重新加载持久化配置,使修改即时生效
|
||
SHOTEDITOR="$(load_editor)"
|
||
BACKEND_MODE="$(load_backend_mode)"
|
||
|
||
# 检查后端是否被改动;若改动则通知上层切换后端
|
||
NEW_BACKEND="$(detect_backend)"
|
||
if [[ "$NEW_BACKEND" != "niri" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
continue
|
||
;;
|
||
*)
|
||
return 2
|
||
;;
|
||
esac
|
||
|
||
edit_mode="$(load_edit_mode)"
|
||
if [[ "$edit_mode" == "yes" ]]; then
|
||
niri_capture_and_maybe_edit "$mode" "yes"
|
||
else
|
||
niri_capture_and_maybe_edit "$mode" "no"
|
||
fi
|
||
|
||
# 截图完成后退出(主循环会根据返回码决定是否结束脚本)
|
||
return 0
|
||
done
|
||
}
|
||
|
||
########################
|
||
# Grim + slurp 相关
|
||
########################
|
||
|
||
grim_capture_and_maybe_edit() {
|
||
local mode="$1" # fullscreen / region
|
||
local need_edit="$2" # yes / no
|
||
|
||
mkdir -p "$SCREEN_DIR"
|
||
|
||
local ts shot geo
|
||
ts="$(date +'%Y-%m-%d_%H-%M-%S')"
|
||
|
||
if [[ "$need_edit" == "yes" ]]; then
|
||
# 编辑模式:原图在 /tmp,用完删,只保留编辑后的图
|
||
shot="/tmp/waybar-shot-$ts.png"
|
||
|
||
case "$mode" in
|
||
fullscreen)
|
||
grim "$shot"
|
||
;;
|
||
region)
|
||
geo="$(slurp 2>/dev/null)" || return 0
|
||
grim -g "$geo" "$shot"
|
||
;;
|
||
*)
|
||
return 0 ;;
|
||
esac
|
||
|
||
edit_file_image "$shot" "grim"
|
||
return 0
|
||
else
|
||
# 不编辑:原图保存到 Screenshots
|
||
shot="$SCREEN_DIR/Screenshot_$ts.png"
|
||
|
||
case "$mode" in
|
||
fullscreen)
|
||
grim "$shot"
|
||
;;
|
||
region)
|
||
geo="$(slurp 2>/dev/null)" || return 0
|
||
grim -g "$geo" "$shot"
|
||
;;
|
||
*)
|
||
return 0 ;;
|
||
esac
|
||
return 0
|
||
fi
|
||
}
|
||
|
||
run_grim_flow() {
|
||
mkdir -p "$SCREEN_DIR"
|
||
|
||
while :; do
|
||
local choice mode edit_mode edit_label
|
||
|
||
edit_mode="$(load_edit_mode)"
|
||
if [[ "$edit_mode" == "yes" ]]; then
|
||
edit_label="$LABEL_EDIT_STATE_ON"
|
||
else
|
||
edit_label="$LABEL_EDIT_STATE_OFF"
|
||
fi
|
||
|
||
choice="$(menu_prompt "$PROMPT_MAIN" \
|
||
"$LABEL_GRIM_FULL" \
|
||
"$LABEL_GRIM_REGION" \
|
||
"$edit_label" \
|
||
"$LABEL_SETTINGS" \
|
||
"$LABEL_CANCEL"
|
||
)"
|
||
|
||
[[ -z "$choice" || "$choice" == "$LABEL_CANCEL" ]] && return 2
|
||
|
||
case "$choice" in
|
||
"$LABEL_GRIM_FULL") mode="fullscreen" ;;
|
||
"$LABEL_GRIM_REGION") mode="region" ;;
|
||
"$edit_label")
|
||
if [[ "$edit_mode" == "yes" ]]; then
|
||
save_edit_mode "no"
|
||
else
|
||
save_edit_mode "yes"
|
||
fi
|
||
continue
|
||
;;
|
||
"$LABEL_SETTINGS")
|
||
settings_menu
|
||
|
||
# 立即重新加载持久化配置,使修改即时生效
|
||
SHOTEDITOR="$(load_editor)"
|
||
BACKEND_MODE="$(load_backend_mode)"
|
||
|
||
NEW_BACKEND="$(detect_backend)"
|
||
if [[ "$NEW_BACKEND" != "grim" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
continue
|
||
;;
|
||
*)
|
||
return 2
|
||
;;
|
||
esac
|
||
|
||
edit_mode="$(load_edit_mode)"
|
||
if [[ "$edit_mode" == "yes" ]]; then
|
||
grim_capture_and_maybe_edit "$mode" "yes"
|
||
else
|
||
grim_capture_and_maybe_edit "$mode" "no"
|
||
fi
|
||
|
||
return 0
|
||
done
|
||
}
|
||
|
||
########################
|
||
# 入口(主循环)
|
||
########################
|
||
|
||
while :; do
|
||
BACKEND_MODE="$(load_backend_mode)"
|
||
SHOTEDITOR="$(load_editor)"
|
||
|
||
BACKEND="$(detect_backend)"
|
||
|
||
case "$BACKEND" in
|
||
niri)
|
||
rc=0
|
||
run_niri_flow || rc=$?
|
||
if [[ "$rc" -eq 0 ]]; then
|
||
exit 0
|
||
elif [[ "$rc" -eq 1 ]]; then
|
||
# 后端切换:继续主循环以根据新后端重试
|
||
continue
|
||
else
|
||
# 取消或其他:退出
|
||
exit 0
|
||
fi
|
||
;;
|
||
grim)
|
||
rc=0
|
||
run_grim_flow || rc=$?
|
||
if [[ "$rc" -eq 0 ]]; then
|
||
exit 0
|
||
elif [[ "$rc" -eq 1 ]]; then
|
||
continue
|
||
else
|
||
exit 0
|
||
fi
|
||
;;
|
||
*)
|
||
run_grim_flow
|
||
exit 0
|
||
;;
|
||
esac
|
||
done |