# PyMonCtl
[![Type Checking](https://github.com/Kalmat/PyMonCtl/actions/workflows/type-checking.yml/badge.svg)](https://github.com/Kalmat/PyMonCtl/actions/workflows/type-checking.yml)
[![PyPI version](https://badge.fury.io/py/PyMonCtl.svg)](https://badge.fury.io/py/PyMonCtl)
[![Documentation Status](https://readthedocs.org/projects/pymonctl/badge/?version=latest)](https://pymonctl.readthedocs.io/en/latest/?badge=latest)
Cross-Platform module which provides a set of features to get info on and control monitors.
Additional tools/extensions/APIs used:
- Linux:
- Xlib's randr extension
- xrandr command-line tool
- xset command-line tool
- Windows:
- VCP MCCS API interface
- macOS:
- pmset command-line tool
## General Features
Functions to get monitor instances, get info and arrange monitors plugged to the system.
| General functions: |
|:----------------------------------------------------------------:|
| [getAllMonitors](docstrings.md#getallmonitors) |
| [getAllMonitorsDict](docstrings.md#getallmonitorsdict) |
| [getMonitorsCount](docstrings.md#getmonitorscount) |
| [getPrimary](docstrings.md#getprimary) |
| [findMonitorsAtPoint](docstrings.md#findmonitorsatpoint) |
| [findMonitorsAtPointInfo](docstrings.md#findmonitorsatpointinfo) |
| [findMonitorWithName](docstrings.md#findmonitorwithname) |
| [findMonitorWithNameInfo](docstrings.md#findmonitorwithnameinfo) |
| [saveSetup](docstrings.md#savesetup) |
| [restoreSetup](docstrings.md#restoresetup) |
| [arrangeMonitors](docstrings.md#arrangemonitors) |
| [getMousePos](docstrings.md#getmousepos) |
## Monitor Class
Class to access all methods and functions to get info and control a given monitor plugged to the system.
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 `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
the provided handle is not valid.
| Methods | Windows | Linux | macOS |
|:----------------------------------------------:|:-------:|:-----:|:-----:|
| [size](docstrings.md#size) | X | X | X |
| [workarea](docstrings.md#workarea) | X | X | X |
| [position](docstrings.md#position) | X | X | X |
| [setPosition](docstrings.md#setposition) | X | X | X |
| [box](docstrings.md#box) | X | X | X |
| [rect](docstrings.md#rect) | X | X | X |
| [frequency](docstrings.md#frequency) | X | X | X |
| [colordepth](docstrings.md#colordepth) | X | X | X |
| [dpi](docstrings.md#dpi) | X | X | X |
| [scale](docstrings.md#scale) | X | X | X |
| [setScale](docstrings.md#setscale) | X | X | X |
| [orientation](docstrings.md#orientation) | X | X | X |
| [setOrientation](docstrings.md#setorientation) | X | X | X (1) |
| [brightness](docstrings.md#brightness) | X (2) | X | X |
| [setBrightness](docstrings.md#setbrightness) | X (2) | X | X |
| [contrast](docstrings.md#contrast) | X (2) | X (3) | X (3) |
| [setContrast](docstrings.md#setcontrast) | X (2) | X (3) | X (3) |
| [mode](docstrings.md#mode) | X | X | X |
| [setMode](docstrings.md#setmode) | X | X | X |
| [defaultMode](docstrings.md#defaultmode) | X | X | X |
| [setDefaultMode](docstrings.md#setdefaultmode) | X | X | X |
| [allModes](docstrings.md#allmodes) | X | X | X |
| [setPrimary](docstrings.md#setprimary) | X | X | X |
| [isPrimary](docstrings.md#isprimary) | X | X | X |
| [turnOn](docstrings.md#turnon) | X (4) | X | X (4) |
| [turnOff](docstrings.md#turnoff) | X (4) | X | X (4) |
| [isOn](docstrings.md#ison) | X (2) | X | X |
| [suspend](docstrings.md#suspend) | X (4) | X (4) | X (4) |
| [isSuspended](docstrings.md#issuspended) | X (2) | X | X |
| [attach](docstrings.md#attach) | X | X | |
| [detach](docstrings.md#detach) | X | X | |
| [isAttached](docstrings.md#isattached) | X | X | X |
(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.
(3) It doesn't exactly return / change contrast, but gamma values.
(4) Different behaviour according to OS:
- Windows: Working with VCP MCCS support only.
- Linux: It will suspend ALL monitors. To address just one monitor, try using turnOff() / turnOn() / detach() / attach() methods.
- macOS: It will suspend ALL monitors. Use turnOn() to wake them up again
***WARNING: Most of these properties may return ''None'' in case the value can not be obtained***
### Important OS-dependent behaviors and limitations:
- Windows:
- Primary monitor is mandatory, and it is always placed at (0, 0) coordinates.
- Monitors can not overlap.
- To set a monitor as Primary, it is necessary to reposition primary monitor first, so the rest of monitors will sequentially be repositioned to RIGHT_TOP.
- If you attach / detach / plug / unplug a monitor, all IDs may change. The module will try to refresh the IDs for all Monitor class instances, but take into account it may fail!
- Linux:
- Primary monitor can be anywhere, and even there can be no primary monitor.
- Monitors can overlap, so take this into account when setting a new monitor position.
- xrandr won't accept negative values, so the whole setup will be referenced to (0, 0) coordinates.
- xrandr will sort primary monitors first. Because of this and for homegeneity, when positioning a monitor as primary (only with setPosition() method), it will be placed at (0 ,0) and all the rest to RIGHT_TOP.
- macOS:
- Primary monitor is mandatory, and it is always placed at (0, 0) coordinates.
- Monitors can overlap, so take this into account when setting a new monitor position.
- To set a monitor as Primary, it is necessary to reposition primary monitor first, so the rest of monitors will sequentially be repositioned to RIGHT_TOP.
- setScale() method uses a workaround by applying the nearest monitor mode to magnify text to given value
It is highly recommended to use `arrangeMonitors()` function for complex setups or just in case there are two or more monitors.
## Keep track of Monitor(s) changes
You can activate a watchdog, running in a separate Thread, which will allow you to keep monitors
information updated, without negatively impacting your main process, and define hooks and its callbacks to be
notified when monitors are plugged / unplugged or their properties change.
| Watchdog methods: |
|:--------------------------------------------------------------:|
| [isWatchdogEnabled](docstrings.md#iswatchdogenabled) |
| [updateWatchdogInterval](docstrings.md#updatewatchdoginterval) |
The watchdog will automatically start while the update information is enabled and / or there are any listeners
registered, and will automatically stop otherwise or if the script finishes.
You can check if the watchdog is working (`isWatchdogEnabled()`) and also change its update interval
(`updateWatchdogInterval()`) in case you need a custom period (default is 0.5 seconds). Adjust this value to your needs,
but take into account that higher values will take longer to detect and notify changes; whilst lower values will
consume more CPU and may produce additional notifications for intermediate (non-final) status.
### Keep Monitors info updated
| Info update methods: |
|:---------------------------------------------------------:|
| [enableUpdateInfo](docstrings.md#enableupdateinfo) |
| [disableUpdateInfo](docstrings.md#disableupdateinfo) |
| [isUpdateInfoEnabled](docstrings.md#isupdateinfoenabled) |
Enable this only if you need to keep track of monitor-related events like changing its resolution, position, scale,
or if monitors can be dynamically plugged or unplugged in a multi-monitor setup. If you need monitors info updated
at a given moment, but not continuously updated, just invoke `getAllMonitors()` at your convenience.
If enabled, it will activate a separate thread which will periodically update the list of monitors and
their properties (see `getAllMonitors()` and `getAllMonitorsDict()` function).
### Get notified on Monitors changes
It is possible to register listeners to be invoked in case the number of connected monitors or their
properties change.
| Listeners methods: |
|:----------------------------------------------------------------------:|
| [plugListenerRegister](docstrings.md#pluglistenerregister) |
| [changeListenerRegister](docstrings.md#changelistenerregister) |
| [plugListenerUnregister](docstrings.md#pluglistenerunregister) |
| [changeListenerUnregister](docstrings.md#changelistenerunregister) |
| [isPlugListenerRegistered](docstrings.md#ispluglistenerregistered) |
| [isChangeListenerRegistered](docstrings.md#ischangelistenerregistered) |
The information passed to the listeners is as follows:
- Names of the monitors which have changed (as a list of strings)
- All monitors info, as returned by `getAllMonitorsDict()`. To access monitors properties, use monitor name/s as dictionary key
Example:
import pymonctl as pmc
import time
def countChanged(names, screensInfo):
print("MONITOR PLUGGED/UNPLUGGED:", names)
for name in names:
print("MONITORS INFO:", screensInfo[name])
def propsChanged(names, screensInfo):
print("MONITOR CHANGED:", names)
for name in names:
print("MONITORS INFO:", screensInfo[name])
pmc.plugListenerRegister(countChanged)
pmc.changeListenerRegister(propsChanged)
print("Plug/Unplug monitors, or change monitor properties while running")
print("Press Ctl-C to Quit")
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
break
pmc.plugListenerUnregister(countChanged)
pmc.changeListenerUnregister(propsChanged)
## Install
To install this module on your system, you can use pip:
pip install pymonctl
or
python3 -m pip install pymonctl
Alternatively, you can download the wheel file (.whl) available in the [Download page](https://pypi.org/project/PyMonCtl/#files) and the [dist folder](https://github.com/Kalmat/PyMonCtl/tree/master/dist), and run this (don't forget to replace 'x.x.xx' with proper version number):
pip install PyMonCtl-x.x.xx-py3-none-any.whl
You may want to add `--force-reinstall` option to be sure you are installing the right dependencies version.
Then, you can use it on your own projects just importing it:
import pymonctl
## Support
In case you have a problem, comments or suggestions, do not hesitate to [open issues](https://github.com/Kalmat/PyMonCtl/issues) on the [project homepage](https://github.com/Kalmat/PyMonCtl)
## Using this code
If you want to use this code or contribute, you can either:
* Create a fork of the [repository](https://github.com/Kalmat/PyMonCtl), or
* [Download the repository](https://github.com/Kalmat/PyMonCtl/archive/refs/heads/master.zip), uncompress, and open it on your IDE of choice (e.g. PyCharm)
Be sure you install all dependencies described on `requirements.txt` by using pip
python3 -m pip install -r requirements.txt
## Test
To test this module on your own system, cd to `tests` folder and run:
python3 test_pymonctl.py