#!/bin/bash # omarchy:summary=Show, set, or adjust focused Hyprland monitor scaling # omarchy:args=[up|down|SCALE] # 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|SCALE]" } 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 clean value. 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 }' } normalize_scale() { awk 'NR == 1 { printf "%g\n", $0 }' } 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")" # GTK only honors integer GDK_SCALE values, so persist the nearest whole # factor even when the monitor scale itself is fractional. local new_gdk_scale="$(awk -v scale="$new_scale" 'BEGIN { printf "%d", int(scale + 0.5) }')" 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_gdk_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_gdk_scale"'")|' \ "$monitor_lua" fi } scale_from_current() { local direction="${1:-}" local width="${2:-}" local height="${3:-}" awk -v direction="$direction" -v list="${SCALES[*]}" -v width="$width" -v height="$height" ' function gcd(a, b, t) { while (b) { t = a % b; a = b; b = t } return a } function clean(scale, g, k) { g = gcd(width * 120, height * 120) k = int(scale * 120 + 0.5) if (k > g) k = g while (g % k != 0) k++ return k / 120 } NR == 1 { scale = $0; found = 1 } END { if (!found) exit 1 preset_count = split(list, presets, " ") for (i = 1; i <= preset_count; i++) { effective = clean(presets[i]) key = sprintf("%.8f", effective) distance = presets[i] - effective if (distance < 0) distance = -distance # Multiple presets can collapse to the same clean scale. Keep only the # closest label so stepping always moves to a distinct effective value. if (!(key in effective_index)) { effective_index[key] = ++n effective_scales[n] = effective scales[n] = presets[i] distances[n] = distance } else { idx = effective_index[key] if (distance < distances[idx]) { scales[idx] = presets[i] distances[idx] = distance } } } # Snap to the nearest effective scale first. Hyprland reports floating # point values, so exact comparisons can otherwise get stuck. best = 1; best_diff = 1e9 for (i = 1; i <= n; i++) { diff = scale - effective_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 | normalize_scale ;; -h | --help) usage ;; up) monitor_info=$(hyprctl monitors -j | jq -e -c '.[] | select(.focused == true)') set_scale "$(echo "$monitor_info" | jq -r '.scale' | scale_from_current next \ "$(echo "$monitor_info" | jq -r '.width')" "$(echo "$monitor_info" | jq -r '.height')")" "up" ;; down) monitor_info=$(hyprctl monitors -j | jq -e -c '.[] | select(.focused == true)') set_scale "$(echo "$monitor_info" | jq -r '.scale' | scale_from_current previous \ "$(echo "$monitor_info" | jq -r '.width')" "$(echo "$monitor_info" | jq -r '.height')")" "down" ;; 1 | 1.25 | 1.6 | 2 | 3 | 4) set_scale "$1" "$1" ;; *) if [[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]] && awk -v scale="$1" 'BEGIN { exit !(scale >= 1 && scale <= 4) }'; then set_scale "$1" "$1" else usage >&2 exit 1 fi ;; esac