Skip to content

feat(monitor-scaling): per-monitor scale targeting with layout-safe position recompute - #11854

Open
ButtercupRobrts wants to merge 11 commits into
omacom:quattrofrom
ButtercupRobrts:feat/per-monitor-scaling
Open

ButtercupRobrts wants to merge 11 commits into
omacom:quattrofrom
ButtercupRobrts:feat/per-monitor-scaling

Conversation

@ButtercupRobrts

Copy link
Copy Markdown

Summary

omarchy hyprland monitor scaling gains an optional monitor argument (scaling 1.6 HDMI-A-1) and becomes layout-safe: rescaling an edge-adjacent monitor now recomputes its position so shared edges stay shared, instead of overlapping a neighbor or stranding a cursor-trapping gap.

Three pieces, one file:

  • Per-monitor persistence. A targeted scale rewrites the target monitor's own hl.monitor() rule in ~/.config/hypr/monitors.lua — by connector name or desc: selector — rather than the shared omarchy_monitor_scale catch-all. Rules without a scale key gain one; monitors with no rule get a single-line appended rule. A timestamped .bak.<timestamp> backup precedes every write.
  • Scale-aware position. A monitor's configured position encodes its old logical size; when scale changes, an edge that touched a neighbor (within ~5 logical px, gap or overlap) keeps touching on both axes. Sandwiched monitors keep their largest shared edge (sacrificed sides are audit-logged), floating monitors keep their position unless growth creates a new overlap (then they get the smallest separating move), and a recompute that would create a new third-monitor overlap aborts to the live position. Scale + position apply in a single hyprctl eval; a post-eval read-back warns on divergence since Hyprland can re-snap scales.
  • omarchy_gdk_scale = round(max(all monitor scales)). Previously it rounded the last-scaled monitor — scaling a 1.25x secondary set the global GTK/XWayland factor to 1 and blurry-upscaled every denser display. Behavior change: the next scale operation writes a different value than before on multi-scale setups; this is a fix, flagged for review.

Bonus fix riding along: the eval and appended rules now replay transform — rescaling a portrait monitor no longer silently un-rotates it.

Design note for reviewers

The in-place rewrite is a small Lua-aware awk pass (comments and string interiors blanked at stable byte offsets, edits spliced descending-offset). The simpler alternative — append-only rules, relying on Hyprland's last-match-wins — was considered and rejected: it accumulates shadowing duplicates and makes user edits to named rules silently stop applying. If upstream prefers append-only simplicity over file hygiene, the position/GDK logic carries over unchanged.

Test plan

  • bash test/shell.d/monitor-scaling-test.sh — 67 cases green: persistence shapes (named/desc:/multi-line/var-ref/auto rules), adjacency truth table (grow/shrink/both axes/±5px tolerance/deliberate gaps/sandwich ties/float-noise scales/portrait transforms), clamp+abort paths, atomic-eval single-call assertion, audit-field pinning
  • Neighbor suites: monitor-state-test, monitor-clamshell-scale-test, monitor-test all green
  • ./test/cli green; ./test/shell shows no delta vs baseline
  • Live UAT on a two-monitor setup: panel-driven rescale keeps the external monitor's edge pinned, persists across hyprctl reload, and leaves GDK_SCALE at the densest display's value

…nitor rule

Rewrite scale persistence so a change lands on the target monitor's own
hl.monitor() entry in monitors.lua instead of the shared
omarchy_monitor_scale variable or the output = "" catch-all, which a
named rule overrides on every reload -- the silent-revert bug behind
upstream issue 9950.

- set_scale gains an optional [monitor] target (default: focused) and
  replays the monitor's live x/y in the hyprctl eval apply instead of
  position = "auto", so configured layouts like -1200x0 survive.
- New persist_monitor_scale runs a single awk pass that blanks comments
  and string contents at identical byte offsets, captures multi-line
  hl.monitor({...}) blocks, and rewrites or inserts scale = for blocks
  keyed by connector name or a desc: selector prefix-matching the
  monitor's description or make/model/serial.
- When no rule names the output, a single-line name-keyed hl.monitor()
  rule carrying live mode/position is appended -- the shape the
  clamshell parser already reads.
- Every write is preceded by a timestamped monitors.lua.bak.<ts> backup
  and goes through the file (cat > / >>) so a symlinked config stays a
  symlink; GDK_SCALE is still pinned to the nearest integer.
- The bare invocation still prints the focused monitor's scale for
  omarchy-monitor-state.
Fixture-per-shape coverage for the awk rewriter: variable-referencing and
multi-line rules rewritten in place, scale-less rules gaining the key,
desc:-keyed rules matched by monitor description, line- and
block-commented rules ignored in favor of an append, the literal
catch-all and other monitors' rules left byte-identical, plus the
timestamped backup and symlink write-through guarantees.
Targeted set and step runs apply and persist to the named monitor with
its live position; unknown names, unsafe names, and extra arguments are
rejected non-zero with no eval and no write; the bare call still prints
the focused monitor's scale; the audit log records the targeted name.
…e changes

A fixed logical position encodes the old scale, so rescaling an
edge-adjacent monitor strands a cursor-trapping gap or creates an
overlap (upstream issue omacom#10922). set_scale now fetches the monitor
array once, recomputes the position via recompute_monitor_position
(adjacency candidates within ~5px, largest shared edge wins, minimal
clamp for growth into non-adjacent monitors, abort back to live on a
new overlap), and applies scale+position+transform in one atomic
hyprctl eval followed by a verify-read. The audit log gains pos= and
note= fields appended at line end, and a BASH_SOURCE guard makes the
script sourceable for unit tests.
…'s own rule

A corrected live position that never reaches monitors.lua recreates the
overlap/gap on every reload. The awk rewriter now collects scale and
position edits as (start, end, replacement) spans against the untouched
block_nostr and applies them highest-offset-first -- splicing
immediately would shift the block and invalidate later span offsets
whenever position precedes scale. OMARCHY_NEW_POSITION flows to the
rewriter through the environment (never -v), set only when the
recompute moved the monitor; auto* positions and missing keys are
never written since they re-derive on every reload.
GDK_SCALE is a global, so letting it follow whichever monitor was last
scaled corrupts it on mixed-DPI setups: downscaling the densest display
used to drop the global and shrink XWayland apps everywhere. The value
is now round(max(new scale, every other monitor's reported scale)) from
the same monitors -j fetch the recompute consumes. Round (nearest
integer) stays correct because xwayland.force_zero_scaling makes this
size fidelity, not sharpness.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant