Skip to content

Commit

Permalink
Merge branch 'release/4.8.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
martin-ueding committed Jan 15, 2017
2 parents 3583a0b + 0a64f3c commit 0a47796
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 31 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
Changelog
#########

v4.8.0
- Ignore a failure by ``xbacklight``. On Martin's laptop, the modesetting
driver currently has no access to the brightness setting. Therefore the
docking will always fail at the brightness step. This update converts the
failure into a warning.

- Change the configuration option ``screen.internal`` to
``screen.internal_regex`` and give a sensible default value that matches
the output name variant reported by the modesetting driver. This change
should also help Yoga users.

v4.7.5
Released: 2017-01-14 20:40:37 +0100

Expand Down
21 changes: 20 additions & 1 deletion tps/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright © 2014-2016 Martin Ueding <dev@martin-ueding.de>
# Copyright © 2014-2017 Martin Ueding <dev@martin-ueding.de>
# Copyright © 2014 Jim Turner <jturner314@gmail.com>
# Licensed under The GNU Public License Version 2 (or later)

Expand Down Expand Up @@ -147,6 +147,25 @@ def assert_python3():
assert sys.version_info >= (3, 0), 'You need Python 3 to run this!'


def static_vars(**kwargs):
'''
Attach static variables to a function.
Python does not have static variables. There is a workaround since all
Python functions are objects really. Therefore one can attach attributes to
it. This decorator conveniently does that.
Taken from a `Stack Overflow answer` by *Claudiu* and *ony*.
__ http://stackoverflow.com/a/279586
'''
def decorated(func):
for name, value in kwargs.items():
setattr(func, name, value)
return func
return decorated


check_call = print_command_decorate(subprocess.check_call)
call = print_command_decorate(subprocess.call)
check_output = print_command_decorate(subprocess.check_output)
Expand Down
4 changes: 2 additions & 2 deletions tps/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright © 2014-2015 Martin Ueding <dev@martin-ueding.de>
# Copyright © 2014-2015, 2017 Martin Ueding <dev@martin-ueding.de>
# Licensed under The GNU Public License Version 2 (or later)

'''
Expand Down Expand Up @@ -135,7 +135,7 @@ def interpret_shell_line(line, config):

known_options = {
'disable_wifi': ('network', 'disable_wifi'),
'internal': ('screen', 'internal'),
'internal': ('screen', 'internal_regex'),
'unmute': ('sound', 'unmute'),
'dock_loudness': ('sound', 'dock_loudness'),
'undock_loudness': ('sound', 'undock_loudness'),
Expand Down
4 changes: 2 additions & 2 deletions tps/default.ini
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright © 2014-2016 Martin Ueding <dev@martin-ueding.de>
# Copyright © 2014-2017 Martin Ueding <dev@martin-ueding.de>
# Copyright © 2015 Jim Turner <jturner314@gmail.com>
# Licensed under The GNU Public License Version 2 (or later)

Expand Down Expand Up @@ -30,7 +30,7 @@ subpixels_with_external = false
xrandr_bug_workaround = false

[screen]
internal = LVDS1
internal_regex = LVDS-?1|eDP-?1
primary =
secondary =
set_brightness = true
Expand Down
31 changes: 17 additions & 14 deletions tps/dock.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright © 2014-2016 Martin Ueding <dev@martin-ueding.de>
# Copyright © 2014-2017 Martin Ueding <dev@martin-ueding.de>
# Copyright © 2015 Jim Turner <jturner314@gmail.com>
# Licensed under The GNU Public License Version 2 (or later)

Expand Down Expand Up @@ -90,8 +90,11 @@ def select_docking_screens(internal, primary='', secondary=''):
output of ``xrandr``. See
:py:class:`tps.testsuite.test_dock.SelectDockingScreensTestCase` for more
examples.
'''

logger.debug('select_docking_screens(internal=%s, primary=%s, secondary=%s)', str(internal),
str(primary), str(secondary))

screens = tps.screen.get_externals(internal) + [internal]
for index, screen in enumerate([primary, secondary]):
if screen in screens:
Expand Down Expand Up @@ -122,7 +125,7 @@ def dock(on, config):
tps.screen.set_brightness(config['screen']['brightness'])

primary, secondary, others = select_docking_screens(
config['screen']['internal'],
tps.screen.get_internal(config),
config['screen']['primary'],
config['screen']['secondary'])

Expand Down Expand Up @@ -150,7 +153,7 @@ def dock(on, config):
if not config['screen'].getboolean('internal_docked_on'):
logger.info('Internal screen is supposed to be off when '
'docked, turning it off.')
tps.screen.disable(config['screen']['internal'])
tps.screen.disable(tps.screen.get_internal(config))

if config['network'].getboolean('disable_wifi') \
and tps.network.has_ethernet():
Expand All @@ -168,23 +171,23 @@ def dock(on, config):
except subprocess.CalledProcessError:
logger.warning('unable to restart ethernet connection')

