diff --git a/docs/source/configuration/listers.rst b/docs/source/configuration/listers.rst index 04e5371e..e6ba06e7 100644 --- a/docs/source/configuration/listers.rst +++ b/docs/source/configuration/listers.rst @@ -16,8 +16,6 @@ their type and ``segments`` key with a list of segments (a bit more details in More information in :ref:`Writing listers ` section. -Currently only Vim listers are available. - Vim listers ----------- @@ -29,3 +27,9 @@ Pdb listers .. automodule:: powerline.listers.pdb :members: + +i3wm listers +---------- + +.. automodule:: powerline.listers.i3wm + :members: diff --git a/docs/source/develop/segments.rst b/docs/source/develop/segments.rst index d96bb1cb..30f4a664 100644 --- a/docs/source/develop/segments.rst +++ b/docs/source/develop/segments.rst @@ -517,6 +517,22 @@ Pdb Equal to the length of :py:attr:`pdb.Pdb.stack` at the first invocation of the prompt decremented by one. +i3wm +---- + +``mode`` + Currently active i3 mode (as a string). + +``output`` + ``xrandr`` output name currently drawing to. Currently only available + in lemonbar bindings. + +``workspace`` + dictionary containing the workspace name under the key ``"name"`` and + boolean values for the ``"visible"``, ``"urgent"`` and ``"focused"`` + keys, indicating the state of the workspace. Currently only provided by + the :py:func:`powerline.listers.i3wm.workspace_lister` lister. + Segment class ============= diff --git a/powerline/bindings/lemonbar/powerline-lemonbar.py b/powerline/bindings/lemonbar/powerline-lemonbar.py index ae4ffb34..76372541 100755 --- a/powerline/bindings/lemonbar/powerline-lemonbar.py +++ b/powerline/bindings/lemonbar/powerline-lemonbar.py @@ -9,8 +9,8 @@ import subprocess from threading import Lock, Timer from powerline.lemonbar import LemonbarPowerline -from powerline.lib.shell import run_cmd from powerline.commands.lemonbar import get_argparser +from powerline.bindings.wm import get_connected_xrandr_outputs if __name__ == '__main__': @@ -20,13 +20,8 @@ if __name__ == '__main__': powerline = LemonbarPowerline() powerline.update_renderer() bars = [] - active_screens = [match.groupdict() for match in re.finditer( - '^(?P[0-9A-Za-z-]+) connected (?P\d+)x(?P\d+)\+(?P\d+)\+(?P\d+)', - run_cmd(powerline.pl, ['xrandr', '-q']), - re.MULTILINE - )] - for screen in active_screens: + for screen in get_connected_xrandr_outputs(powerline.pl): command = [args.bar_command, '-g', '{0}x{1}+{2}'.format(screen['width'], args.height, screen['x'])] + args.args[1:] process = subprocess.Popen(command, stdin=subprocess.PIPE) bars.append((screen['name'], process, int(screen['width']) / 5)) diff --git a/powerline/bindings/wm/__init__.py b/powerline/bindings/wm/__init__.py new file mode 100644 index 00000000..6f8be621 --- /dev/null +++ b/powerline/bindings/wm/__init__.py @@ -0,0 +1,38 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import re + +from powerline.theme import requires_segment_info +from powerline.lib.shell import run_cmd + + +conn = None + + +def get_i3_connection(): + '''Return a valid, cached i3 Connection instance + ''' + global conn + if not conn: + try: + import i3ipc + except ImportError: + import i3 as conn + else: + conn = i3ipc.Connection() + return conn + + +XRANDR_OUTPUT_RE = re.compile(r'^(?P[0-9A-Za-z-]+) connected (?P\d+)x(?P\d+)\+(?P\d+)\+(?P\d+)', re.MULTILINE) + + +def get_connected_xrandr_outputs(pl): + '''Iterate over xrandr outputs + + Outputs are represented by a dictionary with ``name``, ``width``, + ``height``, ``x`` and ``y`` keys. + ''' + return (match.groupdict() for match in XRANDR_OUTPUT_RE.finditer( + run_cmd(pl, ['xrandr', '-q']) + )) diff --git a/powerline/lib/dict.py b/powerline/lib/dict.py index d8d2088b..c06ab30c 100644 --- a/powerline/lib/dict.py +++ b/powerline/lib/dict.py @@ -78,3 +78,11 @@ def mergedicts_copy(d1, d2): else: ret[k] = d2[k] return ret + + +def updated(d, *args, **kwargs): + '''Copy dictionary and update it with provided arguments + ''' + d = d.copy() + d.update(*args, **kwargs) + return d diff --git a/powerline/lint/imp.py b/powerline/lint/imp.py index 6e402132..399654e1 100644 --- a/powerline/lint/imp.py +++ b/powerline/lint/imp.py @@ -21,6 +21,12 @@ class WithPath(object): def import_function(function_type, name, data, context, echoerr, module): havemarks(name, module) + if module == 'powerline.segments.i3wm' and name == 'workspaces': + echoerr(context='Warning while checking segments (key {key})'.format(key=context.key), + context_mark=name.mark, + problem='segment {0} from {1} is deprecated'.format(name, module), + problem_mark=module.mark) + with WithPath(data['import_paths']): try: func = getattr(__import__(str(module), fromlist=[str(name)]), str(name)) diff --git a/powerline/listers/i3wm.py b/powerline/listers/i3wm.py new file mode 100644 index 00000000..188e888b --- /dev/null +++ b/powerline/listers/i3wm.py @@ -0,0 +1,68 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.theme import requires_segment_info +from powerline.lib.dict import updated +from powerline.bindings.wm import get_i3_connection, get_connected_xrandr_outputs + + +@requires_segment_info +def output_lister(pl, segment_info): + '''List all outputs in segment_info format + ''' + + return ( + ( + updated(segment_info, output=output['name']), + { + 'draw_inner_divider': None + } + ) + for output in get_connected_xrandr_outputs(pl) + ) + + +@requires_segment_info +def workspace_lister(pl, segment_info, only_show=None, output=None): + '''List all workspaces in segment_info format + + Sets the segment info values of ``workspace`` and ``output`` to the name of + the i3 workspace and the ``xrandr`` output respectively and the keys + ``"visible"``, ``"urgent"`` and ``"focused`` to a boolean indicating these + states. + + :param list only_show: + Specifies which workspaces to list. Valid entries are ``"visible"``, + ``"urgent"`` and ``"focused"``. If omitted or ``null`` all workspaces + are listed. + + :param str output: + May be set to the name of an X output. If specified, only workspaces + on that output are listed. Overrides automatic output detection by + the lemonbar renderer and bindings. Set to ``false`` to force + all workspaces to be shown. + ''' + + if output == None: + output = output or segment_info.get('output') + + return ( + ( + updated( + segment_info, + output=w['output'], + workspace={ + 'name': w['name'], + 'visible': w['visible'], + 'urgent': w['urgent'], + 'focused': w['focused'], + }, + ), + { + 'draw_inner_divider': None + } + ) + for w in get_i3_connection().get_workspaces() + if (((not only_show or any(w[typ] for typ in only_show)) + and (not output or w['output'] == output))) + ) diff --git a/powerline/segments/i3wm.py b/powerline/segments/i3wm.py index 9d214cfe..eb0d3c44 100644 --- a/powerline/segments/i3wm.py +++ b/powerline/segments/i3wm.py @@ -1,10 +1,13 @@ # vim:fileencoding=utf-8:noet from __future__ import (unicode_literals, division, absolute_import, print_function) +import re + from powerline.theme import requires_segment_info +from powerline.bindings.wm import get_i3_connection -conn = None +WORKSPACE_REGEX = re.compile(r'^[0-9]+: ?') def calcgrp(w): @@ -19,6 +22,12 @@ def calcgrp(w): return group +def format_name(name, strip=False): + if strip: + return WORKSPACE_REGEX.sub('', name, count=1) + return name + + @requires_segment_info def workspaces(pl, segment_info, only_show=None, output=None, strip=0): '''Return list of used workspaces @@ -39,26 +48,59 @@ def workspaces(pl, segment_info, only_show=None, output=None, strip=0): Highlight groups used: ``workspace`` or ``w_visible``, ``workspace`` or ``w_focused``, ``workspace`` or ``w_urgent``. ''' - global conn - if not conn: - try: - import i3ipc - except ImportError: - import i3 as conn - else: - conn = i3ipc.Connection() - output = output or segment_info.get('output') - return [{ - 'contents': w['name'][min(len(w['name']), strip):], - 'highlight_groups': calcgrp(w) - } for w in conn.get_workspaces() - if (not only_show or any(w[typ] for typ in only_show)) - and (not output or w['output'] == output) + return [ + { + 'contents': w['name'][strip:], + 'highlight_groups': calcgrp(w) + } + for w in get_i3_connection().get_workspaces() + if ((not only_show or any(w[typ] for typ in only_show)) + and (not output or w['output'] == output)) ] +@requires_segment_info +def workspace(pl, segment_info, workspace=None, strip=False): + '''Return the specified workspace name + + :param str workspace: + Specifies which workspace to show. If unspecified, may be set by the + ``list_workspaces`` lister if used, otherwise falls back to + currently focused workspace. + + :param bool strip: + Specifies whether workspace numbers (in the ``1: name`` format) should + be stripped from workspace names before being displayed. Defaults to false. + + Highlight groups used: ``workspace`` or ``w_visible``, ``workspace`` or ``w_focused``, ``workspace`` or ``w_urgent``. + ''' + if workspace: + try: + w = next(( + w for w in get_i3_connection().get_workspaces() + if w['name'] == workspace + )) + except StopIteration: + return None + elif segment_info.get('workspace'): + w = segment_info['workspace'] + else: + try: + w = next(( + w for w in get_i3_connection().get_workspaces() + if w['focused'] + )) + except StopIteration: + return None + + return [{ + 'contents': format_name(w['name'], strip=strip), + 'highlight_groups': calcgrp(w) + }] + + @requires_segment_info def mode(pl, segment_info, names={'default': None}): '''Returns current i3 mode diff --git a/tests/test_listers.py b/tests/test_listers.py new file mode 100644 index 00000000..3d3ed094 --- /dev/null +++ b/tests/test_listers.py @@ -0,0 +1,227 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import powerline.listers.i3wm as i3wm + +from tests.lib import Args, replace_attr, Pl +from tests import TestCase + + +class TestI3WM(TestCase): + @staticmethod + def get_workspaces(): + return iter([ + {'name': '1: w1', 'output': 'LVDS1', 'focused': False, 'urgent': False, 'visible': False}, + {'name': '2: w2', 'output': 'LVDS1', 'focused': False, 'urgent': False, 'visible': True}, + {'name': '3: w3', 'output': 'HDMI1', 'focused': False, 'urgent': True, 'visible': True}, + {'name': '4: w4', 'output': 'DVI01', 'focused': True, 'urgent': True, 'visible': True}, + ]) + + @staticmethod + def get_outputs(pl): + return iter([ + {'name': 'LVDS1'}, + {'name': 'HDMI1'}, + {'name': 'DVI01'}, + ]) + + def test_output_lister(self): + pl = Pl() + with replace_attr(i3wm, 'get_connected_xrandr_outputs', self.get_outputs): + self.assertEqual( + list(i3wm.output_lister(pl=pl, segment_info={'a': 1})), + [ + ({'a': 1, 'output': 'LVDS1'}, {'draw_inner_divider': None}), + ({'a': 1, 'output': 'HDMI1'}, {'draw_inner_divider': None}), + ({'a': 1, 'output': 'DVI01'}, {'draw_inner_divider': None}), + ] + ) + + def test_workspace_lister(self): + pl = Pl() + with replace_attr(i3wm, 'get_i3_connection', lambda: Args(get_workspaces=self.get_workspaces)): + self.assertEqual( + list(i3wm.workspace_lister(pl=pl, segment_info={'a': 1})), + [ + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '1: w1', + 'focused': False, + 'urgent': False, + 'visible': False + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '2: w2', + 'focused': False, + 'urgent': False, + 'visible': True + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'HDMI1', + 'workspace': { + 'name': '3: w3', + 'focused': False, + 'urgent': True, + 'visible': True + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'DVI01', + 'workspace': { + 'name': '4: w4', + 'focused': True, + 'urgent': True, + 'visible': True + } + }, {'draw_inner_divider': None}), + ] + ) + + self.assertEqual( + list(i3wm.workspace_lister(pl=pl, segment_info={'a': 1}, output='LVDS1')), + [ + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '1: w1', + 'focused': False, + 'urgent': False, + 'visible': False + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '2: w2', + 'focused': False, + 'urgent': False, + 'visible': True + } + }, {'draw_inner_divider': None}), + ] + ) + + self.assertEqual( + list(i3wm.workspace_lister( + pl=pl, + segment_info={'a': 1, 'output': 'LVDS1'} + )), + [ + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '1: w1', + 'focused': False, + 'urgent': False, + 'visible': False + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '2: w2', + 'focused': False, + 'urgent': False, + 'visible': True + } + }, {'draw_inner_divider': None}), + ] + ) + + self.assertEqual( + list(i3wm.workspace_lister( + pl=pl, + segment_info={'a': 1, 'output': 'LVDS1'}, + output=False + )), + [ + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '1: w1', + 'focused': False, + 'urgent': False, + 'visible': False + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '2: w2', + 'focused': False, + 'urgent': False, + 'visible': True + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'HDMI1', + 'workspace': { + 'name': '3: w3', + 'focused': False, + 'urgent': True, + 'visible': True + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'DVI01', + 'workspace': { + 'name': '4: w4', + 'focused': True, + 'urgent': True, + 'visible': True + } + }, {'draw_inner_divider': None}), + ] + ) + + self.assertEqual( + list(i3wm.workspace_lister( + pl=pl, + segment_info={'a': 1}, + only_show=['focused', 'urgent'] + )), + [ + ({ + 'a': 1, + 'output': 'HDMI1', + 'workspace': { + 'name': '3: w3', + 'focused': False, + 'urgent': True, + 'visible': True + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'DVI01', + 'workspace': { + 'name': '4: w4', + 'focused': True, + 'urgent': True, + 'visible': True + } + }, {'draw_inner_divider': None}), + ] + ) + + +if __name__ == '__main__': + from tests import main + main() diff --git a/tests/test_segments.py b/tests/test_segments.py index edb69a59..55fa8137 100644 --- a/tests/test_segments.py +++ b/tests/test_segments.py @@ -883,14 +883,26 @@ class TestWthr(TestCommon): class TestI3WM(TestCase): - def test_workspaces(self): - pl = Pl() - with replace_attr(i3wm, 'conn', Args(get_workspaces=lambda: iter([ + @staticmethod + def get_workspaces(): + return iter([ {'name': '1: w1', 'output': 'LVDS1', 'focused': False, 'urgent': False, 'visible': False}, {'name': '2: w2', 'output': 'LVDS1', 'focused': False, 'urgent': False, 'visible': True}, {'name': '3: w3', 'output': 'HDMI1', 'focused': False, 'urgent': True, 'visible': True}, {'name': '4: w4', 'output': 'DVI01', 'focused': True, 'urgent': True, 'visible': True}, - ]))): + ]) + + @staticmethod + def get_outputs(pl): + return iter([ + {'name': 'LVDS1'}, + {'name': 'HDMI1'}, + {'name': 'DVI01'}, + ]) + + def test_workspaces(self): + pl = Pl() + with replace_attr(i3wm, 'get_i3_connection', lambda: Args(get_workspaces=self.get_workspaces)): segment_info = {} self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info), [ @@ -936,6 +948,29 @@ class TestI3WM(TestCase): {'contents': 'w2', 'highlight_groups': ['w_visible', 'workspace']}, ]) + def test_workspace(self): + pl = Pl() + with replace_attr(i3wm, 'get_i3_connection', lambda: Args(get_workspaces=self.get_workspaces)): + segment_info = {} + + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, workspace='1: w1'), [ + {'contents': '1: w1', 'highlight_groups': ['workspace']}, + ]) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, workspace='3: w3', strip=True), [ + {'contents': 'w3', 'highlight_groups': ['w_urgent', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, workspace='9: w9'), None) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info), [ + {'contents': '4: w4', 'highlight_groups': ['w_focused', 'w_urgent', 'w_visible', 'workspace']}, + ]) + segment_info['workspace'] = next(self.get_workspaces()) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, workspace='4: w4'), [ + {'contents': '4: w4', 'highlight_groups': ['w_focused', 'w_urgent', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, strip=True), [ + {'contents': 'w1', 'highlight_groups': ['workspace']}, + ]) + def test_mode(self): pl = Pl() self.assertEqual(i3wm.mode(pl=pl, segment_info={'mode': 'default'}), None)