diff --git a/linux_configuration/i3-configuration/i3/config b/linux_configuration/i3-configuration/i3/config index 95e4b6d..1819c88 100644 --- a/linux_configuration/i3-configuration/i3/config +++ b/linux_configuration/i3-configuration/i3/config @@ -213,3 +213,7 @@ bar { } bindsym $mod+d exec "dmenu_run -nf '#F8F8F2' -nb '#282A36' -sb '#6272A4' -sf '#F8F8F2' -fn 'monospace-10' -p 'dmenu%'" + +# Screen replay buffer +exec --no-startup-id /home/kuchy/linux-configuration/scripts/utils/screen_replay.sh start +bindsym $mod+Shift+F12 exec --no-startup-id /home/kuchy/linux-configuration/scripts/utils/screen_replay.sh save diff --git a/linux_configuration/scripts/utils/screen_replay.sh b/linux_configuration/scripts/utils/screen_replay.sh new file mode 100755 index 0000000..aed4b27 --- /dev/null +++ b/linux_configuration/scripts/utils/screen_replay.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# screen_replay.sh - Instant replay buffer for Linux (X11) +# Continuously records screen in segments; on hotkey, saves last 90 seconds +# and immediately plays it in mpv. + +set -euo pipefail + +REPLAY_DIR="${REPLAY_DIR:-/tmp/screen_replay}" +SAVE_DIR="${SAVE_DIR:-$HOME/Videos/replays}" +PID_FILE="$REPLAY_DIR/daemon.pid" +LOCK_FILE="$REPLAY_DIR/save.lock" +BUFFER_SECS="${BUFFER_SECS:-90}" +SEG_SECS=15 +# segments needed: ceil(buffer/seg) + 1 for the one being written + 1 margin +SEG_WRAP=$(( (BUFFER_SECS / SEG_SECS) + 2 )) +FRAMERATE="${FRAMERATE:-30}" + +die() { printf 'Error: %s\n' "$*" >&2; exit 1; } + +FFMPEG="/usr/bin/ffmpeg" +FFPROBE="/usr/bin/ffprobe" + +check_deps() { + local missing=() + [[ -x "$FFMPEG" ]] || missing+=("ffmpeg") + [[ -x "$FFPROBE" ]] || missing+=("ffprobe") + for cmd in mpv xrandr; do + command -v "$cmd" &>/dev/null || missing+=("$cmd") + done + if (( ${#missing[@]} )); then + die "Missing dependencies: ${missing[*]}" + fi +} + +is_running() { + [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null +} + +cmd_start() { + check_deps + mkdir -p "$REPLAY_DIR" "$SAVE_DIR" + + if is_running; then + echo "Already running (PID: $(cat "$PID_FILE"))" + return 0 + fi + + local display="${DISPLAY:-:0}" + + # Detect primary monitor geometry via xrandr + local xrandr_line + xrandr_line=$(xrandr | grep ' connected primary ' | head -1) + [[ -n "$xrandr_line" ]] || die "Could not detect primary monitor (no 'primary' flag in xrandr)" + + local geometry + geometry=$(printf '%s' "$xrandr_line" | grep -oP '\d+x\d+\+\d+\+\d+') + [[ -n "$geometry" ]] || die "Could not parse primary monitor geometry" + + local resolution offset_x offset_y + resolution=$(printf '%s' "$geometry" | grep -oP '^\d+x\d+') + offset_x=$(printf '%s' "$geometry" | grep -oP '(?<=\+)\d+' | sed -n '1p') + offset_y=$(printf '%s' "$geometry" | grep -oP '(?<=\+)\d+' | sed -n '2p') + + # Clean stale segments + rm -f "$REPLAY_DIR"/seg*.mkv + + "$FFMPEG" -f x11grab -video_size "$resolution" -framerate "$FRAMERATE" \ + -i "${display}+${offset_x},${offset_y}" \ + -c:v libx264 -preset ultrafast -crf 23 \ + -g "$FRAMERATE" \ + -f segment -segment_time "$SEG_SECS" \ + -segment_wrap "$SEG_WRAP" \ + -reset_timestamps 1 \ + "$REPLAY_DIR/seg%02d.mkv" \ + "$REPLAY_DIR/ffmpeg.log" 2>&1 & + + local pid=$! + echo "$pid" > "$PID_FILE" + disown "$pid" + + # Give ffmpeg a moment to start (or fail) + sleep 1 + if ! kill -0 "$pid" 2>/dev/null; then + rm -f "$PID_FILE" + echo "ffmpeg failed to start. Log:" >&2 + cat "$REPLAY_DIR/ffmpeg.log" >&2 + return 1 + fi + + echo "Replay daemon started (PID: $pid, buffer: ${BUFFER_SECS}s, segments: ${SEG_WRAP}x${SEG_SECS}s)" +} + +cmd_save() { + is_running || die "Daemon not running. Start with: $0 start" + + # Prevent concurrent saves + exec 9>"$LOCK_FILE" + flock -n 9 || die "Another save is already in progress" + + local timestamp output concat_file + timestamp=$(date +%Y%m%d_%H%M%S) + output="$SAVE_DIR/replay_${timestamp}.mp4" + concat_file=$(mktemp /tmp/replay_concat.XXXXXX) + + # Collect segments sorted by modification time (oldest first) + local -a segments=() + while IFS=$'\t' read -r _ path; do + segments+=("$path") + done < <(find "$REPLAY_DIR" -maxdepth 1 -name 'seg*.mkv' -printf '%T@\t%p\n' 2>/dev/null | sort -n) + + (( ${#segments[@]} >= 1 )) || die "No recorded data yet" + + # The last segment is being actively written by ffmpeg. + # Snapshot it so we capture everything up to this moment. + local active_seg="${segments[-1]}" + local snapshot="$REPLAY_DIR/_snapshot.mkv" + cp -- "$active_seg" "$snapshot" + + # Use completed segments + snapshot of the active one + local -a use_segments=() + for seg in "${segments[@]:0:${#segments[@]}-1}"; do + use_segments+=("$seg") + done + use_segments+=("$snapshot") + + # Build ffmpeg concat list + for seg in "${use_segments[@]}"; do + printf "file '%s'\n" "$seg" + done > "$concat_file" + + # Concatenate with stream copy (near-instant) + "$FFMPEG" -f concat -safe 0 -i "$concat_file" \ + -c copy -y "$output" 2>/dev/null + + # Trim to last BUFFER_SECS if the recording is longer + local duration start + duration=$("$FFPROBE" -v error -show_entries format=duration -of csv=p=0 "$output") + start=$(awk "BEGIN {v = $duration - $BUFFER_SECS; print (v > 0 ? v : 0)}") + + if awk "BEGIN {exit !($start > 1)}"; then + local trimmed="${output%.mp4}_tmp.mp4" + "$FFMPEG" -ss "$start" -i "$output" \ + -c copy -avoid_negative_ts make_zero \ + -y "$trimmed" 2>/dev/null + mv -- "$trimmed" "$output" + fi + + trap - EXIT + rm -f "$concat_file" "$REPLAY_DIR/_snapshot.mkv" + + echo "Saved: $output" + mpv --force-window=immediate "$output" &>/dev/null & + disown +} + +cmd_stop() { + if is_running; then + local pid + pid=$(cat "$PID_FILE") + kill -INT "$pid" 2>/dev/null || true + for _ in {1..20}; do + kill -0 "$pid" 2>/dev/null || break + sleep 0.1 + done + kill -9 "$pid" 2>/dev/null || true + rm -f "$PID_FILE" "$REPLAY_DIR"/seg*.mkv "$LOCK_FILE" + echo "Daemon stopped" + else + echo "Not running" + rm -f "$PID_FILE" + fi +} + +cmd_status() { + if is_running; then + echo "Running (PID: $(cat "$PID_FILE"))" + local count size + count=$(find "$REPLAY_DIR" -name 'seg*.mkv' 2>/dev/null | wc -l) + size=$(du -sh "$REPLAY_DIR" 2>/dev/null | cut -f1) + echo "Segments: $count/${SEG_WRAP}, Disk: $size" + else + echo "Not running" + fi +} + +usage() { + cat <