Keyboard window capture in the region picker: Return, Tab, arrows (#6466)
* Keyboard window capture in the region picker: Ctrl+Return, Tab, arrows Return already captures the entire focused monitor while slurp is open, but grabbing a single window required the mouse: hover highlights a window, a click captures it. Now the keyboard can do the same. Ctrl+Return captures the selected window — the one under the cursor, falling back to the focused window. Tab and Ctrl+Tab cycle that selection through the workspace's windows in reading order, and the arrow keys move it spatially; both warp the cursor to the target window's center, so slurp's own hover highlight tracks the selection. Ctrl+Return is implemented like --take-fullscreen: a layer-scoped bind flags the intent via a marker file and dismisses slurp, and the picker resolves the window under the cursor. On an empty workspace the pick resolves to nothing and exits as cancelled. Ctrl is the chord modifier because slurp reacts to held Shift by squaring the selection's aspect ratio, which visibly reshapes the hover highlight mid-chord. None of the keys slurp itself uses (Escape, held Space to move a selection, held Shift) are bound, so its own keyboard behavior is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Resolve overlapping windows the way slurp highlights them slurp highlights the smallest box under the cursor, but the capture and selection hit-tests returned the first geometric match in hyprctl clients order. With a floating window over a tiled one, Ctrl+Return could capture the window underneath the visibly highlighted one. Track the smallest containing window instead, in both the capture resolution and the selection-movement origin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Document the region picker's keyboard modes in its usage line Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Resolve keyboard capture and navigation from one rectangle list Duplicate window geometry (tabbed groups, stacked floating windows) stalled the Tab cycle on the first copy, and the smallest-window-under-cursor tie-break was applied to two differently ordered lists, so navigation could cycle from one window while Ctrl+Return captured another. Ctrl+Return over a gap or an empty workspace also ignored the monitor rectangle slurp was highlighting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Capture the highlighted window with Return, the display with Ctrl+Return The picker highlights a window under the cursor far more often than a whole display, so the unmodified key takes the highlighted rectangle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: David Heinemeier Hansson <david@hey.com>
This commit is contained in:
co-authored by
Claude Opus 5
David Heinemeier Hansson
parent
03b825f59a
commit
366c708e44
+196
-29
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Pick a screen region over frozen screen content
|
||||
# omarchy:args=[region|windows|smart|fullscreen] [--keep-freeze] [--match-monitor]
|
||||
# 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
|
||||
@@ -21,28 +21,7 @@
|
||||
# exactly matches a monitor
|
||||
|
||||
FULLSCREEN_MARKER="${XDG_RUNTIME_DIR:-/tmp}/omarchy-capture-region-fullscreen"
|
||||
|
||||
# Pressing Return while slurp is open captures the entire focused monitor:
|
||||
# a bind scoped to slurp's layer surface (default/hypr/bindings/utilities.lua)
|
||||
# invokes this mode, which flags the intent and dismisses slurp.
|
||||
if [[ ${1:-} == "--take-fullscreen" ]]; then
|
||||
pgrep -x slurp >/dev/null || exit 0
|
||||
touch "$FULLSCREEN_MARKER"
|
||||
pkill -x slurp
|
||||
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
|
||||
WINDOW_MARKER="${XDG_RUNTIME_DIR:-/tmp}/omarchy-capture-region-window"
|
||||
|
||||
# accounting for portrait/transformed displays
|
||||
JQ_MONITOR_GEO='
|
||||
@@ -58,26 +37,214 @@ JQ_MONITOR_GEO='
|
||||
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() {
|
||||
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])"'
|
||||
monitor_rects
|
||||
window_rects
|
||||
}
|
||||
|
||||
focused_monitor_geo() {
|
||||
hyprctl monitors -j | jq -r "${JQ_MONITOR_GEO} .[] | select(.focused == true) | format_geo"
|
||||
}
|
||||
|
||||
# Runs slurp; an empty result with the fullscreen marker present means Return
|
||||
# was pressed, so the focused monitor is the selection.
|
||||
# slurp highlights the smallest box containing the cursor 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() {
|
||||
local x=$1 y=$2
|
||||
local rect area
|
||||
local smallest=""
|
||||
local smallest_area=0
|
||||
|
||||
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
|
||||
smallest_area=$area
|
||||
fi
|
||||
done
|
||||
|
||||
[[ -n $smallest ]] || return 1
|
||||
echo "$smallest"
|
||||
}
|
||||
|
||||
# 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#*, }
|
||||
|
||||
smallest_rect_at "$x" "$y" < <(window_rects) ||
|
||||
smallest_rect_at "$x" "$y" < <(monitor_rects) ||
|
||||
focused_monitor_geo
|
||||
}
|
||||
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
|
||||
# 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")
|
||||
|
||||
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 [[ $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))
|
||||
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"
|
||||
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"
|
||||
|
||||
@@ -41,16 +41,22 @@ o.bind("SUPER + ALT + code:35", "Make webcam overlay larger", "omarchy-capture-w
|
||||
o.bind("SUPER + PRINT", "Color picker", "pkill hyprpicker || hyprpicker -a")
|
||||
o.bind("SUPER + CTRL + PRINT", "Extract text (OCR) from screenshot", "omarchy-capture-text")
|
||||
|
||||
-- While the slurp region picker is open, Return captures the entire focused
|
||||
-- monitor. The bind lives exactly as long as a selection layer is on screen
|
||||
-- (slurp opens one per monitor), so it cannot leak or get stuck.
|
||||
-- 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.
|
||||
local selection_layers = 0
|
||||
|
||||
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-fullscreen"), { description = "Capture entire screen" })
|
||||
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" })
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -60,6 +66,12 @@ hl.on("layer.closed", function(layer)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user