ffgrab
Features
Region selection using slop (with visual highlight)
Fullscreen capture option
Live recording timer via notifications
Audio capture (PulseAudio)
Post-recording statsFile size
Duration
State tracking (robust across async calls)🧩 Designed to work nicely with XFCE Genmon plugin
Tech stack (DEPENDENCIES)ffmpeg (x11grab + libx264 / libopus)
slop (region selection)
notify-send (UI feedback)
ffprobe (stats extraction)
Key design detailsUses /tmp state files to track:
active PID
start time
output file path
Avoids race conditions when stopping recording
Ensures file is fully written before stats are shown
Works cleanly with set -euo pipefail (no silent failures)
🧪 Example workflow
- Trigger via panel (Genmon)
- Select region visually
- Recording starts after a short delay
- Timer notification updates every second
- Stop recording → clean shutdown
- Final notification shows:
- path
- size
- duration
Future ideas* Hardware encoding (VAAPI / NVENC)
* Optional overlay (dimensions, guides, etc.)
ffgrab script follows:
Code: Select all
#!/usr/bin/env bash
set -euo pipefail
PIDFILE="/tmp/ffgrab.pid"
STARTFILE="/tmp/ffgrab.start"
STATEFILE="/tmp/ffgrab.outfile"
OUTDIR="$HOME" #~/Videos
OUTFILE=""
FPS=30
CRF=23
PRESET="veryfast"
ABR=(-b:a 96k)
#TODO: 🎥 Hardware encoding (significant performance improvement)
# Check dependencies ffmpeg slop xdpyinfo & xfce4-genmon-plugin
for cmd in ffmpeg slop xdpyinfo; do command -v $cmd &> /dev/null || exit 1; done
[ -f /usr/lib/xfce4/panel/plugins/libgenmon.so ] || exit 1
mkdir -p "$OUTDIR"
notify() {
notify-send -u low -t 5000 "$1"
}
update_timer() {
while true; do
PID=$(cat "$PIDFILE" 2>/dev/null) || break
[[ -n "$PID" ]] || break
kill -0 "$PID" 2>/dev/null || break
START=$(cat "$STARTFILE" 2>/dev/null || date +%s)
NOW=$(date +%s)
ELAPSED=$((NOW - START))
TIME=$(printf '%02d:%02d' $((ELAPSED/60)) $((ELAPSED%60)))
notify-send -r 9999 -t 1100 "🔴 REC $TIME"
sleep 1
done
}
show_stats() {
local file
file="$(cat "$STATEFILE" 2>/dev/null)"
if [[ -z "$file" || ! -f "$file" ]]; then
notify-send -t 3000 "⚠️ Recording canceled" "No file was created"
return
fi
# wait real flush
for i in {1..20}; do
[[ -s "$file" ]] && break
sleep 0.2
done
if [[ ! -s "$file" ]]; then
notify-send -t 3000 "⚠️ Recording canceled" "Incomplete file"
return
fi
local SIZE DURATION MINS SECS
SIZE=$(ls -lh "$file" 2>/dev/null | awk '{print $5}')
DURATION=$(ffprobe -v error \
-show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 \
"$file" 2>/dev/null || echo "")
DURATION=${DURATION%.*}
if [[ -n "$DURATION" ]]; then
MINS=$((DURATION / 60))
SECS=$((DURATION % 60))
notify-send -i video-x-generic -t 0 \
"✅ Recording finished" \
"📁 ${file}\n💾 ${SIZE}\n⏱️ ${MINS}m ${SECS}s"
else
notify-send -i video-x-generic -t 5000 \
"✅ Recording finished" \
"📁 ${file}\n💾 ${SIZE}\n📍 ${OUTDIR}"
fi
}
align8() {
local v=$1
echo $(( (v + 7) & ~7 ))
}
record_region() {
OUTFILE="$OUTDIR/rec_$(date +"%Y-%m-%d_%H-%M-%S").mp4"
echo "$OUTFILE" > "$STATEFILE"
GEO=$(slop --format "%w %h %x %y" \
--bordersize=5 \
--nodecorations=1 \
--tolerance=5 \
--highlight \
--color=0.3,0.4,1.0,0.4)
if [[ -z "$GEO" ]]; then
notify "❌ Canceled"
return 0
fi
read -r W H X Y <<< "$GEO"
W8=$(align8 "$W")
H8=$(align8 "$H")
notify "📐 ${W}x${H} → ${W8}x${H8}
Starting recording"
sleep 5
FFPARAMS=(
-f x11grab
-framerate "$FPS"
-video_size "${W8}x${H8}"
-i ":0.0+${X},${Y}"
-f pulse -i default
-c:v libx264
-crf "$CRF"
-preset "$PRESET"
-c:a libopus
"${ABR[@]}"
"$OUTFILE"
)
PID=$(cat "$PIDFILE" 2>/dev/null || true)
if [[ -n "${PID:-}" ]] && kill -0 "$PID" 2>/dev/null; then
notify "⚠️ A recording is already in progress"
return 1
fi
date +%s > "$STARTFILE"
ffmpeg "${FFPARAMS[@]}" >/dev/null 2>&1 &
echo $! > "$PIDFILE"
update_timer &
echo $! > /tmp/ffgrab.timer.pid
}
record_screen() {
OUTFILE="$OUTDIR/rec_$(date +"%Y-%m-%d_%H-%M-%S").mp4"
echo "$OUTFILE" > "$STATEFILE"
GEO=$(xdpyinfo | awk '/dimensions/{print $2}')
W=${GEO%x*}
H=${GEO#*x}
FFPARAMS=(
-f x11grab
-framerate "$FPS"
-video_size "${W}x${H}"
-i ":0.0+0,0"
-f pulse -i default
-c:v libx264
-crf "$CRF"
-preset "$PRESET"
-c:a libopus
"${ABR[@]}"
"$OUTFILE"
)
PID=$(cat "$PIDFILE" 2>/dev/null || true)
if [[ -n "${PID:-}" ]] && kill -0 "$PID" 2>/dev/null; then
notify "⚠️ A recording is already in progress"
exit 1
fi
date +%s > "$STARTFILE"
ffmpeg "${FFPARAMS[@]}" >/dev/null 2>&1 &
echo $! > "$PIDFILE"
update_timer &
echo $! > /tmp/ffgrab.timer.pid
}
toggle() {
PID=$(cat "$PIDFILE" 2>/dev/null || true)
if [[ -n "${PID:-}" ]] && kill -0 "$PID" 2>/dev/null; then
stop_record
else
record_region
fi
}
stop_record() {
if [[ -f "$PIDFILE" ]]; then
PID=$(cat "$PIDFILE" 2>/dev/null || true)
if [[ -n "$PID" ]]; then
kill -INT "$PID" 2>/dev/null
while kill -0 "$PID" 2>/dev/null; do
sleep 0.1
done
fi
if [[ -f /tmp/ffgrab.timer.pid ]]; then
kill "$(cat /tmp/ffgrab.timer.pid)" 2>/dev/null
rm -f /tmp/ffgrab.timer.pid
fi
notify "🟥 STOP"
sleep 0.5
show_stats
rm -f "$PIDFILE" "$STARTFILE" "$STATEFILE"
else
notify "No active recording"
fi
}
trap 'stop_record' INT TERM
case "$1" in
region|-r) record_region ;;
screen|-s) record_screen ;;
stop|-q) stop_record ;;
toggle|-t) toggle ;;
*) echo "Usage: ffgrab {region|screen|stop|toggle}" ;;
esac
Howto implement this thing in your XFCE4 panel:
Save this companion as say.. ~/.local/bin/ffgrab-status and attach-it to a new genmon-plugin panel element
Code: Select all
#!/usr/bin/env bash
# ffgrab-status
# switches genmon status icon
if [[ -f "/tmp/ffgrab.pid" ]]; then
# REC on: Icono de stop detiene al clicar
echo "<img>$HOME/.local/share/icons/ffgrab-stop.svg</img><click>$HOME/.local/bin/ffgrab -q</click>"
else
# REC off: Icono de rec inicia al clicar
echo "<img>$HOME/.local/share/icons/ffgrab-rec.svg</img><click>$HOME/.local/bin/ffgrab -r</click>"
fi
ffgrab-rec.svg
Code: Select all
<svg width="18" height="18" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="9" fill="#FF4444"/>
</svg>
Code: Select all
<svg width="18" height="18" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="4" width="18" height="18" rx="3" ry="3" fill="#FF4444"/>
</svg>

Needless to say, I had someoneGPT help me polish the script and translate this






