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.
19 import argparse, sys, os, os.path, shutil, re, subprocess
21 import gui, screen, util, database
22 frontend = gui.getFrontend("cli") # the fallback, until we got a proper frontend. This is guaranteed to be available.
25 # for auto-config: common names of internal connectors
26 commonInternalConnectorPrefixes = ['LVDS', 'eDP']
27 commonInternalConnectorSuffices = ['', '0', '1', '-0', '-1']
28 def commonInternalConnectorNames():
29 for prefix in commonInternalConnectorPrefixes:
30 for suffix in commonInternalConnectorSuffices:
33 # Load a section-less config file: maps parameter names to space-separated lists of strings (with shell quotation)
34 def loadConfigFile(filename):
37 if not os.path.exists(filename):
38 return result # no config file
41 with open(filename) as f:
45 if not len(line) or line.startswith("#"): continue # skip empty and comment lines
48 pos = line.index("=") # will raise exception when substring is not found
49 curKey = line[:pos].strip()
50 result[curKey] = shlex.split(line[pos+1:]) # shlex.split also strips
52 raise Exception("Invalid config, line %d: Error parsing line (may be a quoting issue)." % linenr)
53 # add some convencience get functions
57 # Make sure the backlight is turned on
58 def turnOnBacklight():
60 backlight = float(subprocess.check_output(["xbacklight", "-get"]).strip())
61 if backlight == 0: # it's completely turned off, we better enable it
62 subprocess.check_call(["xbacklight", "-set", "100"])
63 except FileNotFoundError:
64 print("xbacklight has not been found, unable to turn your laptop backlight on.")
65 except subprocess.CalledProcessError:
66 print("xbacklight returned an error while attempting to turn your laptop backlight on.")
69 # return the current sceen situation, using the configuration to control connecor detection
70 def situationByConfig(config):
72 if 'internalConnector' in config:
73 if len(config['internalConnector']) != 1:
74 raise Exception("You must specify exactly one internal connector.")
75 internalConnectors = config['internalConnector']
77 internalConnectors = commonInternalConnectorNames()
79 return screen.ScreenSituation(internalConnectors, config.get('externalConnectors'))
81 class ShowLevels(Enum):
82 ONEXTERNAL = ("on-external")
84 ONERROR = ("on-error")
85 def __init__(self, text):
88 self._value_ = len(cls.__members__) + 1
92 return list(x.text for x in cls)
95 if __name__ == "__main__":
97 # how do we filter the RelativeScreenPosition for the CLI?
98 relPosFilter = str.lower
100 # parse command-line arguments
101 parser = argparse.ArgumentParser(description='easy Display Setup for Laptops')
102 parser.add_argument("-f", "--frontend",
104 help="The frontend to be used for user interaction")
105 parser.add_argument("-r", "--relative-position",
106 dest="rel_position", choices=list(map(relPosFilter, screen.RelativeScreenPosition.__members__.keys())),
107 help="Set the position of external screen relative to internal one, in case it is not found in the DB.")
108 parser.add_argument("-e", "--external-only",
109 dest="external_only", action='store_true',
110 help="If an external screen is connected, disable all the others.")
111 parser.add_argument("-i", "--internal-only",
112 dest="internal_only", action='store_true',
113 help="Enable internal screen, disable all the others.")
114 parser.add_argument("-s", "--silent",
115 dest="silent", action='store_true',
116 help="Prefer to be silent: Opens a UI only if the external screen is not known *and* no default configuration (-r/-e/-i) is given.")
117 parser.add_argument("--no-db",
118 dest="use_db", action='store_false',
119 help="Do not use the database of known screens.")
120 parser.add_argument("-v", "--verbose",
121 dest="verbose", action='store_true',
122 help="More verbose output on stderr.")
123 cmdArgs = parser.parse_args()
125 # load frontend early (for error mssages)
126 frontend = gui.getFrontend(cmdArgs.frontend)
130 legacyConfigFilePath = os.getenv('HOME') + '/.lilass.conf'
131 configDirectory = util.getConfigDirectory()
132 configFilePath = os.path.join(configDirectory, "lilass.conf")
133 if os.path.isfile(legacyConfigFilePath) and not os.path.isfile(configFilePath):
134 # looks like we just upgraded to a new version of lilass
135 util.mkdirP(configDirectory)
136 shutil.move(legacyConfigFilePath, configFilePath)
138 dataDirectory = util.getDataDirectory()
139 util.mkdirP(dataDirectory)
140 databaseFilePath = os.path.join(dataDirectory, "collected_data.sqlite")
143 config = loadConfigFile(configFilePath)
145 # see what situation we are in
146 situation = situationByConfig(config)
148 # construct the ScreenSetup
150 if situation.externalConnector is not None:
151 # There's an external screen connected that we may want to use.
152 # Fetch info about this screen from the database.
153 # NOTE: If it is too slow to open the DB twice (reading and saving), we can keep it open all the time
155 with database.Database(databaseFilePath) as db:
156 situation.fetchDBInfo(db)
158 have_default_conf = bool(cmdArgs.external_only or cmdArgs.internal_only or cmdArgs.rel_position)
159 no_ui = bool(have_default_conf or (situation.previousSetup and cmdArgs.silent))
161 # ask the user what to do
162 setup = frontend.setup(situation)
163 if setup is None: sys.exit(1) # the user canceled
165 # persists this to disk
166 with database.Database(databaseFilePath) as db:
167 situation.putDBInfo(db, setup)
168 elif situation.previousSetup:
169 # apply the old setup again
170 setup = situation.previousSetup
171 # use default config from CLI
172 elif cmdArgs.external_only:
173 setup = screen.ScreenSetup(intResolution = None, extResolution = situation.externalConnector.getPreferredResolution())
174 elif cmdArgs.rel_position is not None:
175 # construct automatically, based on CLI arguments
176 # first, figure out the desired RelativeScreenPosition... waht a bad hack...
177 relPos = list(filter(lambda relPosItem: relPosFilter(relPosItem[0]) == cmdArgs.rel_position, screen.RelativeScreenPosition.__members__.items()))
178 assert len(relPos) == 1, "CLI argument is ambigue"
179 relPos = relPos[0][1]
180 # now we construct the ScreenSetup
181 if relPos == screen.RelativeScreenPosition.MIRROR:
182 res = situation.commonResolutions()[0]
183 setup = screen.ScreenSetup(res, res, relPos)
185 setup = screen.ScreenSetup(intResolution = situation.internalConnector.getPreferredResolution(),
186 extResolution = situation.externalConnector.getPreferredResolution(),
187 relPosition = relPos)
188 # cmdArgs.internal_only: fall-through
190 assert cmdArgs.internal_only or situation.externalConnector is None
191 # Nothing chosen yet? Use first resolution of internal connector.
192 setup = screen.ScreenSetup(intResolution = situation.internalConnector.getPreferredResolution(), extResolution = None)
195 xrandrCall = situation.forXrandr(setup)
196 print("Call that will be made:",xrandrCall)
197 subprocess.check_call(xrandrCall)
199 # make sure the internal screen is really, *really* turned on if there is no external screen
200 if setup.extResolution is None:
202 except Exception as e:
203 frontend.error(str(e))
204 if cmdArgs is None or cmdArgs.verbose: