X-Git-Url: https://git.ralfj.de/lilass.git/blobdiff_plain/a6c79626b2d248d0484b5242c0a2051da8ec84d1..381dd16b7011170fac8413d93f0d175ef2f22c7a:/screen.py?ds=inline diff --git a/screen.py b/screen.py index d567912..39c9367 100644 --- a/screen.py +++ b/screen.py @@ -18,6 +18,7 @@ import re, subprocess from enum import Enum +from fractions import Fraction ## utility functions @@ -44,16 +45,33 @@ class RelativeScreenPosition(Enum): def __init__(self, text): # auto numbering cls = self.__class__ - self._value_ = len(cls.__members__) + self._value_ = len(cls.__members__) + 1 self.text = text - class Resolution: '''Represents a resolution of a screen''' def __init__(self, width, height): 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 @@ -62,22 +80,20 @@ class Resolution: 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: @@ -119,11 +135,56 @@ 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 """""" % (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): @@ -131,6 +192,9 @@ class ScreenSituation: just choose any remaining connector.''' # which connectors are there? self._getXrandrInformation() + 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: @@ -138,17 +202,27 @@ class ScreenSituation: 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) 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 @@ -157,55 +231,61 @@ class ScreenSituation: # 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 , 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 , 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 @@ -214,3 +294,19 @@ class ScreenSituation: 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)