Add BOORU_VIEWER_NO_HYPR_RULES + BOORU_VIEWER_NO_POPOUT_ASPECT_LOCK env vars for ricers with their own windowrules
This commit is contained in:
parent
33293dfbae
commit
72150fc98b
44
README.md
44
README.md
@ -142,6 +142,50 @@ Type=Application
|
||||
Categories=Graphics;
|
||||
```
|
||||
|
||||
### Hyprland integration
|
||||
|
||||
I daily-drive booru-viewer on Hyprland and I've baked in my own opinions on
|
||||
how the app should behave there. By default, a handful of `hyprctl` dispatches
|
||||
run at runtime to:
|
||||
|
||||
- Restore the main window's last floating mode + dimensions on launch
|
||||
- Restore the popout's position, center-pin it around its content during
|
||||
navigation, and suppress F11 / fullscreen-transition flicker
|
||||
- "Prime" Hyprland's per-window floating cache at startup so a mid-session
|
||||
toggle to floating uses your saved dimensions
|
||||
- Lock the popout's aspect ratio to its content so you can't accidentally
|
||||
stretch mpv playback by dragging the popout corner
|
||||
|
||||
If you're a ricer with your own `windowrule`s targeting `class:^(booru-viewer)$`
|
||||
and you'd rather the app keep its hands off your setup, there are two
|
||||
independent opt-out env vars:
|
||||
|
||||
- **`BOORU_VIEWER_NO_HYPR_RULES=1`** — disables every in-code hyprctl dispatch
|
||||
*except* the popout's `keep_aspect_ratio` lock. Use this if you want app-side
|
||||
window management out of the way but you still want the popout to size itself
|
||||
to its content.
|
||||
- **`BOORU_VIEWER_NO_POPOUT_ASPECT_LOCK=1`** — independently disables the popout's
|
||||
aspect ratio enforcement. Useful if you want to drag the popout to whatever
|
||||
shape you like (square, panoramic, monitor-aspect, whatever) and accept that
|
||||
mpv playback will letterbox or stretch to match.
|
||||
|
||||
For the full hands-off experience, set both:
|
||||
|
||||
```ini
|
||||
[Desktop Entry]
|
||||
Name=booru-viewer
|
||||
Exec=env BOORU_VIEWER_NO_HYPR_RULES=1 BOORU_VIEWER_NO_POPOUT_ASPECT_LOCK=1 /path/to/booru-viewer/.venv/bin/booru-viewer
|
||||
Icon=/path/to/booru-viewer/icon.png
|
||||
Type=Application
|
||||
Categories=Graphics;
|
||||
```
|
||||
|
||||
Or for one-off launches from a shell:
|
||||
|
||||
```bash
|
||||
BOORU_VIEWER_NO_HYPR_RULES=1 booru-viewer
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Python 3.11+
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@ -10,6 +11,33 @@ APPNAME = "booru-viewer"
|
||||
IS_WINDOWS = sys.platform == "win32"
|
||||
|
||||
|
||||
def hypr_rules_enabled() -> bool:
|
||||
"""Whether the in-code hyprctl dispatches that change window state
|
||||
should run.
|
||||
|
||||
Returns False when BOORU_VIEWER_NO_HYPR_RULES is set in the environment.
|
||||
Callers should skip any hyprctl `dispatch` that would mutate window
|
||||
state (resize, move, togglefloating, setprop no_anim, the floating
|
||||
"prime" sequence). Read-only queries (`hyprctl clients -j`) are still
|
||||
fine — only mutations are blocked.
|
||||
|
||||
The popout's keep_aspect_ratio enforcement is gated by the separate
|
||||
popout_aspect_lock_enabled() — it's a different concern.
|
||||
"""
|
||||
return not os.environ.get("BOORU_VIEWER_NO_HYPR_RULES")
|
||||
|
||||
|
||||
def popout_aspect_lock_enabled() -> bool:
|
||||
"""Whether the popout's keep_aspect_ratio setprop should run.
|
||||
|
||||
Returns False when BOORU_VIEWER_NO_POPOUT_ASPECT_LOCK is set in the
|
||||
environment. Independent of hypr_rules_enabled() so a ricer can free
|
||||
up the popout's shape (e.g. for fixed-square or panoramic popouts)
|
||||
while keeping the rest of the in-code hyprctl behavior, or vice versa.
|
||||
"""
|
||||
return not os.environ.get("BOORU_VIEWER_NO_POPOUT_ASPECT_LOCK")
|
||||
|
||||
|
||||
def data_dir() -> Path:
|
||||
"""Return the platform-appropriate data/cache directory."""
|
||||
if IS_WINDOWS:
|
||||
|
||||
@ -278,9 +278,16 @@ class BooruApp(QMainWindow):
|
||||
self._setup_ui()
|
||||
self._setup_menu()
|
||||
self._load_sites()
|
||||
# Restore window state (geometry, floating, maximized) on the next
|
||||
# event-loop iteration — by then main.py has called show() and the
|
||||
# window has been registered with the compositor.
|
||||
# Debounced save for the main window state — fires from resizeEvent
|
||||
# (and from the splitter timer's flush on close). Uses the same
|
||||
# 300ms debounce pattern as the splitter saver.
|
||||
self._main_window_save_timer = QTimer(self)
|
||||
self._main_window_save_timer.setSingleShot(True)
|
||||
self._main_window_save_timer.setInterval(300)
|
||||
self._main_window_save_timer.timeout.connect(self._save_main_window_state)
|
||||
# Restore window state (geometry, floating) on the next event-loop
|
||||
# iteration — by then main.py has called show() and the window has
|
||||
# been registered with the compositor.
|
||||
QTimer.singleShot(0, self._restore_main_window_state)
|
||||
|
||||
def _setup_signals(self) -> None:
|
||||
@ -1487,15 +1494,28 @@ class BooruApp(QMainWindow):
|
||||
self._run_async(_dl)
|
||||
|
||||
def _save_main_splitter_sizes(self) -> None:
|
||||
"""Persist the main grid/preview splitter sizes (debounced)."""
|
||||
"""Persist the main grid/preview splitter sizes (debounced).
|
||||
|
||||
Refuses to save when either side is collapsed (size 0). The user can
|
||||
end up with a collapsed right panel transiently — e.g. while the
|
||||
popout is open and the right panel is empty — and persisting that
|
||||
state traps them next launch with no visible preview area until they
|
||||
manually drag the splitter back.
|
||||
"""
|
||||
sizes = self._splitter.sizes()
|
||||
if sum(sizes) > 0:
|
||||
if len(sizes) >= 2 and all(s > 0 for s in sizes):
|
||||
self._db.set_setting(
|
||||
"main_splitter_sizes", ",".join(str(s) for s in sizes)
|
||||
)
|
||||
|
||||
def _hyprctl_main_window(self) -> dict | None:
|
||||
"""Look up this main window in hyprctl clients. None off Hyprland."""
|
||||
"""Look up this main window in hyprctl clients. None off Hyprland.
|
||||
|
||||
Matches by Wayland app_id (Hyprland reports it as `class`), which is
|
||||
set in run() via setDesktopFileName. Title would also work but it
|
||||
changes whenever the search bar updates the window title — class is
|
||||
constant for the lifetime of the window.
|
||||
"""
|
||||
import os, subprocess, json
|
||||
if not os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"):
|
||||
return None
|
||||
@ -1505,67 +1525,100 @@ class BooruApp(QMainWindow):
|
||||
capture_output=True, text=True, timeout=1,
|
||||
)
|
||||
for c in json.loads(result.stdout):
|
||||
if c.get("title") == self.windowTitle():
|
||||
cls = c.get("class") or c.get("initialClass")
|
||||
if cls == "booru-viewer":
|
||||
# Skip the popout — it shares our class but has a
|
||||
# distinct title we set explicitly.
|
||||
if (c.get("title") or "").endswith("Popout"):
|
||||
continue
|
||||
return c
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _save_main_window_state(self) -> None:
|
||||
"""Persist the main window's size, position, floating, maximized state.
|
||||
"""Persist the main window's last mode and (separately) the last
|
||||
known floating geometry.
|
||||
|
||||
On Hyprland, queries hyprctl for the real geometry (Qt's frameGeometry
|
||||
is unreliable for floating windows on Wayland). Maximized/fullscreen is
|
||||
recorded separately so the next launch can re-maximize without
|
||||
clobbering the underlying windowed geometry.
|
||||
Two settings keys are used:
|
||||
- main_window_was_floating ("1" / "0"): the *last* mode the window
|
||||
was in (floating or tiled). Updated on every save.
|
||||
- main_window_floating_geometry ("x,y,w,h"): the position+size the
|
||||
window had the *last time it was actually floating*. Only updated
|
||||
when the current state is floating, so a tile→close→reopen→float
|
||||
sequence still has the user's old floating dimensions to use.
|
||||
|
||||
This split is important because Hyprland's resizeEvent for a tiled
|
||||
window reports the tile slot size — saving that into the floating
|
||||
slot would clobber the user's chosen floating dimensions every time
|
||||
they tiled the window.
|
||||
"""
|
||||
try:
|
||||
self._db.set_setting(
|
||||
"main_window_maximized",
|
||||
"1" if self.isMaximized() else "0",
|
||||
)
|
||||
if self.isMaximized():
|
||||
return # don't overwrite the windowed-mode geometry
|
||||
win = self._hyprctl_main_window()
|
||||
if win and win.get("at") and win.get("size"):
|
||||
if win is None:
|
||||
# Non-Hyprland fallback: just track Qt's frameGeometry as
|
||||
# floating. There's no real tiled concept off-Hyprland.
|
||||
g = self.frameGeometry()
|
||||
self._db.set_setting(
|
||||
"main_window_floating_geometry",
|
||||
f"{g.x()},{g.y()},{g.width()},{g.height()}",
|
||||
)
|
||||
self._db.set_setting("main_window_was_floating", "1")
|
||||
return
|
||||
floating = bool(win.get("floating"))
|
||||
self._db.set_setting(
|
||||
"main_window_was_floating", "1" if floating else "0"
|
||||
)
|
||||
if floating and win.get("at") and win.get("size"):
|
||||
x, y = win["at"]
|
||||
w, h = win["size"]
|
||||
floating = 1 if win.get("floating") else 0
|
||||
self._db.set_setting(
|
||||
"main_window_geometry",
|
||||
f"{x},{y},{w},{h},{floating}",
|
||||
"main_window_floating_geometry", f"{x},{y},{w},{h}"
|
||||
)
|
||||
return
|
||||
g = self.frameGeometry()
|
||||
self._db.set_setting(
|
||||
"main_window_geometry",
|
||||
f"{g.x()},{g.y()},{g.width()},{g.height()},0",
|
||||
)
|
||||
# When tiled, intentionally do NOT touch floating_geometry —
|
||||
# preserve the last good floating dimensions.
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _restore_main_window_state(self) -> None:
|
||||
"""One-shot restore of saved geometry, floating and maximized state.
|
||||
"""One-shot restore of saved floating geometry and last mode.
|
||||
|
||||
Called from __init__ via QTimer.singleShot(0, ...) so it fires on the
|
||||
next event-loop iteration — by which time the window has been shown
|
||||
and (on Hyprland) registered with the compositor.
|
||||
|
||||
Entirely skipped when BOORU_VIEWER_NO_HYPR_RULES is set — that flag
|
||||
means the user wants their own windowrules to handle the main
|
||||
window. Even seeding Qt's geometry could fight a `windowrule = size`,
|
||||
so we leave the initial Qt geometry alone too.
|
||||
"""
|
||||
if self._db.get_setting_bool("main_window_maximized"):
|
||||
self.showMaximized()
|
||||
from ..core.config import hypr_rules_enabled
|
||||
if not hypr_rules_enabled():
|
||||
return
|
||||
saved = self._db.get_setting("main_window_geometry")
|
||||
if not saved:
|
||||
# Migration: clear obsolete keys from earlier schemas so they can't
|
||||
# interfere. main_window_maximized came from a buggy version that
|
||||
# used Qt's isMaximized() which lies for Hyprland tiled windows.
|
||||
# main_window_geometry was the combined-format key that's now split.
|
||||
for stale in ("main_window_maximized", "main_window_geometry"):
|
||||
if self._db.get_setting(stale):
|
||||
self._db.set_setting(stale, "")
|
||||
|
||||
floating_geo = self._db.get_setting("main_window_floating_geometry")
|
||||
was_floating = self._db.get_setting_bool("main_window_was_floating")
|
||||
if not floating_geo:
|
||||
return
|
||||
parts = saved.split(",")
|
||||
if len(parts) != 5:
|
||||
parts = floating_geo.split(",")
|
||||
if len(parts) != 4:
|
||||
return
|
||||
try:
|
||||
x, y, w, h, floating = (int(p) for p in parts)
|
||||
x, y, w, h = (int(p) for p in parts)
|
||||
except ValueError:
|
||||
return
|
||||
# Seed Qt with the size; Hyprland may ignore the position for the
|
||||
# initial map, but we follow up with a hyprctl batch below.
|
||||
# Seed Qt with the floating geometry — even if we're going to leave
|
||||
# the window tiled now, this becomes the xdg-toplevel preferred size,
|
||||
# which Hyprland uses when the user later toggles to floating. So
|
||||
# mid-session float-toggle picks up the saved dimensions even when
|
||||
# the window opened tiled.
|
||||
self.setGeometry(x, y, w, h)
|
||||
import os
|
||||
if not os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"):
|
||||
@ -1573,24 +1626,56 @@ class BooruApp(QMainWindow):
|
||||
# Slight delay so the window is registered before we try to find
|
||||
# its address. The popout uses the same pattern.
|
||||
QTimer.singleShot(
|
||||
50, lambda: self._hyprctl_apply_main_state(x, y, w, h, bool(floating))
|
||||
50, lambda: self._hyprctl_apply_main_state(x, y, w, h, was_floating)
|
||||
)
|
||||
|
||||
def _hyprctl_apply_main_state(self, x: int, y: int, w: int, h: int, floating: bool) -> None:
|
||||
"""Apply saved floating/position/size to the main window via hyprctl."""
|
||||
"""Apply saved floating mode + geometry to the main window via hyprctl.
|
||||
|
||||
If floating==True, ensures the window is floating and resizes/moves it
|
||||
to the saved dimensions.
|
||||
|
||||
If floating==False, the window is left tiled but we still "prime"
|
||||
Hyprland's per-window floating cache by briefly toggling to floating,
|
||||
applying the saved geometry, and toggling back. This is wrapped in
|
||||
a transient `no_anim` so the toggles are instant. Without this prime,
|
||||
a later mid-session togglefloating uses Hyprland's default size
|
||||
(Qt's xdg-toplevel preferred size doesn't carry through). With it,
|
||||
the user's saved floating dimensions are used.
|
||||
|
||||
Skipped entirely when BOORU_VIEWER_NO_HYPR_RULES is set — that flag
|
||||
means the user wants their own windowrules to govern the main
|
||||
window and the app should keep its hands off.
|
||||
"""
|
||||
import subprocess
|
||||
from ..core.config import hypr_rules_enabled
|
||||
if not hypr_rules_enabled():
|
||||
return
|
||||
win = self._hyprctl_main_window()
|
||||
if not win:
|
||||
return
|
||||
addr = win.get("address")
|
||||
if not addr:
|
||||
return
|
||||
cmds = []
|
||||
if bool(win.get("floating")) != floating:
|
||||
cmds.append(f"dispatch togglefloating address:{addr}")
|
||||
cur_floating = bool(win.get("floating"))
|
||||
cmds: list[str] = []
|
||||
if floating:
|
||||
# Want floating: ensure floating, then size/move.
|
||||
if not cur_floating:
|
||||
cmds.append(f"dispatch togglefloating address:{addr}")
|
||||
cmds.append(f"dispatch resizewindowpixel exact {w} {h},address:{addr}")
|
||||
cmds.append(f"dispatch movewindowpixel exact {x} {y},address:{addr}")
|
||||
else:
|
||||
# Want tiled: prime the floating cache, then end on tiled. Use
|
||||
# transient no_anim so the toggles don't visibly flash through
|
||||
# a floating frame.
|
||||
cmds.append(f"dispatch setprop address:{addr} no_anim 1")
|
||||
if not cur_floating:
|
||||
cmds.append(f"dispatch togglefloating address:{addr}")
|
||||
cmds.append(f"dispatch resizewindowpixel exact {w} {h},address:{addr}")
|
||||
cmds.append(f"dispatch movewindowpixel exact {x} {y},address:{addr}")
|
||||
cmds.append(f"dispatch togglefloating address:{addr}")
|
||||
cmds.append(f"dispatch setprop address:{addr} no_anim 0")
|
||||
if not cmds:
|
||||
return
|
||||
try:
|
||||
@ -2550,6 +2635,19 @@ class BooruApp(QMainWindow):
|
||||
super().resizeEvent(event)
|
||||
if hasattr(self, '_privacy_overlay') and self._privacy_on:
|
||||
self._privacy_overlay.setGeometry(self.rect())
|
||||
# Capture window state proactively so the saved value is always
|
||||
# fresh — closeEvent's hyprctl query can fail if the compositor has
|
||||
# already started unmapping. Debounced via the 300ms timer.
|
||||
if hasattr(self, '_main_window_save_timer'):
|
||||
self._main_window_save_timer.start()
|
||||
|
||||
def moveEvent(self, event) -> None:
|
||||
super().moveEvent(event)
|
||||
# moveEvent is unreliable on Wayland for floating windows but it
|
||||
# does fire on configure for some compositors — start the save
|
||||
# timer regardless. resizeEvent is the more reliable trigger.
|
||||
if hasattr(self, '_main_window_save_timer'):
|
||||
self._main_window_save_timer.start()
|
||||
|
||||
# -- Keyboard shortcuts --
|
||||
|
||||
@ -2703,11 +2801,14 @@ class BooruApp(QMainWindow):
|
||||
self._library_view.refresh()
|
||||
|
||||
def closeEvent(self, event) -> None:
|
||||
# Flush any pending splitter save (debounce timer might still be
|
||||
# running if the user dragged it within the last 300ms) and capture
|
||||
# the final main window state. Both must run BEFORE _db.close().
|
||||
# Flush any pending splitter / window-state saves (debounce timers
|
||||
# may still be running if the user moved/resized within the last
|
||||
# 300ms) and capture the final state. Both must run BEFORE
|
||||
# _db.close().
|
||||
if self._main_splitter_save_timer.isActive():
|
||||
self._main_splitter_save_timer.stop()
|
||||
if self._main_window_save_timer.isActive():
|
||||
self._main_window_save_timer.stop()
|
||||
self._save_main_splitter_sizes()
|
||||
self._save_main_window_state()
|
||||
self._async_loop.call_soon_threadsafe(self._async_loop.stop)
|
||||
@ -2805,6 +2906,13 @@ def run() -> None:
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# Set a stable Wayland app_id so Hyprland and other compositors can
|
||||
# consistently identify our windows by class (not by title, which
|
||||
# changes when search terms appear in the title bar). Qt translates
|
||||
# setDesktopFileName into the xdg-shell app_id on Wayland.
|
||||
app.setApplicationName("booru-viewer")
|
||||
app.setDesktopFileName("booru-viewer")
|
||||
|
||||
# mpv requires LC_NUMERIC=C — Qt resets the locale in QApplication(),
|
||||
# so we must restore it after Qt init but before creating any mpv instances.
|
||||
import locale
|
||||
|
||||
@ -438,60 +438,93 @@ class FullscreenPreview(QMainWindow):
|
||||
return None
|
||||
|
||||
def _hyprctl_resize(self, w: int, h: int) -> None:
|
||||
"""Ask Hyprland to resize this window and lock aspect ratio. No-op on other WMs or tiled."""
|
||||
"""Ask Hyprland to resize this window and lock aspect ratio. No-op on other WMs or tiled.
|
||||
|
||||
Behavior is gated by two independent env vars (see core/config.py):
|
||||
- BOORU_VIEWER_NO_HYPR_RULES: skip the resize and no_anim parts
|
||||
- BOORU_VIEWER_NO_POPOUT_ASPECT_LOCK: skip the keep_aspect_ratio
|
||||
setprop
|
||||
Either, both, or neither may be set. The aspect-ratio carve-out
|
||||
means a ricer can opt out of in-code window management while
|
||||
still keeping mpv playback at the right shape (or vice versa).
|
||||
"""
|
||||
import os, subprocess
|
||||
from ..core.config import hypr_rules_enabled, popout_aspect_lock_enabled
|
||||
if not os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"):
|
||||
return
|
||||
rules_on = hypr_rules_enabled()
|
||||
aspect_on = popout_aspect_lock_enabled()
|
||||
if not rules_on and not aspect_on:
|
||||
return # nothing to dispatch
|
||||
win = self._hyprctl_get_window()
|
||||
if not win:
|
||||
return
|
||||
addr = win.get("address")
|
||||
if not addr:
|
||||
return
|
||||
cmds: list[str] = []
|
||||
if not win.get("floating"):
|
||||
# Tiled — don't resize (fights the layout), just set aspect lock
|
||||
# and disable animations to prevent flashing on later transitions.
|
||||
try:
|
||||
subprocess.Popen(
|
||||
["hyprctl", "--batch",
|
||||
f"dispatch setprop address:{addr} no_anim 1"
|
||||
f" ; dispatch setprop address:{addr} keep_aspect_ratio 1"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
# Tiled — don't resize (fights the layout). Optionally set
|
||||
# aspect lock and no_anim depending on the env vars.
|
||||
if rules_on:
|
||||
cmds.append(f"dispatch setprop address:{addr} no_anim 1")
|
||||
if aspect_on:
|
||||
cmds.append(f"dispatch setprop address:{addr} keep_aspect_ratio 1")
|
||||
else:
|
||||
if rules_on:
|
||||
cmds.append(f"dispatch setprop address:{addr} no_anim 1")
|
||||
if aspect_on:
|
||||
cmds.append(f"dispatch setprop address:{addr} keep_aspect_ratio 0")
|
||||
if rules_on:
|
||||
cmds.append(f"dispatch resizewindowpixel exact {w} {h},address:{addr}")
|
||||
if aspect_on:
|
||||
cmds.append(f"dispatch setprop address:{addr} keep_aspect_ratio 1")
|
||||
if not cmds:
|
||||
return
|
||||
try:
|
||||
subprocess.Popen(
|
||||
["hyprctl", "--batch",
|
||||
f"dispatch setprop address:{addr} no_anim 1"
|
||||
f" ; dispatch setprop address:{addr} keep_aspect_ratio 0"
|
||||
f" ; dispatch resizewindowpixel exact {w} {h},address:{addr}"
|
||||
f" ; dispatch setprop address:{addr} keep_aspect_ratio 1"],
|
||||
["hyprctl", "--batch", " ; ".join(cmds)],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def _hyprctl_resize_and_move(self, w: int, h: int, x: int, y: int) -> None:
|
||||
"""Atomically resize and move this window via a single hyprctl batch."""
|
||||
"""Atomically resize and move this window via a single hyprctl batch.
|
||||
|
||||
Gated by BOORU_VIEWER_NO_HYPR_RULES (resize/move/no_anim parts) and
|
||||
BOORU_VIEWER_NO_POPOUT_ASPECT_LOCK (the keep_aspect_ratio parts) —
|
||||
see core/config.py.
|
||||
"""
|
||||
import os, subprocess
|
||||
from ..core.config import hypr_rules_enabled, popout_aspect_lock_enabled
|
||||
if not os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"):
|
||||
return
|
||||
rules_on = hypr_rules_enabled()
|
||||
aspect_on = popout_aspect_lock_enabled()
|
||||
if not rules_on and not aspect_on:
|
||||
return
|
||||
win = self._hyprctl_get_window()
|
||||
if not win or not win.get("floating"):
|
||||
return
|
||||
addr = win.get("address")
|
||||
if not addr:
|
||||
return
|
||||
cmds: list[str] = []
|
||||
if rules_on:
|
||||
cmds.append(f"dispatch setprop address:{addr} no_anim 1")
|
||||
if aspect_on:
|
||||
cmds.append(f"dispatch setprop address:{addr} keep_aspect_ratio 0")
|
||||
if rules_on:
|
||||
cmds.append(f"dispatch resizewindowpixel exact {w} {h},address:{addr}")
|
||||
cmds.append(f"dispatch movewindowpixel exact {x} {y},address:{addr}")
|
||||
if aspect_on:
|
||||
cmds.append(f"dispatch setprop address:{addr} keep_aspect_ratio 1")
|
||||
if not cmds:
|
||||
return
|
||||
try:
|
||||
subprocess.Popen(
|
||||
["hyprctl", "--batch",
|
||||
f"dispatch setprop address:{addr} no_anim 1"
|
||||
f" ; dispatch setprop address:{addr} keep_aspect_ratio 0"
|
||||
f" ; dispatch resizewindowpixel exact {w} {h},address:{addr}"
|
||||
f" ; dispatch movewindowpixel exact {x} {y},address:{addr}"
|
||||
f" ; dispatch setprop address:{addr} keep_aspect_ratio 1"],
|
||||
["hyprctl", "--batch", " ; ".join(cmds)],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user