Share one region picker between screenshot and recording
Both capture scripts carried the same pipeline: workspace rectangles from hyprctl, a hyprpicker freeze, slurp, and the bare-click snap loop. The copies had already diverged once (rotated-display handling), so the pipeline now lives in one hidden helper, omarchy-capture-region-pick, which prints the picked geometry in slurp format. The two callers differ only in what they layer on top. The screenshot needs the freeze to outlive the pick so grim captures frozen content: --keep-freeze leaves hyprpicker running and prints its PID first, with the caller owning the kill. The recording prefers native full-monitor capture: --match-monitor prints monitor:NAME when the geometry exactly matches a display. One deliberate fix along the way: the screenshot snap regex rejected negative coordinates, so bare clicks on monitors positioned left of or above the origin never snapped. The helper uses the recording variant, which handles them. Verified with an 11-case shim harness (snap precedence, rotated-monitor match, freeze ownership, cancellation, negative coordinates) and the live end-to-end screenshot sanity test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3bd6e72211
commit
362d97c151
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Pick a screen region over frozen screen content
|
||||
# omarchy:args=[region|windows|smart|fullscreen] [--keep-freeze] [--match-monitor]
|
||||
# omarchy:hidden=true
|
||||
|
||||
# Prints the picked geometry in slurp's "X,Y WxH" format, or exits 1 when the
|
||||
# pick is cancelled. Shared by screenshot and screen recording so the picker
|
||||
# UX stays identical.
|
||||
#
|
||||
# region freeform selection
|
||||
# windows snap selection to a monitor or window rectangle
|
||||
# smart freeform with window/monitor rects hinted; a bare click
|
||||
# (area < 20px^2) snaps to the rectangle it landed in
|
||||
# fullscreen the focused monitor, no interaction
|
||||
#
|
||||
# --keep-freeze leave the hyprpicker screen freeze running and print its
|
||||
# PID as the first output line (empty when no freeze was
|
||||
# started); the caller owns killing it
|
||||
# --match-monitor print "monitor:NAME" instead when the picked geometry
|
||||
# exactly matches a monitor
|
||||
|
||||
MODE=smart
|
||||
KEEP_FREEZE=false
|
||||
MATCH_MONITOR=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--keep-freeze) KEEP_FREEZE=true ;;
|
||||
--match-monitor) MATCH_MONITOR=true ;;
|
||||
*) MODE=$arg ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# accounting for portrait/transformed displays
|
||||
JQ_MONITOR_GEO='
|
||||
def format_geo:
|
||||
.x as $x | .y as $y |
|
||||
(.width / .scale | floor) as $w |
|
||||
(.height / .scale | floor) as $h |
|
||||
.transform as $t |
|
||||
if $t == 1 or $t == 3 then
|
||||
"\($x),\($y) \($h)x\($w)"
|
||||
else
|
||||
"\($x),\($y) \($w)x\($h)"
|
||||
end;
|
||||
'
|
||||
|
||||
get_rectangles() {
|
||||
local active_workspace=$(hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .activeWorkspace.id')
|
||||
hyprctl monitors -j | jq -r --arg ws "$active_workspace" "${JQ_MONITOR_GEO} .[] | select(.activeWorkspace.id == (\$ws | tonumber)) | format_geo"
|
||||
hyprctl clients -j | jq -r --arg ws "$active_workspace" '.[] | select(.workspace.id == ($ws | tonumber)) | "\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"'
|
||||
}
|
||||
|
||||
FREEZE_PID=""
|
||||
freeze_screen() {
|
||||
hyprpicker -r -z >/dev/null 2>&1 &
|
||||
FREEZE_PID=$!
|
||||
sleep .1
|
||||
}
|
||||
|
||||
cleanup_freeze() {
|
||||
[[ $KEEP_FREEZE == true ]] && return
|
||||
[[ -n $FREEZE_PID ]] && kill $FREEZE_PID 2>/dev/null
|
||||
}
|
||||
trap cleanup_freeze EXIT
|
||||
|
||||
case "$MODE" in
|
||||
region)
|
||||
freeze_screen
|
||||
SELECTION=$(slurp 2>/dev/null)
|
||||
;;
|
||||
windows)
|
||||
freeze_screen
|
||||
SELECTION=$(get_rectangles | slurp -r 2>/dev/null)
|
||||
;;
|
||||
fullscreen)
|
||||
SELECTION=$(hyprctl monitors -j | jq -r "${JQ_MONITOR_GEO} .[] | select(.focused == true) | format_geo")
|
||||
;;
|
||||
smart | *)
|
||||
RECTS=$(get_rectangles)
|
||||
freeze_screen
|
||||
SELECTION=$(echo "$RECTS" | slurp 2>/dev/null)
|
||||
|
||||
# A bare click (area < 20px^2) snaps to whichever rectangle it landed in,
|
||||
# so users don't end up with accidental 2px captures. X and Y can be
|
||||
# negative (Hyprland monitor positions in multi-display layouts).
|
||||
if [[ $SELECTION =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]] && ((BASH_REMATCH[3] * BASH_REMATCH[4] < 20)); then
|
||||
click_x=${BASH_REMATCH[1]}
|
||||
click_y=${BASH_REMATCH[2]}
|
||||
|
||||
while IFS= read -r rect; do
|
||||
[[ $rect =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]] || continue
|
||||
rect_x=${BASH_REMATCH[1]}
|
||||
rect_y=${BASH_REMATCH[2]}
|
||||
rect_width=${BASH_REMATCH[3]}
|
||||
rect_height=${BASH_REMATCH[4]}
|
||||
|
||||
if ((click_x >= rect_x && click_x < rect_x + rect_width && click_y >= rect_y && click_y < rect_y + rect_height)); then
|
||||
SELECTION="${rect_x},${rect_y} ${rect_width}x${rect_height}"
|
||||
break
|
||||
fi
|
||||
done <<<"$RECTS"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
[[ $KEEP_FREEZE == true ]] && echo "$FREEZE_PID"
|
||||
|
||||
[[ -n $SELECTION ]] || exit 1
|
||||
|
||||
if [[ $MATCH_MONITOR == true ]]; then
|
||||
monitor=$(hyprctl monitors -j | jq -r --arg geo "$SELECTION" "${JQ_MONITOR_GEO} .[] | select(format_geo == \$geo) | .name" | head -1)
|
||||
if [[ -n $monitor ]]; then
|
||||
echo "monitor:$monitor"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$SELECTION"
|
||||
@@ -102,75 +102,25 @@ default_resolution() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Monitor + window rectangles on the focused workspace, in slurp's "X,Y WxH" format.
|
||||
# Mirrors omarchy-capture-screenshot so the picker UX is identical, including
|
||||
# swapping monitor dimensions on rotated displays (transform 1/3).
|
||||
JQ_MONITOR_GEO='
|
||||
def format_geo:
|
||||
.x as $x | .y as $y |
|
||||
(.width / .scale | floor) as $w |
|
||||
(.height / .scale | floor) as $h |
|
||||
.transform as $t |
|
||||
if $t == 1 or $t == 3 then
|
||||
"\($x),\($y) \($h)x\($w)"
|
||||
else
|
||||
"\($x),\($y) \($w)x\($h)"
|
||||
end;
|
||||
'
|
||||
|
||||
get_rectangles() {
|
||||
local active_workspace=$(hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .activeWorkspace.id')
|
||||
hyprctl monitors -j | jq -r --arg ws "$active_workspace" "${JQ_MONITOR_GEO} .[] | select(.activeWorkspace.id == (\$ws | tonumber)) | format_geo"
|
||||
hyprctl clients -j | jq -r --arg ws "$active_workspace" '
|
||||
.[] | select(.workspace.id == ($ws | tonumber)) |
|
||||
"\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"'
|
||||
}
|
||||
|
||||
# Echoes "monitor:NAME" when the selection matches an entire monitor, otherwise
|
||||
# "region:WxH+X+Y" with physical-pixel coordinates ready for gpu-screen-recorder.
|
||||
# Returns non-zero if the user cancelled the picker.
|
||||
# Echoes "monitor:NAME" when the selection matches an entire monitor (prefer
|
||||
# -w <monitor> over a region capture — same kms backend, but no scaling math
|
||||
# and full native res), otherwise "region:WxH+X+Y". Returns non-zero if the
|
||||
# user cancelled the picker.
|
||||
select_capture_target() {
|
||||
local rects=$(get_rectangles)
|
||||
hyprpicker -r -z >/dev/null 2>&1 &
|
||||
local picker_pid=$!
|
||||
sleep .1
|
||||
local selection=$(echo "$rects" | slurp 2>/dev/null)
|
||||
kill $picker_pid 2>/dev/null
|
||||
local target
|
||||
target=$(omarchy-capture-region-pick smart --match-monitor) || return 1
|
||||
|
||||
# X and Y can be negative (Hyprland monitor positions in multi-display layouts);
|
||||
# widths and heights are always positive.
|
||||
[[ $selection =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]] || return 1
|
||||
local sx=${BASH_REMATCH[1]} sy=${BASH_REMATCH[2]}
|
||||
local sw=${BASH_REMATCH[3]} sh=${BASH_REMATCH[4]}
|
||||
|
||||
# A bare click (area < 20px²) snaps to whichever rectangle the click landed
|
||||
# inside, so users don't end up with accidental 2px recordings.
|
||||
if ((sw * sh < 20)); then
|
||||
while IFS= read -r rect; do
|
||||
[[ $rect =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]] || continue
|
||||
local rx=${BASH_REMATCH[1]} ry=${BASH_REMATCH[2]}
|
||||
local rw=${BASH_REMATCH[3]} rh=${BASH_REMATCH[4]}
|
||||
if ((sx >= rx && sx < rx + rw && sy >= ry && sy < ry + rh)); then
|
||||
sx=$rx sy=$ry sw=$rw sh=$rh
|
||||
break
|
||||
fi
|
||||
done <<<"$rects"
|
||||
fi
|
||||
|
||||
# When the selection exactly matches a monitor, prefer -w <monitor> over a
|
||||
# region capture — same kms backend, but no scaling math and full native res.
|
||||
local monitor=$(hyprctl monitors -j | jq -r --arg geo "${sx},${sy} ${sw}x${sh}" \
|
||||
"${JQ_MONITOR_GEO} .[] | select(format_geo == \$geo) | .name" | head -1)
|
||||
|
||||
if [[ -n $monitor ]]; then
|
||||
echo "monitor:$monitor"
|
||||
if [[ $target == monitor:* ]]; then
|
||||
echo "$target"
|
||||
return
|
||||
fi
|
||||
|
||||
[[ $target =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]] || return 1
|
||||
|
||||
# gpu-screen-recorder wants region geometry in the compositor's logical
|
||||
# coordinate space — same space slurp returns — so pass the values through
|
||||
# untouched. (gsr scales to physical pixels itself based on the monitor.)
|
||||
echo "region:${sw}x${sh}+${sx}+${sy}"
|
||||
echo "region:${BASH_REMATCH[3]}x${BASH_REMATCH[4]}+${BASH_REMATCH[1]}+${BASH_REMATCH[2]}"
|
||||
}
|
||||
|
||||
start_screenrecording() {
|
||||
|
||||
@@ -37,81 +37,15 @@ open_editor() {
|
||||
MODE="${1:-smart}"
|
||||
PROCESSING="${2:-slurp}"
|
||||
|
||||
# accounting for portrait/transformed displays
|
||||
JQ_MONITOR_GEO='
|
||||
def format_geo:
|
||||
.x as $x | .y as $y |
|
||||
(.width / .scale | floor) as $w |
|
||||
(.height / .scale | floor) as $h |
|
||||
.transform as $t |
|
||||
if $t == 1 or $t == 3 then
|
||||
"\($x),\($y) \($h)x\($w)"
|
||||
else
|
||||
"\($x),\($y) \($w)x\($h)"
|
||||
end;
|
||||
'
|
||||
|
||||
get_rectangles() {
|
||||
local active_workspace=$(hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .activeWorkspace.id')
|
||||
hyprctl monitors -j | jq -r --arg ws "$active_workspace" "${JQ_MONITOR_GEO} .[] | select(.activeWorkspace.id == (\$ws | tonumber)) | format_geo"
|
||||
hyprctl clients -j | jq -r --arg ws "$active_workspace" '.[] | select(.workspace.id == ($ws | tonumber)) | "\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"'
|
||||
}
|
||||
|
||||
# Keep hyprpicker alive until after grim captures so the screenshot sees the
|
||||
# frozen overlay rather than live content shifting during teardown.
|
||||
# The picker leaves the screen freeze running (PID on its first output line)
|
||||
# so grim captures the frozen overlay rather than live content shifting
|
||||
# during teardown.
|
||||
cleanup_freeze() {
|
||||
[[ -n $PID ]] && kill $PID 2>/dev/null
|
||||
[[ -n $FREEZE_PID ]] && kill $FREEZE_PID 2>/dev/null
|
||||
}
|
||||
trap cleanup_freeze EXIT
|
||||
|
||||
# Select based on mode
|
||||
case "$MODE" in
|
||||
region)
|
||||
hyprpicker -r -z >/dev/null 2>&1 &
|
||||
PID=$!
|
||||
sleep .1
|
||||
SELECTION=$(slurp 2>/dev/null)
|
||||
;;
|
||||
windows)
|
||||
hyprpicker -r -z >/dev/null 2>&1 &
|
||||
PID=$!
|
||||
sleep .1
|
||||
SELECTION=$(get_rectangles | slurp -r 2>/dev/null)
|
||||
;;
|
||||
fullscreen)
|
||||
SELECTION=$(hyprctl monitors -j | jq -r "${JQ_MONITOR_GEO} .[] | select(.focused == true) | format_geo")
|
||||
;;
|
||||
smart | *)
|
||||
RECTS=$(get_rectangles)
|
||||
hyprpicker -r -z >/dev/null 2>&1 &
|
||||
PID=$!
|
||||
sleep .1
|
||||
SELECTION=$(echo "$RECTS" | slurp 2>/dev/null)
|
||||
|
||||
# If the selection area is L * W < 20, we'll assume you were trying to select whichever
|
||||
# window or output it was inside of to prevent accidental 2px snapshots
|
||||
if [[ $SELECTION =~ ^([0-9]+),([0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]]; then
|
||||
if ((${BASH_REMATCH[3]} * ${BASH_REMATCH[4]} < 20)); then
|
||||
click_x="${BASH_REMATCH[1]}"
|
||||
click_y="${BASH_REMATCH[2]}"
|
||||
|
||||
while IFS= read -r rect; do
|
||||
if [[ $rect =~ ^([0-9]+),([0-9]+)[[:space:]]([0-9]+)x([0-9]+) ]]; then
|
||||
rect_x="${BASH_REMATCH[1]}"
|
||||
rect_y="${BASH_REMATCH[2]}"
|
||||
rect_width="${BASH_REMATCH[3]}"
|
||||
rect_height="${BASH_REMATCH[4]}"
|
||||
|
||||
if ((click_x >= rect_x && click_x < rect_x + rect_width && click_y >= rect_y && click_y < rect_y + rect_height)); then
|
||||
SELECTION="${rect_x},${rect_y} ${rect_width}x${rect_height}"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done <<<"$RECTS"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
{ read -r FREEZE_PID; read -r SELECTION; } < <(omarchy-capture-region-pick "$MODE" --keep-freeze)
|
||||
|
||||
[[ -z $SELECTION ]] && exit 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user