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)