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. Added setScale() method (using a workaround). Added wakeup feature to turnOn() method
  • Loading branch information
Kalmat committed Apr 19, 2024
1 parent e04f1b2 commit 868d2d8
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 66 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Class to access all methods and functions to get info and control a given monito

This class is not meant to be directly instantiated. Instead, use convenience functions like `getAllMonitors()`,
`getPrimary()` or `findMonitorsAtPoint(x, y)`. Use [PyWinCtl](https://github.com/Kalmat/PyWinCtl) module in case you need to
find the monitor a given window is in, by using `getDisplay()` method which returns the name of the monitor that
find the monitor a given window is in, by using `getMonitor()` method which returns the name of the monitor that
can directly be used to invoke `findMonitorWithName(name)` function.

To instantiate it, you need to pass the monitor handle (OS-dependent). It can raise ValueError exception in case
Expand All @@ -62,8 +62,8 @@ the provided handle is not valid.
| setScale | X | X | X |
| orientation | X | X | X |
| setOrientation | X | X | X (1) |
| brightness | X (2) | X | X |
| setBrightness | X (2) | X | X |
| brightness | X (2) | X | X (1) |
| setBrightness | X (2) | X | X (1) |
| contrast | X (2) | X (3) | X (3) |
| setContrast | X (2) | X (3) | X (3) |
| mode | X | X | X |
Expand All @@ -83,7 +83,7 @@ the provided handle is not valid.
| isAttached | X | X | X |


(1) Working only in versions older than Catalina (thanks to University of Utah - Marriott Library - Apple Infrastructure)
(1) Maybe not working in all macOS versions and/or architectures (thanks to University of [Utah - Marriott Library - Apple Infrastructure](https://github.com/univ-of-utah-marriott-library-apple/privacy_services_manager), [eryksun](https://stackoverflow.com/questions/22841741/calling-functions-with-arguments-from-corefoundation-using-ctypes) and [nriley](https://github.com/nriley/brightness/blob/master/brightness.c) for pointing me to the solution)

(2) If monitor has no VCP MCCS support, these methods won't likely work.

Expand Down
2 changes: 1 addition & 1 deletion src/pymonctl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"DisplayMode", "ScreenValue", "Size", "Point", "Box", "Rect", "Position", "Orientation"
]

__version__ = "0.91"
__version__ = "0.92"


def version(numberOnly: bool = True) -> str:
Expand Down
126 changes: 67 additions & 59 deletions src/pymonctl/_pymonctl_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,14 @@ def __init__(self, handle: Optional[int] = None):
self._useDS = True
self._cd: Optional[ctypes.CDLL] = None
self._useCD = True
# self._iokit: Optional[ctypes.CDLL] = None
self._iokit: Optional[ctypes.CDLL] = None
self._useIOBrightness = True
self._useIOOrientation = True
self._cf: Optional[ctypes.CDLL] = None
self._ioservice: Optional[int] = None
# In Catalina and above IOKit.IODisplayGetFloatParameter fails
v = platform.mac_ver()[0].split(".")
self._ver = float(v[0] + "." + v[1])
# In some versions / systems, IOKit may fail
# v = platform.mac_ver()[0].split(".")
# self._ver = float(v[0] + "." + v[1])
else:
raise ValueError

Expand Down Expand Up @@ -363,9 +364,9 @@ def orientation(self) -> Optional[Union[int, Orientation]]:
def setOrientation(self, orientation: Optional[Union[int, Orientation]]):
if (self._useIOOrientation
and orientation and orientation in (Orientation.NORMAL, Orientation.INVERTED, Orientation.LEFT, Orientation.RIGHT)):
if self._ioservice is None:
self._ioservice = _loadIOKit(self.handle)
if self._ioservice is not None:
if self._iokit is None:
self._iokit, self._cf, self._ioservice = _loadIOKit(self.handle)
if self._iokit is not None and self._ioservice is not None:
swapAxes = 0x10
invertX = 0x20
invertY = 0x40
Expand All @@ -376,10 +377,10 @@ def setOrientation(self, orientation: Optional[Union[int, Orientation]]):
270: (swapAxes | invertY) << 16,
}
rotateCode = 0x400
options = rotateCode | angleCodes[(orientation*90) % 360]
options = rotateCode | angleCodes[(orientation * 90) % 360]

try:
ret = globals()["IOServiceRequestProbe"](self._ioservice, options)
ret = self._iokit.IOServiceRequestProbe(self._ioservice, options)
except:
ret = 1
if ret != 0:
Expand Down Expand Up @@ -431,16 +432,18 @@ def brightness(self) -> Optional[int]:
self._useCD = False
else:
self._useCD = False
if ret != 0 and self._useIOBrightness: # and self._ver > 10.15:
if self._ioservice is None:
self._ioservice = _loadIOKit(self.handle)
if self._ioservice is not None:
if ret != 0 and self._useIOBrightness: # and self._ver > 10.15:
if self._iokit is None:
self._iokit, self._cf, self._ioservice = _loadIOKit(self.handle)
if self._iokit is not None and self._cf is not None and self._ioservice is not None:
var_name = CF.CFStringCreateWithCString(None, b"brightness", 0)
value = ctypes.c_float()
try:
(ret, value) = globals()["IODisplayGetFloatParameter"](self._ioservice, 0, globals()["kIODisplayBrightnessKey"], None)
ret = self._iokit.IODisplayGetFloatParameter(self._ioservice, 0, var_name, ctypes.byref(value))
except:
ret = 1
if ret == 0:
res = value
res = value.value
else:
self._useIOBrightness = False
else:
Expand Down Expand Up @@ -481,13 +484,14 @@ def setBrightness(self, brightness: Optional[int]):
self._useCD = False
else:
self._useCD = False
if ret != 0 and self._useIOBrightness and self._ver > 10.15:
if self._ioservice is None:
self._ioservice = _loadIOKit(self.handle)
if self._ioservice is not None:
if ret != 0 and self._useIOBrightness:
if self._iokit is None:
self._iokit, self._cf, self._ioservice = _loadIOKit(self.handle)
if self._iokit is not None and self._cf is not None and self._ioservice is not None:
var_name = CF.CFStringCreateWithCString(None, b"brightness", 0)
value = ctypes.c_float(brightness / 100)
try:
ret = globals()["IODisplaySetFloatParameter"](self._ioservice, 0, globals()["kIODisplayBrightnessKey"], value)
ret = self._iokit.IODisplaySetFloatParameter(self._ioservice, 0, var_name, value)
except:
ret = 1
if ret != 0:
Expand Down Expand Up @@ -716,59 +720,63 @@ def _loadDisplayServices():
def _loadCoreDisplay():
# Another option is to use Core Display Services
try:
lib = ctypes.util.find_library("CoreDisplay")
if lib:
cd: 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:
return None
cd: ctypes.CDLL = 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]
except:
return None
return cd


