Warping to a target window's center selects the wrong window when a smaller one covers that center: slurp keeps highlighting the coverer, so Tab could never leave it. Navigation now warps to the most central point that resolves back to the target, and skips windows that hovering could not reach either. Unbinding the picker's transient keys by name also took a same-key binding out of the user's own config with it; the bind handles are now kept and removed individually. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
371 lines
11 KiB
Bash
Executable File
371 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Pick a screen region over frozen screen content
|
|
# omarchy:args=[region|windows|smart|fullscreen] [--keep-freeze] [--match-monitor] | --take-fullscreen | --take-window | --select-window <next|prev|left|right|up|down>
|
|
# 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
|
|
|
|
FULLSCREEN_MARKER="${XDG_RUNTIME_DIR:-/tmp}/omarchy-capture-region-fullscreen"
|
|
WINDOW_MARKER="${XDG_RUNTIME_DIR:-/tmp}/omarchy-capture-region-window"
|
|
|
|
# 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;
|
|
'
|
|
|
|
active_workspace() {
|
|
hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .activeWorkspace.id'
|
|
}
|
|
|
|
# Hidden group members and windows stacked at identical geometry collapse to
|
|
# one rectangle: slurp cannot tell them apart, and duplicates would stall the
|
|
# Tab cycle on the first copy.
|
|
window_rects() {
|
|
hyprctl clients -j | jq -r --arg ws "$(active_workspace)" \
|
|
'[.[] | select(.workspace.id == ($ws | tonumber) and .hidden != true) | "\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"] | unique[]'
|
|
}
|
|
|
|
monitor_rects() {
|
|
hyprctl monitors -j | jq -r --arg ws "$(active_workspace)" "${JQ_MONITOR_GEO} .[] | select(.activeWorkspace.id == (\$ws | tonumber)) | format_geo"
|
|
}
|
|
|
|
get_rectangles() {
|
|
monitor_rects
|
|
window_rects
|
|
}
|
|
|
|
focused_monitor_geo() {
|
|
hyprctl monitors -j | jq -r "${JQ_MONITOR_GEO} .[] | select(.focused == true) | format_geo"
|
|
}
|
|
|
|
# slurp highlights the smallest box containing the point and keeps the first
|
|
# one on a tie, so overlapping rectangles resolve the same way here (e.g.
|
|
# floating over tiled). Reads candidates on stdin and leaves the answer in
|
|
# RESOLVED_RECT; returns 1 when no candidate contains the point. Assigning to
|
|
# a global rather than printing keeps the probing in warp_point_in fork-free.
|
|
resolve_rect_at() {
|
|
local x=$1 y=$2
|
|
local rect area
|
|
local smallest_area=0
|
|
|
|
RESOLVED_RECT=""
|
|
|
|
while IFS= read -r rect; do
|
|
[[ $rect =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]] || continue
|
|
((x >= BASH_REMATCH[1] && x < BASH_REMATCH[1] + BASH_REMATCH[3] && y >= BASH_REMATCH[2] && y < BASH_REMATCH[2] + BASH_REMATCH[4])) || continue
|
|
|
|
area=$((BASH_REMATCH[3] * BASH_REMATCH[4]))
|
|
if [[ -z $RESOLVED_RECT ]] || ((area < smallest_area)); then
|
|
RESOLVED_RECT=$rect
|
|
smallest_area=$area
|
|
fi
|
|
done
|
|
|
|
[[ -n $RESOLVED_RECT ]]
|
|
}
|
|
|
|
# A rectangle whose center is covered by a smaller one cannot be selected by
|
|
# warping to that center: slurp would go on highlighting the coverer. Probe
|
|
# points inside the rectangle, nearest its center first, for one that resolves
|
|
# back to it, and leave that point in WARP_X / WARP_Y. Returns 1 when the
|
|
# rectangle is buried well enough that no point resolves to it, in which case
|
|
# hovering could not reach it either.
|
|
warp_point_in() {
|
|
local rect=$1 candidates=$2
|
|
[[ $rect =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]] || return 1
|
|
local rect_x=${BASH_REMATCH[1]} rect_y=${BASH_REMATCH[2]}
|
|
local rect_width=${BASH_REMATCH[3]} rect_height=${BASH_REMATCH[4]}
|
|
local probe x y
|
|
|
|
for probe in "${WARP_PROBES[@]}"; do
|
|
x=$((rect_x + rect_width * ${probe% *} / 8))
|
|
y=$((rect_y + rect_height * ${probe#* } / 8))
|
|
|
|
resolve_rect_at "$x" "$y" <<<"$candidates" || continue
|
|
[[ $RESOLVED_RECT == "$rect" ]] || continue
|
|
|
|
WARP_X=$x
|
|
WARP_Y=$y
|
|
return 0
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# Whatever slurp is highlighting under the cursor. It is fed monitor rects as
|
|
# well as window rects, so a cursor in a gap or over the bar highlights the
|
|
# monitor rather than any window.
|
|
geo_at_cursor() {
|
|
local pos=$(hyprctl cursorpos)
|
|
local x=${pos%,*}
|
|
local y=${pos#*, }
|
|
|
|
if resolve_rect_at "$x" "$y" < <(window_rects) || resolve_rect_at "$x" "$y" < <(monitor_rects); then
|
|
echo "$RESOLVED_RECT"
|
|
else
|
|
focused_monitor_geo
|
|
fi
|
|
}
|
|
|
|
# Keyboard control while slurp is open: binds scoped to slurp's layer
|
|
# surface (default/hypr/bindings/utilities.lua) invoke these modes. The
|
|
# --take-* modes flag the intent with a marker file and dismiss slurp.
|
|
if [[ ${1:-} == "--take-fullscreen" ]]; then
|
|
pgrep -x slurp >/dev/null || exit 0
|
|
touch "$FULLSCREEN_MARKER"
|
|
pkill -x slurp
|
|
exit 0
|
|
fi
|
|
|
|
if [[ ${1:-} == "--take-window" ]]; then
|
|
pgrep -x slurp >/dev/null || exit 0
|
|
touch "$WINDOW_MARKER"
|
|
pkill -x slurp
|
|
exit 0
|
|
fi
|
|
|
|
# Warps the cursor to another window's center, so slurp's own hover
|
|
# highlight tracks the selection.
|
|
if [[ ${1:-} == "--select-window" ]]; then
|
|
pgrep -x slurp >/dev/null || exit 0
|
|
|
|
direction=${2:-}
|
|
pos=$(hyprctl cursorpos)
|
|
origin_x=${pos%,*}
|
|
origin_y=${pos#*, }
|
|
|
|
candidates=$(window_rects)
|
|
|
|
# Eighth fractions of a rectangle's width and height, ordered by distance
|
|
# from its center so warp_point_in prefers the most central point it can use.
|
|
mapfile -t WARP_PROBES < <(
|
|
for fx in {1..7}; do
|
|
for fy in {1..7}; do
|
|
printf '%d %d %d\n' $(((fx - 4) * (fx - 4) + (fy - 4) * (fy - 4))) "$fx" "$fy"
|
|
done
|
|
done | sort -n | cut -d' ' -f2-
|
|
)
|
|
|
|
# Only rectangles that hovering could actually reach take part in navigation,
|
|
# each paired with the point to warp to.
|
|
declare -A warp_points
|
|
reachable=""
|
|
while IFS= read -r rect; do
|
|
warp_point_in "$rect" "$candidates" || continue
|
|
warp_points[$rect]="$WARP_X $WARP_Y"
|
|
reachable+="$rect"$'\n'
|
|
done <<<"$candidates"
|
|
[[ -n $reachable ]] || exit 0
|
|
|
|
# Reading order: top-to-bottom, then left-to-right.
|
|
rects=$(while IFS= read -r rect; do
|
|
[[ $rect =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]] || continue
|
|
printf '%d\t%d\t%s\n' "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}" "$rect"
|
|
done <<<"$reachable" | sort -n -k1,1 -k2,2 | cut -f3-)
|
|
|
|
# The selection to move from is the one slurp highlights, resolved from the
|
|
# same list in the same order as --take-window so navigation and capture
|
|
# never disagree. Measure from its center.
|
|
current=""
|
|
resolve_rect_at "$origin_x" "$origin_y" <<<"$candidates" && current=$RESOLVED_RECT
|
|
|
|
if [[ $current =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]]; then
|
|
origin_x=$((BASH_REMATCH[1] + BASH_REMATCH[3] / 2))
|
|
origin_y=$((BASH_REMATCH[2] + BASH_REMATCH[4] / 2))
|
|
fi
|
|
|
|
target=""
|
|
|
|
case $direction in
|
|
next | prev)
|
|
mapfile -t ordered <<<"$rects"
|
|
count=${#ordered[@]}
|
|
current_index=-1
|
|
|
|
for i in "${!ordered[@]}"; do
|
|
if [[ -n $current && ${ordered[i]} == "$current" ]]; then
|
|
current_index=$i
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ $direction == next ]]; then
|
|
target=${ordered[$(((current_index + 1) % count))]}
|
|
elif ((current_index == -1)); then
|
|
target=${ordered[count - 1]}
|
|
else
|
|
target=${ordered[$(((current_index - 1 + count) % count))]}
|
|
fi
|
|
;;
|
|
left | right | up | down)
|
|
best_score=""
|
|
|
|
while IFS= read -r rect; do
|
|
[[ $rect == "$current" ]] && continue
|
|
[[ $rect =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]] || continue
|
|
center_x=$((BASH_REMATCH[1] + BASH_REMATCH[3] / 2))
|
|
center_y=$((BASH_REMATCH[2] + BASH_REMATCH[4] / 2))
|
|
|
|
case $direction in
|
|
left)
|
|
primary=$((origin_x - center_x))
|
|
perp=$((center_y - origin_y))
|
|
;;
|
|
right)
|
|
primary=$((center_x - origin_x))
|
|
perp=$((center_y - origin_y))
|
|
;;
|
|
up)
|
|
primary=$((origin_y - center_y))
|
|
perp=$((center_x - origin_x))
|
|
;;
|
|
down)
|
|
primary=$((center_y - origin_y))
|
|
perp=$((center_x - origin_x))
|
|
;;
|
|
esac
|
|
|
|
((primary > 0)) || continue
|
|
((perp < 0)) && perp=$((-perp))
|
|
score=$((primary + perp * 2))
|
|
|
|
if [[ -z $best_score ]] || ((score < best_score)); then
|
|
best_score=$score
|
|
target=$rect
|
|
fi
|
|
done <<<"$rects"
|
|
;;
|
|
*)
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if [[ -n $target && -n ${warp_points[$target]} ]]; then
|
|
read -r target_x target_y <<<"${warp_points[$target]}"
|
|
hyprctl eval "hl.dispatch(hl.dsp.cursor.move({ x = $target_x, y = $target_y }))" >/dev/null
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
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
|
|
|
|
# Runs slurp; an empty result with a marker present means one of the --take-*
|
|
# binds was pressed, so the highlighted rectangle or the monitor is the
|
|
# selection.
|
|
pick() {
|
|
local selection
|
|
rm -f "$FULLSCREEN_MARKER" "$WINDOW_MARKER"
|
|
selection=$(slurp "$@" 2>/dev/null)
|
|
|
|
if [[ -z $selection && -e $FULLSCREEN_MARKER ]]; then
|
|
rm -f "$FULLSCREEN_MARKER"
|
|
selection=$(focused_monitor_geo)
|
|
elif [[ -z $selection && -e $WINDOW_MARKER ]]; then
|
|
rm -f "$WINDOW_MARKER"
|
|
selection=$(geo_at_cursor)
|
|
fi
|
|
|
|
printf '%s' "$selection"
|
|
}
|
|
|
|
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=$(pick)
|
|
;;
|
|
windows)
|
|
freeze_screen
|
|
SELECTION=$(get_rectangles | pick -r)
|
|
;;
|
|
fullscreen)
|
|
SELECTION=$(focused_monitor_geo)
|
|
;;
|
|
smart | *)
|
|
RECTS=$(get_rectangles)
|
|
freeze_screen
|
|
SELECTION=$(echo "$RECTS" | pick)
|
|
|
|
# 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"
|