if primary == config['screen']['internal'] or \
secondary == config['screen']['internal']:
if primary == tps.screen.get_internal(config) or \
secondary == tps.screen.get_internal(config):
try:
tps.input.map_rotate_all_input_devices(
config['screen']['internal'],
tps.screen.get_rotation(config['screen']['internal']))
tps.screen.get_internal(config),
tps.screen.get_rotation(tps.screen.get_internal(config)))
except tps.screen.ScreenNotFoundException as e:
logger.error('Unable to map input devices to "{}": {}'.format(
config['screen']['internal'], e))
tps.screen.get_internal(config), e))

else:
externals = tps.screen.get_externals(config['screen']['internal'])
externals = tps.screen.get_externals(tps.screen.get_internal(config))
# Disable all but one screen (xrandr complains otherwise).
for external in externals[:-1]:
tps.screen.disable(external)
# Enable the internal screen.
tps.screen.enable(config['screen']['internal'], primary=True)
tps.screen.enable(tps.screen.get_internal(config), primary=True)
# It's now safe to disable the last external screen.
if externals:
tps.screen.disable(externals[-1])
Expand All @@ -197,11 +200,11 @@ def dock(on, config):

try:
tps.input.map_rotate_all_input_devices(
config['screen']['internal'],
tps.screen.get_rotation(config['screen']['internal']))
tps.screen.get_internal(config),
tps.screen.get_rotation(tps.screen.get_internal(config)))
except tps.screen.ScreenNotFoundException as e:
logger.error('Unable to map input devices to "{}": {}'.format(
config['screen']['internal'], e))
tps.screen.get_internal(config), e))

tps.hooks.postdock(on, config)

Expand Down
14 changes: 7 additions & 7 deletions tps/rotate.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright © 2014-2016 Martin Ueding <dev@martin-ueding.de>
# Copyright © 2014-2017 Martin Ueding <dev@martin-ueding.de>
# Licensed under The GNU Public License Version 2 (or later)

import argparse
Expand Down Expand Up @@ -37,14 +37,14 @@ def main():

try:
new_direction = new_rotation(
tps.screen.get_rotation(config['screen']['internal']),
tps.screen.get_rotation(tps.screen.get_internal(config)),
options.direction, config, options.force_direction)
except tps.UnknownDirectionException:
logger.error('Direction cannot be understood.')
sys.exit(1)
except tps.screen.ScreenNotFoundException as e:
logger.error('Unable to determine rotation of "{}": {}'.format(
config['screen']['internal'], e))
tps.screen.get_internal(config), e))
sys.exit(1)

