2 # DSL - easy Display Setup for Laptops
3 # Copyright (C) 2012-2015 Ralf Jung <post@ralfj.de>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24 # execute a process, return output as iterator, throw exception if there was an error
25 # you *must* iterate to the end if you use this!
26 def processOutputGen(*args):
27 with subprocess.Popen(args, stdout=subprocess.PIPE) as p:
29 yield line.decode("utf-8")
31 raise Exception("Error executing "+str(args))
32 def processOutputIt(*args):
33 return list(processOutputGen(*args)) # list() iterates over the generator
37 class RelativeScreenPosition(Enum):
38 '''Represents the relative position of the external screen to the internal one'''
44 def __init__(self, text):
47 self._value_ = len(cls.__members__)
52 '''Represents a resolution of a screen'''
53 def __init__(self, width, height):
57 def __eq__(self, other):
58 if not isinstance(other, Resolution):
60 return self.width == other.width and self.height == other.height
62 def __ne__(self, other):
63 return not self.__eq__(other)
67 ratio = int(round(16.0*self.height/self.width))
68 if ratio == 12: # 16:12 = 4:3
70 elif ratio == 13: # 16:12.8 = 5:4
72 else: # let's just hope this will never be 14 or more...
73 strRatio = '16:%d' % ratio
74 return '%dx%d (%s)' %(self.width, self.height, strRatio)
77 return 'screen.Resolution('+self.forXrandr()+')'
80 return str(self.width)+'x'+str(self.height)
84 '''Represents a screen configuration (relative to some notion of an "internal" and an "external" screen): Which screens are enabled with which resolution, how
85 are they positioned, which is the primary screen.'''
86 def __init__(self, intResolution, extResolution, relPosition = None, extIsPrimary = True):
87 '''The resolutions can be None to disable the screen, instances of Resolution. The last two arguments only matter if both screens are enabled.'''
88 assert intResolution is None or isinstance(intResolution, Resolution)
89 assert extResolution is None or isinstance(extResolution, Resolution)
91 self.intResolution = intResolution
92 self.extResolution = extResolution
93 self.relPosition = relPosition
94 self.extIsPrimary = extIsPrimary or self.intResolution is None # external is always primary if it is the only one
96 def getInternalArgs(self):
97 if self.intResolution is None:
99 args = ["--mode", self.intResolution.forXrandr()] # set internal screen to desired resolution
100 if not self.extIsPrimary:
101 args.append('--primary')
104 def getExternalArgs(self, intName):
105 if self.extResolution is None:
107 args = ["--mode", self.extResolution.forXrandr()] # set external screen to desired resolution
108 if self.extIsPrimary:
109 args.append('--primary')
110 if self.intResolution is None:
114 RelativeScreenPosition.LEFT : '--left-of',
115 RelativeScreenPosition.RIGHT : '--right-of',
116 RelativeScreenPosition.ABOVE : '--above',
117 RelativeScreenPosition.BELOW : '--below',
118 RelativeScreenPosition.MIRROR: '--same-as',
119 }[self.relPosition], intName]
123 name = None # connector name, e.g. "HDMI1"
124 edid = None # EDID string for the connector, or None if disconnected
125 resolutions = [] # list of Resolution objects, empty if disconnected
127 def __init__(self, name=None):
130 return str(self.name)
131 def isConnected(self):
132 assert (self.edid is None) == (len(self.resolutions)==0)
133 return self.edid is not None
134 def addResolution(self, resolution):
135 assert isinstance(resolution, Resolution)
136 self.resolutions.append(resolution)
137 def appendToEdid(self, s):
138 if self.edid is None:
143 class ScreenSituation:
144 connectors = [] # contains all the Connector objects
145 internalConnector = None # the internal Connector object (will be an enabled one)
146 externalConnector = None # the used external Connector object (an enabled one), or None
148 '''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'''
149 def __init__(self, internalConnectorNames, externalConnectorNames = None):
150 '''Both arguments are lists of connector names. The first one which exists and has a screen attached is chosen for that class. <externalConnectorNames> can be None to
151 just choose any remaining connector.'''
152 # which connectors are there?
153 self._getXrandrInformation()
154 # figure out which is the internal connector
155 self.internalConnector = self._findAvailableConnector(internalConnectorNames)
156 if self.internalConnector is None:
157 raise Exception("Could not automatically find internal connector, please use (or fix) ~/.dsl.conf to specify it manually.")
158 print("Detected internal connector:",self.internalConnector)
159 # and the external one
160 if externalConnectorNames is None:
161 externalConnectorNames = map(lambda c: c.name, self.connectors)
162 externalConnectorNames = filter(lambda name: name != self.internalConnector.name, externalConnectorNames)
163 self.externalConnector = self._findAvailableConnector(externalConnectorNames)
164 if self.internalConnector == self.externalConnector:
165 raise Exception("Internal and external connector are the same. This must not happen. Please fix ~/.dsl.conf.");
166 print("Detected external connector:",self.externalConnector)
168 # Run xrandr and fill the dict of connector names mapped to lists of available resolutions.
169 def _getXrandrInformation(self):
170 connector = None # current connector
172 for line in processOutputGen("xrandr", "-q", "--verbose"):
174 m = re.match(r'^\s*([0-9a-f]+)\s*$', line)
176 connector.appendToEdid(m.group(1))
180 # fallthrough to the rest of the loop for parsing of this line
182 m = re.search(r'^Screen [0-9]+: ', line)
183 if m is not None: # ignore this line
187 m = re.search(r'^([\w\-]+) (dis)?connected ', line)
189 connector = Connector(m.group(1))
190 assert not any(c.name == connector.name for c in self.connectors)
191 self.connectors.append(connector)
194 m = re.search(r'^\s*([\d]+)x([\d]+)', line)
196 resolution = Resolution(int(m.group(1)), int(m.group(2)))
197 assert connector is not None
198 connector.addResolution(resolution)
201 m = re.search(r'^\s*EDID:\s*$', line)
206 # not fatal, e.g. xrandr shows strange stuff when a display is enabled, but not connected
207 print("Warning: Unknown xrandr line %s" % line)
209 # return the first available connector from those listed in <tryConnectorNames>, skipping disabled connectors
210 def _findAvailableConnector(self, tryConnectorNames):
211 for c in filter(lambda c: c.name in tryConnectorNames and c.isConnected(), self.connectors):
215 # return available internal resolutions
216 def internalResolutions(self):
217 return self.internalConnector.resolutions
219 # return available external resolutions (or None, if there is no external screen connected)
220 def externalResolutions(self):
221 if self.externalConnector is None:
223 return self.externalConnector.resolutions
225 # return resolutions available for both internal and external screen
226 def commonResolutions(self):
227 internalRes = self.internalResolutions()
228 externalRes = self.externalResolutions()
229 assert externalRes is not None
230 return [res for res in externalRes if res in internalRes]
232 # compute the xrandr call
233 def forXrandr(self, setup):
234 # turn all screens off
235 connectorArgs = {} # maps connector names to xrand arguments
236 for c in self.connectors:
237 connectorArgs[c.name] = ["--off"]
238 # set arguments for the relevant ones
239 connectorArgs[self.internalConnector.name] = setup.getInternalArgs()
240 if self.externalConnector is not None:
241 connectorArgs[self.externalConnector.name] = setup.getExternalArgs(self.internalConnector.name)
243 assert setup.extResolution is None, "There's no external screen to set a resolution for"
244 # now compose the arguments
246 for name in connectorArgs:
247 call += ["--output", name] + connectorArgs[name]