7b94e188e7aa5e5eecd9b0896854fcee551cbf65
[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, re, subprocess
20 from enum import Enum
21 import gui, screen
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
34 # Load a section-less config file: maps parameter names to space-separated lists of strings (with shell quotation)
35 def loadConfigFile(filename):
36     import shlex
37     result = {}
38     if not os.path.exists(filename):
39         return result # no config file
40     # read config file
41     linenr = 0
42     with open(filename) as f:
43         for line in f:
44             linenr += 1
45             line = line.strip()
46             if not len(line) or line.startswith("#"): continue # skip empty and comment lines
47             try:
48                 # parse line
49                 pos = line.index("=") # will raise exception when substring is not found
50                 curKey = line[:pos].strip()
51                 result[curKey] = shlex.split(line[pos+1:]) # shlex.split also strips
52             except Exception:
53                 raise Exception("Invalid config, line %d: Error parsing line (may be a quoting issue)." % linenr)
54     # add some convencience get functions
55     return result
56
57
58 # Make sure the backlight is turned on
59 def turnOnBacklight():
60     try:
61         backlight = float(subprocess.check_output(["xbacklight", "-get"]).strip())
62         if backlight == 0: # it's completely turned off, we better enable it
63             subprocess.check_call(["xbacklight", "-set", "100"])
64     except FileNotFoundError:
65         print("xbacklight has not been found, unable to turn your laptop backlight on.")
66     except subprocess.CalledProcessError:
67         print("xbacklight returned an error while attempting to turn your laptop backlight on.")
68
69
70 # return the current sceen situation, using the configuration to control connecor detection
71 def situationByConfig(config):
72     # internal connectors
73     if 'internalConnector' in config:
74         if len(config['internalConnector']) != 1:
75             raise Exception("You must specify exactly one internal connector.")
76         internalConnectors = config['internalConnector']
77     else:
78         internalConnectors = commonInternalConnectorNames()
79     # run!
80     return screen.ScreenSituation(internalConnectors, config.get('externalConnectors'))
81
82
83 # if we run top-level
84 if __name__ == "__main__":
85     try:
86         # how do we filter the RelativeScreenPosition for the CLI?
87         relPosFilter = str.lower
88         
89         # parse command-line arguments
90         parser = argparse.ArgumentParser(description='easy Display Setup for Laptops')
91         parser.add_argument("-f", "--frontend",
92                             dest="frontend",
93                             help="The frontend to be used for user interaction")
94         parser.add_argument("-r", "--relative-position",
95                             dest="rel_position", choices=list(map(relPosFilter, screen.RelativeScreenPosition.__members__.keys())),
96                             help="Set the position of external screen relative to internal one.")
97         parser.add_argument("-e", "--external-only",
98                             dest="external_only", action='store_true',
99                             help="If an external screen is connected, disable all the others.")
100         parser.add_argument("-i", "--internal-only",
101                             dest="internal_only", action='store_true',
102                             help="Enable internal screen, disable all the others.")
103         cmdArgs = parser.parse_args()
104     
105         # load frontend early (for error mssages)
106         frontend = gui.getFrontend(cmdArgs.frontend)
107         
108         # load configuration
109         config = loadConfigFile(os.getenv('HOME') + '/.lilass.conf')
110         
111         # see what situation we are in
112         situation = situationByConfig(config)
113         
114         # construct the ScreenSetup
115         setup = None
116         if not cmdArgs.internal_only and situation.externalResolutions() is not None:
117             # there's an external screen connected that we may want to use
118             if cmdArgs.external_only:
119                 setup = screen.ScreenSetup(intResolution = None, extResolution = situation.externalResolutions()[0])
120             elif cmdArgs.rel_position is not None:
121                 # construct automatically, based on CLI arguments
122                 # first, figure out the desired RelativeScreenPosition... waht a bad hack...
123                 relPos = list(filter(lambda relPosItem: relPosFilter(relPosItem[0]) == cmdArgs.rel_position, screen.RelativeScreenPosition.__members__.items()))
124                 assert len(relPos) == 1, "CLI argument is ambigue"
125                 relPos = relPos[0][1]
126                 # now we construct the ScreenSetup
127                 if relPos == screen.RelativeScreenPosition.MIRROR:
128                     res = situation.commonResolutions()[0]
129                     setup = screen.ScreenSetup(res, res, relPos)
130                 else:
131                     setup = screen.ScreenSetup(intResolution = situation.internalResolutions()[0], extResolution = situation.externalResolutions()[0], relPosition = relPos)
132             else:
133                 # ask the user
134                 setup = frontend.setup(situation)
135                 if setup is None: sys.exit(1) # the user canceled
136         else:
137             # use first resolution of internal connector
138             setup = screen.ScreenSetup(intResolution = situation.internalResolutions()[0], extResolution = None)
139         
140         # call xrandr
141         xrandrCall = situation.forXrandr(setup)
142         print("Call that will be made:",xrandrCall)
143         subprocess.check_call(xrandrCall)
144         
145         # make sure the internal screen is really, *really* turned on if there is no external screen
146         if setup.extResolution is None:
147             turnOnBacklight()
148     except Exception as e:
149         raise e
150         frontend.error(str(e))