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'''
47 '''Represents a resolution of a screen'''
48 def __init__(self, width, height):
52 def __eq__(self, other):
53 if not isinstance(other, Resolution):
55 return self.width == other.width and self.height == other.height
57 def __ne__(self, other):
58 return not self.__eq__(other)
62 ratio = int(round(16.0*self.height/self.width))
63 if ratio == 12: # 16:12 = 4:3
65 elif ratio == 13: # 16:12.8 = 5:4
67 else: # let's just hope this will never be 14 or more...
68 strRatio = '16:%d' % ratio
69 return '%dx%d (%s)' %(self.width, self.height, strRatio)
72 return 'screen.Resolution('+self.forXrandr()+')'
75 return str(self.width)+'x'+str(self.height)
79 '''Represents a screen configuration (relative to some notion of an "internal" and an "external" screen): Which screens are enabled with which resolution, how
80 are they positioned, which is the primary screen.'''
81 def __init__(self, intResolution, extResolution, relPosition = None, extIsPrimary = True):
82 '''The resolutions can be None to disable the screen, instances of Resolution. The last two arguments only matter if both screens are enabled.'''
83 assert intResolution is None or isinstance(intResolution, Resolution)
84 assert extResolution is None or isinstance(extResolution, Resolution)
86 self.intResolution = intResolution
87 self.extResolution = extResolution
88 self.relPosition = relPosition
89 self.extIsPrimary = extIsPrimary or self.intResolution is None # external is always primary if it is the only one
91 def getInternalArgs(self):
92 if self.intResolution is None:
94 args = ["--mode", self.intResolution.forXrandr()] # set internal screen to desired resolution
95 if not self.extIsPrimary:
96 args.append('--primary')
99 def getExternalArgs(self, intName):
100 if self.extResolution is None:
102 args = ["--mode", self.extResolution.forXrandr()] # set external screen to desired resolution
103 if self.extIsPrimary:
104 args.append('--primary')
105 if self.intResolution is None:
109 RelativeScreenPosition.LEFT : '--left-of',
110 RelativeScreenPosition.RIGHT : '--right-of',
111 RelativeScreenPosition.ABOVE : '--above',
112 RelativeScreenPosition.BELOW : '--below',
113 RelativeScreenPosition.MIRROR: '--same-as',
114 }[self.relPosition], intName]
118 class ScreenSituation:
119 connectors = {} # maps connector names to lists of Resolution (empty list -> disabled connector)
120 internalConnector = None # name of the internal connector (will be an enabled one)
121 externalConnector = None # name of the used external connector (an enabled one), or None
123 '''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'''
124 def __init__(self, internalConnectorNames, externalConnectorNames = None):
125 '''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
126 just choose any remaining connector.'''
127 # which connectors are there?
128 self._getXrandrInformation()
129 # figure out which is the internal connector
130 self.internalConnector = self._findAvailableConnector(internalConnectorNames)
131 if self.internalConnector is None:
132 raise Exception("Could not automatically find internal connector, please use (or fix) ~/.dsl.conf to specify it manually.")
133 print("Detected internal connector:",self.internalConnector)
134 # and the external one
135 if externalConnectorNames is None:
136 externalConnectorNames = list(self.connectors.keys())
137 externalConnectorNames.remove(self.internalConnector)
138 self.externalConnector = self._findAvailableConnector(externalConnectorNames)
139 if self.internalConnector == self.externalConnector:
140 raise Exception("Internal and external connector are the same. This must not happen. Please fix ~/.dsl.conf.");
141 print("Detected external connector:",self.externalConnector)
143 # Run xrandr and fill the dict of connector names mapped to lists of available resolutions.
144 def _getXrandrInformation(self):
145 connector = None # current connector
146 for line in processOutputGen("xrandr", "-q"):
148 m = re.search(r'^Screen [0-9]+: ', line)
149 if m is not None: # ignore this line
153 m = re.search(r'^([\w\-]+) (dis)?connected ', line)
155 connector = m.groups()[0]
156 assert connector not in self.connectors
157 self.connectors[connector] = []
160 m = re.search(r'^ ([\d]+)x([\d]+) +', line)
162 resolution = Resolution(int(m.groups()[0]), int(m.groups()[1]))
163 assert connector is not None
164 self.connectors[connector].append(resolution)
167 # not fatal, e.g. xrandr shows strange stuff when a display is enabled, but not connected
168 print("Warning: Unknown xrandr line %s" % line)
170 # return the first available connector from those listed in <tryConnectors>, skipping disabled connectors
171 def _findAvailableConnector(self, tryConnectors):
172 for connector in tryConnectors:
173 if connector in self.connectors and len(self.connectors[connector]): # if the connector exists and is active (i.e. there is a resolution)
177 # return available internal resolutions
178 def internalResolutions(self):
179 return self.connectors[self.internalConnector]
181 # return available external resolutions (or None, if there is no external screen connected)
182 def externalResolutions(self):
183 if self.externalConnector is None:
185 return self.connectors[self.externalConnector]
187 # return resolutions available for both internal and external screen
188 def commonResolutions(self):
189 internalRes = self.internalResolutions()
190 externalRes = self.externalResolutions()
191 assert externalRes is not None
192 return [res for res in externalRes if res in internalRes]
194 # compute the xrandr call
195 def forXrandr(self, setup):
196 # turn all screens off
197 connectorArgs = {} # maps connector names to xrand arguments
198 for c in self.connectors.keys():
199 connectorArgs[c] = ["--off"]
200 # set arguments for the relevant ones
201 connectorArgs[self.internalConnector] = setup.getInternalArgs()
202 if self.externalConnector is not None:
203 connectorArgs[self.externalConnector] = setup.getExternalArgs(self.internalConnector)
205 assert setup.extResolution is None, "There's no external screen to set a resolution for"
206 # now compose the arguments
208 for name in connectorArgs:
209 call += ["--output", name] + connectorArgs[name]