def _loadIOKit(displayID = Quartz.CGMainDisplayID()):
# https://gist.github.com/wadimw/4ac972d07ed1f3b6f22a101375ecac41?permalink_comment_id=4932162
# In other systems, we can try to use IOKit
# https://stackoverflow.com/questions/22841741/calling-functions-with-arguments-from-corefoundation-using-ctypes

try:
# lib = ctypes.util.find_library('IOKit')
# if lib:
# iokit: ctypes.CDLL = ctypes.cdll.LoadLibrary(lib)
# iokit.IODisplayGetFloatParameter.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p]
# iokit.IODisplayGetFloatParameter.restype = ctypes.py_object
import objc # type: ignore[import-untyped, import-not-found]
iokit = AppKit.NSBundle.bundleWithIdentifier_("com.apple.framework.IOKit")
functions = [
("IOServiceGetMatchingService", b"II@"),
("IOServiceMatching", b"@*"),
("IODisplayGetFloatParameter", b"iII@o^f"),
("IODisplaySetFloatParameter", b"iII@f"),
("IOServiceRequestProbe", b"iII"),
("IOObjectRelease", b"iI")
]
objc.loadBundleFunctions(iokit, globals(), functions)
variables = [
("kIOMasterPortDefault", b"I")
]
objc.loadBundleVariables(iokit, globals(), variables)
globals()['kIODisplayBrightnessKey'] = CF.CFSTR("brightness")
class _CFString(ctypes.Structure):
pass

CFStringRef = ctypes.POINTER(_CFString)

CF: ctypes.CDLL = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation"))
CF.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32]
CF.CFStringCreateWithCString.restype = CFStringRef

iokit: ctypes.CDLL = ctypes.cdll.LoadLibrary(ctypes.util.find_library('IOKit'))
iokit.IODisplayGetFloatParameter.argtypes = [ctypes.c_void_p, ctypes.c_uint, CFStringRef, ctypes.POINTER(ctypes.c_float)]
iokit.IODisplayGetFloatParameter.restype = ctypes.c_int
iokit.IODisplaySetFloatParameter.argtypes = [ctypes.c_void_p, ctypes.c_uint, CFStringRef, ctypes.c_float]
iokit.IODisplaySetFloatParameter.restype = ctypes.c_int
iokit.IOServiceRequestProbe.argtypes = [ctypes.c_void_p, ctypes.c_uint]
iokit.IOServiceRequestProbe.restype = ctypes.c_int

try:
service: Optional[int] = Quartz.CGDisplayIOServicePort(displayID)
service: int = Quartz.CGDisplayIOServicePort(displayID)
except:
service = None
service = 0

if not service:
err, service = globals()["IOServiceGetMatchingService"](
globals()["kIOMasterPortDefault"],
globals()["IOServiceMatching"](b'IODisplayConnect')
)
if err != 0 or not service:
service = None

iokit.IOServiceMatching.restype = ctypes.c_void_p
iokit.IOServiceGetMatchingService.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
iokit.IOServiceGetMatchingService.restype = ctypes.c_void_p

try:
kIOMasterPortDefault = ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault")

service = iokit.IOServiceGetMatchingService(
kIOMasterPortDefault,
iokit.IOServiceMatching(b'IODisplayConnect')
)
except:
service = 0

if service:
return iokit, CF, service
except:
service = None
return service
pass

return None, None, None


def _eventLoop(kill: threading.Event, interval: float):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_pymonctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,10 @@ def changedCB(names: List[str], info: dict[str, pmc.ScreenValue]):
time.sleep(_TIMELAP)
print()

print("CHANGE ORIENTATION", "Current:", monitor.orientation, "Target:", pmc.Orientation.INVERTED*90)
print("CHANGE ORIENTATION", "Current:", monitor.orientation*90, "Target:", pmc.Orientation.INVERTED*90)
monitor.setOrientation(pmc.Orientation.INVERTED)
time.sleep(_TIMELAP*2)
print("RESTORE ORIENTATION", "Current:", monitor.orientation, "Target:", pmc.Orientation.NORMAL)
print("RESTORE ORIENTATION", "Current:", monitor.orientation*90, "Target:", pmc.Orientation.NORMAL)
monitor.setOrientation(pmc.Orientation.NORMAL)
time.sleep(_TIMELAP*2)
print()
Expand Down

0 comments on commit 868d2d8

Please sign in to comment.