rotate_to(new_direction, config)
Expand All @@ -56,13 +56,13 @@ def rotate_to(direction, config):
'''
tps.hooks.prerotate(direction, config)

tps.screen.rotate(config['screen']['internal'], direction)
tps.input.map_rotate_all_input_devices(config['screen']['internal'],
tps.screen.rotate(tps.screen.get_internal(config), direction)
tps.input.map_rotate_all_input_devices(tps.screen.get_internal(config),
direction)

if config['rotate'].getboolean('subpixels'):
if config['rotate'].getboolean('subpixels_with_external') \
or not tps.screen.get_externals(config['screen']['internal']):
or not tps.screen.get_externals(tps.screen.get_internal(config)):
tps.screen.set_subpixel_order(direction)

if config['unity'].getboolean('toggle_launcher'):
Expand Down Expand Up @@ -165,7 +165,7 @@ def has_external_screens(config):
'''
Checks whether any external screens are attached.
'''
externals = tps.screen.get_externals(config['screen']['internal'])
externals = tps.screen.get_externals(tps.screen.get_internal(config))
return len(externals) > 0


Expand Down
64 changes: 62 additions & 2 deletions tps/screen.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright © 2014-2015 Martin Ueding <dev@martin-ueding.de>
# Copyright © 2014-2015, 2017 Martin Ueding <dev@martin-ueding.de>
# Copyright © 2015 Jim Turner <jturner314@gmail.com>
# Licensed under The GNU Public License Version 2 (or later)

Expand Down Expand Up @@ -124,7 +124,9 @@ def set_brightness(brightness):
logger.warning('xbacklight is not installed')
return

tps.check_call(['xbacklight', '-set', brightness], logger)
status = tps.call(['xbacklight', '-set', brightness], logger)
if status != 0:
logger.warning('`xbacklight` cannot set the brightness, this is ignored.')


def disable(screen):
Expand Down Expand Up @@ -217,3 +219,61 @@ def get_resolution_and_shift(output):
'report a bug otherwise.'.format(output))

return result


@tps.static_vars(cached_internal=None)
def get_internal(config, cache=True):
'''
Matches the regular expression in the config and retrieves the actual name
of the internal screen.
The names of the outputs that XRandR reports may be ``LVDS1`` or
``LVDS-1``. The former happens with the Intel driver, the latter with the
generic kernel modesetting driver. We do not know what the system will
provide, therefore it was decided in GH-125 to use a regular expression in
the configuration file. This also gives out-of-the-box support for Yoga
users where the internal screen is called ``eDP1`` or ``eDP-1``.
:param config: Configuration parser instance
:param bool cache: Compute the value again even if it is cached
'''

if cache and get_internal.cached_internal is not None:
return get_internal.cached_internal

if 'internal' in config['screen']:
# The user has this key in his configuration. The default does not have
# it any more, so this must be manual. The user could have specified
# that by hand, it is perhaps not really what is wanted.
logger.warning('You have specified the screen.internal option in your configuration file. Since version 4.8.0 this option is not used by default but screen.internal_regex (valued `%s`) is used instead. Please take a look at the new default regular expression and see whether that covers your use case already. In that case you can delete the entry from your own configuration file. This program will use your value and not try to match the regular expression.', config['screen']['internal_regex'])
internal = config['screen']['internal']
else:
# There is no such option, therefore we need to match the regular
# expression against the output of XRandR now.
output = tps.check_output(['xrandr'], logger).decode().strip()
screens = get_available_screens(output)
logger.debug('Screens available on this system are %s.', ', '.join(screens))
internal = filter_outputs(screens, config['screen']['internal_regex'])
logger.debug('Internal screen is determined to be %s.', internal)

get_internal.cached_internal = internal

return internal


def get_available_screens(output):
lines = output.split('\n')
pattern = re.compile(r'^(?P<name>[\w\d-]+) connected')
results = []
for line in lines:
m = pattern.search(line)
if m:
results.append(m.groupdict()['name'])
results.sort()
return results


def filter_outputs(outputs, regex):
matched = list(filter(lambda output: re.match(regex, output), outputs))
assert len(matched) == 1, 'There should be exactly one matching screen for the `screen.internal_regex`. The outputs detected are {}, the regular expression is `{}`. If you have tinkered with that configuration option, please check it. Otherwise please file a bug report.'.format(', '.join(outputs), regex)
return matched[0]
6 changes: 3 additions & 3 deletions tps/testsuite/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ def assertConfigEqual(self, first, second, msg=None):
self.assertEqual({sect:dict(first[sect]) for sect in first.sections()},
{sect:dict(second[sect]) for sect in second.sections()})

class InterpretShellLineTestCase(ConfigTestCase):

class InterpretShellLineTestCase(ConfigTestCase):
def test_interpret_shell_line_normal(self):
'''
`interpret_shell_line` should work properly with normal input.
'''
expected = ConfigParser(interpolation=None)
expected.read_dict({'network': {'disable_wifi': 'true'},
'screen': {'internal': 'LVDS1',
'screen': {'internal_regex': 'LVDS1',
'set_brightness': 'true',
'brightness': '100%',
'relative_position': 'right'},
Expand Down Expand Up @@ -71,7 +71,7 @@ def test_interpret_shell_line_white_space(self):
expected = ConfigParser(interpolation=None)
expected.read_dict({'screen': {'brightness': ' 50%',
'set_brightness': 'true ',
'internal': 'LVDS1',
'internal_regex': 'LVDS1',
'relative_position': 'foo bar'},
'gui': {'kdialog': 'true'}})

Expand Down
52 changes: 52 additions & 0 deletions tps/testsuite/test_screen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright © 2017 Martin Ueding <dev@martin-ueding.de>
# Licensed under The GNU Public License Version 2 (or later)

import unittest

import tps.screen


class XrandrParserTestCase(unittest.TestCase):
def test_xrandr_parsing(self):
output = '''Screen 0: minimum 320 x 200, current 3286 x 1080, maximum 8192 x 8192
LVDS-1 connected 1366x768+1920+0 (normal left inverted right x axis y axis) 277mm x 156mm
1366x768 60.02*+
1024x768 60.04 60.00
960x720 60.00
928x696 60.05
896x672 60.01
800x600 60.00 60.32 56.25
700x525 59.98
640x512 60.02
640x480 60.00 59.94
512x384 60.00
400x300 60.32 56.34
320x240 60.05
VGA-1 disconnected (normal left inverted right x axis y axis)
HDMI-1 disconnected (normal left inverted right x axis y axis)
DP-1 disconnected (normal left inverted right x axis y axis)
HDMI-2 disconnected (normal left inverted right x axis y axis)
HDMI-3 disconnected (normal left inverted right x axis y axis)
DP-2 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 509mm x 286mm
1920x1080 60.00*+
1600x900 60.00
1280x1024 75.02 60.02
1152x864 75.00
1024x768 75.03 60.00
800x600 75.00 60.32
640x480 75.00 59.94
720x400 70.08
DP-3 disconnected (normal left inverted right x axis y axis)'''

self.assertEqual(tps.screen.get_available_screens(output), ['DP-2', 'LVDS-1'])

def test_filter_outputs(self):
outputs = ['DP-2', 'LVDS-1']
regex = r'LVDS-?1|eDP-?1'
self.assertEqual(tps.screen.filter_outputs(outputs, regex), 'LVDS-1')

def test_filter_outputs_assert(self):
outputs = ['eDP1', 'LVDS-1']
regex = r'LVDS-?1|eDP-?1'
with self.assertRaises(AssertionError):
tps.screen.filter_outputs(outputs, regex)

0 comments on commit 0a47796

Please sign in to comment.