lastSetup support in question frontends
[lilass.git] / lilass
1 #!/usr/bin/env python3
2 # DSL - easy Display Setup for Laptops
3 # Copyright (C) 2012-2015 Ralf Jung <post@ralfj.de>
4 #
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.
9 #
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.
14 #
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.
18
19 import argparse, sys, os, os.path, shutil, re, subprocess
20 from enum import Enum
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.
23
24
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:
31             yield prefix+suffix
32
33 # Load a section-less config file: maps parameter names to space-separated lists of strings (with shell quotation)
34 def loadConfigFile(filename):
35     import shlex
36     result = {}
37     if not os.path.exists(filename):
38         return result # no config file
39     # read config file
40     linenr = 0
41     with open(filename) as f:
42         for line in f:
43             linenr += 1
44             line = line.strip()
45             if not len(line) or line.startswith("#"): continue # skip empty and comment lines
46             try:
47                 # parse line
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
51             except Exception:
52                 raise Exception("Invalid config, line %d: Error parsing line (may be a quoting issue)." % linenr)
53     # add some convencience get functions
54     return result
55
56
57 # Make sure the backlight is turned on
58 def turnOnBacklight():
59     try:
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.")
67
68
69 # return the current sceen situation, using the configuration to control connecor detection
70 def situationByConfig(config):
71     # internal connectors
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']
76     else:
77         internalConnectors = commonInternalConnectorNames()
78     # run!
79     return screen.ScreenSituation(internalConnectors, config.get('externalConnectors'))
80
81 class ShowLevels(Enum):
82     ONEXTERNAL = ("on-external")
83     ONNEW = ("on-new")
84     ONERROR = ("on-error")
85     def __init__(self, text):
86         # auto numbering
87         cls = self.__class__
88         self._value_ = len(cls.__members__) + 1
89         self.text = text
90     @classmethod
91     def getNames(cls):
92         return list(x.text for x in cls)
93
94 # if we run top-level
95 if __name__ == "__main__":
96     try:
97         # how do we filter the RelativeScreenPosition for the CLI?
98         relPosFilter = str.lower
99         
100         # parse command-line arguments
101         parser = argparse.ArgumentParser(description='easy Display Setup for Laptops')
102         parser.add_argument("-f", "--frontend",
103                             dest="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.")
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", "--show",
115                             dest="show", choices=ShowLevels.getNames(), default=ShowLevels.ONEXTERNAL.text,
116                             help="In which situations should the UI be displayed?")
117         cmdArgs = parser.parse_args()
118     
119         # load frontend early (for error mssages)
120         frontend = gui.getFrontend(cmdArgs.frontend)
121         
122         # find files
123         ## find config file
124         legacyConfigFilePath = os.getenv('HOME') + '/.lilass.conf'
125         configDirectory = util.getConfigDirectory()
126         configFilePath = os.path.join(configDirectory, "lilass.conf")
127         if os.path.isfile(legacyConfigFilePath) and not os.path.isfile(configFilePath):
128             # looks like we just upgraded to a new version of lilass
129             util.mkdirP(configDirectory)
130             shutil.move(legacyConfigFilePath, configFilePath)
131         ## find database
132         dataDirectory = util.getDataDirectory()
133         util.mkdirP(dataDirectory)
134         databaseFilePath = os.path.join(dataDirectory, "collected_data.sqlite")
135
136         # load configuration
137         config = loadConfigFile(configFilePath)
138         
139         # see what situation we are in
140         situation = situationByConfig(config)
141         
142         # fetch info from the database
143         # if it is too slow to open the DB twice (reading and saving), we can keep it open all the time
144         with database.Database(databaseFilePath) as db:
145             situation.fetchDBInfo(db)
146         
147         # construct the ScreenSetup
148         setup = None
149         if not cmdArgs.internal_only and situation.externalResolutions() is not None:
150             # there's an external screen connected that we may want to use
151             if cmdArgs.external_only:
152                 setup = screen.ScreenSetup(intResolution = None, extResolution = situation.externalResolutions()[0])
153             elif cmdArgs.rel_position is not None:
154                 # construct automatically, based on CLI arguments
155                 # first, figure out the desired RelativeScreenPosition... waht a bad hack...
156                 relPos = list(filter(lambda relPosItem: relPosFilter(relPosItem[0]) == cmdArgs.rel_position, screen.RelativeScreenPosition.__members__.items()))
157                 assert len(relPos) == 1, "CLI argument is ambigue"
158                 relPos = relPos[0][1]
159                 # now we construct the ScreenSetup
160                 if relPos == screen.RelativeScreenPosition.MIRROR:
161                     res = situation.commonResolutions()[0]
162                     setup = screen.ScreenSetup(res, res, relPos)
163                 else:
164                     setup = screen.ScreenSetup(intResolution = situation.internalResolutions()[0], extResolution = situation.externalResolutions()[0], relPosition = relPos)
165             else:
166                 showlvl = ShowLevels(cmdArgs.show)
167                 if showlvl != ShowLevels.ONEXTERNAL and situation.lastSetup:
168                     # use last config
169                     setup = situation.lastSetup
170                 elif showlvl == ShowLevels.ONERROR:
171                     # guess config
172                     setup = screen.ScreenSetup(situation.internalResolutions()[0], situation.externalResolutions()[0], screen.RelativeScreenPosition.RIGHT)
173                     # TODO make default relative position configurable in the config file
174                     # TODO this has a bit of code duplication with the cmdArgs method above
175                 else:
176                     # ask the user
177                     setup = frontend.setup(situation)
178                     if setup is None: sys.exit(1) # the user canceled
179                     with database.Database(databaseFilePath) as db:
180                         situation.putDBInfo(db, setup)
181         else:
182             # use first resolution of internal connector
183             setup = screen.ScreenSetup(intResolution = situation.internalConnector.getResolutionList()[0], extResolution = None)
184         
185         # call xrandr
186         xrandrCall = situation.forXrandr(setup)
187         print("Call that will be made:",xrandrCall)
188         subprocess.check_call(xrandrCall)
189         
190         # make sure the internal screen is really, *really* turned on if there is no external screen
191         if setup.extResolution is None:
192             turnOnBacklight()
193     except Exception as e:
194         raise e
195         frontend.error(str(e))