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 (50 px)
|
||||||
H J K L move monitor (10 px fine)
|
H J K L move monitor (10 px fine)
|
||||||
u / i rotate CCW / CW
|
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
|
m toggle mirror (pick target) / un-mirror
|
||||||
n / N cycle display mode forward / backward
|
n / N cycle display mode forward / backward
|
||||||
s save to hypr/usr/monitors.lua
|
s save to hypr/usr/monitors.lua
|
||||||
|
|
@ -22,6 +22,7 @@ Mirror-pick mode:
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
@ -37,9 +38,9 @@ from typing import List, Optional
|
||||||
MONITORS_LUA = Path.home() / "Dotfiles/desktopenvs/hyprlua/hypr/usr/monitors.lua"
|
MONITORS_LUA = Path.home() / "Dotfiles/desktopenvs/hyprlua/hypr/usr/monitors.lua"
|
||||||
MOVE_STEP = 50
|
MOVE_STEP = 50
|
||||||
MOVE_STEP_FINE = 10
|
MOVE_STEP_FINE = 10
|
||||||
SCALE_STEP = 0.25
|
|
||||||
MIN_SCALE = 0.25
|
MIN_SCALE = 0.25
|
||||||
MAX_SCALE = 4.0
|
MAX_SCALE = 4.0
|
||||||
|
_SCALE_MAX_DENOM = 6 # max denominator when enumerating valid Hyprland scales
|
||||||
MIN_BOX_W = 14
|
MIN_BOX_W = 14
|
||||||
MIN_BOX_H = 4
|
MIN_BOX_H = 4
|
||||||
INFO_W = 32
|
INFO_W = 32
|
||||||
|
|
@ -68,6 +69,29 @@ def rotate_cw(t: int) -> int:
|
||||||
def rotate_ccw(t: int) -> int:
|
def rotate_ccw(t: int) -> int:
|
||||||
return (t & 4) | ((t - 1) & 3)
|
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
|
# Data model
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -387,9 +411,9 @@ class App:
|
||||||
self.rotate_monitor(+1)
|
self.rotate_monitor(+1)
|
||||||
# Scale
|
# Scale
|
||||||
elif ch == ord("T"):
|
elif ch == ord("T"):
|
||||||
self.scale_monitor(+SCALE_STEP)
|
self.scale_monitor(+1)
|
||||||
elif ch == ord("G"):
|
elif ch == ord("G"):
|
||||||
self.scale_monitor(-SCALE_STEP)
|
self.scale_monitor(-1)
|
||||||
# Mirror
|
# Mirror
|
||||||
elif ch == ord("m"):
|
elif ch == ord("m"):
|
||||||
if mon.mirror_of:
|
if mon.mirror_of:
|
||||||
|
|
@ -471,19 +495,27 @@ class App:
|
||||||
self.dirty = True
|
self.dirty = True
|
||||||
self.status_msg = err or f"Rotated {mon.name} → {TRANSFORM_LABEL[mon.transform]}"
|
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]
|
mon = self.monitors[self.selected_idx]
|
||||||
new_scale = round(max(MIN_SCALE, min(MAX_SCALE, mon.scale + delta)), 10)
|
scales = valid_scales(mon.width, mon.height)
|
||||||
# round to nearest SCALE_STEP to avoid floating-point drift
|
cur = round(mon.scale, 10)
|
||||||
new_scale = round(new_scale / SCALE_STEP) * SCALE_STEP
|
if direction > 0:
|
||||||
if new_scale == mon.scale:
|
candidates = [s for s in scales if s > cur + 1e-9]
|
||||||
self.status_msg = f"Scale already at {mon.scale}x (limit)"
|
if not candidates:
|
||||||
return
|
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
|
mon.scale = new_scale
|
||||||
err = apply_monitor(mon)
|
err = apply_monitor(mon)
|
||||||
mon.dirty = True
|
mon.dirty = True
|
||||||
self.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):
|
def cycle_mode(self, delta: int):
|
||||||
mon = self.monitors[self.selected_idx]
|
mon = self.monitors[self.selected_idx]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue