#!/usr/bin/env bash # Capture an area, active monitor, or all monitors and edit in Satty. set -euo pipefail umask 077 fail() { echo "ferese-screenshot: $*" >&2 if command -v notify-send >/dev/null; then notify-send --app-name=Ferese --urgency=critical 'Screenshot failed' "$*" || true fi exit 1 } mode=area case ${1:-} in '') ;; --full) mode=full; shift ;; --all) mode=all; shift ;; --help|-h) echo 'Usage: ferese-screenshot [--full | --all]' echo 'Select an area; --full captures the active monitor; --all captures all monitors.' exit 0 ;; *) fail 'Usage: ferese-screenshot [--full | --all]' ;; esac (($# == 0)) || fail 'Usage: ferese-screenshot [--full | --all]' for tool in feresectl satty wl-copy flock python3; do command -v "$tool" >/dev/null || fail "Required command not found: $tool" done : "${XDG_RUNTIME_DIR:?A Wayland login session is required}" exec 9>"$XDG_RUNTIME_DIR/ferese-screenshot.lock" flock -n 9 || exit 0 capture_args=() if [[ $mode == area ]]; then command -v slurp >/dev/null || fail 'Required command not found: slurp' # Escape cancels without creating a file or changing the clipboard. geometry=$(slurp) || exit 0 [[ -n $geometry ]] || exit 0 capture_args=(-g "$geometry") elif [[ $mode == full ]]; then # Resolve the active monitor before Satty can change keyboard focus. # Never fall back to an all-monitor capture when IPC is unavailable. # This is the only mode that needs the output list; the editor is sized # from the captured image instead. geometry=$(feresectl get-outputs | python3 -c ' import json, sys outputs = [output for output in json.load(sys.stdin) if output.get("enabled")] focused = [output for output in outputs if output.get("focused")] selected = focused if len(focused) == 1 else outputs if len(outputs) == 1 else [] if len(selected) != 1 or any(selected[0].get(k) is None for k in ("x", "y", "width", "height")): sys.exit(1) print("%d,%d %dx%d" % (selected[0]["x"], selected[0]["y"], selected[0]["width"], selected[0]["height"])) ') || fail 'Could not determine the active monitor. Use area selection or --all.' capture_args=(-g "$geometry") fi pictures=$(xdg-user-dir PICTURES 2>/dev/null) || pictures="$HOME/Pictures" directory=${pictures:-$HOME/Pictures}/Screenshots mkdir -p -- "$directory" || fail 'Could not create the screenshot directory.' file=$(mktemp "$directory/Screenshot-$(date +%Y-%m-%d_%H-%M-%S)-XXXXXX.png") || fail 'Could not create the screenshot file.' input='' capture_error='' trap '[[ -z $input ]] || rm -f -- "$input"; [[ -z $capture_error ]] || rm -f -- "$capture_error"; [[ -s $file ]] || rm -f -- "$file"' EXIT input=$(mktemp "$XDG_RUNTIME_DIR/ferese-screenshot-XXXXXX.png") || fail 'Could not create the temporary capture.' capture_error=$(mktemp "$XDG_RUNTIME_DIR/ferese-screenshot-error-XXXXXX") || fail 'Could not create the capture log.' if ! feresectl screenshot "${capture_args[@]}" >"$input" 2>"$capture_error"; then detail=$(<"$capture_error") fail "Could not capture the screen. ${detail:-The compositor returned no error details.}" fi # Size the editor to the image that was actually captured, so a small area # selection does not open a huge window and a multi-monitor --all capture is not # sized from whichever output happened to be focused. Satty treats this as a # default size, and Ferese floats the window at whatever it commits. editor_size=$(python3 -c ' import sys header = open(sys.argv[1], "rb").read(24) if header[:8] != b"\x89PNG\r\n\x1a\n" or header[12:16] != b"IHDR": sys.exit(1) width = int.from_bytes(header[16:20], "big") height = int.from_bytes(header[20:24], "big") if width <= 0 or height <= 0: sys.exit(1) MAX_W, MAX_H = 1500, 920 MIN_W, MIN_H = 480, 360 factor = min(MAX_W / width, MAX_H / height, 2.0) target_w = max(MIN_W, round(width * factor)) target_h = max(MIN_H, round(height * factor)) # The minimums can push past the maximums for extreme aspect ratios. shrink = min(MAX_W / target_w, MAX_H / target_h, 1.0) print("%dx%d" % (max(1, round(target_w * shrink)), max(1, round(target_h * shrink)))) ' "$input") || editor_size=1000x700 # Enter saves the annotated image and copies it; Escape discards the capture. # Keep the parent lock through editing. Close it only in Satty so clipboard # helpers cannot inherit the lock and keep later screenshots blocked. satty --filename "$input" --output-filename "$file" \ --app-id dev.ferese.Screenshot \ --resize "$editor_size" \ --no-window-decoration \ --copy-command 'wl-copy --type image/png' \ --save-after-copy \ --actions-on-enter save-to-file,save-to-clipboard,exit \ --actions-on-escape exit 9>&- || fail 'Could not open the screenshot in Satty.' if [[ -s $file ]]; then printf '%s\n' "$file" fi