From 27d1b6bebccd4524b668c80203ba382881575c28 Mon Sep 17 00:00:00 2001 From: sergio <64654394+s-gato@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:58:47 +0200 Subject: [PATCH] Resolve omarchy_monitor_scale variable reference in clamshell recovery (#6688) A pinned internal monitor rule that referenced the omarchy_monitor_scale local had the variable name captured as the scale, so clamshell recovery fell all the way to the hardcoded 2. Read the config the way Hyprland writes it: a key's value is whatever sits between the `=` and the next separator, and a bare word resolves against a local only when one exists, so a quoted string stays a string. Comments are cut before anything is matched, and position gets the same resolution since it had the identical bug. Co-Authored-By: Claude Opus 5 (1M context) --- bin/omarchy-hyprland-monitor-clamshell | 81 +++++-- test/shell.d/monitor-clamshell-scale-test.sh | 232 +++++++++++++++++++ 2 files changed, 298 insertions(+), 15 deletions(-) diff --git a/bin/omarchy-hyprland-monitor-clamshell b/bin/omarchy-hyprland-monitor-clamshell index 2efbc36b..a55e2028 100755 --- a/bin/omarchy-hyprland-monitor-clamshell +++ b/bin/omarchy-hyprland-monitor-clamshell @@ -7,6 +7,7 @@ TOGGLES_DIR="$HOME/.local/state/omarchy/toggles/hypr" CLAMSHELL_FLAG="$TOGGLES_DIR/internal-monitor-clamshell.lua" MANUAL_DISABLE_FLAG="$TOGGLES_DIR/internal-monitor-disable.lua" SCALE_STATE="$TOGGLES_DIR/internal-monitor-scale" +MONITOR_LUA="$HOME/.config/hypr/monitors.lua" INTERNAL=$(omarchy-hyprland-monitor-laptop) @@ -26,26 +27,76 @@ scales_match() { }' } -configured_monitor_scale() { - local monitor_lua="$HOME/.config/hypr/monitors.lua" - [[ -f $monitor_lua ]] || return 0 - local scale - scale=$(configured_internal_monitor_value scale) - if [[ -z $scale ]]; then - scale=$(sed -n 's/^local omarchy_monitor_scale = //p' "$monitor_lua" | head -1) +lua_identifier() { + [[ $1 =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] +} + +# Value assigned by `local = ...`, without its quotes or trailing comment. +# Only a lone scalar counts: an expression like `1080 / 720` must stay unresolved +# so the caller falls back rather than applying the first half of the sum. +lua_local_value() { + local name="$1" value + lua_identifier "$name" && [[ -f $MONITOR_LUA ]] || return 0 + + value=$(sed -nE 's/^[[:space:]]*local[[:space:]]+'"$name"'[[:space:]]*=[[:space:]]*("[^"]*"|[^"[:space:]]+)[[:space:]]*(--.*)?$/\1/p' "$MONITOR_LUA" | head -1) + [[ $value == \"*\" ]] && value="${value:1:-1}" + printf '%s\n' "$value" +} + +# A quoted capture is a Lua string and stands for itself; a bare word may instead +# name a local the rule refers to, as the shipped scale = omarchy_monitor_scale +# does. A bare word naming no local is left alone to fail validation. +lua_scalar() { + local value="$1" resolved + + if [[ $value == \"*\" ]]; then + printf '%s\n' "${value:1:-1}" + return fi - if [[ -z $scale ]]; then - scale=$(sed -nE 's/^hl\.monitor\(\{ output = "", mode = "preferred", position = "auto", scale = ([^ ]+) \}\)/\1/p' "$monitor_lua" | head -1) - fi - echo "$scale" + + resolved=$(lua_local_value "$value") + printf '%s\n' "${resolved:-$value}" +} + +# The config with its comments cut away, so commented-out text can pose neither +# as a rule nor as one of its keys. +monitor_rules() { + [[ -f $MONITOR_LUA ]] || return 0 + + sed -E -e 's/--\[\[[^]]*\]\]//g' -e 's/--.*$//' "$MONITOR_LUA" +} + +monitor_rule_regex() { + printf '^[[:space:]]*hl\\.monitor\\(\\{.*output[[:space:]]*=[[:space:]]*"%s"' "$1" +} + +# A key is preceded by a table separator, so a longer key cannot stand in for it, +# and its value is the whole of what sits between the `=` and the next separator. +# Anything else is an expression this cannot evaluate. +configured_monitor_value() { + local output="$1" key="$2" value + + value=$(monitor_rules | sed -nE '/'"$(monitor_rule_regex "$output")"'/s/.*[{,;[:space:]]'"$key"'[[:space:]]*=[[:space:]]*("[^"]*"|[^,;}[:space:]]+)[[:space:]]*([,;}].*)?$/\1/p' | head -1) + lua_scalar "$value" } configured_internal_monitor_value() { - local key="$1" - local monitor_lua="$HOME/.config/hypr/monitors.lua" - [[ -n $INTERNAL && -f $monitor_lua ]] || return 0 + [[ -n $INTERNAL ]] || return 0 - sed -nE '/^hl\.monitor\(\{.*output = "'"$INTERNAL"'".*\}\)/s/.*'"$key"' = "?([^", }]+)"?.*/\1/p' "$monitor_lua" | head -1 + configured_monitor_value "$INTERNAL" "$1" +} + +configured_monitor_scale() { + local scale + scale=$(configured_internal_monitor_value scale) + # An internal rule that names a scale settles it, even when the name resolves + # to nothing usable. Only a rule that names none at all defers to the catch-all, + # which is the scale Omarchy has always applied for that config. + [[ -n $scale ]] || scale=$(configured_monitor_value "" scale) + # No rule carries a scale at all: fall back to Omarchy's own knob. + [[ -n $scale ]] || scale=$(lua_local_value omarchy_monitor_scale) + + printf '%s\n' "$scale" } current_internal_scale() { diff --git a/test/shell.d/monitor-clamshell-scale-test.sh b/test/shell.d/monitor-clamshell-scale-test.sh index 7bd7a554..17f6a527 100644 --- a/test/shell.d/monitor-clamshell-scale-test.sh +++ b/test/shell.d/monitor-clamshell-scale-test.sh @@ -72,6 +72,131 @@ hl.monitor({ output = "", mode = "preferred", position = "auto-right", scale = 1 LUA } +# A specific-output rule that references the omarchy_monitor_scale variable +# (the default template's pattern) instead of a literal value. The variable +# must be resolved, not captured as the literal string "omarchy_monitor_scale". +write_internal_monitor_var_config() { + cat >"$monitor_lua" <<'LUA' +local omarchy_gdk_scale = 1.5 +local omarchy_monitor_scale = 1.5 +hl.env("GDK_SCALE", tostring(omarchy_gdk_scale)) +hl.monitor({ output = "eDP-1", mode = "preferred", position = "auto", scale = omarchy_monitor_scale }) +LUA +} + +# An explicit non-numeric scale on the internal rule, alongside an unrelated +# omarchy_monitor_scale local that must not be substituted for it. +write_internal_monitor_auto_config() { + cat >"$monitor_lua" <<'LUA' +local omarchy_monitor_scale = 2 +hl.monitor({ output = "eDP-1", mode = "preferred", position = "auto", scale = "auto" }) +LUA +} + +# The internal rule names a local that does not exist. The catch-all rule below +# it never applies to an output that has its own rule, so it must not be read. +write_internal_monitor_unresolvable_config() { + cat >"$monitor_lua" <<'LUA' +hl.monitor({ output = "eDP-1", mode = "preferred", position = "auto", scale = my_scale }) +hl.monitor({ output = "", mode = "preferred", position = "auto", scale = 1 }) +LUA +} + +# The shipped default: no rule for the internal panel, and the catch-all rule +# references the omarchy_monitor_scale local. Quoted here, and commented below. +write_catch_all_var_config() { + cat >"$monitor_lua" <<'LUA' +local omarchy_monitor_scale = "1.5" +hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale }) +LUA +} + +write_commented_catch_all_var_config() { + cat >"$monitor_lua" <<'LUA' +local omarchy_monitor_scale = 1.25 -- HiDPI panel +hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale }) +LUA +} + +# A position that references a local, the same pattern the scale key allows. +write_internal_monitor_position_var_config() { + cat >"$monitor_lua" <<'LUA' +local omarchy_monitor_position = "0x0" +hl.monitor({ output = "eDP-1", mode = "preferred", position = omarchy_monitor_position, scale = 1.25 }) +LUA +} + +# An internal rule that names no scale at all, so the catch-all supplies one. +write_internal_monitor_scaleless_config() { + cat >"$monitor_lua" <<'LUA' +hl.monitor({ output = "eDP-1", mode = "preferred", position = "auto", transform = 1 }) +hl.monitor({ output = "", mode = "preferred", position = "auto", scale = 1 }) +LUA +} + +# A local holding an expression rather than a scalar. Half of it is not a scale. +write_expression_scale_config() { + cat >"$monitor_lua" <<'LUA' +local omarchy_monitor_scale = 3 / 2 +hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale }) +LUA +} + +# A local that happens to be named after a quoted scale value. The quotes make +# the rule's "auto" a string, so the local must not be substituted for it. +write_shadowed_auto_config() { + cat >"$monitor_lua" <<'LUA' +local auto = 1.5 +hl.monitor({ output = "eDP-1", mode = "preferred", position = "auto", scale = "auto" }) +LUA +} + +# An expression written straight into the rule rather than into a local. +write_expression_rule_config() { + cat >"$monitor_lua" <<'LUA' +hl.monitor({ output = "", mode = "preferred", position = "auto", scale = 3 / 2 }) +LUA +} + +# Commented-out text after a rule is not a rule, and not one of its keys either. +write_commented_rule_config() { + cat >"$monitor_lua" <<'LUA' +hl.monitor({ output = "DP-1", mode = "preferred", position = "auto", scale = 1 }) -- output = "eDP-1" +hl.monitor({ output = "", mode = "preferred", position = "auto", scale = 1.5 }) +LUA +} + +write_commented_internal_rule_config() { + cat >"$monitor_lua" <<'LUA' +hl.monitor({ output = "eDP-1", mode = "preferred", position = "auto", scale = 1.25 }) -- scale = 3 +LUA +} + +# A nested table before the keys, and semicolons for separators. Both are Lua a +# user can reasonably write, and neither ends the rule. +write_nested_table_config() { + cat >"$monitor_lua" <<'LUA' +hl.monitor({ output = "eDP-1", reserved_area = { top = 24 }, position = "0x0", scale = 1.25 }) +LUA +} + +write_block_comment_config() { + cat >"$monitor_lua" <<'LUA' +hl.monitor({ output = "eDP-1", --[[ internal panel ]] position = "0x0", scale = 1.25 }) +LUA +} + +write_semicolon_config() { + cat >"$monitor_lua" <<'LUA' +hl.monitor({ output = "eDP-1"; position = "0x0"; scale = 1.25; transform = 1 }) +LUA +} + +remember_scale() { + mkdir -p "$state_dir" + printf '%s\n' "$1" >"$scale_state" +} + run_clamshell() { HOME="$home_dir" \ PATH="$stub_bin:$PATH" \ @@ -116,3 +241,110 @@ OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell grep -F 'position = "0x0"' "$eval_log" >/dev/null || fail "clamshell recovery uses configured internal position" grep -F 'scale = 1.25' "$eval_log" >/dev/null || fail "clamshell recovery uses configured internal scale" pass "clamshell recovery uses configured internal monitor rule" + +# Regression: specific-output rule referencing the omarchy_monitor_scale +# variable must resolve to the variable's value, not fall back to the default. +write_internal_monitor_var_config +rm -f "$scale_state" +: >"$eval_log" +OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell +grep -F 'scale = 1.5' "$eval_log" >/dev/null || fail "clamshell recovery resolves omarchy_monitor_scale variable reference" +pass "clamshell recovery resolves omarchy_monitor_scale variable reference" + +# An explicit "auto" on the internal rule is a value, not a variable reference, +# so it must not pick up an unrelated omarchy_monitor_scale local. +write_internal_monitor_auto_config +remember_scale 1.75 +: >"$eval_log" +OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell +grep -F 'scale = 1.75' "$eval_log" >/dev/null || fail "clamshell recovery keeps auto scale off the omarchy_monitor_scale local" +pass "clamshell recovery keeps auto scale off the omarchy_monitor_scale local" + +# The catch-all rule does not apply to an output that has its own rule. +write_internal_monitor_unresolvable_config +remember_scale 1.75 +: >"$eval_log" +OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell +grep -F 'scale = 1.75' "$eval_log" >/dev/null || fail "clamshell recovery ignores the catch-all rule when the internal panel has its own" +pass "clamshell recovery ignores the catch-all rule when the internal panel has its own" + +write_catch_all_var_config +rm -f "$scale_state" +: >"$eval_log" +OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell +grep -F 'scale = 1.5' "$eval_log" >/dev/null || fail "clamshell recovery resolves a quoted scale through the catch-all rule" +pass "clamshell recovery resolves a quoted scale through the catch-all rule" + +write_commented_catch_all_var_config +rm -f "$scale_state" +: >"$eval_log" +OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell +grep -F 'scale = 1.25' "$eval_log" >/dev/null || fail "clamshell recovery ignores a trailing comment on the scale local" +pass "clamshell recovery ignores a trailing comment on the scale local" + +write_internal_monitor_position_var_config +rm -f "$scale_state" +: >"$eval_log" +OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell +grep -F 'position = "0x0"' "$eval_log" >/dev/null || fail "clamshell recovery resolves a position variable reference" +pass "clamshell recovery resolves a position variable reference" + +# An internal rule that names no scale leaves the catch-all to supply one. Only +# a rule that names a scale keeps the catch-all away from the internal panel. +write_internal_monitor_scaleless_config +remember_scale 1.75 +: >"$eval_log" +OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell +grep -F 'scale = 1' "$eval_log" >/dev/null || fail "clamshell recovery takes the catch-all scale when the internal rule omits one" +! grep -F 'scale = 1.75' "$eval_log" >/dev/null || fail "clamshell recovery prefers the catch-all scale over the remembered one" +pass "clamshell recovery takes the catch-all scale when the internal rule omits one" + +# Half of `3 / 2` is not the scale the user asked for. +write_expression_scale_config +remember_scale 1.75 +: >"$eval_log" +OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell +grep -F 'scale = 1.75' "$eval_log" >/dev/null || fail "clamshell recovery leaves an expression scale unresolved" +pass "clamshell recovery leaves an expression scale unresolved" + +# A quoted value is a string, not a reference to a local of the same name. +write_shadowed_auto_config +remember_scale 1.75 +: >"$eval_log" +OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell +grep -F 'scale = 1.75' "$eval_log" >/dev/null || fail "clamshell recovery does not resolve a quoted scale against a local" +pass "clamshell recovery does not resolve a quoted scale against a local" + +# Half of `3 / 2` is no better inside the rule than inside a local. +write_expression_rule_config +remember_scale 1.75 +: >"$eval_log" +OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell +grep -F 'scale = 1.75' "$eval_log" >/dev/null || fail "clamshell recovery leaves an expression scale in a rule unresolved" +pass "clamshell recovery leaves an expression scale in a rule unresolved" + +# A commented-out output must not pass for a rule the internal panel owns. +write_commented_rule_config +remember_scale 1.75 +: >"$eval_log" +OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell +grep -F 'scale = 1.5' "$eval_log" >/dev/null || fail "clamshell recovery does not read a rule out of a trailing comment" +pass "clamshell recovery does not read a rule out of a trailing comment" + +# Nor must a commented-out key displace the real one. +write_commented_internal_rule_config +remember_scale 1.75 +: >"$eval_log" +OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell +grep -F 'scale = 1.25' "$eval_log" >/dev/null || fail "clamshell recovery does not read a scale out of a trailing comment" +pass "clamshell recovery does not read a scale out of a trailing comment" + +for config in nested_table semicolon block_comment; do + "write_${config}_config" + remember_scale 1.75 + : >"$eval_log" + OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell + grep -F 'position = "0x0"' "$eval_log" >/dev/null || fail "clamshell recovery reads the position out of a ${config//_/ } rule" + grep -F 'scale = 1.25' "$eval_log" >/dev/null || fail "clamshell recovery reads the scale out of a ${config//_/ } rule" + pass "clamshell recovery reads a ${config//_/ } rule" +done