From 362d97c151a3a0be03be69f8f53fdeb061767902 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 3 Jul 2026 09:26:46 -0700 Subject: [PATCH] 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 --- bin/omarchy-capture-region-pick | 120 ++++++++++++++++++++++++++++ bin/omarchy-capture-screenrecording | 72 +++-------------- bin/omarchy-capture-screenshot | 76 ++---------------- 3 files changed, 136 insertions(+), 132 deletions(-) create mode 100755 bin/omarchy-capture-region-pick diff --git a/bin/omarchy-capture-region-pick b/bin/omarchy-capture-region-pick new file mode 100755 index 00000000..bf9c5526 --- /dev/null +++ b/bin/omarchy-capture-region-pick @@ -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" diff --git a/bin/omarchy-capture-screenrecording b/bin/omarchy-capture-screenrecording index 51a1ea9f..92ea4c3a 100755 --- a/bin/omarchy-capture-screenrecording +++ b/bin/omarchy-capture-screenrecording @@ -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 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 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() { diff --git a/bin/omarchy-capture-screenshot b/bin/omarchy-capture-screenshot index 822e66e2..be133a3d 100755 --- a/bin/omarchy-capture-screenshot +++ b/bin/omarchy-capture-screenshot @@ -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