import re, subprocess
from enum import Enum
+from fractions import Fraction
## utility functions
class RelativeScreenPosition(Enum):
'''Represents the relative position of the external screen to the internal one'''
- LEFT = 0
- RIGHT = 1
- ABOVE = 2
- BELOW = 3
- MIRROR = 4
-
+ LEFT = ("left of")
+ RIGHT = ("right of")
+ ABOVE = ("above")
+ BELOW = ("below")
+ MIRROR = ("same as")
+ def __init__(self, text):
+ # auto numbering
+ cls = self.__class__
+ self._value_ = len(cls.__members__) + 1
+ self.text = text
class Resolution:
'''Represents a resolution of a screen'''
self.width = width
self.height = height
+ @classmethod
+ def fromDatabase(cls, dbstr):
+ if dbstr is None:
+ return None
+ parts = dbstr.split("x")
+ if len(parts) != 2:
+ raise ValueError(xrandrstr)
+ return Resolution(*map(int,parts))
+
+ def forDatabase(self):
+ return str(self.width)+'x'+str(self.height)
+
+ def forXrandr(self):
+ return self.forDatabase()
+
+ def toTuple(self):
+ return (self.width, self.height)
+
def __eq__(self, other):
if not isinstance(other, Resolution):
return False
def __ne__(self, other):
return not self.__eq__(other)
+ def __hash__(self):
+ return hash(("Resolution",self.width,self.height))
+
def __str__(self):
# get ratio
- ratio = int(round(16.0*self.height/self.width))
- if ratio == 12: # 16:12 = 4:3
- strRatio = '4:3'
- elif ratio == 13: # 16:12.8 = 5:4
- strRatio = '5:4'
- else: # let's just hope this will never be 14 or more...
- strRatio = '16:%d' % ratio
+ ratio = Fraction(self.width, self.height) # automatically divides by the gcd
+ strRatio = "%d:%d" % (ratio.numerator, ratio.denominator)
return '%dx%d (%s)' %(self.width, self.height, strRatio)
def __repr__(self):
return 'screen.Resolution('+self.forXrandr()+')'
- def forXrandr(self):
- return str(self.width)+'x'+str(self.height)
+ def pixelCount(self):
+ return self.width * self.height
class ScreenSetup:
}[self.relPosition], intName]
return args
+class Connector:
+ def __init__(self, name=None):
+ self.name = name # connector name, e.g. "HDMI1"
+ self.edid = None # EDID string for the connector, or None if disconnected
+ self._resolutions = set() # list of Resolution objects, empty if disconnected
+ self.preferredResolution = None
+ self.__lastResolution = None
+ self.hasLastResolution = False
+
+ def __str__(self):
+ return str(self.name)
+
+ def __repr__(self):
+ return """<Connector "%s" EDID="%s" resolutions="%s">""" % (str(self.name), str(self.edid), ", ".join(str(r) for r in self.getResolutionList()))
+
+ def __setLastRes(self, res):
+ # res == None means this display was last switched off
+ if res is not None and not res in self._resolutions:
+ raise ValueError("Resolution "+res+" not available for "+self.name+".")
+ self.__lastResolution = res
+ self.hasLastResolution = True
+
+ def __getLastRes(self):
+ if not self.hasLastResolution:
+ raise ValueError("Connector %s has no last known resolution." % self.name)
+ return self.__lastResolution
+
+ lastResolution = property(__getLastRes, __setLastRes)
+
+ def isConnected(self):
+ assert (self.edid is None) == (len(self._resolutions)==0)
+ return self.edid is not None
+
+ def addResolution(self, resolution):
+ assert isinstance(resolution, Resolution)
+ self._resolutions.add(resolution)
+
+ def appendToEdid(self, s):
+ if self.edid is None:
+ self.edid = s
+ else:
+ self.edid += s
+
+ def getResolutionList(self):
+ return sorted(self._resolutions, key=lambda r: (0 if self.hasLastResolution and r==self.lastResolution else 1, 0 if r==self.preferredResolution else 1, -r.pixelCount()))
class ScreenSituation:
- connectors = {} # maps connector names to lists of Resolution (empty list -> disabled connector)
- internalConnector = None # name of the internal connector (will be an enabled one)
- externalConnector = None # name of the used external connector (an enabled one), or None
+ connectors = [] # contains all the Connector objects
+ internalConnector = None # the internal Connector object (will be an enabled one)
+ externalConnector = None # the used external Connector object (an enabled one), or None
'''Represents the "screen situation" a machine can be in: Which connectors exist, which resolutions do they have, what are the names for the internal and external screen'''
def __init__(self, internalConnectorNames, externalConnectorNames = None):
just choose any remaining connector.'''
# which connectors are there?
self._getXrandrInformation()
- print(self.connectors)
+ for c in self.connectors:
+ print(repr(c))
+ print()
# figure out which is the internal connector
self.internalConnector = self._findAvailableConnector(internalConnectorNames)
if self.internalConnector is None:
- raise Exception("Could not automatically find internal connector, please use ~/.dsl.conf to specify it manually.")
- print(self.internalConnector)
+ raise Exception("Could not automatically find internal connector, please use (or fix) ~/.dsl.conf to specify it manually.")
+ print("Detected internal connector:",self.internalConnector)
# and the external one
if externalConnectorNames is None:
- externalConnectorNames = list(self.connectors.keys())
- externalConnectorNames.remove(self.internalConnector)
+ externalConnectorNames = map(lambda c: c.name, self.connectors)
+ externalConnectorNames = set(filter(lambda name: name != self.internalConnector.name, externalConnectorNames))
self.externalConnector = self._findAvailableConnector(externalConnectorNames)
- print(self.externalConnector)
+ if self.internalConnector == self.externalConnector:
+ raise Exception("Internal and external connector are the same. This must not happen. Please fix ~/.dsl.conf.");
+ print("Detected external connector:",self.externalConnector)
+ # self.preferredSetup is left uninitialized so you can't access it before trying a lookup in the database
# Run xrandr and fill the dict of connector names mapped to lists of available resolutions.
def _getXrandrInformation(self):
connector = None # current connector
- for line in processOutputGen("xrandr", "-q"):
+ readingEdid = False
+ for line in processOutputGen("xrandr", "-q", "--verbose"):
+ if readingEdid:
+ m = re.match(r'^\s*([0-9a-f]+)\s*$', line)
+ if m is not None:
+ connector.appendToEdid(m.group(1))
+ continue
+ else:
+ readingEdid = False
+ # fallthrough to the rest of the loop for parsing of this line
# screen?
m = re.search(r'^Screen [0-9]+: ', line)
if m is not None: # ignore this line
# new connector?
m = re.search(r'^([\w\-]+) (dis)?connected ', line)
if m is not None:
- connector = m.groups()[0]
- assert connector not in self.connectors
- self.connectors[connector] = []
+ connector = Connector(m.group(1))
+ assert not any(c.name == connector.name for c in self.connectors)
+ self.connectors.append(connector)
continue
# new resolution?
- m = re.search(r'^ ([\d]+)x([\d]+) +', line)
+ m = re.search(r'^\s*([\d]+)x([\d]+)', line)
if m is not None:
- resolution = Resolution(int(m.groups()[0]), int(m.groups()[1]))
+ resolution = Resolution(int(m.group(1)), int(m.group(2)))
assert connector is not None
- self.connectors[connector].append(resolution)
+ connector.addResolution(resolution)
+ if '+preferred' in line:
+ connector.preferredResolution = resolution
+ continue
+ # EDID?
+ m = re.search(r'^\s*EDID:\s*$', line)
+ if m is not None:
+ readingEdid = True
continue
# unknown line
# not fatal, e.g. xrandr shows strange stuff when a display is enabled, but not connected
- print("Warning: Unknown xrandr line %s" % line)
+ #print("Warning: Unknown xrandr line %s" % line)
- # return the first available connector from those listed in <tryConnectors>, skipping disabled connectors
- def _findAvailableConnector(self, tryConnectors):
- for connector in tryConnectors:
- if connector in self.connectors and len(self.connectors[connector]): # if the connector exists and is active (i.e. there is a resolution)
- return connector
+ # return the first available connector from those listed in <tryConnectorNames>, skipping disabled connectors
+ def _findAvailableConnector(self, tryConnectorNames):
+ for c in filter(lambda c: c.name in tryConnectorNames and c.isConnected(), self.connectors):
+ return c
return None
# return available internal resolutions
def internalResolutions(self):
- return self.connectors[self.internalConnector]
+ return self.internalConnector.getResolutionList()
# return available external resolutions (or None, if there is no external screen connected)
def externalResolutions(self):
if self.externalConnector is None:
return None
- return self.connectors[self.externalConnector]
+ return self.externalConnector.getResolutionList()
# return resolutions available for both internal and external screen
def commonResolutions(self):
internalRes = self.internalResolutions()
externalRes = self.externalResolutions()
assert externalRes is not None
- return [res for res in externalRes if res in internalRes]
+ return sorted(set(externalRes).intersection(internalRes), key=lambda r: -r.pixelCount())
# compute the xrandr call
def forXrandr(self, setup):
# turn all screens off
connectorArgs = {} # maps connector names to xrand arguments
- for c in self.connectors.keys():
- connectorArgs[c] = ["--off"]
+ for c in self.connectors:
+ connectorArgs[c.name] = ["--off"]
# set arguments for the relevant ones
- connectorArgs[self.internalConnector] = setup.getInternalArgs()
+ connectorArgs[self.internalConnector.name] = setup.getInternalArgs()
if self.externalConnector is not None:
- connectorArgs[self.externalConnector] = setup.getExternalArgs(self.internalConnector)
+ connectorArgs[self.externalConnector.name] = setup.getExternalArgs(self.internalConnector.name)
else:
assert setup.extResolution is None, "There's no external screen to set a resolution for"
# now compose the arguments
call += ["--output", name] + connectorArgs[name]
return call
+ def fetchDBInfo(self, db):
+ if self.externalConnector and self.externalConnector.edid:
+ self.preferredSetup = db.getConfig(self.externalConnector.edid) # may also return None
+ else:
+ self.preferredSetup = None
+ if self.preferredSetup:
+ print("SETUP FOUND", self.preferredSetup)
+ self.externalConnector.lastResolution = self.preferredSetup.extResolution
+ self.internalConnector.lastResolution = self.preferredSetup.intResolution
+ else:
+ print("NO SETUP FOUND")
+
+ def putDBInfo(self, db, setup):
+ if not self.externalConnector or not self.externalConnector.edid:
+ return
+ db.putConfig(self.externalConnector.edid, setup)