X-Git-Url: https://git.ralfj.de/lilass.git/blobdiff_plain/158f0efbe4109a5d3df516404caa8567ddf5e03c..e683c0b07928d9b51505477c85dfd8dc9838ecbc:/screen.py?ds=sidebyside diff --git a/screen.py b/screen.py index 21774df..f0de7e9 100644 --- a/screen.py +++ b/screen.py @@ -18,7 +18,6 @@ import re, subprocess from enum import Enum -from fractions import Fraction ## utility functions @@ -33,6 +32,14 @@ def processOutputGen(*args): def processOutputIt(*args): return list(processOutputGen(*args)) # list() iterates over the generator +# for auto-config: common names of internal connectors +def commonInternalConnectorNames(): + commonInternalConnectorPrefixes = ['LVDS', 'eDP'] + commonInternalConnectorSuffices = ['', '0', '1', '-0', '-1'] + for prefix in commonInternalConnectorPrefixes: + for suffix in commonInternalConnectorSuffices: + yield prefix+suffix + ## the classes class RelativeScreenPosition(Enum): @@ -87,8 +94,15 @@ class Resolution: def __str__(self): # get ratio - ratio = Fraction(self.width, self.height) # automatically divides by the gcd - strRatio = "%d:%d" % (ratio.numerator, ratio.denominator) + ratio = int(round(16.0*self.height/self.width)) + if ratio == 11: # 16:10.66 = 3:2 + strRatio = "3:2" + elif 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 return '%dx%d (%s)' %(self.width, self.height, strRatio) def __repr__(self): @@ -147,10 +161,10 @@ class ScreenSetup: 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.edid = None # EDID string for the connector, or None if disconnected / unavailable + self._resolutions = set() # set of Resolution objects, empty if disconnected + self._preferredResolution = None + self.previousResolution = None self.hasLastResolution = False def __str__(self): @@ -159,23 +173,9 @@ class Connector: 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 + # It is very possible not to have an EDID even for a connected connector + return len(self._resolutions) > 0 def addResolution(self, resolution): assert isinstance(resolution, Resolution) @@ -187,23 +187,31 @@ class Connector: else: self.edid += s + def setPreferredResolution(self, resolution): + assert isinstance(resolution, Resolution) and resolution in self._resolutions + self._preferredResolution = resolution + + def getPreferredResolution(self): + if self._preferredResolution is not None: + return self._preferredResolution + return self.getResolutionList()[0] # prefer the largest resolution + 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())) + return sorted(self._resolutions, key=lambda r: -r.pixelCount()) class ScreenSituation: - connectors = [] # contains all the Connector objects + connectors = None # 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 + previousSetup = None # None or the ScreenSetup used the last time this external screen was connected '''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): + def __init__(self, internalConnectorNames, externalConnectorNames = None, xrandrSource = None): '''Both arguments are lists of connector names. The first one which exists and has a screen attached is chosen for that class. can be None to just choose any remaining connector.''' # which connectors are there? - self._getXrandrInformation() - for c in self.connectors: - print(repr(c)) - print() + self.connectors = [] + self._getXrandrInformation(xrandrSource) # figure out which is the internal connector self.internalConnector = self._findAvailableConnector(internalConnectorNames) if self.internalConnector is None: @@ -217,13 +225,14 @@ class ScreenSituation: 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.lastSetup 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): + def _getXrandrInformation(self, xrandrSource = None): connector = None # current connector readingEdid = False - for line in processOutputGen("xrandr", "-q", "--verbose"): + if xrandrSource is None: + xrandrSource = processOutputGen("xrandr", "-q", "--verbose") + for line in xrandrSource: if readingEdid: m = re.match(r'^\s*([0-9a-f]+)\s*$', line) if m is not None: @@ -241,8 +250,10 @@ class ScreenSituation: m = re.search(r'^([\w\-]+) (dis)?connected ', line) if m is not None: connector = Connector(m.group(1)) - assert not any(c.name == connector.name for c in self.connectors) - self.connectors.append(connector) + assert not any(c.name == connector.name for c in self.connectors), "Duplicate connector {}".format(connector.name) + if not connector.name.startswith("VIRTUAL"): + # skip "VIRTUAL" connectors + self.connectors.append(connector) continue # new resolution? m = re.search(r'^\s*([\d]+)x([\d]+)', line) @@ -250,8 +261,8 @@ class ScreenSituation: resolution = Resolution(int(m.group(1)), int(m.group(2))) assert connector is not None connector.addResolution(resolution) - if '+preferred' in line: - connector.preferredResolution = resolution + if re.search(r' [+]preferred\b', line): + connector.setPreferredResolution(resolution) continue # EDID? m = re.search(r'^\s*EDID:\s*$', line) @@ -259,8 +270,7 @@ class ScreenSituation: 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) + # not fatal, e.g. xrandr shows strange stuff when a display is enabled, but not connected; --verbose adds a whole lot of other weird stuff # return the first available connector from those listed in , skipping disabled connectors def _findAvailableConnector(self, tryConnectorNames): @@ -268,21 +278,11 @@ class ScreenSituation: return c return None - # return available internal resolutions - def internalResolutions(self): - 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.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 + assert self.externalConnector is not None, "Common resolutions may only be queried if there is an external screen connected." + internalRes = self.internalConnector.getResolutionList() + externalRes = self.externalConnector.getResolutionList() return sorted(set(externalRes).intersection(internalRes), key=lambda r: -r.pixelCount()) # compute the xrandr call @@ -305,15 +305,13 @@ class ScreenSituation: def fetchDBInfo(self, db): if self.externalConnector and self.externalConnector.edid: - self.lastSetup = db.getConfig(self.externalConnector.edid) # may also return None - else: - self.lastSetup = None - if self.lastSetup: - print("SETUP FOUND", self.lastSetup) - self.externalConnector.lastResolution = self.lastSetup.extResolution - self.internalConnector.lastResolution = self.lastSetup.intResolution + self.previousSetup = db.getConfig(self.externalConnector.edid) # may also return None else: - print("NO SETUP FOUND") + self.previousSetup = None + if self.previousSetup: + print("Known screen, previous setup:", self.previousSetup) + self.externalConnector.previousResolution = self.previousSetup.extResolution + self.internalConnector.previousResolution = self.previousSetup.intResolution def putDBInfo(self, db, setup): if not self.externalConnector or not self.externalConnector.edid: