diff options
| author | Ultrabug <ultrabug@gentoo.org> | 2015-03-20 12:28:53 +0100 |
|---|---|---|
| committer | Ultrabug <ultrabug@gentoo.org> | 2015-03-20 12:28:53 +0100 |
| commit | 7e7155b993413f462cc7a4970d84e77cb6d8dd8b (patch) | |
| tree | edc911cb64296a4aa773141686eb3c89f76037f0 /py3status/modules/xrandr.py | |
| parent | 56f92dafc76df816dbb86012da63e1c01a2371c7 (diff) | |
new xrandr module to handle your screens layout from your bar
Diffstat (limited to 'py3status/modules/xrandr.py')
| -rw-r--r-- | py3status/modules/xrandr.py | 387 |
1 files changed, 387 insertions, 0 deletions
diff --git a/py3status/modules/xrandr.py b/py3status/modules/xrandr.py new file mode 100644 index 0000000..2d3e757 --- /dev/null +++ b/py3status/modules/xrandr.py @@ -0,0 +1,387 @@ +# -*- coding: utf-8 -*- +""" +This modules allows you to handle your screens outputs directly from your bar! + - Detect and propose every possible screen combinations + - Switch between combinations using click events and mouse scroll + - Activate the screen or screen combination on a single click + - It will detect any newly connected or removed screen automatically + +For convenience, this module also proposes some added features: + - Dynamic parameters for POSITION and WORKSPACES assignment (see below) + - Automatic fallback to a given screen or screen combination when no more + screen is available (handy for laptops) + - Automatically apply this screen combination on start: no need for xorg! + - Automatically move workspaces to screens when they are available + +Example config: + xrandr { + force_on_start = "eDP1+DP1" + DP1_pos = "left-of eDP1" + VGA_workspaces = "7" + } + +@author ultrabug +""" +import shlex + +from collections import deque +from collections import OrderedDict +from itertools import combinations +from subprocess import call, Popen, PIPE +from syslog import syslog, LOG_INFO +from time import sleep, time + + +class Py3status: + """ + Configuration parameters: + - cache_timeout: how often to (re)detect the outputs + - fallback: when the current output layout is not available anymore, + fallback to this layout if available. This is very handy if you + have a laptop and switched to an external screen for presentation + and want to automatically fallback to your laptop screen when you + disconnect the external screen. + - force_on_start: switch to the given combination mode if available + when the module starts (saves you from having to configure xorg) + - format_clone: string used to display a 'clone' combination + - format_extend: string used to display a 'extend' combination + + Dynamic configuration parameters: + - <OUTPUT>_pos: apply the given position to the OUTPUT + Example: DP1_pos = "-2560x0" + Example: DP1_pos = "left-of LVDS1" + Example: DP1_pos = "right-of eDP1" + + - <OUTPUT>_workspaces: comma separated list of workspaces to move to + the given OUTPUT when it is activated + Example: DP1_workspaces = "1,2,3" + """ + # available configuration parameters + cache_timeout = 10 + fallback = True + fixed_width = True + force_on_start = None + format_clone = '=' + format_extend = '+' + + def __init__(self): + """ + """ + self.active_comb = None + self.active_layout = None + self.active_mode = 'extend' + self.displayed = None + self.max_width = 0 + + def _get_layout(self): + """ + Get the current outputs layout from xrandr and try to detect the + currently active layout as best as we can on start. + """ + connected = list() + active_layout = list() + disconnected = list() + layout = OrderedDict( + { + 'connected': OrderedDict(), + 'disconnected': OrderedDict() + } + ) + + current = Popen(['xrandr', '--current'], stdout=PIPE) + for line in current.stdout.readlines(): + try: + # python3 + line = line.decode() + except: + pass + try: + s = line.split(' ') + if s[1] == 'connected': + output, state = s[0], s[1] + if s[2][0] == '(': + mode, infos = None, ' '.join(s[2:]).strip('\n') + else: + mode, infos = s[2], ' '.join(s[3:]).strip('\n') + active_layout.append(output) + connected.append(output) + elif s[1] == 'disconnected': + output, state = s[0], s[1] + mode, infos = None, ' '.join(s[2:]).strip('\n') + disconnected.append(output) + else: + continue + except Exception as err: + syslog(LOG_INFO, 'xrandr error="{}"'.format(err)) + else: + layout[state][output] = { + 'infos': infos, + 'mode': mode, + 'state': state + } + + # initialize the active layout + if self.active_layout is None: + self.active_comb = tuple(active_layout) + self.active_layout = self._get_string_and_set_width( + tuple(active_layout), + self.active_mode + ) + + return layout + + def _set_available_combinations(self): + """ + Generate all connected outputs combinations and + set the max display width while iterating. + """ + available_combinations = set() + combinations_map = {} + + self.max_width = 0 + for output in range(len(self.layout['connected'])+1): + for comb in combinations(self.layout['connected'], output): + if comb: + for mode in ['clone', 'extend']: + string = self._get_string_and_set_width(comb, mode) + if len(comb) == 1: + combinations_map[string] = (comb, None) + else: + combinations_map[string] = (comb, mode) + available_combinations.add(string) + self.available_combinations = deque(available_combinations) + self.combinations_map = combinations_map + + def _get_string_and_set_width(self, combination, mode): + """ + Construct the string to be displayed and record the max width. + """ + show = '{}'.format(self._separator(mode)).join(combination) + show = show.rstrip('{}'.format(self._separator(mode))) + self.max_width = max([self.max_width, len(show)]) + return show + + def _choose_what_to_display(self, force_refresh=False): + """ + Choose what combination to display on the bar. + + By default we try to display the active layout on the first run, else + we display the last selected combination. + """ + for _ in range(len(self.available_combinations)): + if ( + self.displayed is None and + self.available_combinations[0] == self.active_layout + ): + self.displayed = self.available_combinations[0] + break + else: + if self.displayed == self.available_combinations[0]: + break + else: + self.available_combinations.rotate(1) + else: + if force_refresh: + self.displayed = self.available_combinations[0] + else: + syslog( + LOG_INFO, + 'xrandr error="displayed combination is not available"' + ) + + def _center(self, s): + """ + Center the given string on the detected max width. + """ + fmt = '{:^%d}' % self.max_width + return fmt.format(s) + + def _apply(self, force=False): + """ + Call xrandr and apply the selected (displayed) combination mode. + """ + if self.displayed == self.active_layout and not force: + # no change, do nothing + return + + combination, mode = self.combinations_map.get( + self.displayed, (None, None) + ) + if combination is None and mode is None: + # displayed combination cannot be activated, ignore + return + + cmd = 'xrandr' + outputs = list(self.layout['connected'].keys()) + outputs += list(self.layout['disconnected'].keys()) + previous_output = None + for output in outputs: + cmd += ' --output {}'.format(output) + # + if output in combination: + pos = getattr(self, '{}_pos'.format(output), '0x0') + # + if mode == 'clone' and previous_output is not None: + cmd += ' --auto --same-as {}'.format(previous_output) + else: + if 'left-of' in pos: + cmd += ' --auto --{} --rotate normal'.format(pos) + elif 'right-of' in pos: + cmd += ' --auto --{} --rotate normal'.format(pos) + else: + cmd += ' --auto --pos {} --rotate normal'.format(pos) + previous_output = output + else: + cmd += ' --off' + # + code = call(shlex.split(cmd)) + if code == 0: + self.active_comb = combination + self.active_layout = self.displayed + self.active_mode = mode + syslog(LOG_INFO, 'command "{}" exit code {}'.format(cmd, code)) + + # move workspaces to outputs as configured + self._apply_workspaces(combination, mode) + + def _apply_workspaces(self, combination, mode): + """ + Allows user to force move a comma separated list of workspaces to the + given output when it's activated. + + Example: + - DP1_workspaces = "1,2,3" + """ + if len(combination) > 1 and mode == 'extend': + sleep(3) + for output in combination: + workspaces = getattr( + self, '{}_workspaces'.format(output), '').split(',') + for workspace in workspaces: + if not workspace: + continue + # switch to workspace + cmd = 'i3-msg workspace "{}"'.format(workspace) + call(shlex.split(cmd), stdout=PIPE, stderr=PIPE) + # move it to output + cmd = 'i3-msg move workspace to output "{}"'.format(output) + call(shlex.split(cmd), stdout=PIPE, stderr=PIPE) + # log this + syslog( + LOG_INFO, + 'moved workspace {} to output {}'.format( + workspace, output) + ) + + def _refresh_py3status(self): + """ + Send a SIGUSR1 signal to py3status to force a bar refresh. + """ + call(shlex.split('killall -s USR1 py3status')) + + def _fallback_to_available_output(self): + """ + Fallback to the first available output when the active layout + was composed of only one output. + + This allows us to avoid cases where you get stuck with a black sreen + on your laptop by switching back to the integrated screen + automatically ! + """ + if len(self.active_comb) == 1: + self._choose_what_to_display(force_refresh=True) + self._apply() + self._refresh_py3status() + + def _force_force_on_start(self): + """ + Force the user configured mode on start. + """ + if self.force_on_start in self.available_combinations: + self.displayed = self.force_on_start + self.force_on_start = None + self._choose_what_to_display(force_refresh=True) + self._apply(force=True) + self._refresh_py3status() + + def _separator(self, mode): + """ + Return the separator for the given mode. + """ + if mode == 'extend': + return self.format_extend + if mode == 'clone': + return self.format_clone + + def _switch_selection(self, direction): + self.available_combinations.rotate(direction) + self.displayed = self.available_combinations[0] + + def on_click(self, i3s_output_list, i3s_config, event): + """ + Click events + - left click & scroll up/down: switch between modes + - right click: apply selected mode + - middle click: force refresh of available modes + """ + button = event['button'] + if button == 4: + self._switch_selection(-1) + if button in [1, 5]: + self._switch_selection(1) + if button == 2: + self._choose_what_to_display(force_refresh=True) + if button == 3: + self._apply() + + def xrandr(self, i3s_output_list, i3s_config): + """ + This is the main py3status method, it will orchestrate what's being + displayed on the bar. + """ + self.layout = self._get_layout() + self._set_available_combinations() + self._choose_what_to_display() + + if self.fixed_width is True: + full_text = self._center(self.displayed) + else: + full_text = self.displayed + + response = { + 'cached_until': time() + self.cache_timeout, + 'full_text': full_text + } + + # coloration + if self.displayed == self.active_layout: + response['color'] = i3s_config['color_good'] + elif self.displayed not in self.available_combinations: + response['color'] = i3s_config['color_bad'] + + # force default layout setup + if self.force_on_start is not None: + sleep(1) + self._force_force_on_start() + + # fallback detection + if self.active_layout not in self.available_combinations: + response['color'] = i3s_config['color_degraded'] + if self.fallback is True: + self._fallback_to_available_output() + + return response + +if __name__ == "__main__": + """ + Test this module by calling it directly. + """ + x = Py3status() + config = { + 'color_bad': '#FF0000', + 'color_degraded': '#FFFF00', + 'color_good': '#00FF00' + } + while True: + print(x.xrandr([], config)) + sleep(1) |
