Files
omarchycn/bin/omarchy-hyprland-monitor-scaling
T
David Heinemeier HanssonandClaude Fable 5 0526ebefcc Snap monitor scaling up to the nearest clean scale Hyprland accepts
Hyprland only takes scales where the mode divides into whole logical
pixels (in 1/120 steps), so picking 3x on a 1280x800 QEMU display threw
a red error overlay and silently kept the old scale. Round the request
up to the nearest clean divisor instead, so 3x becomes 3.2x.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 21:52:32 -07:00

152 lines
5.1 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Show, set, or adjust focused Hyprland monitor scaling
# omarchy:args=[up|down|1|1.25|1.6|2|3|4]
# omarchy:examples=omarchy hyprland monitor scaling | omarchy hyprland monitor scaling 1.6 | omarchy hyprland monitor scaling up | omarchy hyprland monitor scaling down
SCALES=(1 1.25 1.6 2 3 4)
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy"
SCALE_LOG="$STATE_DIR/monitor-scaling.log"
usage() {
echo "Usage: omarchy-hyprland-monitor-scaling [up|down|1|1.25|1.6|2|3|4]"
}
focused_monitor_scale() {
hyprctl monitors -j | jq -er '.[] | select(.focused == true) | .scale'
}
cmdline_for_pid() {
local pid="$1"
[[ -r /proc/$pid/cmdline ]] || return 0
tr '\0\t\n' ' ' <"/proc/$pid/cmdline" | sed -E 's/[[:space:]]+/ /g; s/[[:space:]]+$//'
}
audit_scale_change() {
local requested="$1"
local active_monitor="$2"
local current_scale="$3"
local new_scale="$4"
local parent_pid="$PPID"
local grandparent_pid
local parent_cmd
local grandparent_cmd
mkdir -p "$STATE_DIR" || return 0
grandparent_pid=$(ps -o ppid= -p "$parent_pid" 2>/dev/null | tr -d ' ')
parent_cmd=$(cmdline_for_pid "$parent_pid")
grandparent_cmd=$(cmdline_for_pid "$grandparent_pid")
printf 'at=%s\trequested=%s\tcurrent=%s\tnew=%s\tmonitor=%s\tpid=%s\tppid=%s\tparent=%s\tgppid=%s\tgrandparent=%s\n' \
"$(date --iso-8601=seconds)" \
"$requested" \
"$current_scale" \
"$new_scale" \
"$active_monitor" \
"$$" \
"$parent_pid" \
"$parent_cmd" \
"$grandparent_pid" \
"$grandparent_cmd" >>"$SCALE_LOG"
}
# Hyprland only accepts scales where the mode divides into whole logical
# pixels (in 1/120 steps), so clean scales are divisors of gcd(w*120, h*120).
# Round the requested scale up to the nearest one, so 3x on a 1280x800 QEMU
# display becomes 3.2x instead of a red error overlay.
clean_scale() {
awk -v scale="$1" -v width="$2" -v height="$3" '
function gcd(a, b, t) { while (b) { t = a % b; a = b; b = t } return a }
BEGIN {
g = gcd(width * 120, height * 120)
k = int(scale * 120 + 0.5)
if (k > g) k = g
while (g % k != 0) k++
printf "%g\n", k / 120
}'
}
set_scale() {
local requested_scale="$1"
local requested="${2:-$requested_scale}"
local monitor_info="$(hyprctl monitors -j | jq -e -c '.[] | select(.focused == true)')"
local active_monitor="$(echo "$monitor_info" | jq -r '.name')"
local current_scale="$(echo "$monitor_info" | jq -r '.scale')"
local width="$(echo "$monitor_info" | jq -r '.width')"
local height="$(echo "$monitor_info" | jq -r '.height')"
local refresh_rate="$(echo "$monitor_info" | jq -r '.refreshRate')"
local new_scale="$(clean_scale "$requested_scale" "$width" "$height")"
local monitor_lua="$HOME/.config/hypr/monitors.lua"
hyprctl eval "hl.monitor({ output = \"$active_monitor\", mode = \"${width}x${height}@${refresh_rate}\", position = \"auto\", scale = $new_scale })" >/dev/null
audit_scale_change "$requested" "$active_monitor" "$current_scale" "$new_scale"
# Persist to monitors.lua if the user still has Omarchy's generic catch-all
# defaults, so the scale survives reboots.
if [[ -f $monitor_lua ]] && grep -q '^local omarchy_monitor_scale = ' "$monitor_lua"; then
sed -i -E \
-e "s|^local omarchy_monitor_scale = .*|local omarchy_monitor_scale = ${new_scale}|" \
-e "s|^local omarchy_gdk_scale = .*|local omarchy_gdk_scale = ${new_scale}|" \
"$monitor_lua"
elif [[ -f $monitor_lua ]] && grep -Eq '^hl\.monitor\(\{ output = "", mode = "preferred", position = "auto", scale = ("auto"|[0-9.]+) \}\)' "$monitor_lua"; then
sed -i -E \
-e "s|^(hl\.monitor\(\{ output = \"\", mode = \"preferred\", position = \"auto\", scale = )([^ ]+)( \}\))|\\1${new_scale}\\3|" \
-e 's|^hl\.env\("GDK_SCALE", ".*"\)|hl.env("GDK_SCALE", "'"$new_scale"'")|' \
"$monitor_lua"
fi
}
scale_from_current() {
local direction="${1:-}"
awk -v direction="$direction" -v list="${SCALES[*]}" '
NR == 1 { scale = $0; found = 1 }
END {
if (!found) exit 1
n = split(list, scales, " ")
# Snap to the nearest preset first. Hyprland reports scales as floating
# point values, so a scale set to 3 can come back as 3.0000000000000004.
# Without snapping, the directional comparisons below can misidentify
# the current preset and refuse to step down (or up) any further.
best = 1; best_diff = 1e9
for (i = 1; i <= n; i++) {
diff = scale - scales[i]; if (diff < 0) diff = -diff
if (diff < best_diff) { best_diff = diff; best = i }
}
if (direction == "next") {
print scales[(best < n ? best + 1 : n)]
} else if (direction == "previous") {
print scales[(best > 1 ? best - 1 : 1)]
} else {
print scales[best]
}
}'
}
case "${1:-}" in
"")
focused_monitor_scale | scale_from_current
;;
-h | --help)
usage
;;
up)
set_scale "$(focused_monitor_scale | scale_from_current next)" "up"
;;
down)
set_scale "$(focused_monitor_scale | scale_from_current previous)" "down"
;;
1 | 1.25 | 1.6 | 2 | 3 | 4)
set_scale "$1" "$1"
;;
*)
usage >&2
exit 1
;;
esac