diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 73c9943a..0ab55695 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -305,8 +305,22 @@ Themes step 2 is obviously avoided. ``segments`` - A dict with a ``left`` and a ``right`` list, consisting of segment - dicts. Each segment has the following options: + A dict with a ``left`` and a ``right`` lists, consisting of segment + dictionaries. Shell themes may also contain ``above`` list of dictionaries. + Each item in ``above`` list may have ``left`` and ``right`` keys like this + dictionary, but no ``above`` key. + + .. _config-themes-above: + + ``above`` list is used for multiline shell configurations. + + ``left`` and ``right`` lists are used for segments that should be put on the + left or right side in the output. Actual mechanizm of putting segments on + the left or the right depends on used renderer, but most renderers require + one to specify segment with :ref:`width ` ``auto`` + on either side to make generated line fill all of the available width. + + Each segment dictionary has the following options: ``type`` The segment type. Can be one of ``function`` (default), ``string`` @@ -368,6 +382,8 @@ Themes right (``r``). ``width`` + .. _config-themes-seg-width: + Enforces a specific width for this segment. This segment will work as a spacer if the width is set to ``auto``. @@ -566,6 +582,15 @@ configuration script was sourced (in fish case: after ``powerline-setup`` function was run). To disable specific feature support set one of these variables to some non-empty value. +If you do not want to disable prompt in shell, but yet do not want to launch +python twice to get :ref:`above ` lines you do not use in +tcsh you should set ``$POWERLINE_NO_TCSH_ABOVE`` or +``$POWERLINE_NO_SHELL_ABOVE`` variable. + +If you do not want to see additional space which is added to the right prompt in +fish in order to support multiline prompt you should set +``$POWERLINE_NO_FISH_ABOVE`` or ``$POWERLINE_NO_SHELL_ABOVE`` variables. + .. note:: Most supported shells’ configuration scripts check for additional diff --git a/powerline/__init__.py b/powerline/__init__.py index c82163a9..1f7b71cc 100644 --- a/powerline/__init__.py +++ b/powerline/__init__.py @@ -413,6 +413,25 @@ class Powerline(object): pass return FailedUnicode(safe_unicode(e)) + def render_above_lines(self, *args, **kwargs): + '''Like .render(), but for ``self.renderer.render_above_lines()`` + ''' + try: + self.update_renderer() + for line in self.renderer.render_above_lines(*args, **kwargs): + yield line + except Exception as e: + try: + self.exception('Failed to render: {0}', str(e)) + except Exception as e: + # Updates e variable to new value, masking previous one. + # Normally it is the same exception (due to raise in case pl is + # unset), but it may also show error in logger. Note that latter + # is not logged by logger for obvious reasons, thus this also + # prevents us from seeing logger traceback. + pass + yield FailedUnicode(safe_unicode(e)) + def shutdown(self): '''Shut down all background threads. Must be run only prior to exiting current application. diff --git a/powerline/bindings/bash/powerline.sh b/powerline/bindings/bash/powerline.sh index 9271e9bd..e6cb88f7 100644 --- a/powerline/bindings/bash/powerline.sh +++ b/powerline/bindings/bash/powerline.sh @@ -28,9 +28,15 @@ _powerline_init_tmux_support() { fi } +_run_powerline() { + # Arguments: side, last_exit_code, jobnum + $POWERLINE_COMMAND shell $1 -w $COLUMNS -r bash_prompt --last_exit_code=$2 --jobnum=$3 +} + _powerline_prompt() { local last_exit_code=$? - PS1="$($POWERLINE_COMMAND shell left -r bash_prompt --last_exit_code=$last_exit_code --jobnum="$(jobs -p|wc -l)")" + local jobnum="$(jobs -p|wc -l)" + PS1="$(_run_powerline aboveleft $last_exit_code $jobnum)" _powerline_tmux_set_pwd return $last_exit_code } diff --git a/powerline/bindings/fish/powerline-setup.fish b/powerline/bindings/fish/powerline-setup.fish index 50eb5302..6cb807fa 100644 --- a/powerline/bindings/fish/powerline-setup.fish +++ b/powerline/bindings/fish/powerline-setup.fish @@ -10,15 +10,35 @@ function powerline-setup end end function --on-variable POWERLINE_COMMAND _powerline_update - set -l addargs "--last_exit_code=\$status --last_pipe_status=\$status --jobnum=(jobs -p | wc -l)" + set -l addargs "--last_exit_code=\$status" + set -l addargs "$addargs --last_pipe_status=\$status" + set -l addargs "$addargs --jobnum=(jobs -p | wc -l)" + set -l addargs "$addargs --width=\$_POWERLINE_COLUMNS" + set -l promptside + set -l rpromptpast + set -l columnsexpr + if test -z "$POWERLINE_NO_FISH_ABOVE$POWERLINE_NO_SHELL_ABOVE" + set promptside aboveleft + set rpromptpast 'echo -n " "' + set columnsexpr '(math $COLUMNS - 1)' + else + set promptside left + set rpromptpast + set columnsexpr '$COLUMNS' + end eval " function fish_prompt - $POWERLINE_COMMAND shell left $addargs + $POWERLINE_COMMAND shell $promptside $addargs end function fish_right_prompt $POWERLINE_COMMAND shell right $addargs + $rpromptpast + end + function --on-signal WINCH _powerline_set_columns + set -g _POWERLINE_COLUMNS $columnsexpr end " + _powerline_set_columns end _powerline_update end diff --git a/powerline/bindings/tcsh/powerline.tcsh b/powerline/bindings/tcsh/powerline.tcsh index eb6acac1..91b08993 100644 --- a/powerline/bindings/tcsh/powerline.tcsh +++ b/powerline/bindings/tcsh/powerline.tcsh @@ -19,7 +19,13 @@ if ! ( $?POWERLINE_NO_TCSH_TMUX_SUPPORT || $?POWERLINE_NO_SHELL_TMUX_SUPPORT ) t alias cwdcmd "`alias cwdcmd` ; _powerline_tmux_set_pwd" endif if ! ( $?POWERLINE_NO_TCSH_PROMPT || $?POWERLINE_NO_SHELL_PROMPT ) then - alias _powerline_set_prompt 'set prompt="`$POWERLINE_COMMAND shell left -r tcsh_prompt --last_exit_code=$?`"' - alias _powerline_set_rprompt 'set rprompt="`$POWERLINE_COMMAND shell right -r tcsh_prompt --last_exit_code=$?` "' - alias precmd "`alias precmd` ; _powerline_set_prompt ; _powerline_set_rprompt" + if ( $?POWERLINE_NO_TCSH_ABOVE || $?POWERLINE_NO_SHELL_ABOVE ) then + alias _powerline_above true + else + alias _powerline_above '$POWERLINE_COMMAND shell above --last_exit_code=$POWERLINE_STATUS --width=$POWERLINE_COLUMNS' + endif + alias _powerline_set_prompt 'set prompt="`$POWERLINE_COMMAND shell left -r tcsh_prompt --last_exit_code=$POWERLINE_STATUS --width=$POWERLINE_COLUMNS`"' + alias _powerline_set_rprompt 'set rprompt="`$POWERLINE_COMMAND shell right -r tcsh_prompt --last_exit_code=$POWERLINE_STATUS --width=$POWERLINE_COLUMNS` "' + alias _powerline_set_columns 'set POWERLINE_COLUMNS=`stty size|cut -d" " -f2` ; set POWERLINE_COLUMNS=`expr $POWERLINE_COLUMNS - 2`' + alias precmd 'set POWERLINE_STATUS=$? ; '"`alias precmd`"' ; _powerline_set_columns ; _powerline_above ; _powerline_set_prompt ; _powerline_set_rprompt' endif diff --git a/powerline/bindings/zsh/__init__.py b/powerline/bindings/zsh/__init__.py index 0ffdee19..2bbf3184 100644 --- a/powerline/bindings/zsh/__init__.py +++ b/powerline/bindings/zsh/__init__.py @@ -1,4 +1,6 @@ # vim:fileencoding=utf-8:noet +from __future__ import absolute_import, unicode_literals, division, print_function + import zsh import atexit from powerline.shell import ShellPowerline @@ -89,21 +91,22 @@ class ZshPowerline(ShellPowerline): def precmd(self): self.args.last_pipe_status = zsh.pipestatus() self.args.last_exit_code = zsh.last_exit_code() - zsh.eval('_POWERLINE_PARSER_STATE="${(%):-%_}"') class Prompt(object): - __slots__ = ('powerline', 'side', 'savedpsvar', 'savedps', 'args', 'theme') + __slots__ = ('powerline', 'side', 'savedpsvar', 'savedps', 'args', 'theme', 'above') - def __init__(self, powerline, side, theme, savedpsvar=None, savedps=None): + def __init__(self, powerline, side, theme, savedpsvar=None, savedps=None, above=False): self.powerline = powerline self.side = side + self.above = above self.savedpsvar = savedpsvar self.savedps = savedps self.args = powerline.args self.theme = theme def __str__(self): + zsh.eval('_POWERLINE_PARSER_STATE="${(%):-%_}"') segment_info = { 'args': self.args, 'environ': environ, @@ -111,7 +114,14 @@ class Prompt(object): 'local_theme': self.theme, 'parser_state': zsh.getvalue('_POWERLINE_PARSER_STATE'), } - r = self.powerline.render( + r = '' + if self.above: + for line in self.powerline.render_above_lines( + width=zsh.columns() - 1, + segment_info=segment_info, + ): + r += line + '\n' + r += self.powerline.render( width=zsh.columns(), side=self.side, segment_info=segment_info, @@ -131,13 +141,13 @@ class Prompt(object): self.powerline.shutdown() -def set_prompt(powerline, psvar, side, theme): +def set_prompt(powerline, psvar, side, theme, above=False): try: savedps = zsh.getvalue(psvar) except IndexError: savedps = None zpyvar = 'ZPYTHON_POWERLINE_' + psvar - prompt = Prompt(powerline, side, theme, psvar, savedps) + prompt = Prompt(powerline, side, theme, psvar, savedps, above) zsh.set_special_string(zpyvar, prompt) zsh.setvalue(psvar, '${' + zpyvar + '}') @@ -146,7 +156,7 @@ def setup(): powerline = ZshPowerline(Args()) used_powerlines.append(powerline) used_powerlines.append(powerline) - set_prompt(powerline, 'PS1', 'left', None) + set_prompt(powerline, 'PS1', 'left', None, above=True) set_prompt(powerline, 'RPS1', 'right', None) set_prompt(powerline, 'PS2', 'left', 'continuation') set_prompt(powerline, 'RPS2', 'right', 'continuation') diff --git a/powerline/bindings/zsh/powerline.zsh b/powerline/bindings/zsh/powerline.zsh index 1f358b36..797c216f 100644 --- a/powerline/bindings/zsh/powerline.zsh +++ b/powerline/bindings/zsh/powerline.zsh @@ -111,21 +111,27 @@ _powerline_setup_prompt() { fi done precmd_functions+=( _powerline_set_jobnum ) - if zmodload libzpython &>/dev/null || zmodload zsh/zpython &>/dev/null ; then + if test -z "${POWERLINE_NO_ZSH_ZPYTHON}" && { zmodload libzpython || zmodload zsh/zpython } &>/dev/null ; then precmd_functions+=( _powerline_update_counter ) zpython 'from powerline.bindings.zsh import setup as _powerline_setup' zpython '_powerline = _powerline_setup()' zpython 'del _powerline_setup' else - local add_args='--last_exit_code=$? --last_pipe_status="$pipestatus"' + local add_args='--last_exit_code=$?' + add_args+=' --last_pipe_status="$pipestatus"' add_args+=' --renderer_arg="client_id=$$"' add_args+=' --jobnum=$_POWERLINE_JOBNUM' - local add_args_2=$add_args' -R parser_state=${(%%):-%_} -R local_theme=continuation' - PS1='$($POWERLINE_COMMAND shell left -r zsh_prompt '$add_args')' + local new_args_2=' -R parser_state=${(%%):-%_}' + new_args_2+=' -R local_theme=continuation' + local add_args_3=$add_args' -R local_theme=select' + local add_args_2=$add_args$new_args_2 + add_args+=' --width=$(( COLUMNS - 1 ))' + local add_args_r2=$add_args$new_args_2 + PS1='$($POWERLINE_COMMAND shell aboveleft -r zsh_prompt '$add_args')' RPS1='$($POWERLINE_COMMAND shell right -r zsh_prompt '$add_args')' PS2='$($POWERLINE_COMMAND shell left -r zsh_prompt '$add_args_2')' - RPS2='$($POWERLINE_COMMAND shell right -r zsh_prompt '$add_args_2')' - PS3='$($POWERLINE_COMMAND shell left -r zsh_prompt -R local_theme=select '$add_args')' + RPS2='$($POWERLINE_COMMAND shell right -r zsh_prompt '$add_args_r2')' + PS3='$($POWERLINE_COMMAND shell left -r zsh_prompt '$add_args_3')' fi } diff --git a/powerline/lint/__init__.py b/powerline/lint/__init__.py index ec4d4608..9f73b9a8 100644 --- a/powerline/lint/__init__.py +++ b/powerline/lint/__init__.py @@ -73,25 +73,31 @@ class DelayedEchoErr(EchoErr): class Spec(object): def __init__(self, **keys): - new_keys = {} - self.specs = list(keys.values()) - for k, v in keys.items(): - new_keys[k] = len(self.specs) - self.specs.append(v) - self.keys = new_keys + self.specs = [] + self.keys = {} self.checks = [] self.cmsg = '' self.isoptional = False self.uspecs = [] self.ufailmsg = lambda key: 'found unknown key: {0}'.format(key) - if keys: + self.did_type = False + self.update(**keys) + + def update(self, **keys): + for k, v in keys.items(): + self.keys[k] = len(self.specs) + self.specs.append(v) + if self.keys and not self.did_type: self.type(dict) + self.did_type = True + return self def copy(self): - return self.__class__().update(self.__dict__) + return self.__class__()._update(self.__dict__) - def update(self, d): + def _update(self, d): self.__dict__.update(d) + self.keys = copy(self.keys) self.checks = copy(self.checks) self.uspecs = copy(self.uspecs) self.specs = [spec.copy() for spec in self.specs] @@ -1005,6 +1011,13 @@ segments_spec = Spec().optional().list( lambda value: 'it is recommended that divider highlight group names end with ":divider"').optional(), ).func(check_full_segment_data), ).copy +segdict_spec=Spec( + left=segments_spec().context_message('Error while loading segments from left side (key {key})'), + right=segments_spec().context_message('Error while loading segments from right side (key {key})'), +).func( + lambda value, *args: (True, True, not (('left' in value) or ('right' in value))), + lambda value: 'segments dictionary must contain either left, right or both keys' +).context_message('Error while loading segments (key {key})').copy theme_spec = (Spec( default_module=segment_module_spec(), segment_data=Spec().unknown_spec( @@ -1016,13 +1029,7 @@ theme_spec = (Spec( contents=Spec().type(unicode).optional(), ), ).optional().context_message('Error while loading segment data (key {key})'), - segments=Spec( - left=segments_spec().context_message('Error while loading segments from left side (key {key})'), - right=segments_spec().context_message('Error while loading segments from right side (key {key})'), - ).func( - lambda value, *args: (True, True, not (('left' in value) or ('right' in value))), - lambda value: 'segments dictionary must contain either left, right or both keys' - ).context_message('Error while loading segments (key {key})'), + segments=segdict_spec().update(above=Spec().list(segdict_spec()).optional()), ).context_message('Error while loading theme')) diff --git a/powerline/renderer.py b/powerline/renderer.py index 7445ff0f..002f777c 100644 --- a/powerline/renderer.py +++ b/powerline/renderer.py @@ -175,7 +175,20 @@ class Renderer(object): r['getcwd'] = lambda: r['environ']['PWD'] return r - def render(self, mode=None, width=None, side=None, output_raw=False, segment_info=None, matcher_info=None): + def render_above_lines(self, **kwargs): + '''Render all segments in the {theme}/segments/above list + + Rendering happens in the reversed order. Parameters are the same as in + .render() method. + + :yield: rendered line. + ''' + + theme = self.get_theme(kwargs.get('matcher_info', None)) + for line in range(theme.get_line_number() - 1, 0, -1): + yield self.render(side=None, line=line, **kwargs) + + def render(self, mode=None, width=None, side=None, line=0, output_raw=False, segment_info=None, matcher_info=None): '''Render all segments. When a width is provided, low-priority segments are dropped one at @@ -193,6 +206,9 @@ class Renderer(object): :param str side: One of ``left``, ``right``. Determines which side will be rendered. If not present all sides are rendered. + :param int line: + Line number for which segments should be obtained. Is counted from + zero (botmost line). :param bool output_raw: Changes the output: if this parameter is ``True`` then in place of one string this method outputs a pair ``(colored_string, @@ -203,7 +219,7 @@ class Renderer(object): Matcher information. Is processed in ``.get_theme()`` method. ''' theme = self.get_theme(matcher_info) - segments = theme.get_segments(side, self.get_segment_info(segment_info, mode)) + segments = theme.get_segments(side, line, self.get_segment_info(segment_info, mode)) # Handle excluded/included segments for the current mode segments = [self._get_highlighting(segment, mode) for segment in segments diff --git a/powerline/shell.py b/powerline/shell.py index 4b817ae1..bffa1df8 100644 --- a/powerline/shell.py +++ b/powerline/shell.py @@ -51,7 +51,7 @@ def get_argparser(parser=None, *args, **kwargs): parser = argparse.ArgumentParser p = parser(*args, **kwargs) p.add_argument('ext', nargs=1) - p.add_argument('side', nargs='?', choices=('left', 'right')) + p.add_argument('side', nargs='?', choices=('left', 'right', 'above', 'aboveleft')) p.add_argument('-r', '--renderer_module', metavar='MODULE', type=str) p.add_argument('-w', '--width', type=int) p.add_argument('--last_exit_code', metavar='INT', type=int) diff --git a/powerline/theme.py b/powerline/theme.py index d6b185f2..c6157e8f 100644 --- a/powerline/theme.py +++ b/powerline/theme.py @@ -2,6 +2,7 @@ from powerline.segment import gen_segment_getter from powerline.lib.unicode import u +import itertools def requires_segment_info(func): @@ -9,6 +10,13 @@ def requires_segment_info(func): return func +def new_empty_segment_line(): + return { + 'left': [], + 'right': [] + } + + class Theme(object): def __init__(self, ext, @@ -20,10 +28,7 @@ class Theme(object): shutdown_event=None): self.dividers = theme_config.get('dividers', common_config['dividers']) self.spaces = theme_config.get('spaces', common_config['spaces']) - self.segments = { - 'left': [], - 'right': [], - } + self.segments = [] self.EMPTY_SEGMENT = { 'contents': None, 'highlight': {'fg': False, 'bg': False, 'attr': 0} @@ -33,25 +38,29 @@ class Theme(object): if top_theme_config: theme_configs.append(top_theme_config) get_segment = gen_segment_getter(pl, ext, common_config['paths'], theme_configs, theme_config.get('default_module')) - for side in ['left', 'right']: - for segment in theme_config['segments'].get(side, []): - segment = get_segment(segment, side) - if not run_once: - if segment['startup']: - try: - segment['startup'](pl, shutdown_event) - except Exception as e: - pl.error('Exception during {0} startup: {1}', segment['name'], str(e)) - continue - self.segments[side].append(segment) + for segdict in itertools.chain((theme_config['segments'],), + theme_config['segments'].get('above', ())): + self.segments.append(new_empty_segment_line()) + for side in ['left', 'right']: + for segment in segdict.get(side, []): + segment = get_segment(segment, side) + if not run_once: + if segment['startup']: + try: + segment['startup'](pl, shutdown_event) + except Exception as e: + pl.error('Exception during {0} startup: {1}', segment['name'], str(e)) + continue + self.segments[-1][side].append(segment) def shutdown(self): - for segments in self.segments.values(): - for segment in segments: - try: - segment['shutdown']() - except TypeError: - pass + for line in self.segments: + for segments in line.values(): + for segment in segments: + try: + segment['shutdown']() + except TypeError: + pass def get_divider(self, side='left', type='soft'): '''Return segment divider.''' @@ -60,15 +69,22 @@ class Theme(object): def get_spaces(self): return self.spaces - def get_segments(self, side=None, segment_info=None): + def get_line_number(self): + return len(self.segments) + + def get_segments(self, side=None, line=0, segment_info=None): '''Return all segments. Function segments are called, and all segments get their before/after and ljust/rjust properties applied. + + :param int line: + Line number for which segments should be obtained. Is counted from + zero (botmost line). ''' for side in [side] if side else ['left', 'right']: parsed_segments = [] - for segment in self.segments[side]: + for segment in self.segments[line][side]: if segment['type'] == 'function': self.pl.prefix = segment['name'] try: diff --git a/scripts/powerline b/scripts/powerline index 74d0fc98..8b125217 100755 --- a/scripts/powerline +++ b/scripts/powerline @@ -10,6 +10,12 @@ except ImportError: sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(os.path.realpath(__file__))))) from powerline.shell import ShellPowerline, get_argparser, finish_args # NOQA +def write(output): + try: + sys.stdout.write(output) + except UnicodeEncodeError: + sys.stdout.write(output.encode('utf-8')) + if __name__ == '__main__': args = get_argparser(description=__doc__).parse_args() finish_args(args) @@ -17,13 +23,21 @@ if __name__ == '__main__': segment_info = {'args': args, 'environ': os.environ} if args.renderer_arg: segment_info.update(args.renderer_arg) - rendered = powerline.render( - width=args.width, - side=args.side, - segment_info=segment_info, - mode=os.environ.get('_POWERLINE_MODE'), - ) - try: - sys.stdout.write(rendered) - except UnicodeEncodeError: - sys.stdout.write(rendered.encode('utf-8')) + if args.side.startswith('above'): + for line in powerline.render_above_lines( + width=args.width, + segment_info=segment_info, + mode=os.environ.get('_POWERLINE_MODE'), + ): + write(line) + sys.stdout.write('\n') + args.side = args.side[len('above'):] + + if args.side: + rendered = powerline.render( + width=args.width, + side=args.side, + segment_info=segment_info, + mode=os.environ.get('_POWERLINE_MODE'), + ) + write(rendered) diff --git a/tests/lib/config_mock.py b/tests/lib/config_mock.py index a0c083df..1a27d7cd 100644 --- a/tests/lib/config_mock.py +++ b/tests/lib/config_mock.py @@ -4,6 +4,7 @@ from powerline.renderer import Renderer from powerline.lib.config import ConfigLoader from powerline import Powerline from copy import deepcopy +from time import sleep from functools import wraps @@ -96,6 +97,15 @@ class SimpleRenderer(Renderer): return '<{fg} {bg} {attr}>'.format(fg=fg and fg[0], bg=bg and bg[0], attr=attr) +class EvenSimplerRenderer(Renderer): + def hlstyle(self, fg=None, bg=None, attr=None): + return '{{{fg}{bg}{attr}}}'.format( + fg=fg and fg[0] or '-', + bg=bg and bg[0] or '-', + attr=attr if attr else '', + ) + + class TestPowerline(Powerline): _created = False @@ -111,7 +121,12 @@ renderer = SimpleRenderer def get_powerline(**kwargs): + global renderer watcher = Watcher() + if kwargs.pop('simpler_renderer', False): + renderer = EvenSimplerRenderer + else: + renderer = SimpleRenderer pl = TestPowerline( ext='test', renderer_module='tests.lib.config_mock', @@ -138,3 +153,11 @@ def swap_attributes(cfg_container, powerline_module, replaces): setattr(powerline_module, attr, val) replaces[attr] = old_val return replaces + + +def add_watcher_events(p, *args, **kwargs): + p._watcher._reset(args) + while not p._will_create_renderer(): + sleep(kwargs.get('interval', 0.1)) + if not kwargs.get('wait', True): + return diff --git a/tests/test_config_reload.py b/tests/test_config_reload.py index 282d611f..f3ce58aa 100644 --- a/tests/test_config_reload.py +++ b/tests/test_config_reload.py @@ -1,10 +1,10 @@ # vim:fileencoding=utf-8:noet from __future__ import unicode_literals import powerline as powerline_module -import time +from time import sleep from tests import TestCase from tests.lib import replace_item -from tests.lib.config_mock import swap_attributes, get_powerline, pop_events +from tests.lib.config_mock import swap_attributes, get_powerline, pop_events, add_watcher_events from copy import deepcopy @@ -92,18 +92,6 @@ config = { } -def sleep(interval): - time.sleep(interval) - - -def add_watcher_events(p, *args, **kwargs): - p._watcher._reset(args) - while not p._will_create_renderer(): - sleep(kwargs.get('interval', 0.1)) - if not kwargs.get('wait', True): - return - - class TestConfigReload(TestCase): def assertAccessEvents(self, *args): self.assertEqual(set(pop_events()), set(args)) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 64e9d6a6..ed6996ad 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1,151 +1,136 @@ # vim:fileencoding=utf-8:noet - -'''Dynamic configuration files tests.''' - - -import tests.vim as vim_module -import sys -import os -import json -from tests.lib import Args, urllib_read, replace_attr +from __future__ import unicode_literals, absolute_import, division +import powerline as powerline_module from tests import TestCase +from tests.lib import replace_item +from tests.lib.config_mock import swap_attributes, get_powerline, pop_events +from functools import wraps +from copy import deepcopy -VBLOCK = chr(ord('V') - 0x40) -SBLOCK = chr(ord('S') - 0x40) +config = { + 'config': { + 'common': { + 'dividers': { + 'left': { + 'hard': '>>', + 'soft': '>', + }, + 'right': { + 'hard': '<<', + 'soft': '<', + }, + }, + 'spaces': 0, + 'interval': 0, + }, + 'ext': { + 'test': { + 'theme': 'default', + 'colorscheme': 'default', + }, + }, + }, + 'colors': { + 'colors': { + 'col1': 1, + 'col2': 2, + 'col3': 3, + 'col4': 4, + }, + 'gradients': { + }, + }, + 'colorschemes/test/default': { + 'groups': { + 'str1': {'fg': 'col1', 'bg': 'col2', 'attr': ['bold']}, + 'str2': {'fg': 'col3', 'bg': 'col4', 'attr': ['underline']}, + }, + }, + 'themes/test/default': { + 'segments': { + 'left': [ + { + 'type': 'string', + 'contents': 's', + 'width': 'auto', + 'highlight_group': ['str1'], + }, + { + 'type': 'string', + 'contents': 'g', + 'highlight_group': ['str2'], + }, + ], + 'right': [ + { + 'type': 'string', + 'contents': 'f', + 'width': 'auto', + 'align': 'right', + 'highlight_group': ['str2'], + }, + ], + }, + }, +} -class TestConfig(TestCase): - def test_vim(self): - from powerline.vim import VimPowerline - cfg_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'powerline', 'config_files') - buffers = ( - (('bufoptions',), {'buftype': 'help'}), - (('bufname', '[Command Line]'), {}), - (('bufoptions',), {'buftype': 'quickfix'}), - (('bufname', 'NERD_tree_1'), {}), - (('bufname', '__Gundo__'), {}), - (('bufname', '__Gundo_Preview__'), {}), - (('bufname', 'ControlP'), {}), - ) - with open(os.path.join(cfg_path, 'config.json'), 'r') as f: - local_themes_raw = json.load(f)['ext']['vim']['local_themes'] - # Don't run tests on external/plugin segments - local_themes = dict((k, v) for (k, v) in local_themes_raw.items()) - self.assertEqual(len(buffers), len(local_themes)) - outputs = {} - i = 0 - - with vim_module._with('split'): - with VimPowerline() as powerline: - def check_output(mode, args, kwargs): - if mode == 'nc': - window = vim_module.windows[0] - window_id = 2 - else: - vim_module._start_mode(mode) - window = vim_module.current.window - window_id = 1 - winnr = window.number - out = powerline.render(window, window_id, winnr) - if out in outputs: - self.fail('Duplicate in set #{0} ({1}) for mode {2!r} (previously defined in set #{3} ({4!r}) for mode {5!r})'.format(i, (args, kwargs), mode, *outputs[out])) - outputs[out] = (i, (args, kwargs), mode) - - with vim_module._with('bufname', '/tmp/foo.txt'): - with vim_module._with('globals', powerline_config_path=cfg_path): - exclude = set(('no', 'v', 'V', VBLOCK, 's', 'S', SBLOCK, 'R', 'Rv', 'c', 'cv', 'ce', 'r', 'rm', 'r?', '!')) - try: - for mode in ['n', 'nc', 'no', 'v', 'V', VBLOCK, 's', 'S', SBLOCK, 'i', 'R', 'Rv', 'c', 'cv', 'ce', 'r', 'rm', 'r?', '!']: - check_output(mode, None, None) - for args, kwargs in buffers: - i += 1 - if mode in exclude: - continue - if mode == 'nc' and args == ('bufname', 'ControlP'): - # ControlP window is not supposed to not - # be in the focus - continue - with vim_module._with(*args, **kwargs): - check_output(mode, args, kwargs) - finally: - vim_module._start_mode('n') - - def test_tmux(self): - from powerline.segments import common - from imp import reload - reload(common) - from powerline.shell import ShellPowerline - with replace_attr(common, 'urllib_read', urllib_read): - with ShellPowerline(Args(ext=['tmux']), run_once=False) as powerline: - powerline.render() - with ShellPowerline(Args(ext=['tmux']), run_once=False) as powerline: - powerline.render() - - def test_zsh(self): - from powerline.shell import ShellPowerline - args = Args(last_pipe_status=[1, 0], jobnum=0, ext=['shell'], renderer_module='zsh_prompt') - segment_info = {'args': args} - with ShellPowerline(args, run_once=False) as powerline: - powerline.render(segment_info=segment_info) - with ShellPowerline(args, run_once=False) as powerline: - powerline.render(segment_info=segment_info) - segment_info['local_theme'] = 'select' - with ShellPowerline(args, run_once=False) as powerline: - powerline.render(segment_info=segment_info) - segment_info['local_theme'] = 'continuation' - segment_info['parser_state'] = 'if cmdsubst' - with ShellPowerline(args, run_once=False) as powerline: - powerline.render(segment_info=segment_info) - - def test_bash(self): - from powerline.shell import ShellPowerline - args = Args(last_exit_code=1, jobnum=0, ext=['shell'], renderer_module='bash_prompt', config={'ext': {'shell': {'theme': 'default_leftonly'}}}) - with ShellPowerline(args, run_once=False) as powerline: - powerline.render(segment_info={'args': args}) - with ShellPowerline(args, run_once=False) as powerline: - powerline.render(segment_info={'args': args}) - - def test_ipython(self): - from powerline.ipython import IpythonPowerline - - class IpyPowerline(IpythonPowerline): - path = None - config_overrides = None - theme_overrides = {} - - with IpyPowerline() as powerline: - segment_info = Args(prompt_count=1) - for prompt_type in ['in', 'in2', 'out', 'rewrite']: - powerline.render(matcher_info=prompt_type, segment_info=segment_info) - powerline.render(matcher_info=prompt_type, segment_info=segment_info) - - def test_wm(self): - from powerline.segments import common - from imp import reload - reload(common) - from powerline import Powerline - with replace_attr(common, 'urllib_read', urllib_read): - Powerline(ext='wm', renderer_module='pango_markup', run_once=True).render() - reload(common) +def add_p_arg(func): + @wraps(func) + def f(self): + with get_powerline(run_once=True, simpler_renderer=True) as p: + func(self, p) + return f -old_cwd = None +class TestSingleLine(TestCase): + def assertRenderEqual(self, p, output, **kwargs): + self.assertEqual(p.render(**kwargs).replace(' ', ' '), output) + + def assertRenderLinesEqual(self, p, output, **kwargs): + self.assertEqual([l.replace(' ', ' ') for l in p.render_above_lines(**kwargs)], output) + + @add_p_arg + def test_without_above(self, p): + self.assertRenderEqual(p, '{121} s{24}>>{344}g{34}>{34}<{344}f {--}') + self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}<{344}f {--}', width=10) + # self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}<{344} f {--}', width=11) + self.assertEqual(list(p.render_above_lines()), []) + + def test_with_above(self): + with replace_item(globals(), 'config', deepcopy(config)): + old_segments = deepcopy(config['themes/test/default']['segments']) + config['themes/test/default']['segments']['above'] = [old_segments] + with get_powerline(run_once=True, simpler_renderer=True) as p: + self.assertRenderLinesEqual(p, [ + '{121} s{24}>>{344}g{34}>{34}<{344}f {--}', + ]) + self.assertRenderLinesEqual(p, [ + '{121} s {24}>>{344}g{34}>{34}<{344}f {--}', + ], width=10) + + config['themes/test/default']['segments']['above'] = [old_segments] * 2 + with get_powerline(run_once=True, simpler_renderer=True) as p: + self.assertRenderLinesEqual(p, [ + '{121} s{24}>>{344}g{34}>{34}<{344}f {--}', + '{121} s{24}>>{344}g{34}>{34}<{344}f {--}', + ]) + self.assertRenderLinesEqual(p, [ + '{121} s {24}>>{344}g{34}>{34}<{344}f {--}', + '{121} s {24}>>{344}g{34}>{34}<{344}f {--}', + ], width=10) + + +replaces = {} def setUpModule(): - global old_cwd - sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'path'))) - old_cwd = os.getcwd() - from powerline.segments import vim - globals()['vim'] = vim + global replaces + replaces = swap_attributes(globals(), powerline_module, replaces) -def tearDownModule(): - global old_cwd - os.chdir(old_cwd) - old_cwd = None - sys.path.pop(0) +tearDownModule = setUpModule if __name__ == '__main__': diff --git a/tests/test_provided_config_files.py b/tests/test_provided_config_files.py new file mode 100644 index 00000000..64e9d6a6 --- /dev/null +++ b/tests/test_provided_config_files.py @@ -0,0 +1,153 @@ +# vim:fileencoding=utf-8:noet + +'''Dynamic configuration files tests.''' + + +import tests.vim as vim_module +import sys +import os +import json +from tests.lib import Args, urllib_read, replace_attr +from tests import TestCase + + +VBLOCK = chr(ord('V') - 0x40) +SBLOCK = chr(ord('S') - 0x40) + + +class TestConfig(TestCase): + def test_vim(self): + from powerline.vim import VimPowerline + cfg_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'powerline', 'config_files') + buffers = ( + (('bufoptions',), {'buftype': 'help'}), + (('bufname', '[Command Line]'), {}), + (('bufoptions',), {'buftype': 'quickfix'}), + (('bufname', 'NERD_tree_1'), {}), + (('bufname', '__Gundo__'), {}), + (('bufname', '__Gundo_Preview__'), {}), + (('bufname', 'ControlP'), {}), + ) + with open(os.path.join(cfg_path, 'config.json'), 'r') as f: + local_themes_raw = json.load(f)['ext']['vim']['local_themes'] + # Don't run tests on external/plugin segments + local_themes = dict((k, v) for (k, v) in local_themes_raw.items()) + self.assertEqual(len(buffers), len(local_themes)) + outputs = {} + i = 0 + + with vim_module._with('split'): + with VimPowerline() as powerline: + def check_output(mode, args, kwargs): + if mode == 'nc': + window = vim_module.windows[0] + window_id = 2 + else: + vim_module._start_mode(mode) + window = vim_module.current.window + window_id = 1 + winnr = window.number + out = powerline.render(window, window_id, winnr) + if out in outputs: + self.fail('Duplicate in set #{0} ({1}) for mode {2!r} (previously defined in set #{3} ({4!r}) for mode {5!r})'.format(i, (args, kwargs), mode, *outputs[out])) + outputs[out] = (i, (args, kwargs), mode) + + with vim_module._with('bufname', '/tmp/foo.txt'): + with vim_module._with('globals', powerline_config_path=cfg_path): + exclude = set(('no', 'v', 'V', VBLOCK, 's', 'S', SBLOCK, 'R', 'Rv', 'c', 'cv', 'ce', 'r', 'rm', 'r?', '!')) + try: + for mode in ['n', 'nc', 'no', 'v', 'V', VBLOCK, 's', 'S', SBLOCK, 'i', 'R', 'Rv', 'c', 'cv', 'ce', 'r', 'rm', 'r?', '!']: + check_output(mode, None, None) + for args, kwargs in buffers: + i += 1 + if mode in exclude: + continue + if mode == 'nc' and args == ('bufname', 'ControlP'): + # ControlP window is not supposed to not + # be in the focus + continue + with vim_module._with(*args, **kwargs): + check_output(mode, args, kwargs) + finally: + vim_module._start_mode('n') + + def test_tmux(self): + from powerline.segments import common + from imp import reload + reload(common) + from powerline.shell import ShellPowerline + with replace_attr(common, 'urllib_read', urllib_read): + with ShellPowerline(Args(ext=['tmux']), run_once=False) as powerline: + powerline.render() + with ShellPowerline(Args(ext=['tmux']), run_once=False) as powerline: + powerline.render() + + def test_zsh(self): + from powerline.shell import ShellPowerline + args = Args(last_pipe_status=[1, 0], jobnum=0, ext=['shell'], renderer_module='zsh_prompt') + segment_info = {'args': args} + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info=segment_info) + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info=segment_info) + segment_info['local_theme'] = 'select' + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info=segment_info) + segment_info['local_theme'] = 'continuation' + segment_info['parser_state'] = 'if cmdsubst' + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info=segment_info) + + def test_bash(self): + from powerline.shell import ShellPowerline + args = Args(last_exit_code=1, jobnum=0, ext=['shell'], renderer_module='bash_prompt', config={'ext': {'shell': {'theme': 'default_leftonly'}}}) + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info={'args': args}) + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info={'args': args}) + + def test_ipython(self): + from powerline.ipython import IpythonPowerline + + class IpyPowerline(IpythonPowerline): + path = None + config_overrides = None + theme_overrides = {} + + with IpyPowerline() as powerline: + segment_info = Args(prompt_count=1) + for prompt_type in ['in', 'in2', 'out', 'rewrite']: + powerline.render(matcher_info=prompt_type, segment_info=segment_info) + powerline.render(matcher_info=prompt_type, segment_info=segment_info) + + def test_wm(self): + from powerline.segments import common + from imp import reload + reload(common) + from powerline import Powerline + with replace_attr(common, 'urllib_read', urllib_read): + Powerline(ext='wm', renderer_module='pango_markup', run_once=True).render() + reload(common) + + +old_cwd = None + + +def setUpModule(): + global old_cwd + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'path'))) + old_cwd = os.getcwd() + from powerline.segments import vim + globals()['vim'] = vim + + +def tearDownModule(): + global old_cwd + os.chdir(old_cwd) + old_cwd = None + sys.path.pop(0) + + +if __name__ == '__main__': + from tests import main + main()