]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/blob - autorandr.py
Detect negative positions
[deb_pkgs/autorandr.git] / autorandr.py
1 #!/usr/bin/env python
2 # encoding: utf-8
3 #
4 # autorandr.py
5 # Copyright (c) 2015, Phillip Berndt
6 #
7 # Autorandr rewrite in Python
8 #
9 # This script aims to be fully compatible with the original autorandr.
10 #
11 # This program is free software: you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation, either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 #
24
25 from __future__ import print_function
26 import copy
27 import getopt
28
29 import binascii
30 import hashlib
31 import os
32 import re
33 import subprocess
34 import sys
35 from distutils.version import LooseVersion as Version
36
37 from itertools import chain
38 from collections import OrderedDict
39
40 virtual_profiles = [
41     # (name, description, callback)
42     ("common", "Clone all connected outputs at the largest common resolution", None),
43     ("horizontal", "Stack all connected outputs horizontally at their largest resolution", None),
44     ("vertical", "Stack all connected outputs vertically at their largest resolution", None),
45 ]
46
47 help_text = """
48 Usage: autorandr [options]
49
50 -h, --help              get this small help
51 -c, --change            reload current setup
52 -s, --save <profile>    save your current setup to profile <profile>
53 -l, --load <profile>    load profile <profile>
54 -d, --default <profile> make profile <profile> the default profile
55 --force                 force (re)loading of a profile
56 --fingerprint           fingerprint your current hardware setup
57 --config                dump your current xrandr setup
58 --dry-run               don't change anything, only print the xrandr commands
59
60  To prevent a profile from being loaded, place a script call "block" in its
61  directory. The script is evaluated before the screen setup is inspected, and
62  in case of it returning a value of 0 the profile is skipped. This can be used
63  to query the status of a docking station you are about to leave.
64
65  If no suitable profile can be identified, the current configuration is kept.
66  To change this behaviour and switch to a fallback configuration, specify
67  --default <profile>.
68
69  Another script called "postswitch" can be placed in the directory
70  ~/.config/autorandr (or ~/.autorandr if you have an old installation) as well
71  as in any profile directories: The scripts are executed after a mode switch
72  has taken place and can notify window managers.
73
74  The following virtual configurations are available:
75 """.strip()
76
77 class XrandrOutput(object):
78     "Represents an XRandR output"
79
80     # This regular expression is used to parse an output in `xrandr --verbose'
81     XRANDR_OUTPUT_REGEXP = """(?x)
82         ^(?P<output>[^ ]+)\s+                                                           # Line starts with output name
83         (?:                                                                             # Differentiate disconnected and connected in first line
84             disconnected |
85             unknown\ connection |
86             (?P<connected>connected)
87         )
88         \s*
89         (?P<primary>primary\ )?                                                         # Might be primary screen
90         (?:\s*
91             (?P<width>[0-9]+)x(?P<height>[0-9]+)                                        # Resolution (might be overridden below!)
92             \+(?P<x>-?[0-9]+)\+(?P<y>-?[0-9]+)\s+                                       # Position
93             (?:\(0x[0-9a-fA-F]+\)\s+)?                                                  # XID
94             (?P<rotate>(?:normal|left|right|inverted))\s+                               # Rotation
95             (?:(?P<reflect>X\ and\ Y|X|Y)\ axis)?                                       # Reflection
96         )?                                                                              # .. but everything of the above only if the screen is in use.
97         (?:[\ \t]*\([^\)]+\))(?:\s*[0-9]+mm\sx\s[0-9]+mm)?
98         (?:[\ \t]*panning\ (?P<panning>[0-9]+x[0-9]+\+[0-9]+\+[0-9]+))?                 # Panning information
99         (?:\s*(?:                                                                       # Properties of the output
100             Gamma: (?P<gamma>[0-9\.: ]+) |                                              # Gamma value
101             Transform: (?P<transform>(?:[\-0-9\. ]+\s+){3}) |                           # Transformation matrix
102             EDID: (?P<edid>\s*?(?:\\n\\t\\t[0-9a-f]+)+) |                               # EDID of the output
103             (?![0-9])[^:\s][^:\n]+:.*(?:\s\\t[\\t ].+)*                                 # Other properties
104         ))+
105         \s*
106         (?P<modes>(?:
107             (?P<mode_width>[0-9]+)x(?P<mode_height>[0-9]+).+?\*current.+\s+
108                 h:.+\s+v:.+clock\s+(?P<rate>[0-9\.]+)Hz\s* |                            # Interesting (current) resolution: Extract rate
109             [0-9]+x[0-9]+.+\s+h:.+\s+v:.+\s*                                            # Other resolutions
110         )*)
111     """
112
113     XRANDR_OUTPUT_MODES_REGEXP = """(?x)
114         (?P<width>[0-9]+)x(?P<height>[0-9]+)
115         .*?(?P<preferred>\+preferred)?
116         \s+h:.+
117         \s+v:.+clock\s+(?P<rate>[0-9\.]+)Hz
118     """
119
120     XRANDR_13_DEFAULTS = {
121         "transform": "1,0,0,0,1,0,0,0,1",
122         "panning": "0x0",
123     }
124
125     XRANDR_12_DEFAULTS = {
126         "reflect": "normal",
127         "rotate": "normal",
128         "gamma": "1.0:1.0:1.0",
129     }
130
131     XRANDR_DEFAULTS = dict(list(XRANDR_13_DEFAULTS.items()) + list(XRANDR_12_DEFAULTS.items()))
132
133     EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
134
135     def __repr__(self):
136         return "<%s%s %s>" % (self.output, (" %s..%s" % (self.edid[:5], self.edid[-5:])) if self.edid else "", " ".join(self.option_vector))
137
138     @property
139     def options_with_defaults(self):
140         "Return the options dictionary, augmented with the default values that weren't set"
141         if "off" in self.options:
142             return self.options
143         options = {}
144         if xrandr_version() >= Version("1.3"):
145             options.update(self.XRANDR_13_DEFAULTS)
146         if xrandr_version() >= Version("1.2"):
147             options.update(self.XRANDR_12_DEFAULTS)
148         options.update(self.options)
149         return options
150
151     @property
152     def option_vector(self):
153         "Return the command line parameters for XRandR for this instance"
154         return sum([["--%s" % option[0], option[1]] if option[1] else ["--%s" % option[0]] for option in chain((("output", self.output),), sorted(self.options_with_defaults.items()))], [])
155
156     @property
157     def option_string(self):
158         "Return the command line parameters in the configuration file format"
159         return "\n".join([ " ".join(option) if option[1] else option[0] for option in chain((("output", self.output),), sorted(self.options.items()))])
160
161     @property
162     def sort_key(self):
163         "Return a key to sort the outputs for xrandr invocation"
164         if not self.edid:
165             return -2
166         if "off" in self.options:
167             return -1
168         if "pos" in self.options:
169             x, y = map(float, self.options["pos"].split("x"))
170         else:
171             x, y = 0, 0
172         return x + 10000 * y
173
174     def __init__(self, output, edid, options):
175         "Instanciate using output name, edid and a dictionary of XRandR command line parameters"
176         self.output = output
177         self.edid = edid
178         self.options = options
179         self.remove_default_option_values()
180
181     def remove_default_option_values(self):
182         "Remove values from the options dictionary that are superflous"
183         if "off" in self.options and len(self.options.keys()) > 1:
184             self.options = { "off": None }
185             return
186         for option, default_value in self.XRANDR_DEFAULTS.items():
187             if option in self.options and self.options[option] == default_value:
188                 del self.options[option]
189
190     @classmethod
191     def from_xrandr_output(cls, xrandr_output):
192         """Instanciate an XrandrOutput from the output of `xrandr --verbose'
193
194         This method also returns a list of modes supported by the output.
195         """
196         try:
197             xrandr_output = xrandr_output.replace("\r\n", "\n")
198             match_object = re.search(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
199         except:
200             raise RuntimeError("Parsing XRandR output failed, there is an error in the regular expression.")
201         if not match_object:
202             debug = debug_regexp(XrandrOutput.XRANDR_OUTPUT_REGEXP, xrandr_output)
203             raise RuntimeError("Parsing XRandR output failed, the regular expression did not match: %s" % debug)
204         remainder = xrandr_output[len(match_object.group(0)):]
205         if remainder:
206             raise RuntimeError(("Parsing XRandR output failed, %d bytes left unmatched after regular expression, "
207                                 "starting at byte %d with ..'%s'.") % (len(remainder), len(len(match_object.group(0))), remainder[:10]))
208
209         match = match_object.groupdict()
210
211         modes = []
212         if match["modes"]:
213             modes = [ x.groupdict() for x in re.finditer(XrandrOutput.XRANDR_OUTPUT_MODES_REGEXP, match["modes"]) ]
214             if not modes:
215                 raise RuntimeError("Parsing XRandR output failed, couldn't find any display modes")
216
217         options = {}
218         if not match["connected"]:
219             edid = None
220         else:
221             edid = "".join(match["edid"].strip().split()) if match["edid"] else "%s-%s" % (XrandrOutput.EDID_UNAVAILABLE, match["output"])
222
223         if not match["width"]:
224             options["off"] = None
225         else:
226             if match["mode_width"]:
227                 options["mode"] = "%sx%s" % (match["mode_width"], match["mode_height"])
228             else:
229                 if match["rotate"] not in ("left", "right"):
230                     options["mode"] = "%sx%s" % (match["width"], match["height"])
231                 else:
232                     options["mode"] = "%sx%s" % (match["height"], match["width"])
233             options["rotate"] = match["rotate"]
234             if match["primary"]:
235                 options["primary"] = None
236             if match["reflect"] == "X":
237                 options["reflect"] = "x"
238             elif match["reflect"] == "Y":
239                 options["reflect"] = "y"
240             elif match["reflect"] == "X and Y":
241                 options["reflect"] = "xy"
242             options["pos"] = "%sx%s" % (match["x"], match["y"])
243             if match["panning"]:
244                 options["panning"] = match["panning"]
245             if match["transform"]:
246                 transformation = ",".join(match["transform"].strip().split())
247                 if transformation != "1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000":
248                     options["transform"] = transformation
249                     if not match["mode_width"]:
250                         # TODO We'd need to apply the reverse transformation here. Let's see if someone complains, I doubt that this
251                         # special case is actually required.
252                         print("Warning: Output %s has a transformation applied. Could not determine correct mode!", file=sys.stderr)
253             if match["gamma"]:
254                 gamma = match["gamma"].strip()
255                 options["gamma"] = gamma
256             if match["rate"]:
257                 options["rate"] = match["rate"]
258
259         return XrandrOutput(match["output"], edid, options), modes
260
261     @classmethod
262     def from_config_file(cls, edid_map, configuration):
263         "Instanciate an XrandrOutput from the contents of a configuration file"
264         options = {}
265         for line in configuration.split("\n"):
266             if line:
267                 line = line.split(None, 1)
268                 options[line[0]] = line[1] if len(line) > 1 else None
269
270         edid = None
271
272         if options["output"] in edid_map:
273             edid = edid_map[options["output"]]
274         else:
275             # This fuzzy matching is for legacy autorandr that used sysfs output names
276             fuzzy_edid_map = [ re.sub("(card[0-9]+|-)", "", x) for x in edid_map.keys() ]
277             fuzzy_output = re.sub("(card[0-9]+|-)", "", options["output"])
278             if fuzzy_output in fuzzy_edid_map:
279                 edid = edid_map[list(edid_map.keys())[fuzzy_edid_map.index(fuzzy_output)]]
280             elif "off" not in options:
281                 raise RuntimeError("Failed to find an EDID for output `%s' in setup file, required as `%s' is not off in config file."
282                                    % (options["output"], options["output"]))
283         output = options["output"]
284         del options["output"]
285
286         return XrandrOutput(output, edid, options)
287
288     def edid_equals(self, other):
289         "Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
290         if self.edid and other.edid:
291             if len(self.edid) == 32 and len(other.edid) != 32 and not other.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
292                 return hashlib.md5(binascii.unhexlify(other.edid)).hexdigest() == self.edid
293             if len(self.edid) != 32 and len(other.edid) == 32 and not self.edid.startswith(XrandrOutput.EDID_UNAVAILABLE):
294                 return hashlib.md5(binascii.unhexlify(self.edid)).hexdigest() == other.edid
295         return self.edid == other.edid
296
297     def __eq__(self, other):
298         return self.edid_equals(other) and self.output == other.output and self.options == other.options
299
300 def xrandr_version():
301     "Return the version of XRandR that this system uses"
302     if getattr(xrandr_version, "version", False) is False:
303         version_string = os.popen("xrandr -v").read()
304         try:
305             version = re.search("xrandr program version\s+([0-9\.]+)", version_string).group(1)
306             xrandr_version.version = Version(version)
307         except AttributeError:
308             xrandr_version.version = Version("1.3.0")
309
310     return xrandr_version.version
311
312 def debug_regexp(pattern, string):
313     "Use the partial matching functionality of the regex module to display debug info on a non-matching regular expression"
314     try:
315         import regex
316         bounds = ( 0, len(string) )
317         while bounds[0] != bounds[1]:
318             half = int((bounds[0] + bounds[1]) / 2)
319             if half == bounds[0]:
320                 break
321             bounds = (half, bounds[1]) if regex.search(pattern, string[:half], partial=True) else (bounds[0], half - 1)
322         partial_length = bounds[0]
323         return ("Regular expression matched until position "
324               "%d, ..'%s', and did not match from '%s'.." % (partial_length, string[max(0, partial_length-20):partial_length],
325                                                              string[partial_length:partial_length+10]))
326     except ImportError:
327         pass
328     return "Debug information available if `regex' module is installed."
329
330 def parse_xrandr_output():
331     "Parse the output of `xrandr --verbose' into a list of outputs"
332     xrandr_output = os.popen("xrandr -q --verbose").read()
333     if not xrandr_output:
334         raise RuntimeError("Failed to run xrandr")
335
336     # We are not interested in screens
337     xrandr_output = re.sub("(?m)^Screen [0-9].+", "", xrandr_output).strip()
338
339     # Split at output boundaries and instanciate an XrandrOutput per output
340     split_xrandr_output = re.split("(?m)^([^ ]+ (?:(?:dis)?connected|unknown connection).*)$", xrandr_output)
341     if len(split_xrandr_output) < 2:
342         raise RuntimeError("No output boundaries found")
343     outputs = OrderedDict()
344     modes = OrderedDict()
345     for i in range(1, len(split_xrandr_output), 2):
346         output_name = split_xrandr_output[i].split()[0]
347         output, output_modes = XrandrOutput.from_xrandr_output("".join(split_xrandr_output[i:i+2]))
348         outputs[output_name] = output
349         if output_modes:
350             modes[output_name] = output_modes
351
352     return outputs, modes
353
354 def load_profiles(profile_path):
355     "Load the stored profiles"
356
357     profiles = {}
358     for profile in os.listdir(profile_path):
359         config_name = os.path.join(profile_path, profile, "config")
360         setup_name  = os.path.join(profile_path, profile, "setup")
361         if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
362             continue
363
364         edids = dict([ x.strip().split() for x in open(setup_name).readlines() if x.strip() ])
365
366         config = {}
367         buffer = []
368         for line in chain(open(config_name).readlines(), ["output"]):
369             if line[:6] == "output" and buffer:
370                 config[buffer[0].strip().split()[-1]] = XrandrOutput.from_config_file(edids, "".join(buffer))
371                 buffer = [ line ]
372             else:
373                 buffer.append(line)
374
375         for output_name in list(config.keys()):
376             if config[output_name].edid is None:
377                 del config[output_name]
378
379         profiles[profile] = { "config": config, "path": os.path.join(profile_path, profile), "config-mtime": os.stat(config_name).st_mtime }
380
381     return profiles
382
383 def find_profiles(current_config, profiles):
384     "Find profiles matching the currently connected outputs"
385     detected_profiles = []
386     for profile_name, profile in profiles.items():
387         config = profile["config"]
388         matches = True
389         for name, output in config.items():
390             if not output.edid:
391                 continue
392             if name not in current_config or not output.edid_equals(current_config[name]):
393                 matches = False
394                 break
395         if not matches or any(( name not in config.keys() for name in current_config.keys() if current_config[name].edid )):
396             continue
397         if matches:
398             detected_profiles.append(profile_name)
399     return detected_profiles
400
401 def profile_blocked(profile_path):
402     "Check if a profile is blocked"
403     script = os.path.join(profile_path, "block")
404     if not os.access(script, os.X_OK | os.F_OK):
405         return False
406     return subprocess.call(script) == 0
407
408 def output_configuration(configuration, config):
409     "Write a configuration file"
410     outputs = sorted(configuration.keys(), key=lambda x: configuration[x].sort_key)
411     for output in outputs:
412         print(configuration[output].option_string, file=config)
413
414 def output_setup(configuration, setup):
415     "Write a setup (fingerprint) file"
416     outputs = sorted(configuration.keys())
417     for output in outputs:
418         if configuration[output].edid:
419             print(output, configuration[output].edid, file=setup)
420
421 def save_configuration(profile_path, configuration):
422     "Save a configuration into a profile"
423     if not os.path.isdir(profile_path):
424         os.makedirs(profile_path)
425     with open(os.path.join(profile_path, "config"), "w") as config:
426         output_configuration(configuration, config)
427     with open(os.path.join(profile_path, "setup"), "w") as setup:
428         output_setup(configuration, setup)
429
430 def update_mtime(filename):
431     "Update a file's mtime"
432     try:
433         os.utime(filename, None)
434         return True
435     except:
436         return False
437
438 def apply_configuration(new_configuration, current_configuration, dry_run=False):
439     "Apply a configuration"
440     outputs = sorted(new_configuration.keys(), key=lambda x: new_configuration[x].sort_key)
441     if dry_run:
442         base_argv = [ "echo", "xrandr" ]
443     else:
444         base_argv = [ "xrandr" ]
445
446     # There are several xrandr / driver bugs we need to take care of here:
447     # - We cannot enable more than two screens at the same time
448     #   See https://github.com/phillipberndt/autorandr/pull/6
449     #   and commits f4cce4d and 8429886.
450     # - We cannot disable all screens
451     #   See https://github.com/phillipberndt/autorandr/pull/20
452     # - We should disable screens before enabling others, because there's
453     #   a limit on the number of enabled screens
454     # - We must make sure that the screen at 0x0 is activated first,
455     #   or the other (first) screen to be activated would be moved there.
456     # - If an active screen already has a transformation and remains active,
457     #   the xrandr call fails with an invalid RRSetScreenSize parameter error.
458     #   Update the configuration in 3 passes in that case.  (On Haswell graphics,
459     #   at least.)
460
461     auxiliary_changes_pre = []
462     disable_outputs = []
463     enable_outputs = []
464     remain_active_count = 0
465     for output in outputs:
466         if not new_configuration[output].edid or "off" in new_configuration[output].options:
467             disable_outputs.append(new_configuration[output].option_vector)
468         else:
469             if "off" not in current_configuration[output].options:
470                 remain_active_count += 1
471             enable_outputs.append(new_configuration[output].option_vector)
472             if xrandr_version() >= Version("1.3.0") and "transform" in current_configuration[output].options:
473                 auxiliary_changes_pre.append(["--output", output, "--transform", "none"])
474
475     # Perform pe-change auxiliary changes
476     if auxiliary_changes_pre:
477         argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
478         if subprocess.call(argv) != 0:
479             raise RuntimeError("Command failed: %s" % " ".join(argv))
480
481     # Disable unused outputs, but make sure that there always is at least one active screen
482     disable_keep = 0 if remain_active_count else 1
483     if len(disable_outputs) > disable_keep:
484         if subprocess.call(base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))) != 0:
485             # Disabling the outputs failed. Retry with the next command:
486             # Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
487             # This does not occur if simultaneously the primary screen is reset.
488             pass
489         else:
490             disable_outputs = disable_outputs[-1:] if disable_keep else []
491
492     # If disable_outputs still has more than one output in it, one of the xrandr-calls below would
493     # disable the last two screens. This is a problem, so if this would happen, instead disable only
494     # one screen in the first call below.
495     if len(disable_outputs) > 0 and len(disable_outputs) % 2 == 0:
496         # In the context of a xrandr call that changes the display state, `--query' should do nothing
497         disable_outputs.insert(0, ['--query'])
498
499     # Enable the remaining outputs in pairs of two operations
500     operations = disable_outputs + enable_outputs
501     for index in range(0, len(operations), 2):
502         argv = base_argv + list(chain.from_iterable(operations[index:index+2]))
503         if subprocess.call(argv) != 0:
504             raise RuntimeError("Command failed: %s" % " ".join(argv))
505
506 def add_unused_outputs(source_configuration, target_configuration):
507     "Add outputs that are missing in target to target, in 'off' state"
508     for output_name, output in source_configuration.items():
509         if output_name not in target_configuration:
510             target_configuration[output_name] = XrandrOutput(output_name, output.edid, { "off": None })
511
512 def remove_irrelevant_outputs(source_configuration, target_configuration):
513     "Remove outputs from target that ought to be 'off' and already are"
514     for output_name, output in source_configuration.items():
515         if "off" in output.options and output_name in target_configuration and "off" in target_configuration[output_name].options:
516             del target_configuration[output_name]
517
518 def generate_virtual_profile(configuration, modes, profile_name):
519     "Generate one of the virtual profiles"
520     configuration = copy.deepcopy(configuration)
521     if profile_name == "common":
522         common_resolution = [ set(( ( mode["width"], mode["height"] ) for mode in output )) for output in modes.values() ]
523         common_resolution = reduce(lambda a, b: a & b, common_resolution[1:], common_resolution[0])
524         common_resolution = sorted(common_resolution, key=lambda a: int(a[0])*int(a[1]))
525         if common_resolution:
526             for output in configuration:
527                 configuration[output].options = {}
528                 if output in modes:
529                     configuration[output].options["mode"] = "%sx%s" % common_resolution[-1]
530                     configuration[output].options["pos"] = "0x0"
531                 else:
532                     configuration[output].options["off"] = None
533     elif profile_name in ("horizontal", "vertical"):
534         shift = 0
535         if profile_name == "horizontal":
536             shift_index = "width"
537             pos_specifier = "%sx0"
538         else:
539             shift_index = "height"
540             pos_specifier = "0x%s"
541
542         for output in configuration:
543             configuration[output].options = {}
544             if output in modes:
545                 mode = sorted(modes[output], key=lambda a: int(a["width"])*int(a["height"]) + (10**6 if a["preferred"] else 0))[-1]
546                 configuration[output].options["mode"] = "%sx%s" % (mode["width"], mode["height"])
547                 configuration[output].options["rate"] = mode["rate"]
548                 configuration[output].options["pos"] = pos_specifier % shift
549                 shift += int(mode[shift_index])
550             else:
551                 configuration[output].options["off"] = None
552     return configuration
553
554 def exit_help():
555     "Print help and exit"
556     print(help_text)
557     for profile in virtual_profiles:
558         print("  %-10s %s" % profile[:2])
559     sys.exit(0)
560
561 def exec_scripts(profile_path, script_name):
562     "Run userscripts"
563     for script in (os.path.join(profile_path, script_name), os.path.join(os.path.dirname(profile_path), script_name)):
564         if os.access(script, os.X_OK | os.F_OK):
565             subprocess.call(script)
566
567 def main(argv):
568     try:
569        options = dict(getopt.getopt(argv[1:], "s:l:d:cfh", [ "dry-run", "change", "default=", "save=", "load=", "force", "fingerprint", "config", "help" ])[0])
570     except getopt.GetoptError as e:
571         print(str(e))
572         options = { "--help": True }
573
574     profiles = {}
575     try:
576         # Load profiles from each XDG config directory
577         for directory in os.environ.get("XDG_CONFIG_DIRS", "").split(":"):
578             system_profile_path = os.path.join(directory, "autorandr")
579             if os.path.isdir(system_profile_path):
580                 profiles.update(load_profiles(system_profile_path))
581         # For the user's profiles, prefer the legacy ~/.autorandr if it already exists
582         # profile_path is also used later on to store configurations
583         profile_path = os.path.expanduser("~/.autorandr")
584         if not os.path.isdir(profile_path):
585             # Elsewise, follow the XDG specification
586             profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
587         if os.path.isdir(profile_path):
588             profiles.update(load_profiles(profile_path))
589         # Sort by descending mtime
590         profiles = OrderedDict(sorted(profiles.items(), key=lambda x: -x[1]["config-mtime"]))
591     except Exception as e:
592         print("Failed to load profiles:\n%s" % str(e), file=sys.stderr)
593         sys.exit(1)
594
595     try:
596         config, modes = parse_xrandr_output()
597     except Exception as e:
598         print("Failed to parse current configuration from XRandR:\n%s" % str(e), file=sys.stderr)
599         sys.exit(1)
600
601     if "--fingerprint" in options:
602         output_setup(config, sys.stdout)
603         sys.exit(0)
604
605     if "--config" in options:
606         output_configuration(config, sys.stdout)
607         sys.exit(0)
608
609     if "-s" in options:
610         options["--save"] = options["-s"]
611     if "--save" in options:
612         if options["--save"] in ( x[0] for x in virtual_profiles ):
613             print("Cannot save current configuration as profile '%s':\nThis configuration name is a reserved virtual configuration." % options["--save"])
614             sys.exit(1)
615         try:
616             save_configuration(os.path.join(profile_path, options["--save"]), config)
617         except Exception as e:
618             print("Failed to save current configuration as profile '%s':\n%s" % (options["--save"], str(e)), file=sys.stderr)
619             sys.exit(1)
620         print("Saved current configuration as profile '%s'" % options["--save"])
621         sys.exit(0)
622
623     if "-h" in options or "--help" in options:
624         exit_help()
625
626     detected_profiles = find_profiles(config, profiles)
627     load_profile = False
628
629     if "-l" in options:
630         options["--load"] = options["-l"]
631     if "--load" in options:
632         load_profile = options["--load"]
633     else:
634         for profile_name in profiles.keys():
635             if profile_blocked(os.path.join(profile_path, profile_name)):
636                 print("%s (blocked)" % profile_name, file=sys.stderr)
637                 continue
638             if profile_name in detected_profiles:
639                 print("%s (detected)" % profile_name, file=sys.stderr)
640                 if ("-c" in options or "--change" in options) and not load_profile:
641                     load_profile = profile_name
642             else:
643                 print(profile_name, file=sys.stderr)
644
645     if "-d" in options:
646         options["--default"] = options["-d"]
647     if not load_profile and "--default" in options:
648         load_profile = options["--default"]
649
650     if load_profile:
651         if load_profile in ( x[0] for x in virtual_profiles ):
652             load_config = generate_virtual_profile(config, modes, load_profile)
653             scripts_path = os.path.join(profile_path, load_profile)
654         else:
655             try:
656                 profile = profiles[load_profile]
657                 load_config = profile["config"]
658                 scripts_path = profile["path"]
659             except KeyError:
660                 print("Failed to load profile '%s':\nProfile not found" % load_profile, file=sys.stderr)
661                 sys.exit(1)
662             if load_profile in detected_profiles and detected_profiles[0] != load_profile:
663                 update_mtime(os.path.join(scripts_path, "config"))
664         add_unused_outputs(config, load_config)
665         if load_config == dict(config) and not "-f" in options and not "--force" in options:
666             print("Config already loaded", file=sys.stderr)
667             sys.exit(0)
668         remove_irrelevant_outputs(config, load_config)
669
670         try:
671             if "--dry-run" in options:
672                 apply_configuration(load_config, config, True)
673             else:
674                 exec_scripts(scripts_path, "preswitch")
675                 apply_configuration(load_config, config, False)
676                 exec_scripts(scripts_path, "postswitch")
677         except Exception as e:
678             print("Failed to apply profile '%s':\n%s" % (load_profile, str(e)), file=sys.stderr)
679             sys.exit(1)
680
681     sys.exit(0)
682
683 if __name__ == '__main__':
684     try:
685         main(sys.argv)
686     except Exception as e:
687         print("General failure. Please report this as a bug:\n%s" % (str(e),), file=sys.stderr)
688         sys.exit(1)