mirror of
https://github.com/zhenyan121/dotfiles.git
synced 2026-04-10 14:34:09 +08:00
792 lines
32 KiB
Bash
Executable File
792 lines
32 KiB
Bash
Executable File
#!/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
|