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
import binascii
2015-11-11 14:24:54 +00:00
import copy
import getopt
2015-01-06 20:13:13 +00:00
import hashlib
import os
2015-11-11 14:24:54 +00:00
import posix
2016-09-16 14:19:24 +00:00
import pwd
2015-01-06 20:13:13 +00:00
import re
import subprocess
import sys
2016-03-27 17:18:01 +00:00
import shutil
2016-06-05 19:15:37 +00:00
import time
2015-01-06 20:13:13 +00:00
2015-11-11 14:24:54 +00:00
from collections import OrderedDict
from distutils . version import LooseVersion as Version
2015-05-17 15:55:26 +00:00
from functools import reduce
2015-01-06 20:13:13 +00:00
from itertools import chain
2015-05-17 15:47:59 +00:00
2016-03-28 12:06:31 +00:00
try :
input = raw_input
except NameError :
pass
2015-05-17 15:47:59 +00:00
2015-01-07 11:56:28 +00:00
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 >
2016-03-27 17:18:01 +00:00
- r , - - remove < profile > remove profile < profile >
2015-01-08 08:51:01 +00:00
- l , - - load < profile > load profile < profile >
2015-01-06 20:13:13 +00:00
- d , - - default < profile > make profile < profile > the default profile
2015-05-17 09:27:36 +00:00
- - skip - options < option > comma separated list of xrandr arguments ( e . g . " gamma " )
to skip both in detecting changes and applying a 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
2015-05-16 19:22:16 +00:00
- - debug enable verbose output
2016-09-16 14:19:24 +00:00
- - batch run autorandr for all users with active X11 sessions
2015-01-06 20:13:13 +00:00
2016-10-05 15:40:04 +00:00
To prevent a profile from being loaded , place a script called " block " in its
2015-01-06 20:13:13 +00:00
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
2016-12-14 11:55:08 +00:00
self . file_name = trace . tb_frame . f_code . co_filename
2015-04-10 09:36:48 +00:00
else :
try :
import inspect
2016-12-14 11:55:08 +00:00
frame = inspect . currentframe ( ) . f_back
self . line = frame . f_lineno
self . file_name = frame . f_code . co_filename
2015-04-10 09:36:48 +00:00
except :
self . line = None
2016-12-14 11:55:08 +00:00
self . file_name = None
2015-04-10 09:36:48 +00:00
self . original_exception = None
2016-12-14 11:55:08 +00:00
if os . path . abspath ( self . file_name ) == os . path . abspath ( sys . argv [ 0 ] ) :
self . file_name = None
2015-04-10 09:36:48 +00:00
def __str__ ( self ) :
retval = [ self . message ]
if self . line :
2016-12-14 11:55:08 +00:00
retval . append ( " (line %d %s ) " % ( self . line , ( " ; %s " % self . file_name ) if self . file_name else " " ) )
2015-04-10 09:36:48 +00:00
if self . original_exception :
2015-04-14 07:23:00 +00:00
retval . append ( " : \n " )
2015-04-10 09:36:48 +00:00
retval . append ( str ( self . original_exception ) . replace ( " \n " , " \n " ) )
if self . report_bug :
2016-05-31 07:16:44 +00:00
retval . append ( " \n This appears to be a bug. Please help improving autorandr by reporting it upstream: "
" \n https://github.com/phillipberndt/autorandr/issues "
2015-04-10 09:36:48 +00:00
" \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-05-17 15:44:52 +00:00
Gamma : ( ? P < gamma > ( ? : inf | [ 0 - 9 \. : e ] ) + ) | # 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 ) :
2015-05-16 19:22:16 +00:00
return " < %s %s %s > " % ( self . output , self . short_edid , " " . join ( self . option_vector ) )
@property
def short_edid ( self ) :
return ( " %s .. %s " % ( self . edid [ : 5 ] , self . edid [ - 5 : ] ) ) if self . edid else " "
2015-01-06 20:13:13 +00:00
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 )
2015-05-17 09:27:36 +00:00
return { a : b for a , b in options . items ( ) if a not in self . ignored_options }
@property
def filtered_options ( self ) :
" Return a dictionary of options without ignored options "
return { a : b for a , b in self . options . items ( ) if a not in self . ignored_options }
2015-01-12 09:35:21 +00:00
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-05-17 09:27:36 +00:00
return " \n " . join ( [ " " . join ( option ) if option [ 1 ] else option [ 0 ] for option in chain ( ( ( " output " , self . output ) , ) , sorted ( self . filtered_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-05-17 09:27:36 +00:00
self . ignored_options = [ ]
2015-01-26 06:45:28 +00:00
self . remove_default_option_values ( )
2015-05-17 09:27:36 +00:00
def set_ignored_options ( self , options ) :
" Set a list of xrandr options that are never used (neither when comparing configurations nor when applying them) "
self . ignored_options = list ( options )
2015-01-26 06:45:28 +00:00
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-05-17 15:44:52 +00:00
# xrandr prints different values in --verbose than it accepts as a parameter value for --gamma
# Also, it is not able to work with non-standard gamma ramps. Finally, it auto-corrects 0 to 1,
# so we approximate by 1e-10.
gamma = " : " . join ( [ str ( max ( 1e-10 , round ( 1. / float ( x ) , 3 ) ) ) for x in gamma . split ( " : " ) ] )
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 )
2017-01-18 13:02:05 +00:00
if line and line [ 0 ] . startswith ( " # " ) :
continue
2015-01-06 20:13:13 +00:00
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
2015-05-19 11:37:33 +00:00
def __ne__ ( self , other ) :
return not ( self == other )
2015-01-06 20:13:13 +00:00
def __eq__ ( self , other ) :
2015-05-17 09:27:36 +00:00
return self . edid_equals ( other ) and self . output == other . output and self . filtered_options == other . filtered_options
2015-01-06 20:13:13 +00:00
2015-05-16 19:22:16 +00:00
def verbose_diff ( self , other ) :
" Compare to another XrandrOutput and return a list of human readable differences "
diffs = [ ]
if not self . edid_equals ( other ) :
diffs . append ( " EDID ` %s ' differs from ` %s ' " % ( self . short_edid , other . short_edid ) )
if self . output != other . output :
diffs . append ( " Output name ` %s ' differs from ` %s ' " % ( self . output , other . output ) )
if " off " in self . options and " off " not in other . options :
diffs . append ( " The output is disabled currently, but active in the new configuration " )
elif " off " in other . options and " off " not in self . options :
diffs . append ( " The output is currently enabled, but inactive in the new configuration " )
else :
for name in set ( chain . from_iterable ( ( self . options . keys ( ) , other . options . keys ( ) ) ) ) :
if name not in other . options :
diffs . append ( " Option -- %s %s is not present in the new configuration " % ( name , " (= ` %s ' ) " % self . options [ name ] if self . options [ name ] else " " ) )
elif name not in self . options :
diffs . append ( " Option -- %s (` %s ' in the new configuration) is not present currently " % ( name , other . options [ name ] ) )
elif self . options [ name ] != other . options [ name ] :
diffs . append ( " Option -- %s %s is ` %s ' in the new configuration " % ( name , " (= ` %s ' ) " % self . options [ name ] if self . options [ name ] else " " , other . options [ name ] ) )
return diffs
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
2017-01-18 13:02:05 +00:00
edids = dict ( [ x . split ( ) for x in ( y . strip ( ) for y in open ( setup_name ) . readlines ( ) ) if x and x [ 0 ] != " # " ] )
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
2016-10-03 16:33:30 +00:00
def get_symlinks ( profile_path ) :
" Load all symlinks from a directory "
symlinks = { }
for link in os . listdir ( profile_path ) :
file_name = os . path . join ( profile_path , link )
if os . path . islink ( file_name ) :
symlinks [ link ] = os . readlink ( file_name )
return symlinks
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
2016-01-03 11:27:59 +00:00
def profile_blocked ( profile_path , meta_information = None ) :
""" Check if a profile is blocked.
meta_information is expected to be an dictionary . It will be passed to the block scripts
in the environment , as variables called AUTORANDR_ < CAPITALIZED_KEY_HERE > .
"""
2016-01-07 14:02:51 +00:00
return not exec_scripts ( profile_path , " block " , meta_information )
2015-01-06 20:13:13 +00:00
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
2016-04-28 13:51:50 +00:00
def call_and_retry ( * args , * * kwargs ) :
""" Wrapper around subprocess.call that retries failed calls.
This function calls subprocess . call and on non - zero exit states ,
waits a second and then retries once . This mitigates #47,
a timing issue with some drivers .
"""
2016-12-14 12:01:04 +00:00
if " dry_run " in kwargs :
dry_run = kwargs [ " dry_run " ]
del kwargs [ " dry_run " ]
2016-04-28 13:51:50 +00:00
else :
2016-12-14 12:01:04 +00:00
dry_run = False
kwargs_redirected = dict ( kwargs )
if not dry_run :
if hasattr ( subprocess , " DEVNULL " ) :
kwargs_redirected [ " stdout " ] = getattr ( subprocess , " DEVNULL " )
else :
kwargs_redirected [ " stdout " ] = open ( os . devnull , " w " )
kwargs_redirected [ " stderr " ] = kwargs_redirected [ " stdout " ]
2016-04-28 13:51:50 +00:00
retval = subprocess . call ( * args , * * kwargs_redirected )
if retval != 0 :
time . sleep ( 1 )
retval = subprocess . call ( * args , * * kwargs )
return retval
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-11-17 09:27:43 +00:00
# - Some implementations can not handle --transform at all, so avoid it unless
# necessary. (See https://github.com/phillipberndt/autorandr/issues/37)
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
2015-11-17 09:27:43 +00:00
option_vector = new_configuration [ output ] . option_vector
if xrandr_version ( ) > = Version ( " 1.3.0 " ) :
if " transform " in current_configuration [ output ] . options :
auxiliary_changes_pre . append ( [ " --output " , output , " --transform " , " none " ] )
else :
try :
transform_index = option_vector . index ( " --transform " )
if option_vector [ transform_index + 1 ] == XrandrOutput . XRANDR_DEFAULTS [ " transform " ] :
option_vector = option_vector [ : transform_index ] + option_vector [ transform_index + 2 : ]
except ValueError :
pass
enable_outputs . append ( option_vector )
2015-03-30 06:05:42 +00:00
# Perform pe-change auxiliary changes
if auxiliary_changes_pre :
argv = base_argv + list ( chain . from_iterable ( auxiliary_changes_pre ) )
2016-12-14 12:01:04 +00:00
if call_and_retry ( argv , dry_run = dry_run ) != 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 :
2016-12-14 12:01:04 +00:00
if call_and_retry ( base_argv + list ( chain . from_iterable ( disable_outputs [ : - 1 ] if disable_keep else disable_outputs ) ) , dry_run = dry_run ) != 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 ] ) )
2016-12-14 12:01:04 +00:00
if call_and_retry ( argv , dry_run = dry_run ) != 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-05-19 11:37:33 +00:00
def is_equal_configuration ( source_configuration , target_configuration ) :
" Check if all outputs from target are already configured correctly in source "
for output in target_configuration . keys ( ) :
if ( output not in source_configuration ) or ( source_configuration [ output ] != target_configuration [ output ] ) :
return False
return True
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 " :
2015-05-30 17:16:21 +00:00
common_resolution = [ set ( ( ( mode [ " width " ] , mode [ " height " ] ) for mode in output_modes ) ) for output , output_modes in modes . items ( ) if configuration [ output ] . edid ]
2015-01-07 11:56:28 +00:00
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 = { }
2015-05-30 17:16:21 +00:00
if output in modes and configuration [ output ] . edid :
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 = { }
2015-05-30 17:16:21 +00:00
if output in modes and configuration [ output ] . edid :
2015-01-07 11:56:28 +00:00
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-05-16 19:22:16 +00:00
def print_profile_differences ( one , another ) :
" Print the differences between two profiles for debugging "
if one == another :
return
print ( " | Differences between the two profiles: " , file = sys . stderr )
for output in set ( chain . from_iterable ( ( one . keys ( ) , another . keys ( ) ) ) ) :
if output not in one :
if " off " not in another [ output ] . options :
print ( " | Output ` %s ' is missing from the active configuration " % output , file = sys . stderr )
elif output not in another :
if " off " not in one [ output ] . options :
print ( " | Output ` %s ' is missing from the new configuration " % output , file = sys . stderr )
else :
for line in one [ output ] . verbose_diff ( another [ output ] ) :
print ( " | [Output %s ] %s " % ( output , line ) , file = sys . stderr )
print ( " \\ - " , file = sys . stderr )
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 )
2016-01-07 14:02:51 +00:00
def exec_scripts ( profile_path , script_name , meta_information = None ) :
""" " Run userscripts
This will run all executables from the profile folder , and global per - user
and system - wide configuration folders , named script_name or residing in
subdirectories named script_name . d .
2016-11-15 14:51:33 +00:00
If profile_path is None , only global scripts will be invoked .
2016-01-07 14:02:51 +00:00
meta_information is expected to be an dictionary . It will be passed to the block scripts
in the environment , as variables called AUTORANDR_ < CAPITALIZED_KEY_HERE > .
Returns True unless any of the scripts exited with non - zero exit status .
"""
all_ok = True
if meta_information :
env = os . environ . copy ( )
env . update ( { " AUTORANDR_ %s " % str ( key ) . upper ( ) : str ( value ) for ( key , value ) in meta_information . items ( ) } )
else :
env = os . environ . copy ( )
2016-01-07 14:16:27 +00:00
# If there are multiple candidates, the XDG spec tells to only use the first one.
ran_scripts = set ( )
user_profile_path = os . path . expanduser ( " ~/.autorandr " )
if not os . path . isdir ( user_profile_path ) :
user_profile_path = os . path . join ( os . environ . get ( " XDG_CONFIG_HOME " , os . path . expanduser ( " ~/.config " ) ) , " autorandr " )
2016-11-15 14:51:33 +00:00
candidate_directories = chain ( ( user_profile_path , ) , ( os . path . join ( x , " autorandr " ) for x in os . environ . get ( " XDG_CONFIG_DIRS " , " /etc/xdg " ) . split ( " : " ) ) )
if profile_path :
candidate_directories = chain ( ( profile_path , ) , candidate_directories )
for folder in candidate_directories :
2016-01-07 14:16:27 +00:00
if script_name not in ran_scripts :
script = os . path . join ( folder , script_name )
if os . access ( script , os . X_OK | os . F_OK ) :
2016-12-18 09:54:52 +00:00
try :
all_ok & = subprocess . call ( script , env = env ) != 0
except :
raise AutorandrException ( " Failed to execute user command: %s " % ( script , ) )
2016-01-07 14:16:27 +00:00
ran_scripts . add ( script_name )
2016-01-07 14:02:51 +00:00
script_folder = os . path . join ( folder , " %s .d " % script_name )
if os . access ( script_folder , os . R_OK | os . X_OK ) and os . path . isdir ( script_folder ) :
for file_name in os . listdir ( script_folder ) :
2016-01-07 14:16:27 +00:00
check_name = " d/ %s " % ( file_name , )
if check_name not in ran_scripts :
script = os . path . join ( script_folder , file_name )
if os . access ( script , os . X_OK | os . F_OK ) :
2016-12-18 09:54:52 +00:00
try :
all_ok & = subprocess . call ( script , env = env ) != 0
except :
raise AutorandrException ( " Failed to execute user command: %s " % ( script , ) )
2016-01-07 14:16:27 +00:00
ran_scripts . add ( check_name )
2016-01-07 14:02:51 +00:00
return all_ok
2015-01-06 20:13:13 +00:00
2016-09-16 14:19:24 +00:00
def dispatch_call_to_sessions ( argv ) :
""" Invoke autorandr for each open local X11 session with the given options.
The function iterates over all processes not owned by root and checks
whether they have a DISPLAY variable set . It strips the screen from any
variable it finds ( i . e . : 0.0 becomes : 0 ) and checks whether this display
has been handled already . If it has not , it forks , changes uid / gid to
the user owning the process , reuses the process ' s environment and runs
autorandr with the parameters from argv .
This function requires root permissions . It only works for X11 servers that
have at least one non - root process running . It is susceptible for attacks
where one user runs a process with another user ' s DISPLAY variable - in
this case , it might happen that autorandr is invoked for the other user ,
which won ' t work. Since no other harm than prevention of automated
execution of autorandr can be done this way , the assumption is that in this
situation , the local administrator will handle the situation . """
X11_displays_done = set ( )
autorandr_binary = os . path . abspath ( argv [ 0 ] )
for directory in os . listdir ( " /proc " ) :
directory = os . path . join ( " /proc/ " , directory )
if not os . path . isdir ( directory ) :
continue
environ_file = os . path . join ( directory , " environ " )
if not os . path . isfile ( environ_file ) :
continue
uid = os . stat ( environ_file ) . st_uid
2016-11-23 20:25:09 +00:00
# The following line assumes that user accounts start at 1000 and that
# no one works using the root or another system account. This is rather
# restrictive, but de facto default. Alternatives would be to use the
# UID_MIN from /etc/login.defs or FIRST_UID from /etc/adduser.conf;
# but effectively, both values aren't binding in any way.
# If this breaks your use case, please file a bug on Github.
if uid < 1000 :
2016-09-16 14:19:24 +00:00
continue
process_environ = { }
for environ_entry in open ( environ_file ) . read ( ) . split ( " \0 " ) :
if " = " in environ_entry :
name , value = environ_entry . split ( " = " , 1 )
if name == " DISPLAY " and " . " in value :
value = value [ : value . find ( " . " ) ]
process_environ [ name ] = value
display = process_environ [ " DISPLAY " ] if " DISPLAY " in process_environ else None
2017-01-20 13:59:43 +00:00
# To allow scripts to detect batch invocation (especially useful for predetect)
process_environ [ " AUTORANDR_BATCH_PID " ] = os . getpid ( )
2016-09-16 14:19:24 +00:00
if display and display not in X11_displays_done :
try :
pwent = pwd . getpwuid ( uid )
except KeyError :
# User has no pwd entry
continue
print ( " Running autorandr as %s for display %s " % ( pwent . pw_name , display ) )
child_pid = os . fork ( )
if child_pid == 0 :
# This will throw an exception if any of the privilege changes fails,
# so it should be safe. Also, note that since the environment
# is taken from a process owned by the user, reusing it should
# not leak any information.
os . setgroups ( [ ] )
os . setresgid ( pwent . pw_gid , pwent . pw_gid , pwent . pw_gid )
os . setresuid ( pwent . pw_uid , pwent . pw_uid , pwent . pw_uid )
os . chdir ( pwent . pw_dir )
os . environ . clear ( )
os . environ . update ( process_environ )
os . execl ( autorandr_binary , autorandr_binary , * argv [ 1 : ] )
os . exit ( 1 )
os . waitpid ( child_pid , 0 )
X11_displays_done . add ( display )
2015-01-06 20:13:13 +00:00
def main ( argv ) :
2015-01-08 08:55:36 +00:00
try :
2016-09-16 14:19:24 +00:00
options = dict ( getopt . getopt ( argv [ 1 : ] , " s:r:l:d:cfh " , [ " batch " , " dry-run " , " change " , " default= " , " save= " , " remove= " , " load= " , " force " , " fingerprint " , " config " , " debug " , " skip-options= " , " help " ] ) [ 0 ] )
2015-01-08 08:55:36 +00:00
except getopt . GetoptError as e :
2015-05-17 15:47:59 +00:00
print ( " Failed to parse options: {0} . \n "
" Use --help to get usage information. " . format ( str ( e ) ) ,
file = sys . stderr )
sys . exit ( posix . EX_USAGE )
2015-01-06 20:13:13 +00:00
2016-09-16 14:19:24 +00:00
# Batch mode
if " --batch " in options :
if ( " DISPLAY " not in os . environ or not os . environ [ " DISPLAY " ] ) and os . getuid ( ) == 0 :
dispatch_call_to_sessions ( [ x for x in argv if x != " --batch " ] )
else :
print ( " --batch mode can only be used by root and if $DISPLAY is unset " )
return
2015-02-09 07:24:25 +00:00
profiles = { }
2016-10-03 16:33:30 +00:00
profile_symlinks = { }
2015-01-08 08:51:01 +00:00
try :
2015-02-09 07:24:25 +00:00
# Load profiles from each XDG config directory
2016-01-07 14:16:27 +00:00
# The XDG spec says that earlier entries should take precedence, so reverse the order
2016-07-05 05:34:29 +00:00
for directory in reversed ( os . environ . get ( " XDG_CONFIG_DIRS " , " /etc/xdg " ) . split ( " : " ) ) :
2015-02-09 07:24:25 +00:00
system_profile_path = os . path . join ( directory , " autorandr " )
if os . path . isdir ( system_profile_path ) :
profiles . update ( load_profiles ( system_profile_path ) )
2016-10-03 16:33:30 +00:00
profile_symlinks . update ( get_symlinks ( system_profile_path ) )
2015-02-09 07:24:25 +00:00
# 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 ) )
2016-10-03 16:33:30 +00:00
profile_symlinks . update ( get_symlinks ( 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
2016-10-03 16:33:30 +00:00
profile_symlinks = { k : v for k , v in profile_symlinks . items ( ) if v in ( x [ 0 ] for x in virtual_profiles ) or v in profiles }
2016-11-15 14:51:33 +00:00
exec_scripts ( None , " predetect " )
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 )
2015-05-17 09:27:36 +00:00
if " --skip-options " in options :
skip_options = [ y [ 2 : ] if y [ : 2 ] == " -- " else y for y in ( x . strip ( ) for x in options [ " --skip-options " ] . split ( " , " ) ) ]
for profile in profiles . values ( ) :
for output in profile [ " config " ] . values ( ) :
output . set_ignored_options ( skip_options )
for output in config . values ( ) :
output . set_ignored_options ( skip_options )
2015-01-06 20:13:13 +00:00
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 :
2016-01-07 14:02:51 +00:00
profile_folder = os . path . join ( profile_path , options [ " --save " ] )
save_configuration ( profile_folder , config )
exec_scripts ( profile_folder , " postsave " , { " CURRENT_PROFILE " : options [ " --save " ] , " PROFILE_FOLDER " : profile_folder } )
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 )
2016-03-27 17:18:01 +00:00
if " -r " in options :
options [ " --remove " ] = options [ " -r " ]
if " --remove " in options :
if options [ " --remove " ] in ( x [ 0 ] for x in virtual_profiles ) :
raise AutorandrException ( " Cannot remove profile ' %s ' : \n This configuration name is a reserved virtual configuration. " % options [ " --remove " ] )
if options [ " --remove " ] not in profiles . keys ( ) :
raise AutorandrException ( " Cannot remove profile ' %s ' : \n This profile does not exist. " % options [ " --remove " ] )
try :
2016-03-28 11:13:03 +00:00
remove = True
2016-03-27 17:18:01 +00:00
profile_folder = os . path . join ( profile_path , options [ " --remove " ] )
2016-03-28 11:13:03 +00:00
profile_dirlist = os . listdir ( profile_folder )
profile_dirlist . remove ( " config " )
profile_dirlist . remove ( " setup " )
if profile_dirlist :
2016-03-28 12:06:31 +00:00
print ( " Profile folder ' %s ' contains the following additional files: \n --- \n %s \n --- " % ( options [ " --remove " ] , " \n " . join ( profile_dirlist ) ) )
2016-03-28 11:13:03 +00:00
response = input ( " Do you really want to remove profile ' %s ' ? If so, type ' yes ' : " % options [ " --remove " ] ) . strip ( )
if response != " yes " :
remove = False
if remove is True :
shutil . rmtree ( profile_folder )
print ( " Removed profile ' %s ' " % options [ " --remove " ] )
else :
print ( " Profile ' %s ' was not removed " % options [ " --remove " ] )
2016-03-27 17:18:01 +00:00
except Exception as e :
raise AutorandrException ( " Failed to remove profile ' %s ' " % ( options [ " --remove " ] , ) , e )
sys . exit ( 0 )
2015-01-06 20:13:13 +00:00
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 :
2016-01-03 11:27:59 +00:00
# Find the active profile(s) first, for the block script (See #42)
current_profiles = [ ]
2015-01-07 11:56:28 +00:00
for profile_name in profiles . keys ( ) :
2016-01-03 11:27:59 +00:00
configs_are_equal = is_equal_configuration ( config , profiles [ profile_name ] [ " config " ] )
if configs_are_equal :
current_profiles . append ( profile_name )
block_script_metadata = {
" CURRENT_PROFILE " : " " . join ( current_profiles [ : 1 ] ) ,
" CURRENT_PROFILES " : " : " . join ( current_profiles )
}
for profile_name in profiles . keys ( ) :
if profile_blocked ( os . path . join ( profile_path , profile_name ) , block_script_metadata ) :
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-05-19 11:37:33 +00:00
props = [ ]
2015-02-23 09:42:54 +00:00
if profile_name in detected_profiles :
2015-05-19 11:37:33 +00:00
props . append ( " (detected) " )
2015-02-23 09:42:54 +00:00
if ( " -c " in options or " --change " in options ) and not load_profile :
load_profile = profile_name
2016-01-03 11:27:59 +00:00
if profile_name in current_profiles :
2015-05-19 11:37:33 +00:00
props . append ( " (current) " )
print ( " %s %s %s " % ( profile_name , " " if props else " " , " " . join ( props ) ) , file = sys . stderr )
2015-05-16 19:22:16 +00:00
if not configs_are_equal and " --debug " in options and profile_name in detected_profiles :
print_profile_differences ( config , profiles [ profile_name ] [ " config " ] )
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 :
2016-10-03 16:33:30 +00:00
if load_profile in profile_symlinks :
if " --debug " in options :
print ( " ' %s ' symlinked to ' %s ' " % ( load_profile , profile_symlinks [ load_profile ] ) )
load_profile = profile_symlinks [ 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-05-16 19:22:16 +00:00
if " --debug " in options and load_config != dict ( config ) :
print ( " Loading profile ' %s ' " % load_profile )
print_profile_differences ( config , load_config )
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 :
2016-01-07 14:02:51 +00:00
script_metadata = {
" CURRENT_PROFILE " : load_profile ,
" PROFILE_FOLDER " : scripts_path ,
}
exec_scripts ( scripts_path , " preswitch " , script_metadata )
2016-04-28 13:38:26 +00:00
if " --debug " in options :
print ( " Going to run: " )
apply_configuration ( load_config , config , True )
2015-03-30 06:05:42 +00:00
apply_configuration ( load_config , config , False )
2016-01-07 14:02:51 +00:00
exec_scripts ( scripts_path , " postswitch " , script_metadata )
2016-12-18 09:54:52 +00:00
except AutorandrException as e :
raise AutorandrException ( " Failed to apply profile ' %s ' " % load_profile , e , e . report_bug )
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
2015-06-08 10:35:44 +00:00
if " --dry-run " not in options and " --debug " in options :
new_config , _ = parse_xrandr_output ( )
if not is_equal_configuration ( new_config , load_config ) :
print ( " The configuration change did not go as expected: " )
print_profile_differences ( new_config , load_config )
2015-01-06 20:13:13 +00:00
sys . exit ( 0 )
2016-11-30 11:48:20 +00:00
def exception_handled_main ( argv = sys . argv ) :
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 ( e , file = sys . stderr )
sys . exit ( 1 )
2015-01-08 08:54:05 +00:00
except Exception as e :
2015-05-16 23:43:19 +00:00
if not len ( str ( e ) ) : # BdbQuit
print ( " Exception: {0} " . format ( e . __class__ . __name__ ) )
sys . exit ( 2 )
2016-05-31 07:16:44 +00:00
print ( " Unhandled exception ( {0} ). Please report this as a bug at https://github.com/phillipberndt/autorandr/issues. " . format ( e ) , file = sys . stderr )
2015-05-16 23:43:19 +00:00
raise
2016-11-30 11:48:20 +00:00
if __name__ == ' __main__ ' :
exception_handled_main ( )