Vendor the command set the menu and shell depend on
This commit is contained in:
Executable
+370
@@ -0,0 +1,370 @@
|
||||
#!/bin/bash
|
||||
|
||||
# blob:summary=Pick a screen region over frozen screen content
|
||||
# blob:args=[region|windows|smart|fullscreen] [--keep-freeze] [--match-monitor] | --take-fullscreen | --take-window | --select-window <next|prev|left|right|up|down>
|
||||
# blob: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}/blob-capture-region-fullscreen"
|
||||
WINDOW_MARKER="${XDG_RUNTIME_DIR:-/tmp}/blob-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"
|
||||
Reference in New Issue
Block a user