Reach every window the region picker can highlight

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>
This commit is contained in:
David Heinemeier Hansson
2026-08-10 10:17:08 -07:00
co-authored by Claude Opus 5
parent 366c708e44
commit 199bd01f94
2 changed files with 84 additions and 27 deletions
+68 -16
View File
@@ -62,28 +62,58 @@ focused_monitor_geo() {
hyprctl monitors -j | jq -r "${JQ_MONITOR_GEO} .[] | select(.focused == true) | format_geo" hyprctl monitors -j | jq -r "${JQ_MONITOR_GEO} .[] | select(.focused == true) | format_geo"
} }
# slurp highlights the smallest box containing the cursor and keeps the first # 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. # one on a tie, so overlapping rectangles resolve the same way here (e.g.
# floating over tiled). Reads candidates on stdin, exits 1 when none match. # floating over tiled). Reads candidates on stdin and leaves the answer in
smallest_rect_at() { # 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 x=$1 y=$2
local rect area local rect area
local smallest=""
local smallest_area=0 local smallest_area=0
RESOLVED_RECT=""
while IFS= read -r rect; do while IFS= read -r rect; do
[[ $rect =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]] || continue [[ $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 ((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])) area=$((BASH_REMATCH[3] * BASH_REMATCH[4]))
if [[ -z $smallest ]] || ((area < smallest_area)); then if [[ -z $RESOLVED_RECT ]] || ((area < smallest_area)); then
smallest=$rect RESOLVED_RECT=$rect
smallest_area=$area smallest_area=$area
fi fi
done done
[[ -n $smallest ]] || return 1 [[ -n $RESOLVED_RECT ]]
echo "$smallest" }
# 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 # Whatever slurp is highlighting under the cursor. It is fed monitor rects as
@@ -94,9 +124,11 @@ geo_at_cursor() {
local x=${pos%,*} local x=${pos%,*}
local y=${pos#*, } local y=${pos#*, }
smallest_rect_at "$x" "$y" < <(window_rects) || if resolve_rect_at "$x" "$y" < <(window_rects) || resolve_rect_at "$x" "$y" < <(monitor_rects); then
smallest_rect_at "$x" "$y" < <(monitor_rects) || echo "$RESOLVED_RECT"
else
focused_monitor_geo focused_monitor_geo
fi
} }
# Keyboard control while slurp is open: binds scoped to slurp's layer # Keyboard control while slurp is open: binds scoped to slurp's layer
@@ -128,17 +160,38 @@ if [[ ${1:-} == "--select-window" ]]; then
candidates=$(window_rects) 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. # Reading order: top-to-bottom, then left-to-right.
rects=$(while IFS= read -r rect; do rects=$(while IFS= read -r rect; do
[[ $rect =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]] || continue [[ $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" printf '%d\t%d\t%s\n' "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}" "$rect"
done <<<"$candidates" | sort -n -k1,1 -k2,2 | cut -f3-) done <<<"$reachable" | sort -n -k1,1 -k2,2 | cut -f3-)
[[ -n $rects ]] || exit 0
# The selection to move from is the one slurp highlights, resolved from the # 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 # same list in the same order as --take-window so navigation and capture
# never disagree. Measure from its center. # never disagree. Measure from its center.
current=$(smallest_rect_at "$origin_x" "$origin_y" <<<"$candidates") 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 if [[ $current =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]]; then
origin_x=$((BASH_REMATCH[1] + BASH_REMATCH[3] / 2)) origin_x=$((BASH_REMATCH[1] + BASH_REMATCH[3] / 2))
@@ -211,9 +264,8 @@ if [[ ${1:-} == "--select-window" ]]; then
;; ;;
esac esac
if [[ $target =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]]; then if [[ -n $target && -n ${warp_points[$target]} ]]; then
target_x=$((BASH_REMATCH[1] + BASH_REMATCH[3] / 2)) read -r target_x target_y <<<"${warp_points[$target]}"
target_y=$((BASH_REMATCH[2] + BASH_REMATCH[4] / 2))
hyprctl eval "hl.dispatch(hl.dsp.cursor.move({ x = $target_x, y = $target_y }))" >/dev/null hyprctl eval "hl.dispatch(hl.dsp.cursor.move({ x = $target_x, y = $target_y }))" >/dev/null
fi fi
exit 0 exit 0
+15 -10
View File
@@ -44,18 +44,26 @@ o.bind("SUPER + CTRL + PRINT", "Extract text (OCR) from screenshot", "omarchy-ca
-- Keyboard control for the slurp region picker (see omarchy-capture-region). -- Keyboard control for the slurp region picker (see omarchy-capture-region).
-- The binds live exactly as long as a selection layer is on screen (slurp -- The binds live exactly as long as a selection layer is on screen (slurp
-- opens one per monitor), so they cannot leak or get stuck. -- opens one per monitor), so they cannot leak or get stuck.
-- Unbinding by key would take a same-key binding out of the user's own config
-- with it, so each handle is kept and removed individually.
local selection_layers = 0 local selection_layers = 0
local selection_binds = {}
hl.on("layer.opened", function(layer) hl.on("layer.opened", function(layer)
if layer.namespace == "selection" then if layer.namespace == "selection" then
selection_layers = selection_layers + 1 selection_layers = selection_layers + 1
if selection_layers == 1 then if selection_layers == 1 then
hl.bind("RETURN", hl.dsp.exec_cmd("omarchy-capture-region --take-window"), { description = "Capture highlighted window" }) selection_binds = {
hl.bind("CTRL + RETURN", hl.dsp.exec_cmd("omarchy-capture-region --take-fullscreen"), { description = "Capture entire screen" }) hl.bind("RETURN", hl.dsp.exec_cmd("omarchy-capture-region --take-window"), { description = "Capture highlighted window" }),
hl.bind("TAB", hl.dsp.exec_cmd("omarchy-capture-region --select-window next"), { description = "Select next window to capture" }) hl.bind("CTRL + RETURN", hl.dsp.exec_cmd("omarchy-capture-region --take-fullscreen"), { description = "Capture entire screen" }),
hl.bind("CTRL + TAB", hl.dsp.exec_cmd("omarchy-capture-region --select-window prev"), { description = "Select previous window to capture" }) hl.bind("TAB", hl.dsp.exec_cmd("omarchy-capture-region --select-window next"), { description = "Select next window to capture" }),
hl.bind("CTRL + TAB", hl.dsp.exec_cmd("omarchy-capture-region --select-window prev"), { description = "Select previous window to capture" }),
}
for _, direction in ipairs({ "left", "right", "up", "down" }) do for _, direction in ipairs({ "left", "right", "up", "down" }) do
table.insert(
selection_binds,
hl.bind(direction:upper(), hl.dsp.exec_cmd("omarchy-capture-region --select-window " .. direction), { description = "Select window to capture" }) hl.bind(direction:upper(), hl.dsp.exec_cmd("omarchy-capture-region --select-window " .. direction), { description = "Select window to capture" })
)
end end
end end
end end
@@ -65,13 +73,10 @@ hl.on("layer.closed", function(layer)
if layer.namespace == "selection" and selection_layers > 0 then if layer.namespace == "selection" and selection_layers > 0 then
selection_layers = selection_layers - 1 selection_layers = selection_layers - 1
if selection_layers == 0 then if selection_layers == 0 then
hl.unbind("RETURN") for _, keybind in ipairs(selection_binds) do
hl.unbind("CTRL + RETURN") keybind:unbind()
hl.unbind("TAB")
hl.unbind("CTRL + TAB")
for _, direction in ipairs({ "LEFT", "RIGHT", "UP", "DOWN" }) do
hl.unbind(direction)
end end
selection_binds = {}
end end
end end
end) end)