feat: 上传我的配置

This commit is contained in:
2026-02-06 17:17:48 +08:00
parent 5ae578a012
commit ac517ad717
126 changed files with 15159 additions and 0 deletions

View File

@@ -0,0 +1,791 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# ================== Runtime state & Persistent Config ==================
APP="wf-recorder"
# --- 运行时状态 (应在每次会话结束时消失, 遵循 XDG_RUNTIME_DIR) ---
RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$UID}"
STATE_DIR="$RUNTIME_DIR/wfrec"
PIDFILE="$STATE_DIR/pid"
STARTFILE="$STATE_DIR/start"
SAVEPATH_FILE="$STATE_DIR/save_path"
MODEFILE="$STATE_DIR/mode" # full/region -> tooltip
GIF_MARKER="$STATE_DIR/is_gif" # [NEW] 标记当前录制是否为 GIF 模式
TICKPIDFILE="$STATE_DIR/tickpid"
WAYBAR_PIDS_CACHE="$STATE_DIR/waybar.pids"
# --- 持久性配置 (缓存/设置, 存放在 .cache/ 下, 遵循 XDG_CACHE_HOME) ---
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
CONFIG_DIR="$XDG_CACHE_HOME/wf-recorder-sh" # 目标目录 .cache/wf-recorder-sh
CFG_CODEC="$CONFIG_DIR/codec"
CFG_FPS="$CONFIG_DIR/framerate"
CFG_AUDIO="$CONFIG_DIR/audio"
CFG_DRM="$CONFIG_DIR/drm_device"
CFG_EXT="$CONFIG_DIR/container_ext" # persisted file format (auto/mp4/mkv/webm)
# 创建所需的目录
mkdir -p "$STATE_DIR"
mkdir -p "$CONFIG_DIR"
# Hold chosen mode
MODE_DECIDED=""
IS_GIF_MODE="false" # [NEW] 临时变量
# ================== Tunables (ENV overridable) ==================
# defaults — 默认使用 CPU 编码 (libx264)
_DEFAULT_CODEC="libx264"
_DEFAULT_FRAMERATE=""
_DEFAULT_AUDIO="on"
_DEFAULT_SAVE_EXT="auto" # auto/mp4/mkv/webm
# --- [NEW] GIF 配置区域 ---
GIF_WIDTH=720
GIF_FPS=30
GIF_DITHER_MODE="bayer:bayer_scale=5"
GIF_STATS_MODE="diff"
# -------------------------
# load persisted settings if exist
codec_from_file=$(cat "$CFG_CODEC" 2>/dev/null || true)
fps_from_file=$(cat "$CFG_FPS" 2>/dev/null || true)
audio_from_file=$(cat "$CFG_AUDIO" 2>/dev/null || true)
drm_from_file=$(cat "$CFG_DRM" 2>/dev/null || true)
ext_from_file=$(cat "$CFG_EXT" 2>/dev/null || true)
# priority: ENV > persisted > default
CODEC="${CODEC:-${codec_from_file:-$_DEFAULT_CODEC}}"
FRAMERATE="${FRAMERATE:-${fps_from_file:-$_DEFAULT_FRAMERATE}}"
AUDIO="${AUDIO:-${audio_from_file:-$_DEFAULT_AUDIO}}"
DRM_DEVICE="${DRM_DEVICE:-${drm_from_file:-}}"
SAVE_EXT="${SAVE_EXT:-${ext_from_file:-$_DEFAULT_SAVE_EXT}}"
TITLE="${TITLE:-}"
SAVE_DIR_ENV="${SAVE_DIR:-}"
SAVE_SUBDIR_FS="${SAVE_SUBDIR_FS:-fullscreen}"
OUTPUT="${OUTPUT:-}" # e.g. eDP-1 / DP-2
OUTPUT_SELECT="${OUTPUT_SELECT:-auto}" # off|auto|menu
MENU_TITLE_OUTPUT="${MENU_TITLE_OUTPUT:-}"
MENU_BACKEND="${MENU_BACKEND:-auto}" # auto|fuzzel|wofi|rofi|bemenu|fzf|term
RECORD_MODE="${RECORD_MODE:-ask}" # ask|full|region
MODE_MENU_TITLE="${MODE_MENU_TITLE:-Select recording mode}"
REC_AREA="${REC_AREA:-}" # "x,y WIDTHxHEIGHT" (optional)
GEOM_IN_NAME="${GEOM_IN_NAME:-off}"
WAYBAR_POKE="${WAYBAR_POKE:-on}"
WAYBAR_SIG="${WAYBAR_SIG:-9}"
ICON_REC="${ICON_REC:-}"
ICON_IDLE="${ICON_IDLE:-}"
PKILL_AFTER_STOP="${PKILL_AFTER_STOP:-on}"
# DEBUG: 若设为 on则在前台运行 wf-recorder并把输出直接显示到终端仅终端不写文件
DEBUG="${DEBUG:-off}"
# ================== Utils ==================
has() { command -v "$1" >/dev/null 2>&1; }
lang_code() {
local l="${LC_MESSAGES:-${LANG:-en}}"
l="${l,,}"; l="${l%%.*}"; l="${l%%-*}"; l="${l%%_*}"
case "$l" in zh|zh-cn|zh-tw|zh-hk) echo zh ;; ja|jp) echo ja ;; *) echo en ;; esac
}
msg() {
local id="$1"; shift
case "$(lang_code)" in
zh)
case "$id" in
err_wf_not_found) printf "未找到 wf-recorder" ;;
err_need_slurp) printf "需要 slurp 以进行区域选择" ;;
err_need_ffmpeg) printf "GIF 转换需要 ffmpeg但未找到。" ;;
warn_drm_ignored) printf "警告DRM_DEVICE=%s 不存在或不可读,将忽略。" "$@" ;;
warn_invalid_fps) printf "警告FRAMERATE=\"%s\" 非法,已忽略。" "$@" ;;
warn_render_unreadable) printf "警告:无效的 render 节点:%s" "$@" ;;
cancel_no_mode) printf "已取消:未选择录制模式。" ;;
cancel_no_output) printf "已取消:未选择输出。" ;;
cancel_no_region) printf "已取消:未选择区域。" ;;
warn_multi_outputs_cancel) printf "检测到多个输出但未选择,已取消。" ;;
notif_started_full) printf "开始录制(全屏:%s→ %s" "$@" ;;
notif_started_region) printf "开始录制(区域)→ %s" "$@" ;;
notif_device_suffix) printf "(设备 %s" "$@" ;;
notif_saved) printf "已保存:%s" "$@" ;;
notif_stopped) printf "已停止录制。" ;;
notif_processing_gif) printf "正在转换为 GIF请稍候..." ;;
notif_gif_failed) printf "GIF 转换失败,保留原视频。" ;;
notif_copied) printf "文件已复制" ;;
already_running) printf "already running" ;;
not_running) printf "not running" ;;
title_mode) printf "选择录制模式" ;;
title_output) printf "选择输出" ;;
menu_fullscreen) printf "全屏" ;;
menu_region) printf "选择区域" ;;
menu_gif_region) printf "录制 GIF (区域)" ;;
# settings labels -> "标签:值"
title_settings) printf "设置..." ;;
menu_settings) printf "设置..." ;;
menu_set_codec) printf "编码格式:%s" "$@" ;;
menu_set_fps) printf "帧率:%s" "$@" ;;
menu_set_filefmt) printf "文件格式:%s" "$@" ;;
menu_toggle_audio) printf "音频:%s" "$@" ;;
menu_set_render) printf "渲染设备:%s" "$@" ;;
menu_back) printf "返回" ;;
fps_unlimited) printf "不限制" ;;
render_auto) printf "自动" ;;
ext_auto) printf "自动" ;;
title_select_codec) printf "选择编码格式" ;;
title_select_fps) printf "选择帧率" ;;
title_select_filefmt) printf "选择文件格式" ;;
title_select_render) printf "选择渲染设备(/dev/dri/renderD*" ;;
mode_full) printf "全屏" ;;
mode_region) printf "区域" ;;
prompt_enter_number) printf "输入编号:" ;;
menu_exit) printf "退出" ;;
*) printf "%s" "$id" ;;
esac
;;
ja)
case "$id" in
err_wf_not_found) printf "wf-recorder が見つかりません" ;;
err_need_slurp) printf "領域選択には slurp が必要です" ;;
err_need_ffmpeg) printf "GIF変換には ffmpeg が必要ですが、見つかりません。" ;;
warn_drm_ignored) printf "警告DRM_DEVICE=%s は無視されます。" "$@" ;;
warn_invalid_fps) printf "警告FRAMERATE=\"%s\" は不正です。" "$@" ;;
warn_render_unreadable) printf "警告:無効なレンダー ノード:%s" "$@" ;;
cancel_no_mode) printf "キャンセル:録画モード未選択。" ;;
cancel_no_output) printf "キャンセル:出力未選択。" ;;
cancel_no_region) printf "キャンセル:領域未選択。" ;;
warn_multi_outputs_cancel) printf "出力が複数ですが未選択のため中止。" ;;
notif_started_full) printf "録画開始(全画面:%s→ %s" "$@" ;;
notif_started_region) printf "録画開始(領域)→ %s" "$@" ;;
notif_device_suffix) printf "(デバイス %s" "$@" ;;
notif_saved) printf "保存しました:%s" "$@" ;;
notif_stopped) printf "録画を停止しました。" ;;
notif_processing_gif) printf "GIF に変換中、お待ちください..." ;;
notif_gif_failed) printf "GIF 変換に失敗しました。元の動画を保持します。" ;;
notif_copied) printf "ファイルをコピーしました" ;;
already_running) printf "already running" ;;
not_running) printf "not running" ;;
title_mode) printf "録画モードを選択" ;;
title_output) printf "出力を選択" ;;
menu_fullscreen) printf "全画面" ;;
menu_region) printf "領域選択" ;;
menu_gif_region) printf "GIF録画 (領域)" ;;
# settings labels -> "ラベル:値"(全角コロン)
title_settings) printf "設定..." ;;
menu_settings) printf "設定..." ;;
menu_set_codec) printf "コーデック:%s" "$@" ;;
menu_set_fps) printf "フレームレート:%s" "$@" ;;
menu_set_filefmt) printf "ファイル形式:%s" "$@" ;;
menu_toggle_audio) printf "音声:%s" "$@" ;;
menu_set_render) printf "レンダーデバイス:%s" "$@" ;;
menu_back) printf "戻る" ;;
fps_unlimited) printf "無制限" ;;
render_auto) printf "自動" ;;
ext_auto) printf "自動" ;;
title_select_codec) printf "コーデックを選択" ;;
title_select_fps) printf "フレームレートを選択" ;;
title_select_filefmt) printf "ファイル形式を選択" ;;
title_select_render) printf "レンダーデバイスを選択(/dev/dri/renderD*" ;;
mode_full) printf "全画面" ;;
mode_region) printf "領域" ;;
prompt_enter_number) printf "番号を入力:" ;;
menu_exit) printf "終了" ;;
*) printf "%s" "$id" ;;
esac
;;
*)
case "$id" in
err_wf_not_found) printf "wf-recorder not found" ;;
err_need_slurp) printf "slurp required for region selection" ;;
err_need_ffmpeg) printf "ffmpeg is required for GIF conversion but not found." ;;
warn_drm_ignored) printf "Warning: DRM_DEVICE=%s ignored." "$@" ;;
warn_invalid_fps) printf "Warning: invalid FRAMERATE=\"%s\"." "$@" ;;
warn_render_unreadable) printf "Warning: invalid render node: %s" "$@" ;;
cancel_no_mode) printf "Canceled: no recording mode selected." ;;
cancel_no_output) printf "Canceled: no output selected." ;;
cancel_no_region) printf "Canceled: no region selected." ;;
warn_multi_outputs_cancel) printf "Multiple outputs but none selected; canceled." ;;
notif_started_full) printf "Recording started (fullscreen: %s) → %s" "$@" ;;
notif_started_region) printf "Recording started (region) → %s" "$@" ;;
notif_device_suffix) printf " (device %s)" "$@" ;;
notif_saved) printf "Saved: %s" "$@" ;;
notif_stopped) printf "Recording stopped." ;;
notif_processing_gif) printf "Converting to GIF, please wait..." ;;
notif_gif_failed) printf "GIF conversion failed. Original video kept." ;;
notif_copied) printf "File copied" ;;
already_running) printf "already running" ;;
not_running) printf "not running" ;;
title_mode) printf "Select recording mode" ;;
title_output) printf "Select output" ;;
menu_fullscreen) printf "Fullscreen" ;;
menu_region) printf "Region" ;;
menu_gif_region) printf "Record GIF (Region)" ;;
# settings labels -> "Label: Value"
title_settings) printf "Settings..." ;;
menu_settings) printf "Settings..." ;;
menu_set_codec) printf "Codec: %s" "$@" ;;
menu_set_fps) printf "Framerate: %s" "$@" ;;
menu_set_filefmt) printf "File Format: %s" "$@" ;;
menu_toggle_audio) printf "Audio: %s" "$@" ;;
menu_set_render) printf "Render Device: %s" "$@" ;;
menu_back) printf "Back" ;;
fps_unlimited) printf "unlimited" ;;
render_auto) printf "Auto" ;;
ext_auto) printf "Auto" ;;
title_select_codec) printf "Select Codec" ;;
title_select_fps) printf "Select Framerate" ;;
title_select_filefmt) printf "Select File Format" ;;
title_select_render) printf "Select Render Device (/dev/dri/renderD*)" ;;
mode_full) printf "Fullscreen" ;;
mode_region) printf "Region" ;;
prompt_enter_number) printf "Enter number: " ;;
menu_exit) printf "Exit" ;;
*) printf "%s" "$id" ;;
esac
;;
esac
}
is_running() {
[[ -r "$PIDFILE" ]] || return 1
local pid; read -r pid <"$PIDFILE" 2>/dev/null || return 1
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
}
notify() { has notify-send && notify-send "wf-recorder" "$1" || true; }
signal_waybar() {
local pids
if [[ -r "$WAYBAR_PIDS_CACHE" ]]; then
pids="$(tr '\n' ' ' <"$WAYBAR_PIDS_CACHE")"
if [[ -n "$pids" ]]; then kill -RTMIN+"$WAYBAR_SIG" $pids 2>/dev/null && return 0; fi
fi
pids="$(pgrep -x -u "$UID" waybar 2>/dev/null | tr '\n' ' ')"
[[ -n "$pids" ]] && printf '%s\n' $pids >"$WAYBAR_PIDS_CACHE"
[[ -n "$pids" ]] && kill -RTMIN+"$WAYBAR_SIG" $pids 2>/dev/null || true
}
emit_waybar_signal() { [[ "${WAYBAR_POKE,,}" == "off" ]] && return 0; signal_waybar; }
start_tick() {
if [[ -f "$TICKPIDFILE" ]]; then
local tpid; read -r tpid <"$TICKPIDFILE" 2>/dev/null || true
[[ -n "$tpid" ]] && kill -TERM "$tpid" 2>/dev/null || true
rm -f "$TICKPIDFILE"
fi
(
while :; do
[[ -r "$PIDFILE" ]] || break
local p; read -r p <"$PIDFILE" 2>/dev/null || p=""
[[ -n "$p" ]] && kill -0 "$p" 2>/dev/null || break
signal_waybar
sleep 1
done
) & echo $! >"$TICKPIDFILE"
}
stop_tick() {
if [[ -f "$TICKPIDFILE" ]]; then
local tpid; read -r tpid <"$TICKPIDFILE" 2>/dev/null || true
[[ -n "$tpid" ]] && kill -TERM "$tpid" 2>/dev/null || true
rm -f "$TICKPIDFILE"
fi
}
get_save_dir() {
local videos
if has xdg-user-dir; then videos="$(xdg-user-dir VIDEOS 2>/dev/null || true)"; fi
videos="${videos:-"$HOME/Videos"}"
echo "${SAVE_DIR_ENV:-"$videos/wf-recorder"}"
}
# --- render device helpers ---
list_render_nodes() {
local d
for d in /dev/dri/renderD*; do
[[ -r "$d" ]] && printf '%s\n' "$d"
done 2>/dev/null || true
}
render_display() {
local cur="${1:-}"
if [[ -z "$cur" ]]; then
msg render_auto
else
printf "%s" "$cur"
fi
}
pick_render_device() {
local dev="${DRM_DEVICE:-}"
if [[ -n "$dev" && ! -r "$dev" ]]; then
printf '%s\n' "$(msg warn_render_unreadable "$dev")" >&2
dev=""
fi
echo -n "$dev"
}
# --- file format helpers ---
ext_for_codec(){ case "${1,,}" in
*h264*|*hevc*) echo mp4 ;;
*vp9*) echo webm ;;
*av1*) echo mkv ;;
*) echo mp4 ;;
esac; }
choose_ext(){
local e="${SAVE_EXT,,}"
if [[ -z "$e" || "$e" == "auto" ]]; then
ext_for_codec "$CODEC"
else
case "$e" in mp4|mkv|webm) echo "$e" ;; *) echo mp4 ;; esac
fi
}
# ================== Menus ==================
__norm() { printf '%s' "$1" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'; }
_pick_menu_backend() {
local pref="${MENU_BACKEND,,}"
case "$pref" in fuzzel|wofi|rofi|bemenu|fzf|term) : ;; auto|"") pref="auto" ;; *) pref="auto" ;; esac
if [[ "$pref" != "auto" ]]; then
if has "$pref"; then echo "$pref"; else [[ -t 0 ]] && echo "term" || echo "none"; fi
return
fi
for b in fuzzel wofi rofi bemenu fzf; do has "$b" && { echo "$b"; return; }; done
[[ -t 0 ]] && echo "term" || echo "none"
}
menu_pick() { # $1:title; items...
local title="${1:-Select}"; shift
local items=("$@")
((${#items[@]})) || return 130
local backend; backend="$(_pick_menu_backend)"
local sel rc=130
case "$backend" in
fuzzel) set +e; sel="$(printf '%s\n' "${items[@]}" | fuzzel --dmenu -p "$title")"; rc=$?; set -e ;;
wofi) set +e; sel="$(printf '%s\n' "${items[@]}" | wofi --dmenu --prompt "$title")"; rc=$?; set -e ;;
rofi) set +e; sel="$(printf '%s\n' "${items[@]}" | rofi -dmenu -p "$title")"; rc=$?; set -e ;;
bemenu) set +e; sel="$(printf '%s\n' "${items[@]}" | bemenu -p "$title")"; rc=$?; set -e ;;
fzf) set +e; sel="$(printf '%s\n' "${items[@]}" | fzf --prompt "$title> ")"; rc=$?; set -e ;;
term)
echo "$title"
local i=1; for it in "${items[@]}"; do printf ' %d) %s\n' "$i" "$it"; ((i++)); done
printf "%s" "$(msg prompt_enter_number)"
local idx; set +e; read -r idx; rc=$?; set -e
if [[ $rc -eq 0 && -n "$idx" && "$idx" =~ ^[0-9]+$ ]]; then
if (( idx>=1 && idx<=${#items[@]} )); then sel="${items[$((idx-1))]}"; rc=0; fi
fi
;;
none) return 130 ;;
esac
[[ $rc -ne 0 || -z "${sel:-}" ]] && return 130
printf '%s' "$(__norm "$sel")"
}
# ---------- Outputs ----------
list_outputs() {
local raw
if raw="$(wf-recorder -L 2>/dev/null)"; then :; elif has wlr-randr; then raw="$(wlr-randr 2>/dev/null | awk '/^[^ ]/{print $1}')"; else raw=""; fi
awk 'BEGIN{RS="[ \t\r\n,]+"} /^[A-Za-z0-9_.:-]+$/ { if ($0 ~ /^(e?DP|HDMI|DVI|VGA|LVDS|Virtual|XWAYLAND)/) seen[$0]=1 } END{for(k in seen) print k}' <<<"$raw" | sort -u
}
decide_output() {
if [[ -n "$OUTPUT" ]]; then printf '%s' "$OUTPUT"; return 0; fi
local -a outs; mapfile -t outs < <(list_outputs || true)
local out_title; out_title="${MENU_TITLE_OUTPUT:-$(msg title_output)}"
if [[ "${OUTPUT_SELECT}" == "menu" ]] || { [[ "${OUTPUT_SELECT}" == "auto" ]] && ((${#outs[@]} > 1)); }; then
local pick; pick="$(menu_pick "$out_title" "${outs[@]}")" || return 130
printf '%s' "$pick"; return 0
fi
if ((${#outs[@]} == 1)); then printf '%s' "${outs[0]}"; else printf '%s\n' "$(msg warn_multi_outputs_cancel)" >&2; return 130; fi
}
# ---------- Settings ----------
choose_render_menu() {
local -a nodes
mapfile -t nodes < <(list_render_nodes | sort -V || true)
local auto_item; auto_item="$(msg render_auto)"
local pick
if ! pick="$(menu_pick "$(msg title_select_render)" "$auto_item" "${nodes[@]}")"; then
return 0
fi
if [[ "$pick" == "$auto_item" ]]; then
DRM_DEVICE=""
rm -f "$CFG_DRM"
return 0
fi
local sel="$pick"
if [[ -n "$sel" && -r "$sel" ]]; then
DRM_DEVICE="$sel"
printf '%s' "$DRM_DEVICE" >"$CFG_DRM"
else
printf '%s\n' "$(msg warn_render_unreadable "$sel")" >&2
fi
}
choose_filefmt_menu() {
local auto_item; auto_item="$(msg ext_auto)"
local pick
if ! pick="$(menu_pick "$(msg title_select_filefmt)" "$auto_item" "mp4" "mkv" "webm")"; then
return 0
fi
if [[ "$pick" == "$auto_item" ]]; then
SAVE_EXT="auto"
rm -f "$CFG_EXT"
else
case "$pick" in
mp4|mkv|webm) SAVE_EXT="$pick"; printf '%s' "$SAVE_EXT" >"$CFG_EXT" ;;
*) : ;;
esac
fi
}
show_settings_menu() {
while :; do
local fps_display="${FRAMERATE:-$(msg fps_unlimited)}"
local audio_display="${AUDIO}"
local render_display_now; render_display_now="$(render_display "$DRM_DEVICE")"
local ff_display; if [[ -z "$SAVE_EXT" || "${SAVE_EXT,,}" == "auto" ]]; then ff_display="$(msg ext_auto)"; else ff_display="$SAVE_EXT"; fi
# ORDER: Framerate → Audio → Codec → File Format → Render → Back
local pick; pick="$(menu_pick "$(msg title_settings)" \
"$(msg menu_set_fps "$fps_display")" \
"$(msg menu_toggle_audio "$audio_display")" \
"$(msg menu_set_codec "$CODEC")" \
"$(msg menu_set_filefmt "$ff_display")" \
"$(msg menu_set_render "$render_display_now")" \
"$(msg menu_back)")" || return 0
if [[ "$pick" == "$(msg menu_set_fps "$fps_display")" ]]; then
local newf; newf="$(menu_pick "$(msg title_select_fps)" "60" "30" "120" "144" "165" "240" "$(msg fps_unlimited)")" || continue
if [[ "$newf" == "$(msg fps_unlimited)" ]]; then
FRAMERATE=""; rm -f "$CFG_FPS"
else
if [[ "$newf" =~ ^[0-9]+$ && "$newf" -gt 0 ]]; then FRAMERATE="$newf"; printf '%s' "$FRAMERATE" >"$CFG_FPS"; fi
fi
elif [[ "$pick" == "$(msg menu_toggle_audio "$audio_display")" ]]; then
if [[ "$AUDIO" == "on" ]]; then AUDIO="off"; else AUDIO="on"; fi
printf '%s' "$AUDIO" >"$CFG_AUDIO"
elif [[ "$pick" == "$(msg menu_set_codec "$CODEC")" ]]; then
# 仅保留 CPU (libx264) 与所有常见 VAAPI 编码选项CPU 放在首位
local newc; newc="$(menu_pick "$(msg title_select_codec)" \
"libx264" "h264_vaapi" "hevc_vaapi" "av1_vaapi" "vp9_vaapi")" || continue
CODEC="$newc"; printf '%s' "$CODEC" >"$CFG_CODEC"
elif [[ "$pick" == "$(msg menu_set_filefmt "$ff_display")" ]]; then
choose_filefmt_menu
elif [[ "$pick" == "$(msg menu_set_render "$render_display_now")" ]]; then
choose_render_menu
elif [[ "$pick" == "$(msg menu_back)" ]]; then
return 0
fi
# loop to refresh values instantly
done
}
# ---------- Mode selection ----------
decide_mode() {
case "${RECORD_MODE,,}" in
full|fullscreen) MODE_DECIDED="full"; return 0 ;;
region|area) MODE_DECIDED="region"; return 0 ;;
*) ;;
esac
local L_FULL L_REGION L_GIF L_SETTINGS L_EXIT
case "$(lang_code)" in
zh) L_FULL="$(msg menu_fullscreen)"; L_REGION="$(msg menu_region)"; L_GIF="$(msg menu_gif_region)"; L_SETTINGS="$(msg menu_settings)"; L_EXIT="$(msg menu_exit)";;
ja) L_FULL="$(msg menu_fullscreen)"; L_REGION="$(msg menu_region)"; L_GIF="$(msg menu_gif_region)"; L_SETTINGS="$(msg menu_settings)"; L_EXIT="$(msg menu_exit)";;
*) L_FULL="Fullscreen"; L_REGION="Region"; L_GIF="$(msg menu_gif_region)"; L_SETTINGS="$(msg menu_settings)"; L_EXIT="$(msg menu_exit)";;
esac
local title; title="$(msg title_mode)"
while :; do
# ORDER: Fullscreen -> Region -> GIF -> Settings -> Exit
# [FIXED] 调整菜单顺序以匹配图片要求全屏在最前GIF在区域之后
local pick; pick="$(menu_pick "$title" "$L_FULL" "$L_REGION" "$L_GIF" "$L_SETTINGS" "$L_EXIT")" || return 130
if [[ "$pick" == "$L_FULL" ]]; then MODE_DECIDED="full"; return 0
elif [[ "$pick" == "$L_REGION" ]]; then MODE_DECIDED="region"; return 0
elif [[ "$pick" == "$L_GIF" ]]; then MODE_DECIDED="region"; IS_GIF_MODE="true"; return 0
elif [[ "$pick" == "$L_SETTINGS" ]]; then show_settings_menu; continue
elif [[ "$pick" == "$L_EXIT" ]]; then return 130
else return 130; fi
done
}
# ---------- Helpers ----------
geom_token() {
local g="$1"
awk 'NF==2{split($1,a,","); split($2,b,"x");
if(a[1]!=""){printf "%sx%s@%s,%s",b[1],b[2],a[1],a[2]}}' <<<"$g"
}
pretty_dur() {
local dur="${1:-0}"
[[ "$dur" =~ ^[0-9]+$ ]] || dur=0
if ((dur>=3600)); then printf "%d:%02d:%02d" $((dur/3600)) $(((dur%3600)/60)) $((dur%60))
else printf "%02d:%02d" $((dur/60)) $((dur%60)); fi
}
json_escape() { sed ':a;N;$!ba;s/\\/\\\\/g;s/"/\\"/g;s/\n/\\n/g'; }
# ================== Start / Stop ==================
start_rec() {
if is_running; then echo "$(msg already_running)"; exit 0; fi
has wf-recorder || { echo "$(msg err_wf_not_found)"; exit 1; }
MODE_DECIDED=""
IS_GIF_MODE="false"
if ! decide_mode; then
echo "$(msg cancel_no_mode)"; emit_waybar_signal; exit 130
fi
local mode="$MODE_DECIDED"
# [NEW] GIF 模式检查
if [[ "$IS_GIF_MODE" == "true" ]]; then
if ! has ffmpeg; then echo "$(msg err_need_ffmpeg)"; emit_waybar_signal; exit 1; fi
# GIF 模式强制使用 mp4 作为中间格式,因为 mp4 兼容性好且编码速度快
SAVE_EXT="mp4"
touch "$GIF_MARKER"
else
rm -f "$GIF_MARKER"
fi
local marker="" output="" GEOM="" gtok=""
local -a args
args=( -c "$CODEC" )
local ROOT_DIR TARGET_DIR
ROOT_DIR="$(get_save_dir)"
if [[ "$mode" == "full" ]]; then TARGET_DIR="$ROOT_DIR/${SAVE_SUBDIR_FS}"; else TARGET_DIR="$ROOT_DIR"; fi
mkdir -p "$TARGET_DIR"
if [[ "$mode" == "full" ]]; then
output="$(decide_output)" || { echo "$(msg cancel_no_output)"; emit_waybar_signal; exit 130; }
[[ -n "$output" ]] && args+=( -o "$output" )
marker="FS${output:+-$output}"
else
if [[ -n "$REC_AREA" ]]; then
GEOM="$REC_AREA"
else
has slurp || { echo "$(msg err_need_slurp)"; emit_waybar_signal; exit 1; }
set +e; GEOM="$(slurp)"; local rc=$?; set -e
if [[ $rc -ne 0 || -z "${GEOM// /}" ]]; then echo "$(msg cancel_no_region)"; emit_waybar_signal; exit 130; fi
fi
GEOM="$(echo -n "$GEOM" | tr -s '[:space:]' ' ')"
args+=( -g "$GEOM" )
if [[ "${GEOM_IN_NAME,,}" == "on" ]]; then gtok="$(geom_token "$GEOM")"; marker="REGION${gtok:+-$gtok}"; else marker="REGION"; fi
fi
local ts safe_title base SAVE_PATH ext
ts="$(date +'%Y-%m-%d-%H%M%S')"; safe_title="${TITLE// /_}"
base="$ts${safe_title:+-$safe_title}-${marker}"
ext="$(choose_ext)"
SAVE_PATH="$TARGET_DIR/$base.$ext"
args=( --file "$SAVE_PATH" "${args[@]}" )
# Render device
local dev; dev="$(pick_render_device)"; [[ -n "$dev" ]] && args+=( -d "$dev" )
# Audio
case "$AUDIO" in off|OFF|0|false) ;; on|ON|1|true|"") args+=( --audio ) ;; *) args+=( --audio="$AUDIO" ) ;; esac
# Framerate
if [[ -n "$FRAMERATE" ]]; then
if [[ "$FRAMERATE" =~ ^[0-9]+$ && "$FRAMERATE" -gt 0 ]]; then args+=( --framerate "$FRAMERATE" )
else printf '%s\n' "$(msg warn_invalid_fps "$FRAMERATE")" >&2; fi
fi
# Pixel format
if [[ "$CODEC" == *"_vaapi" ]]; then args+=( -F "scale_vaapi=format=nv12:out_range=full:out_color_primaries=bt709" )
else args+=( -F "format=yuv420p" ); fi
# === 不保存日志:仅在 DEBUG=on 时将 wf-recorder 输出到终端 ===
if [[ "${DEBUG,,}" == "on" ]]; then
echo "DEBUG=on: running wf-recorder in foreground"
echo "Command: wf-recorder ${args[*]}"
wf-recorder "${args[@]}" 2>&1 &
local pid=$!
echo "$pid" >"$PIDFILE"
date +%s >"$STARTFILE"
echo "$SAVE_PATH" >"$SAVEPATH_FILE"
echo "$mode" >"$MODEFILE"
local note; if [[ "$mode" == "full" ]]; then note="$(msg notif_started_full "$output" "$SAVE_PATH")"; else note="$(msg notif_started_region "$SAVE_PATH")"; fi
[[ -n "$dev" ]] && note+="$(msg notif_device_suffix "$dev")"
echo "$note";
emit_waybar_signal
start_tick
return 0
fi
# 非 DEBUG后台运行且不保存任何日志与原脚本行为相近
setsid nohup wf-recorder "${args[@]}" >/dev/null 2>&1 &
local pid=$!
echo "$pid" >"$PIDFILE"
date +%s >"$STARTFILE"
echo "$SAVE_PATH" >"$SAVEPATH_FILE"
echo "$mode" >"$MODEFILE"
local note; if [[ "$mode" == "full" ]]; then note="$(msg notif_started_full "$output" "$SAVE_PATH")"; else note="$(msg notif_started_region "$SAVE_PATH")"; fi
[[ -n "$dev" ]] && note+="$(msg notif_device_suffix "$dev")"
echo "$note";
emit_waybar_signal
start_tick
}
stop_rec() {
if ! is_running; then echo "$(msg not_running)"; emit_waybar_signal; exit 0; fi
local pid; read -r pid <"$PIDFILE"
kill -INT "$pid" 2>/dev/null || true
for _ in {1..40}; do sleep 0.1; is_running || break; done
is_running && kill -TERM "$pid" 2>/dev/null || true
sleep 0.2
is_running && kill -KILL "$pid" 2>/dev/null || true
# 停止后清理运行时状态文件
rm -f "$PIDFILE" "$MODEFILE"
stop_tick
local save_path=""; [[ -r "$SAVEPATH_FILE" ]] && read -r save_path <"$SAVEPATH_FILE"
# --- [NEW] GIF Conversion Logic ---
if [[ -f "$GIF_MARKER" ]]; then
rm -f "$GIF_MARKER"
if [[ -n "$save_path" && -f "$save_path" ]]; then
notify "$(msg notif_processing_gif)"
# [FIXED] 确保 GIF 目录存在: .../wf-recorder/gif/
local gif_dir="$(get_save_dir)/gif"
mkdir -p "$gif_dir"
local filename=$(basename "$save_path")
local gif_out="$gif_dir/${filename%.*}.gif"
# 使用您提供的滤镜字符串
local filters="fps=$GIF_FPS,scale=$GIF_WIDTH:-1:flags=lanczos,split[s0][s1];[s0]palettegen=stats_mode=$GIF_STATS_MODE[p];[s1][p]paletteuse=dither=$GIF_DITHER_MODE"
# 运行转换,如果成功则删除原文件
if ffmpeg -y -v error -i "$save_path" -vf "$filters" "$gif_out"; then
rm "$save_path"
save_path="$gif_out"
# 更新保存路径以便后续通知使用
echo "$save_path" > "$SAVEPATH_FILE"
else
notify "$(msg notif_gif_failed)"
fi
fi
fi
# -----------------------------------
if [[ -n "$save_path" && -f "$save_path" ]]; then
# 生成不带后缀的 latest例如.../latest
ln -sf "$(basename "$save_path")" "$(dirname "$save_path")/latest" || true
# --- [NEW] Auto Copy to Clipboard (as File Object) ---
local cp_note=""
if command -v wl-copy >/dev/null; then
# [CRITICAL FIX] 使用 text/uri-list MIME 类型,并添加 file:// 前缀
# 这会让剪贴板将其视为一个“文件”,允许在文件管理器或聊天软件中直接粘贴
echo "file://${save_path}" | wl-copy --type text/uri-list
cp_note=" $(msg notif_copied)"
fi
# ------------------------------------
local s; s="$(msg notif_saved "$save_path")${cp_note}"; echo "$s"; notify "$s"
else
local s; s="$(msg notif_stopped)"; echo "$s"; notify "$s"
fi
if [[ "${PKILL_AFTER_STOP,,}" != "off" ]]; then
for sig in INT TERM KILL; do
pgrep -x -u "$UID" "$APP" >/dev/null || break
pkill -"$sig" -x -u "$UID" "$APP" 2>/dev/null || true
sleep 0.1
done
fi
emit_waybar_signal
}
# ================== Waybar JSON/status ==================
tooltip_idle_text() {
case "$(lang_code)" in
zh) cat <<'EOF'
屏幕录制wf-recorder
左键:打开录制菜单
右键:强制关闭
EOF
;;
ja) cat <<'EOF'
画面録画wf-recorder
左クリック:録画メニューを開く
右クリック:強制停止
EOF
;;
*) cat <<'EOF'
Screen recording (wf-recorder)
Left click: open recording menu
Right click: force stop
EOF
;;
esac
}
tooltip_recording_text() { # $1 elapsed, $2 filepath, $3 mode: full|region
local t="$1" p="${2:-}" m="${3:-}"
local mode_label
case "$m" in full|fullscreen) mode_label="$(msg mode_full)";; region|area) mode_label="$(msg mode_region)";; *) mode_label="";; esac
case "$(lang_code)" in
zh) [[ -n "$p" ]] && { [[ -n "$mode_label" ]] && printf "录制中(%s\n已用时%s\n文件%s\n" "$mode_label" "$t" "$p" || printf "录制中\n已用时%s\n文件%s\n" "$t" "$p"; } || { [[ -n "$mode_label" ]] && printf "录制中(%s\n已用时%s\n" "$mode_label" "$t" || printf "录制中\n已用时%s\n" "$t"; } ;;
ja) [[ -n "$p" ]] && { [[ -n "$mode_label" ]] && printf "録画中(%s\n経過時間%s\nファイル%s\n" "$mode_label" "$t" "$p" || printf "録画中\n経過時間%s\nファイル%s\n" "$t" "$p"; } || { [[ -n "$mode_label" ]] && printf "録画中(%s\n経過時間%s\n" "$mode_label" "$t" || printf "録画中\n経過時間%s\n" "$t"; } ;;
*) [[ -n "$p" ]] && { [[ -n "$mode_label" ]] && printf "Recording (%s)\nElapsed: %s\nFile: %s\n" "$mode_label" "$t" "$p" || printf "Recording\nElapsed: %s\nFile: %s\n" "$t" "$p"; } || { [[ -n "$mode_label" ]] && printf "Recording (%s)\nElapsed: %s\n" "$mode_label" "$t" || printf "Recording\nElapsed: %s\n" "$t"; } ;;
esac
}
pretty_status_json() {
local text tooltip class alt
if is_running; then
local start=0; [[ -r "$STARTFILE" ]] && read -r start <"$STARTFILE" || true
[[ "$start" =~ ^[0-9]+$ ]] || start=0
local now dur; now="$(date +%s)"; dur=$((now - start)); (( dur < 0 )) && dur=0
local t; t="$(pretty_dur "$dur")"
local save_path=""; [[ -r "$SAVEPATH_FILE" ]] && read -r save_path <"$SAVEPATH_FILE" || true
local mode=""; [[ -r "$MODEFILE" ]] && read -r mode <"$MODEFILE" || true
text="$ICON_REC$t"
tooltip="$(tooltip_recording_text "$t" "$save_path" "$mode")"
class="recording"; alt="rec"
else
text="$ICON_IDLE"; tooltip="$(tooltip_idle_text)"; class="idle"; alt="idle"
fi
printf '{"text":"%s","tooltip":"%s","class":"%s","alt":"%s"}\n' \
"$(printf '%s' "$text" | json_escape)" \
"$(printf '%s' "$tooltip" | json_escape)" \
"$class" "$alt"
}
status_rec() {
local json="${1:-}"
if [[ "$json" == "--json" ]]; then
pretty_status_json
else
if is_running; then
local start=0; [[ -r "$STARTFILE" ]] && read -r start <"$STARTFILE" || true
[[ "$start" =~ ^[0-9]+$ ]] || start=0
local now dur; now="$(date +%s)"; dur=$((now - start)); (( dur < 0 )) && dur=0
printf "%s%s\n" "$ICON_REC" "$(pretty_dur "$dur")"
else
echo "$ICON_IDLE"
fi
fi
}
# ================== Main ==================
case "${1:-toggle}" in
start) start_rec ;;
stop) stop_rec ;;
status) status_rec ;;
status-json) status_rec --json ;;
waybar) status_rec --json ;;
is-active) if is_running; then exit 0; else exit 1; fi ;;
toggle) is_running && stop_rec || start_rec ;;
settings) show_settings_menu ;;
*) echo "Usage: $0 {start|stop|toggle|status|status-json|waybar|is-active|settings}"; exit 2 ;;
esac