2015-01-06 20:13:13 +00:00
#!/usr/bin/env python
# encoding: utf-8
#
# autorandr.py
# Copyright (c) 2015, Phillip Berndt
#
2015-01-27 07:25:04 +00:00
# Autorandr rewrite in Python
2015-01-06 20:13:13 +00:00
#
# This script aims to be fully compatible with the original autorandr.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import print_function
2015-01-07 11:56:28 +00:00
import copy
2015-01-06 20:13:13 +00:00
import getopt
import binascii
import hashlib
import os
import re
import subprocess
import sys
2015-01-12 09:35:21 +00:00
from distutils . version import LooseVersion as Version
2015-01-06 20:13:13 +00:00
from itertools import chain
2015-01-07 11:56:28 +00:00
from collections import OrderedDict
virtual_profiles = [
# (name, description, callback)
( " common " , " Clone all connected outputs at the largest common resolution " , None ) ,
( " horizontal " , " Stack all connected outputs horizontally at their largest resolution " , None ) ,
( " vertical " , " Stack all connected outputs vertically at their largest resolution " , None ) ,
]
2015-01-06 20:13:13 +00:00
help_text = """
Usage : autorandr [ options ]
2015-01-08 08:51:01 +00:00
- h , - - help get this small help
- c , - - change reload current setup
- s , - - save < profile > save your current setup to profile < profile >
- l , - - load < profile > load profile < profile >
2015-01-06 20:13:13 +00:00
- d , - - default < profile > make profile < profile > the default profile
2015-01-08 08:51:01 +00:00
- - force force ( re ) loading of a profile
- - fingerprint fingerprint your current hardware setup
- - config dump your current xrandr setup
2015-01-06 20:13:13 +00:00
- - dry - run don ' t change anything, only print the xrandr commands
To prevent a profile from being loaded , place a script call " block " in its
directory . The script is evaluated before the screen setup is inspected , and
in case of it returning a value of 0 the profile is skipped . This can be used
to query the status of a docking station you are about to leave .
If no suitable profile can be identified , the current configuration is kept .
To change this behaviour and switch to a fallback configuration , specify
- - default < profile > .
2015-02-09 07:24:10 +00:00
Another script called " postswitch " can be placed in the directory
~ / . config / autorandr ( or ~ / . autorandr if you have an old installation ) as well
as in any profile directories : The scripts are executed after a mode switch
has taken place and can notify window managers .
2015-01-06 20:13:13 +00:00
The following virtual configurations are available :
""" .strip()
2015-04-10 09:36:48 +00:00
class AutorandrException ( Exception ) :
def __init__ ( self , message , original_exception = None , report_bug = False ) :
self . message = message
self . report_bug = report_bug
if original_exception :
self . original_exception = original_exception
trace = sys . exc_info ( ) [ 2 ]
while trace . tb_next :
trace = trace . tb_next
self . line = trace . tb_lineno
else :
try :
import inspect
self . line = inspect . currentframe ( ) . f_back . f_lineno
except :
self . line = None
self . original_exception = None
def __str__ ( self ) :
retval = [ self . message ]
if self . line :
retval . append ( " (line %d ) " % self . line )
if self . original_exception :
retval . append ( " : \n " % self . line )
retval . append ( str ( self . original_exception ) . replace ( " \n " , " \n " ) )
if self . report_bug :
retval . append ( " \n This appears to be a bug. Please help improving autorandr by reporting it upstream. "
" \n Please attach the output of `xrandr --verbose` to your bug report if appropriate. " )
return " " . join ( retval )
2015-01-06 20:13:13 +00:00
class XrandrOutput ( object ) :
" Represents an XRandR output "
# This regular expression is used to parse an output in `xrandr --verbose'
XRANDR_OUTPUT_REGEXP = """ (?x)
^ ( ? P < output > [ ^ ] + ) \s + # Line starts with output name
( ? : # Differentiate disconnected and connected in first line
disconnected |
unknown \ connection |
2015-03-27 16:33:08 +00:00
( ? P < connected > connected )
)
2015-04-05 23:26:32 +00:00
\s *
( ? P < primary > primary \ ) ? # Might be primary screen
2015-04-06 15:51:09 +00:00
( ? : \s *
2015-01-22 10:41:48 +00:00
( ? P < width > [ 0 - 9 ] + ) x ( ? P < height > [ 0 - 9 ] + ) # Resolution (might be overridden below!)
2015-04-08 08:13:36 +00:00
\+ ( ? P < x > - ? [ 0 - 9 ] + ) \+ ( ? P < y > - ? [ 0 - 9 ] + ) \s + # Position
2015-01-07 10:25:20 +00:00
( ? : \( 0 x [ 0 - 9 a - fA - F ] + \) \s + ) ? # XID
( ? P < rotate > ( ? : normal | left | right | inverted ) ) \s + # Rotation
( ? : ( ? P < reflect > X \ and \ Y | X | Y ) \ axis ) ? # Reflection
2015-03-27 16:33:08 +00:00
) ? # .. but everything of the above only if the screen is in use.
2015-04-06 15:51:09 +00:00
( ? : [ \ \t ] * \( [ ^ \) ] + \) ) ( ? : \s * [ 0 - 9 ] + mm \sx \s [ 0 - 9 ] + mm ) ?
( ? : [ \ \t ] * panning \ ( ? P < panning > [ 0 - 9 ] + x [ 0 - 9 ] + \+ [ 0 - 9 ] + \+ [ 0 - 9 ] + ) ) ? # Panning information
2015-04-08 08:14:19 +00:00
( ? : [ \ \t ] * tracking \ ( ? P < tracking > [ 0 - 9 ] + x [ 0 - 9 ] + \+ [ 0 - 9 ] + \+ [ 0 - 9 ] + ) ) ? # Tracking information
( ? : [ \ \t ] * border \ ( ? P < border > ( ? : [ 0 - 9 ] + / ) { 3 } [ 0 - 9 ] + ) ) ? # Border information
2015-01-06 20:36:03 +00:00
( ? : \s * ( ? : # Properties of the output
2015-02-17 09:51:45 +00:00
Gamma : ( ? P < gamma > [ 0 - 9 \. : ] + ) | # Gamma value
2015-02-17 10:07:24 +00:00
Transform : ( ? P < transform > ( ? : [ \- 0 - 9 \. ] + \s + ) { 3 } ) | # Transformation matrix
2015-02-17 09:51:45 +00:00
EDID : ( ? P < edid > \s * ? ( ? : \\n \\t \\t [ 0 - 9 a - f ] + ) + ) | # EDID of the output
2015-01-07 10:25:20 +00:00
( ? ! [ 0 - 9 ] ) [ ^ : \s ] [ ^ : \n ] + : . * ( ? : \s \\t [ \\t ] . + ) * # Other properties
2015-01-06 20:13:13 +00:00
) ) +
\s *
2015-01-07 10:25:20 +00:00
( ? P < modes > ( ? :
2015-04-14 07:15:39 +00:00
( ? P < mode_name > \S + ) . + ? \* current . * \s + # Interesting (current) resolution: Extract rate
h : \s + width \s + ( ? P < mode_width > [ 0 - 9 ] + ) . + \s +
v : \s + height \s + ( ? P < mode_height > [ 0 - 9 ] + ) . + clock \s + ( ? P < rate > [ 0 - 9 \. ] + ) Hz \s * |
\S + ( ? : ( ? ! \* current ) . ) + \s + h : . + \s + v : . + \s * # Other resolutions
2015-01-07 10:25:20 +00:00
) * )
2015-01-06 20:13:13 +00:00
"""
2015-01-07 10:25:20 +00:00
XRANDR_OUTPUT_MODES_REGEXP = """ (?x)
2015-04-14 07:15:39 +00:00
( ? P < name > \S + ) . + ? ( ? P < preferred > \+ preferred ) ? \s +
h : \s + width \s + ( ? P < width > [ 0 - 9 ] + ) . + \s +
v : \s + height \s + ( ? P < height > [ 0 - 9 ] + ) . + clock \s + ( ? P < rate > [ 0 - 9 \. ] + ) Hz \s * |
2015-01-07 10:25:20 +00:00
"""
2015-01-26 06:45:28 +00:00
XRANDR_13_DEFAULTS = {
" transform " : " 1,0,0,0,1,0,0,0,1 " ,
2015-04-06 15:51:09 +00:00
" panning " : " 0x0 " ,
2015-01-26 06:45:28 +00:00
}
XRANDR_12_DEFAULTS = {
" reflect " : " normal " ,
" rotate " : " normal " ,
" gamma " : " 1.0:1.0:1.0 " ,
}
2015-01-27 07:30:30 +00:00
XRANDR_DEFAULTS = dict ( list ( XRANDR_13_DEFAULTS . items ( ) ) + list ( XRANDR_12_DEFAULTS . items ( ) ) )
2015-01-26 06:45:28 +00:00
2015-03-26 08:28:59 +00:00
EDID_UNAVAILABLE = " --CONNECTED-BUT-EDID-UNAVAILABLE- "
2015-01-06 20:13:13 +00:00
def __repr__ ( self ) :
return " < %s %s %s > " % ( self . output , ( " %s .. %s " % ( self . edid [ : 5 ] , self . edid [ - 5 : ] ) ) if self . edid else " " , " " . join ( self . option_vector ) )
2015-01-12 09:35:21 +00:00
@property
def options_with_defaults ( self ) :
" Return the options dictionary, augmented with the default values that weren ' t set "
if " off " in self . options :
return self . options
options = { }
if xrandr_version ( ) > = Version ( " 1.3 " ) :
2015-01-26 06:45:28 +00:00
options . update ( self . XRANDR_13_DEFAULTS )
2015-01-12 09:35:21 +00:00
if xrandr_version ( ) > = Version ( " 1.2 " ) :
2015-01-26 06:45:28 +00:00
options . update ( self . XRANDR_12_DEFAULTS )
2015-01-12 09:35:21 +00:00
options . update ( self . options )
return options
2015-01-06 20:13:13 +00:00
@property
def option_vector ( self ) :
" Return the command line parameters for XRandR for this instance "
2015-02-23 10:20:26 +00:00
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 ( ) ) ) ] , [ ] )
2015-01-06 20:13:13 +00:00
@property
def option_string ( self ) :
" Return the command line parameters in the configuration file format "
2015-02-23 10:20:26 +00:00
return " \n " . join ( [ " " . join ( option ) if option [ 1 ] else option [ 0 ] for option in chain ( ( ( " output " , self . output ) , ) , sorted ( self . options . items ( ) ) ) ] )
2015-01-06 20:13:13 +00:00
@property
def sort_key ( self ) :
" Return a key to sort the outputs for xrandr invocation "
if not self . edid :
2015-03-27 16:33:08 +00:00
return - 2
if " off " in self . options :
2015-01-06 20:13:13 +00:00
return - 1
if " pos " in self . options :
x , y = map ( float , self . options [ " pos " ] . split ( " x " ) )
else :
x , y = 0 , 0
return x + 10000 * y
def __init__ ( self , output , edid , options ) :
" Instanciate using output name, edid and a dictionary of XRandR command line parameters "
self . output = output
self . edid = edid
self . options = options
2015-01-26 06:45:28 +00:00
self . remove_default_option_values ( )
def remove_default_option_values ( self ) :
" Remove values from the options dictionary that are superflous "
if " off " in self . options and len ( self . options . keys ( ) ) > 1 :
self . options = { " off " : None }
return
for option , default_value in self . XRANDR_DEFAULTS . items ( ) :
if option in self . options and self . options [ option ] == default_value :
del self . options [ option ]
2015-01-06 20:13:13 +00:00
@classmethod
def from_xrandr_output ( cls , xrandr_output ) :
2015-01-07 10:25:20 +00:00
""" Instanciate an XrandrOutput from the output of `xrandr --verbose '
This method also returns a list of modes supported by the output .
"""
try :
2015-02-17 09:51:45 +00:00
xrandr_output = xrandr_output . replace ( " \r \n " , " \n " )
2015-01-07 10:25:20 +00:00
match_object = re . search ( XrandrOutput . XRANDR_OUTPUT_REGEXP , xrandr_output )
except :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " Parsing XRandR output failed, there is an error in the regular expression. " , report_bug = True )
2015-01-07 10:25:20 +00:00
if not match_object :
2015-01-08 08:51:01 +00:00
debug = debug_regexp ( XrandrOutput . XRANDR_OUTPUT_REGEXP , xrandr_output )
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " Parsing XRandR output failed, the regular expression did not match: %s " % debug , report_bug = True )
2015-01-06 20:36:03 +00:00
remainder = xrandr_output [ len ( match_object . group ( 0 ) ) : ]
if remainder :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( ( " Parsing XRandR output failed, %d bytes left unmatched after regular expression, "
" starting at byte %d with .. ' %s ' . " ) % ( len ( remainder ) , len ( match_object . group ( 0 ) ) , remainder [ : 10 ] ) , report_bug = True )
2015-01-07 10:25:20 +00:00
2015-01-06 20:36:03 +00:00
match = match_object . groupdict ( )
2015-01-06 20:13:13 +00:00
2015-01-07 10:25:20 +00:00
modes = [ ]
if match [ " modes " ] :
2015-04-14 07:15:39 +00:00
modes = [ x . groupdict ( ) for x in re . finditer ( XrandrOutput . XRANDR_OUTPUT_MODES_REGEXP , match [ " modes " ] ) if x . group ( " name " ) ]
2015-01-08 08:51:01 +00:00
if not modes :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " Parsing XRandR output failed, couldn ' t find any display modes " , report_bug = True )
2015-01-07 10:25:20 +00:00
2015-01-06 20:13:13 +00:00
options = { }
if not match [ " connected " ] :
edid = None
2015-03-27 16:33:08 +00:00
else :
2015-03-26 08:28:59 +00:00
edid = " " . join ( match [ " edid " ] . strip ( ) . split ( ) ) if match [ " edid " ] else " %s - %s " % ( XrandrOutput . EDID_UNAVAILABLE , match [ " output " ] )
2015-03-27 16:33:08 +00:00
if not match [ " width " ] :
options [ " off " ] = None
2015-01-06 20:13:13 +00:00
else :
2015-04-14 07:15:39 +00:00
if match [ " mode_name " ] :
options [ " mode " ] = match [ " mode_name " ]
elif match [ " mode_width " ] :
2015-01-22 10:41:48 +00:00
options [ " mode " ] = " %s x %s " % ( match [ " mode_width " ] , match [ " mode_height " ] )
2015-01-06 20:13:13 +00:00
else :
2015-01-22 10:41:48 +00:00
if match [ " rotate " ] not in ( " left " , " right " ) :
options [ " mode " ] = " %s x %s " % ( match [ " width " ] , match [ " height " ] )
else :
options [ " mode " ] = " %s x %s " % ( match [ " height " ] , match [ " width " ] )
2015-01-26 06:45:28 +00:00
options [ " rotate " ] = match [ " rotate " ]
if match [ " primary " ] :
2015-01-22 13:27:29 +00:00
options [ " primary " ] = None
2015-01-26 06:45:28 +00:00
if match [ " reflect " ] == " X " :
options [ " reflect " ] = " x "
elif match [ " reflect " ] == " Y " :
options [ " reflect " ] = " y "
elif match [ " reflect " ] == " X and Y " :
options [ " reflect " ] = " xy "
2015-01-06 20:13:13 +00:00
options [ " pos " ] = " %s x %s " % ( match [ " x " ] , match [ " y " ] )
2015-04-06 15:51:09 +00:00
if match [ " panning " ] :
2015-04-08 08:14:19 +00:00
panning = [ match [ " panning " ] ]
if match [ " tracking " ] :
panning + = [ " / " , match [ " tracking " ] ]
if match [ " border " ] :
panning + = [ " / " , match [ " border " ] ]
options [ " panning " ] = " " . join ( panning )
2015-01-06 20:13:13 +00:00
if match [ " transform " ] :
transformation = " , " . join ( match [ " transform " ] . strip ( ) . split ( ) )
if transformation != " 1.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,1.000000 " :
options [ " transform " ] = transformation
2015-04-14 07:15:39 +00:00
if not match [ " mode_name " ] :
2015-01-22 10:41:48 +00:00
# TODO We'd need to apply the reverse transformation here. Let's see if someone complains, I doubt that this
# special case is actually required.
2015-04-08 08:14:55 +00:00
print ( " Warning: Output %s has a transformation applied. Could not determine correct mode! Using ` %s ' . " % ( match [ " output " ] , options [ " mode " ] ) , file = sys . stderr )
2015-01-06 20:13:13 +00:00
if match [ " gamma " ] :
gamma = match [ " gamma " ] . strip ( )
2015-01-26 06:45:28 +00:00
options [ " gamma " ] = gamma
2015-01-06 20:13:13 +00:00
if match [ " rate " ] :
options [ " rate " ] = match [ " rate " ]
2015-01-07 10:25:20 +00:00
return XrandrOutput ( match [ " output " ] , edid , options ) , modes
2015-01-06 20:13:13 +00:00
@classmethod
def from_config_file ( cls , edid_map , configuration ) :
" Instanciate an XrandrOutput from the contents of a configuration file "
options = { }
for line in configuration . split ( " \n " ) :
if line :
line = line . split ( None , 1 )
options [ line [ 0 ] ] = line [ 1 ] if len ( line ) > 1 else None
2015-01-22 14:29:08 +00:00
2015-01-22 20:50:05 +00:00
edid = None
2015-01-22 14:29:08 +00:00
if options [ " output " ] in edid_map :
edid = edid_map [ options [ " output " ] ]
2015-01-06 20:13:13 +00:00
else :
2015-01-22 20:50:05 +00:00
# This fuzzy matching is for legacy autorandr that used sysfs output names
2015-01-22 14:29:08 +00:00
fuzzy_edid_map = [ re . sub ( " (card[0-9]+|-) " , " " , x ) for x in edid_map . keys ( ) ]
fuzzy_output = re . sub ( " (card[0-9]+|-) " , " " , options [ " output " ] )
2015-01-22 20:50:05 +00:00
if fuzzy_output in fuzzy_edid_map :
edid = edid_map [ list ( edid_map . keys ( ) ) [ fuzzy_edid_map . index ( fuzzy_output ) ] ]
elif " off " not in options :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " Failed to find an EDID for output ` %s ' in setup file, required as ` %s ' is not off in config file. " % ( options [ " output " ] , options [ " output " ] ) )
2015-01-06 20:13:13 +00:00
output = options [ " output " ]
del options [ " output " ]
return XrandrOutput ( output , edid , options )
def edid_equals ( self , other ) :
" Compare to another XrandrOutput ' s edid and on/off-state, taking legacy autorandr behaviour (md5sum ' ing) into account "
if self . edid and other . edid :
2015-03-26 08:28:59 +00:00
if len ( self . edid ) == 32 and len ( other . edid ) != 32 and not other . edid . startswith ( XrandrOutput . EDID_UNAVAILABLE ) :
2015-01-06 20:13:13 +00:00
return hashlib . md5 ( binascii . unhexlify ( other . edid ) ) . hexdigest ( ) == self . edid
2015-03-26 08:28:59 +00:00
if len ( self . edid ) != 32 and len ( other . edid ) == 32 and not self . edid . startswith ( XrandrOutput . EDID_UNAVAILABLE ) :
2015-01-06 20:13:13 +00:00
return hashlib . md5 ( binascii . unhexlify ( self . edid ) ) . hexdigest ( ) == other . edid
return self . edid == other . edid
def __eq__ ( self , other ) :
2015-01-26 06:45:48 +00:00
return self . edid_equals ( other ) and self . output == other . output and self . options == other . options
2015-01-06 20:13:13 +00:00
2015-01-12 09:35:21 +00:00
def xrandr_version ( ) :
" Return the version of XRandR that this system uses "
if getattr ( xrandr_version , " version " , False ) is False :
version_string = os . popen ( " xrandr -v " ) . read ( )
2015-01-23 12:06:07 +00:00
try :
version = re . search ( " xrandr program version \ s+([0-9 \ .]+) " , version_string ) . group ( 1 )
xrandr_version . version = Version ( version )
except AttributeError :
xrandr_version . version = Version ( " 1.3.0 " )
2015-01-12 09:35:21 +00:00
return xrandr_version . version
2015-01-08 08:51:01 +00:00
def debug_regexp ( pattern , string ) :
" Use the partial matching functionality of the regex module to display debug info on a non-matching regular expression "
try :
import regex
bounds = ( 0 , len ( string ) )
while bounds [ 0 ] != bounds [ 1 ] :
half = int ( ( bounds [ 0 ] + bounds [ 1 ] ) / 2 )
2015-01-22 12:29:54 +00:00
if half == bounds [ 0 ] :
break
2015-01-08 08:51:01 +00:00
bounds = ( half , bounds [ 1 ] ) if regex . search ( pattern , string [ : half ] , partial = True ) else ( bounds [ 0 ] , half - 1 )
partial_length = bounds [ 0 ]
return ( " Regular expression matched until position "
" %d , .. ' %s ' , and did not match from ' %s ' .. " % ( partial_length , string [ max ( 0 , partial_length - 20 ) : partial_length ] ,
string [ partial_length : partial_length + 10 ] ) )
except ImportError :
pass
2015-04-10 09:36:48 +00:00
return " Debug information would be available if the `regex ' module was installed. "
2015-01-08 08:51:01 +00:00
2015-01-06 20:13:13 +00:00
def parse_xrandr_output ( ) :
" Parse the output of `xrandr --verbose ' into a list of outputs "
xrandr_output = os . popen ( " xrandr -q --verbose " ) . read ( )
if not xrandr_output :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " Failed to run xrandr " )
2015-01-06 20:13:13 +00:00
# We are not interested in screens
xrandr_output = re . sub ( " (?m)^Screen [0-9].+ " , " " , xrandr_output ) . strip ( )
# Split at output boundaries and instanciate an XrandrOutput per output
split_xrandr_output = re . split ( " (?m)^([^ ]+ (?:(?:dis)?connected|unknown connection).*)$ " , xrandr_output )
2015-02-23 10:20:43 +00:00
if len ( split_xrandr_output ) < 2 :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " No output boundaries found " , report_bug = True )
2015-01-07 11:56:28 +00:00
outputs = OrderedDict ( )
modes = OrderedDict ( )
2015-01-07 10:25:20 +00:00
for i in range ( 1 , len ( split_xrandr_output ) , 2 ) :
output_name = split_xrandr_output [ i ] . split ( ) [ 0 ]
output , output_modes = XrandrOutput . from_xrandr_output ( " " . join ( split_xrandr_output [ i : i + 2 ] ) )
outputs [ output_name ] = output
2015-01-07 11:56:28 +00:00
if output_modes :
modes [ output_name ] = output_modes
2015-01-06 20:13:13 +00:00
2015-01-07 10:25:20 +00:00
return outputs , modes
2015-01-06 20:13:13 +00:00
def load_profiles ( profile_path ) :
" Load the stored profiles "
profiles = { }
for profile in os . listdir ( profile_path ) :
config_name = os . path . join ( profile_path , profile , " config " )
setup_name = os . path . join ( profile_path , profile , " setup " )
if not os . path . isfile ( config_name ) or not os . path . isfile ( setup_name ) :
continue
2015-03-07 17:45:20 +00:00
edids = dict ( [ x . strip ( ) . split ( ) for x in open ( setup_name ) . readlines ( ) if x . strip ( ) ] )
2015-01-06 20:13:13 +00:00
config = { }
buffer = [ ]
for line in chain ( open ( config_name ) . readlines ( ) , [ " output " ] ) :
if line [ : 6 ] == " output " and buffer :
config [ buffer [ 0 ] . strip ( ) . split ( ) [ - 1 ] ] = XrandrOutput . from_config_file ( edids , " " . join ( buffer ) )
buffer = [ line ]
else :
buffer . append ( line )
2015-01-14 07:56:03 +00:00
for output_name in list ( config . keys ( ) ) :
2015-01-22 14:29:08 +00:00
if config [ output_name ] . edid is None :
2015-01-12 09:36:15 +00:00
del config [ output_name ]
2015-02-23 09:42:54 +00:00
profiles [ profile ] = { " config " : config , " path " : os . path . join ( profile_path , profile ) , " config-mtime " : os . stat ( config_name ) . st_mtime }
2015-01-06 20:13:13 +00:00
return profiles
2015-02-23 09:42:54 +00:00
def find_profiles ( current_config , profiles ) :
" Find profiles matching the currently connected outputs "
detected_profiles = [ ]
2015-01-06 20:13:13 +00:00
for profile_name , profile in profiles . items ( ) :
2015-02-23 09:42:54 +00:00
config = profile [ " config " ]
2015-01-06 20:13:13 +00:00
matches = True
2015-02-23 09:42:54 +00:00
for name , output in config . items ( ) :
2015-01-06 20:13:13 +00:00
if not output . edid :
continue
if name not in current_config or not output . edid_equals ( current_config [ name ] ) :
matches = False
break
2015-02-23 09:42:54 +00:00
if not matches or any ( ( name not in config . keys ( ) for name in current_config . keys ( ) if current_config [ name ] . edid ) ) :
2015-01-06 20:13:13 +00:00
continue
if matches :
2015-02-23 09:42:54 +00:00
detected_profiles . append ( profile_name )
return detected_profiles
2015-01-06 20:13:13 +00:00
def profile_blocked ( profile_path ) :
" Check if a profile is blocked "
2015-03-23 21:01:59 +00:00
script = os . path . join ( profile_path , " block " )
2015-01-06 20:13:13 +00:00
if not os . access ( script , os . X_OK | os . F_OK ) :
return False
return subprocess . call ( script ) == 0
def output_configuration ( configuration , config ) :
" Write a configuration file "
outputs = sorted ( configuration . keys ( ) , key = lambda x : configuration [ x ] . sort_key )
for output in outputs :
print ( configuration [ output ] . option_string , file = config )
def output_setup ( configuration , setup ) :
" Write a setup (fingerprint) file "
2015-01-07 08:58:18 +00:00
outputs = sorted ( configuration . keys ( ) )
2015-01-06 20:13:13 +00:00
for output in outputs :
if configuration [ output ] . edid :
print ( output , configuration [ output ] . edid , file = setup )
def save_configuration ( profile_path , configuration ) :
" Save a configuration into a profile "
if not os . path . isdir ( profile_path ) :
os . makedirs ( profile_path )
with open ( os . path . join ( profile_path , " config " ) , " w " ) as config :
output_configuration ( configuration , config )
with open ( os . path . join ( profile_path , " setup " ) , " w " ) as setup :
output_setup ( configuration , setup )
2015-02-23 09:42:54 +00:00
def update_mtime ( filename ) :
" Update a file ' s mtime "
try :
os . utime ( filename , None )
return True
except :
return False
2015-03-30 06:05:42 +00:00
def apply_configuration ( new_configuration , current_configuration , dry_run = False ) :
2015-01-06 20:13:13 +00:00
" Apply a configuration "
2015-03-30 06:05:42 +00:00
outputs = sorted ( new_configuration . keys ( ) , key = lambda x : new_configuration [ x ] . sort_key )
2015-01-06 20:13:13 +00:00
if dry_run :
base_argv = [ " echo " , " xrandr " ]
else :
base_argv = [ " xrandr " ]
2015-03-07 17:44:25 +00:00
# There are several xrandr / driver bugs we need to take care of here:
# - We cannot enable more than two screens at the same time
# See https://github.com/phillipberndt/autorandr/pull/6
# and commits f4cce4d and 8429886.
# - We cannot disable all screens
# See https://github.com/phillipberndt/autorandr/pull/20
# - We should disable screens before enabling others, because there's
# a limit on the number of enabled screens
# - We must make sure that the screen at 0x0 is activated first,
# or the other (first) screen to be activated would be moved there.
2015-03-30 06:05:42 +00:00
# - If an active screen already has a transformation and remains active,
# the xrandr call fails with an invalid RRSetScreenSize parameter error.
# Update the configuration in 3 passes in that case. (On Haswell graphics,
# at least.)
2015-03-07 17:44:25 +00:00
2015-03-30 06:05:42 +00:00
auxiliary_changes_pre = [ ]
2015-03-07 17:44:25 +00:00
disable_outputs = [ ]
enable_outputs = [ ]
2015-03-30 06:05:42 +00:00
remain_active_count = 0
2015-01-06 20:13:13 +00:00
for output in outputs :
2015-03-30 06:05:42 +00:00
if not new_configuration [ output ] . edid or " off " in new_configuration [ output ] . options :
disable_outputs . append ( new_configuration [ output ] . option_vector )
2015-03-07 17:44:25 +00:00
else :
2015-03-30 06:05:42 +00:00
if " off " not in current_configuration [ output ] . options :
remain_active_count + = 1
enable_outputs . append ( new_configuration [ output ] . option_vector )
if xrandr_version ( ) > = Version ( " 1.3.0 " ) and " transform " in current_configuration [ output ] . options :
auxiliary_changes_pre . append ( [ " --output " , output , " --transform " , " none " ] )
# Perform pe-change auxiliary changes
if auxiliary_changes_pre :
argv = base_argv + list ( chain . from_iterable ( auxiliary_changes_pre ) )
if subprocess . call ( argv ) != 0 :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " Command failed: %s " % " " . join ( argv ) )
2015-03-07 17:44:25 +00:00
2015-03-30 06:05:42 +00:00
# Disable unused outputs, but make sure that there always is at least one active screen
disable_keep = 0 if remain_active_count else 1
if len ( disable_outputs ) > disable_keep :
if subprocess . call ( base_argv + list ( chain . from_iterable ( disable_outputs [ : - 1 ] if disable_keep else disable_outputs ) ) ) != 0 :
2015-02-18 07:02:18 +00:00
# Disabling the outputs failed. Retry with the next command:
# Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
# This does not occur if simultaneously the primary screen is reset.
pass
else :
2015-03-30 06:05:42 +00:00
disable_outputs = disable_outputs [ - 1 : ] if disable_keep else [ ]
2015-03-07 17:44:25 +00:00
2015-03-08 17:14:06 +00:00
# If disable_outputs still has more than one output in it, one of the xrandr-calls below would
# disable the last two screens. This is a problem, so if this would happen, instead disable only
# one screen in the first call below.
if len ( disable_outputs ) > 0 and len ( disable_outputs ) % 2 == 0 :
2015-03-08 17:16:53 +00:00
# In the context of a xrandr call that changes the display state, `--query' should do nothing
2015-03-08 17:14:06 +00:00
disable_outputs . insert ( 0 , [ ' --query ' ] )
2015-03-07 17:44:25 +00:00
# Enable the remaining outputs in pairs of two operations
operations = disable_outputs + enable_outputs
for index in range ( 0 , len ( operations ) , 2 ) :
argv = base_argv + list ( chain . from_iterable ( operations [ index : index + 2 ] ) )
2015-02-18 07:02:18 +00:00
if subprocess . call ( argv ) != 0 :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " Command failed: %s " % " " . join ( argv ) )
2015-01-06 20:13:13 +00:00
2015-01-12 09:36:15 +00:00
def add_unused_outputs ( source_configuration , target_configuration ) :
" Add outputs that are missing in target to target, in ' off ' state "
for output_name , output in source_configuration . items ( ) :
if output_name not in target_configuration :
target_configuration [ output_name ] = XrandrOutput ( output_name , output . edid , { " off " : None } )
2015-03-07 17:44:25 +00:00
def remove_irrelevant_outputs ( source_configuration , target_configuration ) :
" Remove outputs from target that ought to be ' off ' and already are "
for output_name , output in source_configuration . items ( ) :
if " off " in output . options and output_name in target_configuration and " off " in target_configuration [ output_name ] . options :
del target_configuration [ output_name ]
2015-01-07 11:56:28 +00:00
def generate_virtual_profile ( configuration , modes , profile_name ) :
" Generate one of the virtual profiles "
configuration = copy . deepcopy ( configuration )
if profile_name == " common " :
common_resolution = [ set ( ( ( mode [ " width " ] , mode [ " height " ] ) for mode in output ) ) for output in modes . values ( ) ]
common_resolution = reduce ( lambda a , b : a & b , common_resolution [ 1 : ] , common_resolution [ 0 ] )
common_resolution = sorted ( common_resolution , key = lambda a : int ( a [ 0 ] ) * int ( a [ 1 ] ) )
if common_resolution :
for output in configuration :
configuration [ output ] . options = { }
if output in modes :
2015-04-14 07:15:39 +00:00
configuration [ output ] . options [ " mode " ] = [ x [ " name " ] for x in sorted ( modes [ output ] , key = lambda x : 0 if x [ " preferred " ] else 1 ) if x [ " width " ] == common_resolution [ - 1 ] [ 0 ] and x [ " height " ] == common_resolution [ - 1 ] [ 1 ] ] [ 0 ]
2015-01-07 11:56:28 +00:00
configuration [ output ] . options [ " pos " ] = " 0x0 "
else :
configuration [ output ] . options [ " off " ] = None
elif profile_name in ( " horizontal " , " vertical " ) :
shift = 0
if profile_name == " horizontal " :
shift_index = " width "
pos_specifier = " %s x0 "
else :
shift_index = " height "
pos_specifier = " 0x %s "
for output in configuration :
configuration [ output ] . options = { }
if output in modes :
mode = sorted ( modes [ output ] , key = lambda a : int ( a [ " width " ] ) * int ( a [ " height " ] ) + ( 10 * * 6 if a [ " preferred " ] else 0 ) ) [ - 1 ]
2015-04-14 07:15:39 +00:00
configuration [ output ] . options [ " mode " ] = mode [ " name " ]
2015-01-07 11:56:28 +00:00
configuration [ output ] . options [ " rate " ] = mode [ " rate " ]
configuration [ output ] . options [ " pos " ] = pos_specifier % shift
shift + = int ( mode [ shift_index ] )
else :
configuration [ output ] . options [ " off " ] = None
return configuration
2015-01-06 20:13:13 +00:00
def exit_help ( ) :
" Print help and exit "
print ( help_text )
2015-01-07 11:56:28 +00:00
for profile in virtual_profiles :
print ( " %-10s %s " % profile [ : 2 ] )
2015-01-06 20:13:13 +00:00
sys . exit ( 0 )
def exec_scripts ( profile_path , script_name ) :
" Run userscripts "
for script in ( os . path . join ( profile_path , script_name ) , os . path . join ( os . path . dirname ( profile_path ) , script_name ) ) :
if os . access ( script , os . X_OK | os . F_OK ) :
subprocess . call ( script )
def main ( argv ) :
2015-01-08 08:55:36 +00:00
try :
options = dict ( getopt . getopt ( argv [ 1 : ] , " s:l:d:cfh " , [ " dry-run " , " change " , " default= " , " save= " , " load= " , " force " , " fingerprint " , " config " , " help " ] ) [ 0 ] )
except getopt . GetoptError as e :
print ( str ( e ) )
options = { " --help " : True }
2015-01-06 20:13:13 +00:00
2015-02-09 07:24:25 +00:00
profiles = { }
2015-01-08 08:51:01 +00:00
try :
2015-02-09 07:24:25 +00:00
# Load profiles from each XDG config directory
for directory in os . environ . get ( " XDG_CONFIG_DIRS " , " " ) . split ( " : " ) :
system_profile_path = os . path . join ( directory , " autorandr " )
if os . path . isdir ( system_profile_path ) :
profiles . update ( load_profiles ( system_profile_path ) )
# For the user's profiles, prefer the legacy ~/.autorandr if it already exists
# profile_path is also used later on to store configurations
profile_path = os . path . expanduser ( " ~/.autorandr " )
if not os . path . isdir ( profile_path ) :
# Elsewise, follow the XDG specification
profile_path = os . path . join ( os . environ . get ( " XDG_CONFIG_HOME " , os . path . expanduser ( " ~/.config " ) ) , " autorandr " )
if os . path . isdir ( profile_path ) :
profiles . update ( load_profiles ( profile_path ) )
2015-02-23 09:42:54 +00:00
# Sort by descending mtime
profiles = OrderedDict ( sorted ( profiles . items ( ) , key = lambda x : - x [ 1 ] [ " config-mtime " ] ) )
2015-01-08 08:54:05 +00:00
except Exception as e :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " Failed to load profiles " , e )
2015-01-08 08:51:01 +00:00
2015-04-10 09:36:48 +00:00
config , modes = parse_xrandr_output ( )
2015-01-06 20:13:13 +00:00
if " --fingerprint " in options :
output_setup ( config , sys . stdout )
sys . exit ( 0 )
if " --config " in options :
output_configuration ( config , sys . stdout )
sys . exit ( 0 )
if " -s " in options :
options [ " --save " ] = options [ " -s " ]
if " --save " in options :
2015-01-07 11:56:28 +00:00
if options [ " --save " ] in ( x [ 0 ] for x in virtual_profiles ) :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " Cannot save current configuration as profile ' %s ' : \n This configuration name is a reserved virtual configuration. " % options [ " --save " ] )
2015-01-08 08:51:01 +00:00
try :
save_configuration ( os . path . join ( profile_path , options [ " --save " ] ) , config )
2015-01-08 08:54:05 +00:00
except Exception as e :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " Failed to save current configuration as profile ' %s ' " % ( options [ " --save " ] , ) , e )
2015-01-06 20:13:13 +00:00
print ( " Saved current configuration as profile ' %s ' " % options [ " --save " ] )
sys . exit ( 0 )
if " -h " in options or " --help " in options :
exit_help ( )
2015-02-23 09:42:54 +00:00
detected_profiles = find_profiles ( config , profiles )
2015-01-06 20:13:13 +00:00
load_profile = False
if " -l " in options :
options [ " --load " ] = options [ " -l " ]
if " --load " in options :
load_profile = options [ " --load " ]
2015-01-07 11:56:28 +00:00
else :
for profile_name in profiles . keys ( ) :
if profile_blocked ( os . path . join ( profile_path , profile_name ) ) :
2015-01-26 06:48:06 +00:00
print ( " %s (blocked) " % profile_name , file = sys . stderr )
2015-01-07 11:56:28 +00:00
continue
2015-02-23 09:42:54 +00:00
if profile_name in detected_profiles :
2015-01-26 06:48:06 +00:00
print ( " %s (detected) " % profile_name , file = sys . stderr )
2015-02-23 09:42:54 +00:00
if ( " -c " in options or " --change " in options ) and not load_profile :
load_profile = profile_name
2015-01-07 11:56:28 +00:00
else :
2015-01-26 06:48:06 +00:00
print ( profile_name , file = sys . stderr )
2015-01-06 20:13:13 +00:00
if " -d " in options :
options [ " --default " ] = options [ " -d " ]
if not load_profile and " --default " in options :
load_profile = options [ " --default " ]
if load_profile :
2015-01-07 11:56:28 +00:00
if load_profile in ( x [ 0 ] for x in virtual_profiles ) :
2015-02-23 09:42:54 +00:00
load_config = generate_virtual_profile ( config , modes , load_profile )
scripts_path = os . path . join ( profile_path , load_profile )
2015-01-07 11:56:28 +00:00
else :
2015-01-12 09:39:28 +00:00
try :
profile = profiles [ load_profile ]
2015-02-23 09:42:54 +00:00
load_config = profile [ " config " ]
scripts_path = profile [ " path " ]
2015-01-12 09:39:28 +00:00
except KeyError :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " Failed to load profile ' %s ' : Profile not found " % load_profile )
2015-02-23 09:42:54 +00:00
if load_profile in detected_profiles and detected_profiles [ 0 ] != load_profile :
update_mtime ( os . path . join ( scripts_path , " config " ) )
add_unused_outputs ( config , load_config )
if load_config == dict ( config ) and not " -f " in options and not " --force " in options :
2015-01-26 06:48:06 +00:00
print ( " Config already loaded " , file = sys . stderr )
2015-01-06 20:13:13 +00:00
sys . exit ( 0 )
2015-03-07 17:44:25 +00:00
remove_irrelevant_outputs ( config , load_config )
2015-01-06 20:13:13 +00:00
2015-01-08 08:51:01 +00:00
try :
if " --dry-run " in options :
2015-03-30 06:05:42 +00:00
apply_configuration ( load_config , config , True )
2015-01-08 08:51:01 +00:00
else :
2015-02-23 09:42:54 +00:00
exec_scripts ( scripts_path , " preswitch " )
2015-03-30 06:05:42 +00:00
apply_configuration ( load_config , config , False )
2015-02-23 09:42:54 +00:00
exec_scripts ( scripts_path , " postswitch " )
2015-01-08 08:54:05 +00:00
except Exception as e :
2015-04-10 09:36:48 +00:00
raise AutorandrException ( " Failed to apply profile ' %s ' " % load_profile , e , True )
2015-01-06 20:13:13 +00:00
sys . exit ( 0 )
if __name__ == ' __main__ ' :
2015-01-08 08:51:01 +00:00
try :
main ( sys . argv )
2015-04-10 09:36:48 +00:00
except AutorandrException as e :
print ( file = sys . stderr )
print ( e , file = sys . stderr )
sys . exit ( 1 )
2015-01-08 08:54:05 +00:00
except Exception as e :
2015-04-10 09:36:48 +00:00
trace = sys . exc_info ( ) [ 2 ]
while trace . tb_next :
trace = trace . tb_next
print ( " \n Unhandled exception in line %d . Please report this as a bug: \n %s " % ( trace . tb_lineno , " \n " . join ( str ( e ) . split ( " \n " ) ) , ) , file = sys . stderr )
2015-01-08 08:51:01 +00:00
sys . exit ( 1 )