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,134 @@
#!/bin/bash
# =================语言=================
if env | grep -q "zh_CN"; then
STR_NEXT="📸 截取下一张 (仅需定高度)"
STR_FINISH="💾 完成并处理"
STR_ABORT="❌ 放弃"
STR_ERR="错误"
STR_SAVED="已保存"
else
STR_NEXT="📸 Capture Next (Height only)"
STR_FINISH="💾 Finish"
STR_ABORT="❌ Abort"
STR_ERR="Error"
STR_SAVED="Saved"
fi
# =================配置=================
CONFIG_DIR="$HOME/.cache/longshot-sh"
CONFIG_FILE="$CONFIG_DIR/mode"
SAVE_DIR="$HOME/Pictures/Screenshots/longshots"
TMP_DIR="/tmp/longshot_grim_$(date +%s)"
FILENAME="longshot_$(date +%Y%m%d_%H%M%S).png"
RESULT_PATH="$SAVE_DIR/$FILENAME"
TMP_STITCHED="$TMP_DIR/stitched.png"
mkdir -p "$SAVE_DIR" "$TMP_DIR"
cleanup() { rm -rf "$TMP_DIR"; }
trap cleanup EXIT SIGINT SIGTERM
# 菜单工具
# [修改]: 将 fuzzel 宽度从 45 调整为 30使其更紧凑
CMD_FUZZEL="fuzzel -d --anchor=top --y-margin=20 --lines=3 --width=30"
CMD_WOFI="wofi --dmenu --lines 3"
CMD_ROFI="rofi -dmenu -l 3"
if command -v fuzzel &> /dev/null; then MENU_CMD="$CMD_FUZZEL"
elif command -v wofi &> /dev/null; then MENU_CMD="$CMD_WOFI"
elif command -v rofi &> /dev/null; then MENU_CMD="$CMD_ROFI"
else exit 1; fi
# [新增]: 动态计算宽度函数 (主要针对 wofi)
function get_dynamic_width() {
local text="$1"
# 获取最长行的长度
local max_len=$(echo -e "$text" | wc -L)
# 计算: 字符数 * 28px + 60px 边距 (可根据屏幕分辨率微调)
echo $(( max_len * 28 + 60 ))
}
# [修改]: 增加对 wofi 的动态宽度支持
function show_menu() {
local content="$1"
if [[ "$MENU_CMD" == *"wofi"* ]]; then
# 如果是 wofi计算宽度并附加参数
local width=$(get_dynamic_width "$content")
echo -e "$content" | $MENU_CMD --width "$width"
else
# 其他工具 (fuzzel/rofi) 保持原样
echo -e "$content" | $MENU_CMD
fi
}
# ======================================
# Step 1: 第一张截图 (直接开始,不询问)
# ======================================
# 用户在主菜单点击了 "选择区域",所以这里直接 Slurp
GEO_1=$(slurp)
if [ -z "$GEO_1" ]; then exit 0; fi
IFS=', x' read -r FIX_X FIX_Y FIX_W FIX_H <<< "$GEO_1"
grim -g "$GEO_1" "$TMP_DIR/001.png"
# ======================================
# Step 2: 循环截图
# ======================================
INDEX=2
DO_SAVE=false
while true; do
# 菜单提示下一张 (show_menu 会自动处理宽度)
ACTION=$(show_menu "$STR_NEXT\n$STR_FINISH\n$STR_ABORT")
case "$ACTION" in
*"📸"*)
sleep 0.2
GEO_NEXT=$(slurp)
if [ -z "$GEO_NEXT" ]; then continue; fi
# 锁定宽度和X轴只取新高度
IFS=', x' read -r _TX NEW_Y _TW NEW_H <<< "$GEO_NEXT"
FINAL_GEO="${FIX_X},${NEW_Y} ${FIX_W}x${NEW_H}"
IMG_NAME="$(printf "%03d" $INDEX).png"
grim -g "$FINAL_GEO" "$TMP_DIR/$IMG_NAME"
((INDEX++))
;;
*"💾"*) DO_SAVE=true; break ;;
*"❌"*) exit 0 ;;
*) break ;; # 意外退出
esac
done
# ======================================
# Step 3: 处理与自动动作
# ======================================
COUNT=$(ls "$TMP_DIR"/*.png 2>/dev/null | wc -l)
if [ "$COUNT" -gt 0 ] && [ "$DO_SAVE" = true ]; then
# 拼接
magick "$TMP_DIR"/*.png -append "$TMP_STITCHED"
mv "$TMP_STITCHED" "$RESULT_PATH"
# 复制到剪贴板
if command -v wl-copy &> /dev/null; then wl-copy < "$RESULT_PATH"; fi
# 读取配置执行动作
FINAL_MODE=$(cat "$CONFIG_FILE" 2>/dev/null || echo "PREVIEW")
case "$FINAL_MODE" in
"PREVIEW")
imv "$RESULT_PATH"
;;
"EDIT")
if command -v satty &> /dev/null; then satty -f "$RESULT_PATH"
else imv "$RESULT_PATH"; fi
;;
"SAVE")
notify-send -i "$RESULT_PATH" "Longshot" "$STR_SAVED: $(basename "$RESULT_PATH")"
;;
esac
fi

View File

@@ -0,0 +1,80 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_PYTHON="$SCRIPT_DIR/venv/bin/python"
PY_STITCH="$SCRIPT_DIR/stitch.py"
CONFIG_DIR="$HOME/.cache/longshot-sh"
CONFIG_FILE="$CONFIG_DIR/mode"
SAVE_DIR="$HOME/Pictures/Screenshots/longshots"
mkdir -p "$SAVE_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
TMP_VIDEO="/tmp/longshot_${TIMESTAMP}.mp4"
OUTPUT_IMG="${SAVE_DIR}/longshot_${TIMESTAMP}.png"
# 语言
if env | grep -q "zh_CN"; then
TXT_REC="录制中"
TXT_MSG="正在拼接..."
TXT_SAVED="已保存并复制"
WIDTH=6
else
TXT_REC="Recording"
TXT_MSG="Stitching..."
TXT_SAVED="Saved"
WIDTH=10
fi
# 录制菜单
if command -v fuzzel &> /dev/null; then
MENU_REC_CMD="fuzzel -d --anchor top --y-margin 20 --width $WIDTH --lines 0"
elif command -v wofi &> /dev/null; then
MENU_REC_CMD="wofi -d -i -p Rec"
else
MENU_REC_CMD="rofi -dmenu -p Rec"
fi
# Step 1: 选区
GEOMETRY=$(slurp)
if [ -z "$GEOMETRY" ]; then exit 1; fi
# Step 2: 录制
wf-recorder -g "$GEOMETRY" -f "$TMP_VIDEO" \
-c libx264 -p crf=0 -p preset=ultrafast -p pixel_format=yuv420p \
&> /dev/null &
REC_PID=$!
sleep 0.5
if ! kill -0 $REC_PID 2>/dev/null; then
notify-send "Error" "wf-recorder failed"
exit 1
fi
# Step 3: 停止菜单
echo "Stop" | $MENU_REC_CMD -p "$TXT_REC" > /dev/null
kill -SIGINT $REC_PID
wait $REC_PID 2>/dev/null
# Step 4: 处理
if [ -f "$TMP_VIDEO" ]; then
notify-send -t 2000 "Longshot" "$TXT_MSG"
"$VENV_PYTHON" "$PY_STITCH" "$TMP_VIDEO" "$OUTPUT_IMG"
rm -f "$TMP_VIDEO"
if [ -f "$OUTPUT_IMG" ]; then
if command -v wl-copy &> /dev/null; then wl-copy < "$OUTPUT_IMG"; fi
# 自动执行动作
FINAL_MODE=$(cat "$CONFIG_FILE" 2>/dev/null || echo "PREVIEW")
case "$FINAL_MODE" in
"PREVIEW") imv "$OUTPUT_IMG" ;;
"EDIT") if command -v satty &> /dev/null; then satty -f "$OUTPUT_IMG"; else imv "$OUTPUT_IMG"; fi ;;
"SAVE") notify-send "Longshot" "$TXT_SAVED: $(basename "$OUTPUT_IMG")" ;;
esac
fi
else
notify-send "Error" "No video"
fi

View File

@@ -0,0 +1,224 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_PYTHON="$SCRIPT_DIR/venv/bin/python"
# =================配置区=================
CONFIG_DIR="$HOME/.cache/longshot-sh"
mkdir -p "$CONFIG_DIR"
FILE_MODE="$CONFIG_DIR/mode" # PREVIEW / EDIT / SAVE
FILE_BACKEND="$CONFIG_DIR/backend" # WF / GRIM
# 初始化默认值
[ ! -f "$FILE_MODE" ] && echo "PREVIEW" > "$FILE_MODE"
[ ! -f "$FILE_BACKEND" ] && echo "WF" > "$FILE_BACKEND"
# =================语言资源=================
if env | grep -q "zh_CN"; then
TXT_TITLE_WF="缓慢滚动,回车停止"
TXT_TITLE_GRIM="记住截图末尾位置"
TXT_START="📷 选择截图区域"
TXT_SETTING="⚙️ 设置"
TXT_EXIT="❌ 退出"
TXT_BACK="🔙 返回主菜单"
TXT_SW_BACKEND="📹 切换后端"
TXT_SW_ACTION="🛠 切换动作"
TXT_PROMPT_ACTION="请选择截图后的动作:"
TXT_ST_WF="流式录制 (wf-recorder)"
TXT_ST_GRIM="分段截图 (grim)"
TXT_ST_PRE="预览 (imv)"
TXT_ST_EDIT="编辑 (satty)"
TXT_ST_SAVE="仅保存"
# 初始化提示
TXT_MSG_INIT="首次运行,正在初始化环境..."
TXT_MSG_SETUP_DONE="环境初始化完成!"
TXT_ERR_SETUP="环境安装失败,请查看 /tmp/longshot_setup.log"
TXT_ERR_NO_SETUP="未找到 setup.sh 文件"
# 新增:依赖缺失提示
TXT_ERR_DEP_TITLE="缺少系统依赖"
TXT_ERR_DEP_MSG="请安装以下包:"
else
TXT_TITLE_WF="Scroll Slowly, Enter to Stop"
TXT_TITLE_GRIM="Remember End Position"
TXT_START="📷 Select Area"
TXT_SETTING="⚙️ Settings"
TXT_EXIT="❌ Exit"
TXT_BACK="🔙 Back"
TXT_SW_BACKEND="📹 Switch Backend"
TXT_SW_ACTION="🛠 Switch Action"
TXT_PROMPT_ACTION="Select action after capture:"
TXT_ST_WF="Stream (wf-recorder)"
TXT_ST_GRIM="Manual (grim)"
TXT_ST_PRE="Preview"
TXT_ST_EDIT="Edit"
TXT_ST_SAVE="Save Only"
# Init messages
TXT_MSG_INIT="First run, initializing environment..."
TXT_MSG_SETUP_DONE="Environment initialized!"
TXT_ERR_SETUP="Setup failed, check /tmp/longshot_setup.log"
TXT_ERR_NO_SETUP="setup.sh not found"
# New: Dependency error
TXT_ERR_DEP_TITLE="Missing Dependencies"
TXT_ERR_DEP_MSG="Please install:"
fi
# ================= 1. 系统依赖检测 (新增) =================
# 检测核心工具: wf-recorder, grim, slurp, imagemagick (magick)
REQUIRED_TOOLS=("wf-recorder" "grim" "slurp" "magick" "wl-copy")
MISSING_TOOLS=()
for tool in "${REQUIRED_TOOLS[@]}"; do
if ! command -v "$tool" &> /dev/null; then
MISSING_TOOLS+=("$tool")
fi
done
if [ ${#MISSING_TOOLS[@]} -ne 0 ]; then
# 构建错误信息
MSG="${TXT_ERR_DEP_MSG} ${MISSING_TOOLS[*]}"
# 尝试发送通知
if command -v notify-send &> /dev/null; then
notify-send -u critical "$TXT_ERR_DEP_TITLE" "$MSG"
else
# 如果连 notify-send 都没有,这就很尴尬了,尝试用 echo 输出到 stderr
echo "$TXT_ERR_DEP_TITLE: $MSG" >&2
fi
exit 1
fi
# ================= 2. Python 环境自动检测与修复 =================
if [ ! -f "$VENV_PYTHON" ]; then
notify-send -t 5000 "Longshot" "$TXT_MSG_INIT"
if [ -f "$SCRIPT_DIR/setup.sh" ]; then
chmod +x "$SCRIPT_DIR/setup.sh"
"$SCRIPT_DIR/setup.sh" > /tmp/longshot_setup.log 2>&1
if [ ! -f "$VENV_PYTHON" ]; then
notify-send -u critical "Error" "$TXT_ERR_SETUP"
exit 1
else
notify-send -t 3000 "Longshot" "$TXT_MSG_SETUP_DONE"
fi
else
notify-send -u critical "Error" "$TXT_ERR_NO_SETUP"
exit 1
fi
fi
# =================菜单工具=================
if command -v fuzzel &> /dev/null; then
MENU_CMD="fuzzel -d --anchor top --y-margin 20 --width 35 --lines 4"
elif command -v wofi &> /dev/null; then
MENU_CMD="wofi -d -i -p Longshot"
else
MENU_CMD="rofi -dmenu"
fi
# =================主循环=================
while true; do
# 1. 读取当前配置
CUR_MODE=$(cat "$FILE_MODE")
CUR_BACKEND=$(cat "$FILE_BACKEND")
# 2. 动态生成 UI 文本
CURRENT_TITLE=""
if [ "$CUR_BACKEND" == "WF" ]; then
CURRENT_TITLE="$TXT_TITLE_WF"
else
CURRENT_TITLE="$TXT_TITLE_GRIM"
fi
LBL_MODE=""
case "$CUR_MODE" in
"PREVIEW") LBL_MODE="$TXT_ST_PRE" ;;
"EDIT") LBL_MODE="$TXT_ST_EDIT" ;;
"SAVE") LBL_MODE="$TXT_ST_SAVE" ;;
esac
LBL_BACKEND=""
case "$CUR_BACKEND" in
"WF") LBL_BACKEND="$TXT_ST_WF" ;;
"GRIM") LBL_BACKEND="$TXT_ST_GRIM" ;;
esac
# 3. 显示主菜单
OPTION_START="$TXT_START"
OPTION_SETTING="$TXT_SETTING [$LBL_BACKEND | $LBL_MODE]"
OPTION_EXIT="$TXT_EXIT"
if [[ "$MENU_CMD" == *"fuzzel"* ]] || [[ "$MENU_CMD" == *"rofi"* ]]; then
CHOICE=$(echo -e "$OPTION_START\n$OPTION_SETTING\n$OPTION_EXIT" | $MENU_CMD -p "$CURRENT_TITLE")
else
CHOICE=$(echo -e "$OPTION_START\n$OPTION_SETTING\n$OPTION_EXIT" | $MENU_CMD)
fi
# 4. 处理选择
if [[ "$CHOICE" == *"$TXT_START"* ]]; then
# === 启动后端 ===
if [ "$CUR_BACKEND" == "WF" ]; then
exec "$SCRIPT_DIR/longshot-wf-recorder.sh"
else
exec "$SCRIPT_DIR/longshot-grim.sh"
fi
break
elif [[ "$CHOICE" == *"$TXT_SETTING"* ]]; then
# === 设置菜单循环 ===
while true; do
S_MODE=$(cat "$FILE_MODE")
S_BACK=$(cat "$FILE_BACKEND")
D_BACK=""; [ "$S_BACK" == "WF" ] && D_BACK="$TXT_ST_WF" || D_BACK="$TXT_ST_GRIM"
D_MODE="";
case "$S_MODE" in
"PREVIEW") D_MODE="$TXT_ST_PRE" ;;
"EDIT") D_MODE="$TXT_ST_EDIT" ;;
"SAVE") D_MODE="$TXT_ST_SAVE" ;;
esac
ITEM_BACKEND="$TXT_SW_BACKEND [$D_BACK]"
ITEM_ACTION="$TXT_SW_ACTION [$D_MODE]"
if [[ "$MENU_CMD" == *"fuzzel"* ]] || [[ "$MENU_CMD" == *"rofi"* ]]; then
S_CHOICE=$(echo -e "$TXT_BACK\n$ITEM_BACKEND\n$ITEM_ACTION" | $MENU_CMD -p "$TXT_SETTING")
else
S_CHOICE=$(echo -e "$TXT_BACK\n$ITEM_BACKEND\n$ITEM_ACTION" | $MENU_CMD)
fi
if [[ "$S_CHOICE" == *"$TXT_BACK"* ]]; then
break
elif [[ "$S_CHOICE" == *"$TXT_SW_BACKEND"* ]]; then
if [ "$S_BACK" == "WF" ]; then echo "GRIM" > "$FILE_BACKEND"; else echo "WF" > "$FILE_BACKEND"; fi
elif [[ "$S_CHOICE" == *"$TXT_SW_ACTION"* ]]; then
if [[ "$MENU_CMD" == *"fuzzel"* ]] || [[ "$MENU_CMD" == *"rofi"* ]]; then
A_CHOICE=$(echo -e "$TXT_ST_PRE\n$TXT_ST_EDIT\n$TXT_ST_SAVE" | $MENU_CMD -p "$TXT_PROMPT_ACTION")
else
A_CHOICE=$(echo -e "$TXT_ST_PRE\n$TXT_ST_EDIT\n$TXT_ST_SAVE" | $MENU_CMD)
fi
if [[ "$A_CHOICE" == *"$TXT_ST_PRE"* ]]; then echo "PREVIEW" > "$FILE_MODE"; fi
if [[ "$A_CHOICE" == *"$TXT_ST_EDIT"* ]]; then echo "EDIT" > "$FILE_MODE"; fi
if [[ "$A_CHOICE" == *"$TXT_ST_SAVE"* ]]; then echo "SAVE" > "$FILE_MODE"; fi
else
exit 0
fi
done
else
exit 0
fi
done

View File

@@ -0,0 +1,42 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_DIR="$SCRIPT_DIR/venv"
echo "🔧 初始化环境..."
# 1. 检查系统工具
# 涵盖了两个方案所需的所有工具
REQUIRED_TOOLS=("wf-recorder" "grim" "slurp" "wl-copy" "magick" "python3")
MISSING_TOOLS=()
for tool in "${REQUIRED_TOOLS[@]}"; do
if ! command -v "$tool" &> /dev/null; then
MISSING_TOOLS+=("$tool")
fi
done
if [ ${#MISSING_TOOLS[@]} -ne 0 ]; then
echo "⚠️ 警告: 缺少以下系统工具,请使用包管理器安装:"
echo " ${MISSING_TOOLS[*]}"
echo " (例如 Arch: sudo pacman -S wf-recorder grim slurp wl-clipboard imagemagick python)"
echo " (例如 Debian/Ubuntu: sudo apt install wf-recorder grim slurp wl-clipboard imagemagick python3-venv)"
fi
# 2. Python 虚拟环境
if [ ! -d "$VENV_DIR" ]; then
echo "📦 创建 Python 虚拟环境..."
python3 -m venv "$VENV_DIR"
fi
# 3. 安装 Python 依赖
echo "⬇️ 安装 Python 库..."
"$VENV_DIR/bin/pip" install --upgrade pip > /dev/null
"$VENV_DIR/bin/pip" install opencv-python numpy > /dev/null
# 4. 赋予执行权限
chmod +x "$SCRIPT_DIR/longshot.sh"
chmod +x "$SCRIPT_DIR/longshot-wf-recorder.sh"
chmod +x "$SCRIPT_DIR/longshot-grim.sh"
chmod +x "$SCRIPT_DIR/stitch.py"
echo "✅ 完成!请运行 ./longshot.sh"

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
import cv2
import numpy as np
import sys
import os
def stitch_video(video_path, output_path):
if not os.path.exists(video_path):
return
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print("❌ 无法打开视频")
return
frames = []
ret, prev_frame = cap.read()
if not ret: return
frames.append(prev_frame)
anchor_frame = prev_frame.copy()
# ==========================
# 核心参数 (手动滚动优化)
# ==========================
MIN_SCROLL = 2
MATCH_CONFIDENCE = 0.5
# 忽略上下边缘 (防止浏览器地址栏/状态栏干扰)
IGNORE_Y_TOP = 0.15
IGNORE_Y_BOTTOM = 0.15
IGNORE_X = 0.15
h, w, _ = anchor_frame.shape
# 有效特征区
x1 = int(w * IGNORE_X)
x2 = int(w * (1 - IGNORE_X))
y1 = int(h * IGNORE_Y_TOP)
template_h = int(h * 0.2)
print(f"⚡ 正在分析 (梯度匹配模式)...")
last_shift = 0
SEARCH_WINDOW = 50
while True:
ret, curr_frame = cap.read()
if not ret: break
# 1. 梯度处理 (解决白底黑字问题)
curr_gray = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2GRAY)
anchor_gray = cv2.cvtColor(anchor_frame, cv2.COLOR_BGR2GRAY)
curr_grad = cv2.Sobel(curr_gray, cv2.CV_8U, 0, 1, ksize=3)
anchor_grad = cv2.Sobel(anchor_gray, cv2.CV_8U, 0, 1, ksize=3)
# 2. 提取模板
template = curr_grad[y1 : y1 + template_h, x1:x2]
roi = anchor_grad[y1:, x1:x2]
# 3. 匹配
res = cv2.matchTemplate(roi, template, cv2.TM_CCOEFF_NORMED)
# 4. 惯性约束
if last_shift > 0:
mask = np.zeros_like(res)
target_y = last_shift
y_min = max(0, target_y - SEARCH_WINDOW)
y_max = min(res.shape[0], target_y + SEARCH_WINDOW)
mask[y_min:y_max, :] = 1
res = np.multiply(res, mask)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
shift = max_loc[1]
# 5. 判定
if max_val > MATCH_CONFIDENCE and shift > MIN_SCROLL and shift < (roi.shape[0] - 5):
new_content_start_y = h - shift
if new_content_start_y < h:
new_part = curr_frame[new_content_start_y:, :, :]
frames.append(new_part)
anchor_frame = curr_frame.copy()
if last_shift == 0: last_shift = shift
else: last_shift = int(last_shift * 0.6 + shift * 0.4)
cap.release()
if len(frames) > 1:
try:
full_image = np.vstack(frames)
cv2.imwrite(output_path, full_image, [cv2.IMWRITE_PNG_COMPRESSION, 3])
print(f"🎉 处理完成")
except Exception as e:
print(f"❌ 保存失败: {e}")
else:
print("⚠️ 未检测到滚动,保存第一帧")
cv2.imwrite(output_path, frames[0])
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python stitch.py <input_video> <output_image>")
else:
stitch_video(sys.argv[1], sys.argv[2])