diff --git a/powerline/bindings/ipython/post_0_11.py b/powerline/bindings/ipython/post_0_11.py index 3d8aea0d..7d2174ae 100644 --- a/powerline/bindings/ipython/post_0_11.py +++ b/powerline/bindings/ipython/post_0_11.py @@ -3,10 +3,20 @@ from __future__ import (unicode_literals, division, absolute_import, print_funct from weakref import ref -from IPython.core.prompts import PromptManager +try: + from IPython.core.prompts import PromptManager + has_prompt_manager = True +except ImportError: + from IPython.terminal.interactiveshell import TerminalInteractiveShell + has_prompt_manager = False from IPython.core.magic import Magics, magics_class, line_magic -from powerline.ipython import IPythonPowerline, RewriteResult +from powerline.ipython import IPythonPowerline, IPythonInfo + +if has_prompt_manager: + from powerline.ipython import RewriteResult +else: + from powerline.renderers.ipython.since_5 import PowerlinePromptStyle, PowerlinePrompts @magics_class @@ -23,37 +33,29 @@ class PowerlineMagics(Magics): raise ValueError('Expected `reload`, but got {0}'.format(line)) -class IPythonInfo(object): - def __init__(self, shell): - self._shell = shell +if has_prompt_manager: + class PowerlinePromptManager(PromptManager): + def __init__(self, powerline, shell): + self.powerline = powerline + self.powerline_segment_info = IPythonInfo(shell) + self.shell = shell - @property - def prompt_count(self): - return self._shell.execution_count - - -class PowerlinePromptManager(PromptManager): - def __init__(self, powerline, shell): - self.powerline = powerline - self.powerline_segment_info = IPythonInfo(shell) - self.shell = shell - - def render(self, name, color=True, *args, **kwargs): - res = self.powerline.render( - is_prompt=name.startswith('in'), - side='left', - output_width=True, - output_raw=not color, - matcher_info=name, - segment_info=self.powerline_segment_info, - ) - self.txtwidth = res[-1] - self.width = res[-1] - ret = res[0] if color else res[1] - if name == 'rewrite': - return RewriteResult(ret) - else: - return ret + def render(self, name, color=True, *args, **kwargs): + res = self.powerline.render( + is_prompt=name.startswith('in'), + side='left', + output_width=True, + output_raw=not color, + matcher_info=name, + segment_info=self.powerline_segment_info, + ) + self.txtwidth = res[-1] + self.width = res[-1] + ret = res[0] if color else res[1] + if name == 'rewrite': + return RewriteResult(ret) + else: + return ret class ShutdownHook(object): @@ -67,33 +69,59 @@ class ShutdownHook(object): raise TryNext() +old_prompt_manager = None +old_style = None +old_prompts = None + + class ConfigurableIPythonPowerline(IPythonPowerline): def init(self, ip): config = ip.config.Powerline self.config_overrides = config.get('config_overrides') self.theme_overrides = config.get('theme_overrides', {}) self.config_paths = config.get('config_paths') - super(ConfigurableIPythonPowerline, self).init() + if has_prompt_manager: + renderer_module = '.pre_5' + else: + renderer_module = '.since_5' + super(ConfigurableIPythonPowerline, self).init( + renderer_module=renderer_module) + def do_setup(self, ip, shutdown_hook): - prompt_manager = PowerlinePromptManager( - powerline=self, - shell=ip.prompt_manager.shell, - ) + global old_prompt_manager + global old_style + global old_prompts + + if has_prompt_manager: + if old_prompt_manager is None: + old_prompt_manager = ip.prompt_manager + prompt_manager = PowerlinePromptManager( + powerline=self, + shell=ip.prompt_manager.shell, + ) + ip.prompt_manager = prompt_manager + else: + if ip.pt_cli is not None: + if old_style is None: + old_style = ip.pt_cli.application.style + prev_style = ip.pt_cli.application.style + while isinstance(prev_style, PowerlinePromptStyle): + prev_style = prev_style.get_style() + new_style = PowerlinePromptStyle(lambda: prev_style) + ip.pt_cli.application.style = new_style + ip.pt_cli.renderer.style = new_style + + if old_prompts is None: + old_prompts = ip.prompts + ip.prompts = PowerlinePrompts(ip.prompts, self) + magics = PowerlineMagics(ip, self) shutdown_hook.powerline = ref(self) - - ip.prompt_manager = prompt_manager ip.register_magics(magics) -old_prompt_manager = None - - def load_ipython_extension(ip): - global old_prompt_manager - old_prompt_manager = ip.prompt_manager - powerline = ConfigurableIPythonPowerline(ip) shutdown_hook = ShutdownHook() @@ -103,4 +131,11 @@ def load_ipython_extension(ip): def unload_ipython_extension(ip): - ip.prompt_manager = old_prompt_manager + if old_prompt_manager is not None: + ip.prompt_manager = old_prompt_manager + if old_style is not None: + ip.pt_cli.application.style = old_style + ip.pt_cli.renderer.style = old_style + ip.prompts = old_prompts + old_prompt_manager = None + old_style = None diff --git a/powerline/bindings/ipython/pre_0_11.py b/powerline/bindings/ipython/pre_0_11.py index fa34cd86..2bd80959 100644 --- a/powerline/bindings/ipython/pre_0_11.py +++ b/powerline/bindings/ipython/pre_0_11.py @@ -99,7 +99,7 @@ class ConfigurableIPythonPowerline(IPythonPowerline): self.config_overrides = config_overrides self.theme_overrides = theme_overrides self.config_paths = config_paths - super(ConfigurableIPythonPowerline, self).init() + super(ConfigurableIPythonPowerline, self).init(renderer_module='.pre_5') def ipython_magic(self, ip, parameter_s=''): if parameter_s == 'reload': diff --git a/powerline/ipython.py b/powerline/ipython.py index 6e22c41b..cb84fc7e 100644 --- a/powerline/ipython.py +++ b/powerline/ipython.py @@ -6,6 +6,15 @@ from powerline.lib.dict import mergedicts from powerline.lib.unicode import string +class IPythonInfo(object): + def __init__(self, shell): + self._shell = shell + + @property + def prompt_count(self): + return self._shell.execution_count + + # HACK: ipython tries to only leave us with plain ASCII class RewriteResult(object): def __init__(self, prompt): diff --git a/powerline/renderer.py b/powerline/renderer.py index 76a7e796..147751a8 100644 --- a/powerline/renderer.py +++ b/powerline/renderer.py @@ -4,6 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_funct import sys import os import re +import operator from itertools import chain @@ -310,6 +311,19 @@ class Renderer(object): }, } + hl_join = staticmethod(''.join) + '''Join a list of rendered segments into a resulting string + + This method exists to deal with non-string render outputs, so `segments` + may actually be not an iterable with strings. + + :param list segments: + Iterable containing rendered segments. By “rendered segments” + :py:meth:`Renderer.hl` output is meant. + + :return: Results of joining these segments. + ''' + def do_render(self, mode, width, side, line, output_raw, output_width, segment_info, theme): '''Like Renderer.render(), but accept theme in place of matcher_info ''' @@ -323,7 +337,7 @@ class Renderer(object): # No width specified, so we don’t need to crop or pad anything if output_width: current_width = self._render_length(theme, segments, self.compute_divider_widths(theme)) - return construct_returned_value(''.join([ + return construct_returned_value(self.hl_join([ segment['_rendered_hl'] for segment in self._render_segments(theme, segments) ]) + self.hlstyle(), segments, current_width, output_raw, output_width) @@ -378,7 +392,10 @@ class Renderer(object): elif output_width: current_width = self._render_length(theme, segments, divider_widths) - rendered_highlighted = ''.join([segment['_rendered_hl'] for segment in self._render_segments(theme, segments)]) + rendered_highlighted = self.hl_join([ + segment['_rendered_hl'] + for segment in self._render_segments(theme, segments) + ]) if rendered_highlighted: rendered_highlighted += self.hlstyle() diff --git a/powerline/renderers/ipython/__init__.py b/powerline/renderers/ipython/__init__.py index a84418f1..8f463b52 100644 --- a/powerline/renderers/ipython/__init__.py +++ b/powerline/renderers/ipython/__init__.py @@ -1,12 +1,11 @@ # vim:fileencoding=utf-8:noet from __future__ import (unicode_literals, division, absolute_import, print_function) -from powerline.renderers.shell import ShellRenderer -from powerline.renderers.shell.readline import ReadlineRenderer from powerline.theme import Theme +from powerline.renderers.shell import PromptRenderer -class IPythonRenderer(ShellRenderer): +class IPythonRenderer(PromptRenderer): '''Powerline ipython segment renderer.''' def get_segment_info(self, segment_info, mode): r = self.segment_info.copy() @@ -33,50 +32,3 @@ class IPythonRenderer(ShellRenderer): for match in self.local_themes.values(): if 'theme' in match: match['theme'].shutdown() - - def render(self, **kwargs): - # XXX super(ShellRenderer), *not* super(IPythonRenderer) - return super(ShellRenderer, self).render(**kwargs) - - def do_render(self, segment_info, **kwargs): - segment_info.update(client_id='ipython') - return super(IPythonRenderer, self).do_render( - segment_info=segment_info, - **kwargs - ) - - -class IPythonPromptRenderer(IPythonRenderer, ReadlineRenderer): - '''Powerline ipython prompt (in and in2) renderer''' - pass - - -class IPythonNonPromptRenderer(IPythonRenderer): - '''Powerline ipython non-prompt (out and rewrite) renderer''' - pass - - -class RendererProxy(object): - '''Powerline IPython renderer proxy which chooses appropriate renderer - - Instantiates two renderer objects: one will be used for prompts and the - other for non-prompts. - ''' - def __init__(self, **kwargs): - old_widths = {} - self.non_prompt_renderer = IPythonNonPromptRenderer(old_widths=old_widths, **kwargs) - self.prompt_renderer = IPythonPromptRenderer(old_widths=old_widths, **kwargs) - - def render_above_lines(self, *args, **kwargs): - return self.non_prompt_renderer.render_above_lines(*args, **kwargs) - - def render(self, is_prompt, *args, **kwargs): - return (self.prompt_renderer if is_prompt else self.non_prompt_renderer).render( - *args, **kwargs) - - def shutdown(self, *args, **kwargs): - self.prompt_renderer.shutdown(*args, **kwargs) - self.non_prompt_renderer.shutdown(*args, **kwargs) - - -renderer = RendererProxy diff --git a/powerline/renderers/ipython/pre_5.py b/powerline/renderers/ipython/pre_5.py new file mode 100644 index 00000000..9fc8c211 --- /dev/null +++ b/powerline/renderers/ipython/pre_5.py @@ -0,0 +1,56 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.renderers.shell import ShellRenderer +from powerline.renderers.shell.readline import ReadlineRenderer +from powerline.renderers.ipython import IPythonRenderer + + +class IPythonPre50Renderer(IPythonRenderer, ShellRenderer): + '''Powerline ipython segment renderer for pre-5.0 IPython versions.''' + def render(self, **kwargs): + # XXX super(ShellRenderer), *not* super(IPythonPre50Renderer) + return super(ShellRenderer, self).render(**kwargs) + + def do_render(self, segment_info, **kwargs): + segment_info.update(client_id='ipython') + return super(IPythonPre50Renderer, self).do_render( + segment_info=segment_info, + **kwargs + ) + + +class IPythonPromptRenderer(IPythonPre50Renderer, ReadlineRenderer): + '''Powerline ipython prompt (in and in2) renderer''' + pass + + +class IPythonNonPromptRenderer(IPythonPre50Renderer): + '''Powerline ipython non-prompt (out and rewrite) renderer''' + pass + + +class RendererProxy(object): + '''Powerline IPython renderer proxy which chooses appropriate renderer + + Instantiates two renderer objects: one will be used for prompts and the + other for non-prompts. + ''' + def __init__(self, **kwargs): + old_widths = {} + self.non_prompt_renderer = IPythonNonPromptRenderer(old_widths=old_widths, **kwargs) + self.prompt_renderer = IPythonPromptRenderer(old_widths=old_widths, **kwargs) + + def render_above_lines(self, *args, **kwargs): + return self.non_prompt_renderer.render_above_lines(*args, **kwargs) + + def render(self, is_prompt, *args, **kwargs): + return (self.prompt_renderer if is_prompt else self.non_prompt_renderer).render( + *args, **kwargs) + + def shutdown(self, *args, **kwargs): + self.prompt_renderer.shutdown(*args, **kwargs) + self.non_prompt_renderer.shutdown(*args, **kwargs) + + +renderer = RendererProxy diff --git a/powerline/renderers/ipython/since_5.py b/powerline/renderers/ipython/since_5.py new file mode 100644 index 00000000..17e05453 --- /dev/null +++ b/powerline/renderers/ipython/since_5.py @@ -0,0 +1,152 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import operator + +from collections import defaultdict + +try: + from __builtin__ import reduce +except ImportError: + from functools import reduce + +from pygments.token import Token +from prompt_toolkit.styles import DynamicStyle, Attrs +from IPython.terminal.prompts import Prompts + +from powerline.renderers.ipython import IPythonRenderer +from powerline.ipython import IPythonInfo + + +PowerlinePromptToken = Token.Generic.Prompt.Powerline + + +# Note: since 2.7 there is dict.__missing__ with same purpose. But in 2.6 one +# must use defaultdict to get __missing__ working. +class PowerlineStyleDict(defaultdict): + '''Dictionary used for getting pygments style for Powerline groups + ''' + def __new__(cls, missing_func): + return defaultdict.__new__(cls) + + def __init__(self, missing_func): + super(IPythonPygmentsStyle, self).__init__() + self.missing_func = missing_func + + def __missing__(self, key): + return self.missing_func(key) + + +class PowerlinePrompts(Prompts): + '''Class that returns powerline prompts + ''' + def __init__(self, old_prompts, powerline): + self.old_prompts = old_prompts + self.shell = old_prompts.shell + self.powerline = powerline + self.last_output_count = None + self.last_output = {} + + for prompt in ('in', 'continuation', 'rewrite', 'out'): + exec(( + 'def {0}_prompt_tokens(self, *args, **kwargs):\n' + ' if self.last_output_count != self.shell.execution_count:\n' + ' self.last_output.clear()\n' + ' self.last_output_count = self.shell.execution_count\n' + ' if "{0}" not in self.last_output:\n' + ' self.last_output["{0}"] = self.powerline.render(' + ' side="left",' + ' matcher_info="{1}",' + ' segment_info=IPythonInfo(self.shell),' + ' ) + [(Token.Generic.Prompt, " ")]\n' + ' return self.last_output["{0}"]' + ).format(prompt, 'in2' if prompt == 'continuation' else prompt)) + + +class PowerlinePromptStyle(DynamicStyle): + def get_attrs_for_token(self, token): + if ( + token not in PowerlinePromptToken + or len(token) != len(PowerlinePromptToken) + 1 + or not token[-1].startswith('Pl') + or token[-1] == 'Pl' + ): + return super(PowerlinePromptStyle, self).get_attrs_for_token(token) + ret = { + 'color': None, + 'bgcolor': None, + 'bold': None, + 'underline': None, + 'italic': None, + 'reverse': False, + 'blink': False, + } + for prop in token[-1][3:].split('_'): + if prop[0] == 'a': + ret[prop[1:]] = True + elif prop[0] == 'f': + ret['color'] = prop[1:] + elif prop[0] == 'b': + ret['bgcolor'] = prop[1:] + return Attrs(**ret) + + def get_token_to_attributes_dict(self): + dct = super(PowerlinePromptStyle, self).get_token_to_attributes_dict() + + def fallback(key): + try: + return dct[key] + except KeyError: + return self.get_attrs_for_token(key) + + return PowerlineStyleDict(fallback) + + def invalidation_hash(self): + return super(PowerlinePromptStyle, self).invalidation_hash() + 1 + + +class IPythonPygmentsRenderer(IPythonRenderer): + reduce_initial = [] + + @staticmethod + def hl_join(segments): + return reduce(operator.iadd, segments, []) + + def hl(self, contents, fg=None, bg=None, attrs=None): + '''Output highlighted chunk. + + This implementation outputs a list containing a single pair + (:py:class:`pygments.token.Token`, + :py:class:`powerline.lib.unicode.unicode`). + ''' + guifg = None + guibg = None + attrs = [] + if fg is not None and fg is not False: + guifg = fg[1] + if bg is not None and bg is not False: + guibg = bg[1] + if attrs: + attrs = [] + if attrs & ATTR_BOLD: + attrs.append('bold') + if attrs & ATTR_ITALIC: + attrs.append('italic') + if attrs & ATTR_UNDERLINE: + attrs.append('underline') + name = ( + 'Pl' + + ''.join(('_a' + attr for attr in attrs)) + + (('_f%6x' % guifg) if guifg is not None else '') + + (('_b%6x' % guibg) if guibg is not None else '') + ) + return [(getattr(Token.Generic.Prompt.Powerline, name), contents)] + + def hlstyle(self, **kwargs): + return [] + + def get_client_id(self, segment_info): + return id(self) + + +renderer = IPythonPygmentsRenderer diff --git a/powerline/renderers/shell/__init__.py b/powerline/renderers/shell/__init__.py index 8b8b5c85..ebb05019 100644 --- a/powerline/renderers/shell/__init__.py +++ b/powerline/renderers/shell/__init__.py @@ -13,41 +13,30 @@ def int_to_rgb(num): return r, g, b -class ShellRenderer(Renderer): - '''Powerline shell segment renderer.''' - escape_hl_start = '' - escape_hl_end = '' - term_truecolor = False - term_escape_style = 'auto' - tmux_escape = False - screen_escape = False - - character_translations = Renderer.character_translations.copy() +class PromptRenderer(Renderer): + '''Powerline generic prompt segment renderer''' def __init__(self, old_widths=None, **kwargs): - super(ShellRenderer, self).__init__(**kwargs) + super(PromptRenderer, self).__init__(**kwargs) self.old_widths = old_widths if old_widths is not None else {} - def render(self, segment_info, **kwargs): - local_theme = segment_info.get('local_theme') - return super(ShellRenderer, self).render( - matcher_info=local_theme, - segment_info=segment_info, - **kwargs - ) + def get_client_id(self, segment_info): + '''Get client ID given segment info + + This is used by daemon to correctly cache widths for different clients + using a single renderer instance. + + :param dict segment_info: + :ref:`Segment info dictionary `. Out of it only + ``client_id`` key is used. It is OK for this dictionary to not + contain this key. + + :return: Any hashable value or ``None``. + ''' + return segment_info.get('client_id') if isinstance(segment_info, dict) else None def do_render(self, output_width, segment_info, side, theme, width=None, **kwargs): - if self.term_escape_style == 'auto': - if segment_info['environ'].get('TERM') == 'fbterm': - self.used_term_escape_style = 'fbterm' - else: - self.used_term_escape_style = 'xterm' - else: - self.used_term_escape_style = self.term_escape_style - if isinstance(segment_info, dict): - client_id = segment_info.get('client_id') - else: - client_id = None + client_id = self.get_client_id(segment_info) if client_id is not None: local_key = (client_id, side, None if theme is self.theme else id(theme)) key = (client_id, side, None) @@ -70,7 +59,7 @@ class ShellRenderer(Renderer): width -= self.old_widths[(client_id, 'left', local_key[-1])] except KeyError: pass - res = super(ShellRenderer, self).do_render( + res = super(PromptRenderer, self).do_render( output_width=True, width=width, theme=theme, @@ -86,6 +75,36 @@ class ShellRenderer(Renderer): else: return ret + +class ShellRenderer(PromptRenderer): + '''Powerline shell segment renderer.''' + escape_hl_start = '' + escape_hl_end = '' + term_truecolor = False + term_escape_style = 'auto' + tmux_escape = False + screen_escape = False + + character_translations = Renderer.character_translations.copy() + + def render(self, segment_info, **kwargs): + local_theme = segment_info.get('local_theme') + return super(ShellRenderer, self).render( + matcher_info=local_theme, + segment_info=segment_info, + **kwargs + ) + + def do_render(self, segment_info, **kwargs): + if self.term_escape_style == 'auto': + if segment_info['environ'].get('TERM') == 'fbterm': + self.used_term_escape_style = 'fbterm' + else: + self.used_term_escape_style = 'xterm' + else: + self.used_term_escape_style = self.term_escape_style + return super(ShellRenderer, self).do_render(segment_info=segment_info, **kwargs) + def hlstyle(self, fg=None, bg=None, attrs=None): '''Highlight a segment.