feat(hyprlua): snap T/G scale to valid Hyprland steps for current resolution
Replaces fixed 0.25 increments with mathematically valid scales p/q (lowest terms, q≤6) where both width/s and height/s are integers. For 1920x1200 this gives 25 steps including 2.4, matching what Hyprland actually applies — no more mismatch between TUI display and live value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
8343a741b8
commit
08e4e583a3
|
|
@ -7,7 +7,7 @@ Keys (normal mode):
|
|||
h j k l move monitor (50 px)
|
||||
H J K L move monitor (10 px fine)
|
||||
u / i rotate CCW / CW
|
||||
T / G scale up / scale down (0.25 steps)
|
||||
T / G scale up / scale down (valid Hyprland steps)
|
||||
m toggle mirror (pick target) / un-mirror
|
||||
n / N cycle display mode forward / backward
|
||||
s save to hypr/usr/monitors.lua
|
||||
|
|
@ -22,6 +22,7 @@ Mirror-pick mode:
|
|||
|
||||
import curses
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
|
@ -37,9 +38,9 @@ from typing import List, Optional
|
|||
MONITORS_LUA = Path.home() / "Dotfiles/desktopenvs/hyprlua/hypr/usr/monitors.lua"
|
||||
MOVE_STEP = 50
|
||||
MOVE_STEP_FINE = 10
|
||||
SCALE_STEP = 0.25
|
||||
MIN_SCALE = 0.25
|
||||
MAX_SCALE = 4.0
|
||||
_SCALE_MAX_DENOM = 6 # max denominator when enumerating valid Hyprland scales
|
||||
MIN_BOX_W = 14
|
||||
MIN_BOX_H = 4
|
||||
INFO_W = 32
|
||||
|
|
@ -68,6 +69,29 @@ def rotate_cw(t: int) -> int:
|
|||
def rotate_ccw(t: int) -> int:
|
||||
return (t & 4) | ((t - 1) & 3)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scale helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def valid_scales(width: int, height: int) -> List[float]:
|
||||
"""Return sorted list of scales valid for (width, height).
|
||||
|
||||
A scale s = p/q (in lowest terms) is valid iff both width/s and height/s
|
||||
are integers, i.e. p divides gcd(width, height). We limit q ≤
|
||||
_SCALE_MAX_DENOM to keep the step count practical (~20 steps per monitor).
|
||||
"""
|
||||
g = math.gcd(width, height)
|
||||
divisors = [k for k in range(1, g + 1) if g % k == 0]
|
||||
result: set = set()
|
||||
for p in divisors:
|
||||
for q in range(1, _SCALE_MAX_DENOM + 1):
|
||||
if math.gcd(p, q) != 1:
|
||||
continue
|
||||
s = p / q
|
||||
if MIN_SCALE <= s <= MAX_SCALE:
|
||||
result.add(round(s, 10))
|
||||
return sorted(result)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data model
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -387,9 +411,9 @@ class App:
|
|||
self.rotate_monitor(+1)
|
||||
# Scale
|
||||
elif ch == ord("T"):
|
||||
self.scale_monitor(+SCALE_STEP)
|
||||
self.scale_monitor(+1)
|
||||
elif ch == ord("G"):
|
||||
self.scale_monitor(-SCALE_STEP)
|
||||
self.scale_monitor(-1)
|
||||
# Mirror
|
||||
elif ch == ord("m"):
|
||||
if mon.mirror_of:
|
||||
|
|
@ -471,19 +495,27 @@ class App:
|
|||
self.dirty = True
|
||||
self.status_msg = err or f"Rotated {mon.name} → {TRANSFORM_LABEL[mon.transform]}"
|
||||
|
||||
def scale_monitor(self, delta: float):
|
||||
def scale_monitor(self, direction: int):
|
||||
mon = self.monitors[self.selected_idx]
|
||||
new_scale = round(max(MIN_SCALE, min(MAX_SCALE, mon.scale + delta)), 10)
|
||||
# round to nearest SCALE_STEP to avoid floating-point drift
|
||||
new_scale = round(new_scale / SCALE_STEP) * SCALE_STEP
|
||||
if new_scale == mon.scale:
|
||||
self.status_msg = f"Scale already at {mon.scale}x (limit)"
|
||||
scales = valid_scales(mon.width, mon.height)
|
||||
cur = round(mon.scale, 10)
|
||||
if direction > 0:
|
||||
candidates = [s for s in scales if s > cur + 1e-9]
|
||||
if not candidates:
|
||||
self.status_msg = f"Scale already at max ({mon.scale}x)"
|
||||
return
|
||||
new_scale = candidates[0]
|
||||
else:
|
||||
candidates = [s for s in scales if s < cur - 1e-9]
|
||||
if not candidates:
|
||||
self.status_msg = f"Scale already at min ({mon.scale}x)"
|
||||
return
|
||||
new_scale = candidates[-1]
|
||||
mon.scale = new_scale
|
||||
err = apply_monitor(mon)
|
||||
mon.dirty = True
|
||||
self.dirty = True
|
||||
self.status_msg = err or f"{mon.name} scale → {mon.scale}x"
|
||||
self.status_msg = err or f"{mon.name} scale → {new_scale}x"
|
||||
|
||||
def cycle_mode(self, delta: int):
|
||||
mon = self.monitors[self.selected_idx]
|
||||
|
|
|
|||
Loading…
Reference in New Issue