Skip to content

Commit

Permalink
ALL: Added saveSetup() and restoreSetup(). Fixed / Improved watchdog …
Browse files Browse the repository at this point in the history
…(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
  • Loading branch information
Kalmat committed Apr 17, 2024
1 parent f4d6e40 commit ab4b2e1
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 49 deletions.
37 changes: 20 additions & 17 deletions src/pymonctl/_pymonctl_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand Down
64 changes: 36 additions & 28 deletions src/pymonctl/_pymonctl_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions tests/test_pymonctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions typings/randr.pyi
Original file line number Diff line number Diff line change
@@ -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: ...

0 comments on commit ab4b2e1

Please sign in to comment.