From ab4b2e141fd18e5396e08772015e6926e443a1ff Mon Sep 17 00:00:00 2001 From: Kalmat Date: Wed, 17 Apr 2024 12:56:01 +0200 Subject: [PATCH] ALL: Added saveSetup() and restoreSetup(). Fixed / Improved watchdog (especially in Linux). Fixed / improved setPosition() method LINUX: Added ewmhlib as separate module. Fixed watchdog (freezing randomly invoking screen_resources and get_output_info), fixed workarea crash (some apps/environments do not set it), improved to work almost fine in Manjaro/KDE, avoid crashing in Wayland for "fake" :1 display (though module won't likely work) WIN32: Fixed dev.StateFlags returning weird values for multi-monitor. Fixed GetAwarenessFromDpiAwarenessContext not supported on Windows Server MACOS: Replaced display-manager-lib by other alternatives which seem to work in several macOS versions (brightness only) Added setScale() method (using a workaround). Added wakeup feature to turnOn() method --- src/pymonctl/_pymonctl_linux.py | 37 ++++++++++--------- src/pymonctl/_pymonctl_macos.py | 64 ++++++++++++++++++--------------- tests/test_pymonctl.py | 8 ++--- typings/randr.pyi | 14 ++++++++ 4 files changed, 74 insertions(+), 49 deletions(-) create mode 100644 typings/randr.pyi diff --git a/src/pymonctl/_pymonctl_linux.py b/src/pymonctl/_pymonctl_linux.py index e4d8d9e..25480f5 100644 --- a/src/pymonctl/_pymonctl_linux.py +++ b/src/pymonctl/_pymonctl_linux.py @@ -19,8 +19,8 @@ import Xlib.xobject from Xlib.protocol.rq import Struct from Xlib.xobject.drawable import Window as XWindow - from Xlib.ext import randr +from Xlib.ext.randr import GetScreenResourcesCurrent, MonitorInfo, GetOutputInfo, GetCrtcInfo from ._main import BaseMonitor, _pointInBox, _getRelativePosition, getMonitorsData, isWatchdogEnabled, \ DisplayMode, ScreenValue, Box, Rect, Point, Size, Position, Orientation @@ -43,12 +43,12 @@ def _getAllMonitorsDict() -> dict[str, ScreenValue]: def _getAllMonitorsDictThread() -> (Tuple[dict[str, ScreenValue], - List[Tuple[Xlib.display.Display, Struct, XWindow, randr.GetScreenResourcesCurrent, - randr.MonitorInfo, str, int, randr.GetOutputInfo, int, randr.GetCrtcInfo]]]): + List[Tuple[Xlib.display.Display, Struct, XWindow, GetScreenResourcesCurrent, + MonitorInfo, str, int, GetOutputInfo, int, GetCrtcInfo]]]): # display connections seem to fail when shared amongst threads and/or queried too quickly in parallel monitorsDict: dict[str, ScreenValue] = {} - monitorsData: List[Tuple[Xlib.display.Display, Struct, XWindow, randr.GetScreenResourcesCurrent, - randr.MonitorInfo, str, int, randr.GetOutputInfo, int, randr.GetCrtcInfo]] = [] + monitorsData: List[Tuple[Xlib.display.Display, Struct, XWindow, GetScreenResourcesCurrent, + MonitorInfo, str, int, GetOutputInfo, int, GetCrtcInfo]] = [] for monitorData in _getMonitorsData(): display, screen, root, res, monitor, monName, output, outputInfo, crtc, crtcInfo = monitorData monitorsDict[monName] = _buildMonitorsDict(display, screen, root, res, monitor, monName, output, outputInfo, crtc, crtcInfo) @@ -259,8 +259,8 @@ def position(self) -> Optional[Point]: def setPosition(self, relativePos: Union[int, Position, Point, Tuple[int, int]], relativeTo: Optional[str]): # https://askubuntu.com/questions/1193940/setting-monitor-scaling-to-200-with-xrandr - arrangement: dict[str, dict[str, Union[Optional[str], int, Position, Point]]] = {} - monitors: dict[str, dict[str, randr.MonitorInfo]] = _XgetMonitorsDict() + arrangement: dict[str, dict[str, Optional[Union[str, int, Position, Point, Size]]]] = {} + monitors: dict[str, dict[str, MonitorInfo]] = _XgetMonitorsDict() monKeys = list(monitors.keys()) if relativePos == Position.PRIMARY: monitor = monitors[self.name]["monitor"] @@ -344,9 +344,12 @@ def _buildScaleCmd(self, scale: Tuple[float, float]) -> str: if monName == self.name: defMode = self.defaultMode - width, height = defMode.width, defMode.height - panX, panY = int(width * scaleX), int(height * scaleY) - newScaleX, newScaleY = scaleX, scaleY + if defMode is not None: + width, height = defMode.width, defMode.height + panX, panY = int(width * scaleX), int(height * scaleY) + newScaleX, newScaleY = scaleX, scaleY + else: + return "" else: mode = monitor["size"] @@ -526,7 +529,7 @@ def isPrimary(self) -> bool: else: ret = randr.get_output_primary(self.root) if ret and hasattr(ret, "output"): - return ret.output == self.handle + return bool(ret.output == self.handle) return False def setPrimary(self): @@ -831,10 +834,10 @@ class _Monitor(NamedTuple): def _getMonitorsData(handle: Optional[int] = None) -> ( - List[Tuple[Xlib.display.Display, Struct, XWindow, randr.GetScreenResourcesCurrent, - randr.MonitorInfo, str, int, randr.GetOutputInfo, int, randr.GetCrtcInfo]]): - monitors: List[Tuple[Xlib.display.Display, Struct, XWindow, randr.GetScreenResourcesCurrent, - randr.MonitorInfo, str, int, randr.GetOutputInfo, int, randr.GetCrtcInfo]] = [] + List[Tuple[Xlib.display.Display, Struct, XWindow, GetScreenResourcesCurrent, + MonitorInfo, str, int, GetOutputInfo, int, GetCrtcInfo]]): + monitors: List[Tuple[Xlib.display.Display, Struct, XWindow, GetScreenResourcesCurrent, + MonitorInfo, str, int, GetOutputInfo, int, GetCrtcInfo]] = [] stopSearching = False for rootData in getRoots(): display, screen, root = rootData @@ -941,7 +944,7 @@ def _RgetMonitorsInfo(activeOnly: bool = True): def _XgetAllOutputs(name: str = ""): outputs: List[Tuple[Xlib.display.Display, Xlib.protocol.rq.Struct, Xlib.xobject.drawable.Window, - int, randr.GetOutputInfo]] = [] + int, GetOutputInfo]] = [] for rootData in getRoots(): display, screen, root = rootData res = randr.get_screen_resources_current(root) @@ -989,7 +992,7 @@ def _XgetMonitorsDict(): return monitors -def _XgetMonitorData(handle: Optional[int] = None) -> Optional[Tuple[Xlib.display.Display, Struct, XWindow, randr.MonitorInfo, int, str]]: +def _XgetMonitorData(handle: Optional[int] = None) -> Optional[Tuple[Xlib.display.Display, Struct, XWindow, MonitorInfo, int, str]]: for monitorData in _XgetAllMonitors(): display, screen, root, monitor, monName = monitorData output = monitor.crtcs[0] diff --git a/src/pymonctl/_pymonctl_macos.py b/src/pymonctl/_pymonctl_macos.py index 9c02fbf..8f5339e 100644 --- a/src/pymonctl/_pymonctl_macos.py +++ b/src/pymonctl/_pymonctl_macos.py @@ -234,7 +234,7 @@ def position(self) -> Optional[Point]: def setPosition(self, relativePos: Union[int, Position, Point, Tuple[int, int]], relativeTo: Optional[str]): # https://apple.stackexchange.com/questions/249447/change-display-arrangement-in-os-x-macos-programmatically - arrangement: dict[str, dict[str, Union[Optional[str], int, Position, Point]]] = {} + arrangement: dict[str, dict[str, Optional[Union[str, int, Position, Point, Size]]]] = {} monitors = _NSgetAllMonitorsDict() monKeys = list(monitors.keys()) if relativePos == Position.PRIMARY or relativePos == (0, 0): @@ -705,7 +705,7 @@ def _NSgetAllMonitorsDict(): def _loadDisplayServices(): # Display Services Framework can be used in modern systems. It takes A LOT to load try: - ds: Optional[Union[ctypes.CDLL, int]] = ctypes.cdll.LoadLibrary('/System/Library/PrivateFrameworks/DisplayServices.framework/DisplayServices') + ds: Optional[ctypes.CDLL] = ctypes.cdll.LoadLibrary('/System/Library/PrivateFrameworks/DisplayServices.framework/DisplayServices') except: ds = None return ds @@ -714,9 +714,13 @@ def _loadDisplayServices(): def _loadCoreDisplay(): # Another option is to use Core Display Services try: - cd: Optional[Union[ctypes.CDLL, int]] = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreDisplay")) - cd.CoreDisplay_Display_SetUserBrightness.argtypes = [ctypes.c_int, ctypes.c_double] - cd.CoreDisplay_Display_GetUserBrightness.argtypes = [ctypes.c_int, ctypes.c_void_p] + lib = ctypes.util.find_library("CoreDisplay") + if lib is not None: + cd: Optional[ctypes.CDLL] = ctypes.cdll.LoadLibrary(lib) + cd.CoreDisplay_Display_SetUserBrightness.argtypes = [ctypes.c_int, ctypes.c_double] + cd.CoreDisplay_Display_GetUserBrightness.argtypes = [ctypes.c_int, ctypes.c_void_p] + else: + cd = None except: cd = None return cd @@ -726,33 +730,37 @@ def _loadIOKit(displayID = Quartz.CGMainDisplayID()): # In older systems, we can try to use IOKit service: Optional[int] = None try: - iokit: Optional[Union[ctypes.CDLL, int]] = ctypes.cdll.LoadLibrary(ctypes.util.find_library('IOKit')) - try: - service = Quartz.CGDisplayIOServicePort(displayID) - except: - service = None - if not service: - # CGDisplayIOServicePort is deprecated in some systems - # We can try to use 'IODisplayConnect' Service, but it won't work on M1 and above systems either - iokit.IOServiceMatching.restype = ctypes.c_void_p - iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - iokit.IOServiceGetMatchingServices.restype = ctypes.c_void_p - - kIOMasterPortDefault = ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault") - serial_port_iterator = ctypes.c_void_p() + lib = ctypes.util.find_library('IOKit') + if lib is not None: + iokit: Optional[ctypes.CDLL] = ctypes.cdll.LoadLibrary(lib) try: - iokit.IOServiceGetMatchingServices( - kIOMasterPortDefault, - iokit.IOServiceMatching(b'IODisplayConnect'), - ctypes.byref(serial_port_iterator) - ) - # How to find the service corresponding to displayID??? - service = iokit.IOIteratorNext(serial_port_iterator) + service = Quartz.CGDisplayIOServicePort(displayID) except: service = None if not service: - iokit = None - service = None + # CGDisplayIOServicePort is deprecated in some systems + # We can try to use 'IODisplayConnect' Service, but it won't work on M1 and above systems either + iokit.IOServiceMatching.restype = ctypes.c_void_p + iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + iokit.IOServiceGetMatchingServices.restype = ctypes.c_void_p + + kIOMasterPortDefault = ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault") + serial_port_iterator = ctypes.c_void_p() + try: + iokit.IOServiceGetMatchingServices( + kIOMasterPortDefault, + iokit.IOServiceMatching(b'IODisplayConnect'), + ctypes.byref(serial_port_iterator) + ) + # How to find the service corresponding to displayID??? + service = iokit.IOIteratorNext(serial_port_iterator) + except: + service = None + if not service: + iokit = None + service = None + else: + iokit = None except: iokit = None return iokit, service diff --git a/tests/test_pymonctl.py b/tests/test_pymonctl.py index 15f2e16..00fd9de 100644 --- a/tests/test_pymonctl.py +++ b/tests/test_pymonctl.py @@ -34,11 +34,11 @@ def changedCB(names: List[str], info: dict[str, pmc.ScreenValue]): print() monitorsPlugged: List[pmc.Monitor] = pmc.getAllMonitors() -initArrangement = [] +initArrangement: List[Tuple[pmc.Monitor, pmc.ScreenValue]] = [] initDict: dict[str, dict[str, str | int | pmc.Position | pmc.Point | pmc.Size]] = {} setAsPrimary: Optional[pmc.Monitor] = None try: - initArrangement: List[Tuple[pmc.Monitor, pmc.ScreenValue]] = pmc.saveSetup() + initArrangement = pmc.saveSetup() print("INITIAL POSITIONS:", initArrangement) except: for monitor in monitorsPlugged: @@ -83,7 +83,7 @@ def changedCB(names: List[str], info: dict[str, pmc.ScreenValue]): currMode = monitor.mode targetMode = None - if monitor.mode: + if monitor.mode is not None: monWidth = currMode.width targetWidth = monWidth if monWidth == 5120: @@ -229,7 +229,7 @@ def changedCB(names: List[str], info: dict[str, pmc.ScreenValue]): print() print("CHANGE ARRANGEMENT: MONITOR 2 AS PRIMARY, REST OF MONITORS AT LEFT_BOTTOM") - arrangement: dict[str, dict[str, Union[str, int, pmc.Position, pmc.Point, pmc.Size]]] = { + arrangement: dict[str, dict[str, Union[str, int, pmc.Position, pmc.Point, pmc.Size, None]]] = { str(mon2.name): {"relativePos": pmc.Position.PRIMARY, "relativeTo": ""} } relativeTo = mon2.name diff --git a/typings/randr.pyi b/typings/randr.pyi new file mode 100644 index 0000000..86d6da1 --- /dev/null +++ b/typings/randr.pyi @@ -0,0 +1,14 @@ +from typing import Any + + +class MonitorInfo(str): + def __getattr__(self, name: str) -> Any: ... + +class GetScreenResourcesCurrent(str): + def __getattr__(self, name: str) -> Any: ... + +class GetOutputInfo(str): + def __getattr__(self, name: str) -> Any: ... + +class GetCrtcInfo(str): + def __getattr__(self, name: str) -> Any: ...