From 199bd01f9400a6b784365b686f9c51ae9d45a150 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 10 Aug 2026 10:17:08 -0700 Subject: [PATCH] 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) --- bin/omarchy-capture-region | 84 +++++++++++++++++++++++------ default/hypr/bindings/utilities.lua | 27 ++++++---- 2 files changed, 84 insertions(+), 27 deletions(-) diff --git a/bin/omarchy-capture-region b/bin/omarchy-capture-region index 45a1bcc1..50b7e246 100755 --- a/bin/omarchy-capture-region +++ b/bin/omarchy-capture-region @@ -62,28 +62,58 @@ focused_monitor_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. -# floating over tiled). Reads candidates on stdin, exits 1 when none match. -smallest_rect_at() { +# 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="" 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 $smallest ]] || ((area < smallest_area)); then - smallest=$rect + if [[ -z $RESOLVED_RECT ]] || ((area < smallest_area)); then + RESOLVED_RECT=$rect smallest_area=$area fi done - [[ -n $smallest ]] || return 1 - echo "$smallest" + [[ -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 @@ -94,9 +124,11 @@ geo_at_cursor() { local x=${pos%,*} local y=${pos#*, } - smallest_rect_at "$x" "$y" < <(window_rects) || - smallest_rect_at "$x" "$y" < <(monitor_rects) || + 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 @@ -128,17 +160,38 @@ if [[ ${1:-} == "--select-window" ]]; then 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 <<<"$candidates" | sort -n -k1,1 -k2,2 | cut -f3-) - [[ -n $rects ]] || exit 0 + 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=$(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 origin_x=$((BASH_REMATCH[1] + BASH_REMATCH[3] / 2)) @@ -211,9 +264,8 @@ if [[ ${1:-} == "--select-window" ]]; then ;; esac - if [[ $target =~ ^(-?[0-9]+),(-?[0-9]+)[[:space:]]([0-9]+)x([0-9]+)$ ]]; then - target_x=$((BASH_REMATCH[1] + BASH_REMATCH[3] / 2)) - target_y=$((BASH_REMATCH[2] + BASH_REMATCH[4] / 2)) + 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 diff --git a/default/hypr/bindings/utilities.lua b/default/hypr/bindings/utilities.lua index aa800ad6..ad0e23be 100644 --- a/default/hypr/bindings/utilities.lua +++ b/default/hypr/bindings/utilities.lua @@ -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). -- 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. +-- 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_binds = {} hl.on("layer.opened", function(layer) if layer.namespace == "selection" then selection_layers = selection_layers + 1 if selection_layers == 1 then - hl.bind("RETURN", hl.dsp.exec_cmd("omarchy-capture-region --take-window"), { description = "Capture highlighted window" }) - hl.bind("CTRL + RETURN", hl.dsp.exec_cmd("omarchy-capture-region --take-fullscreen"), { description = "Capture entire screen" }) - 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" }) + selection_binds = { + hl.bind("RETURN", hl.dsp.exec_cmd("omarchy-capture-region --take-window"), { description = "Capture highlighted window" }), + hl.bind("CTRL + RETURN", hl.dsp.exec_cmd("omarchy-capture-region --take-fullscreen"), { description = "Capture entire screen" }), + 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 - hl.bind(direction:upper(), hl.dsp.exec_cmd("omarchy-capture-region --select-window " .. direction), { description = "Select window to capture" }) + table.insert( + selection_binds, + hl.bind(direction:upper(), hl.dsp.exec_cmd("omarchy-capture-region --select-window " .. direction), { description = "Select window to capture" }) + ) end end end @@ -65,13 +73,10 @@ hl.on("layer.closed", function(layer) if layer.namespace == "selection" and selection_layers > 0 then selection_layers = selection_layers - 1 if selection_layers == 0 then - hl.unbind("RETURN") - hl.unbind("CTRL + RETURN") - hl.unbind("TAB") - hl.unbind("CTRL + TAB") - for _, direction in ipairs({ "LEFT", "RIGHT", "UP", "DOWN" }) do - hl.unbind(direction) + for _, keybind in ipairs(selection_binds) do + keybind:unbind() end + selection_binds = {} end end end)