From c31f83f831f96f4b83a22b5f0d2f34acb30ee150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Schieli?= Date: Wed, 6 Jan 2021 10:20:15 +0100 Subject: [PATCH] Emulate a right prompt in bash (#2148) * Allow passing args from render() to hl() and hlstyle() functions * New escape arg in ShellRenderer's hlstyle() to enable/disable escaping * Fix invalid escape sequence * Emulate a right prompt in bash Fixes #2103 * Document the new hl_args argument --- powerline/renderer.py | 42 +++++++---- powerline/renderers/i3bar.py | 2 +- powerline/renderers/ipython/since_5.py | 2 +- powerline/renderers/lemonbar.py | 2 +- powerline/renderers/pango_markup.py | 2 +- powerline/renderers/shell/__init__.py | 4 +- powerline/renderers/shell/bash.py | 82 +++++++++++++++++++++- powerline/renderers/tmux.py | 2 +- powerline/renderers/vim.py | 2 +- tests/test_shells/inputs/bash | 12 ++-- tests/test_shells/outputs/bash.daemon.ok | 21 +++--- tests/test_shells/outputs/bash.nodaemon.ok | 21 +++--- 12 files changed, 147 insertions(+), 47 deletions(-) diff --git a/powerline/renderer.py b/powerline/renderer.py index d1e3f1d4..31aca80e 100644 --- a/powerline/renderer.py +++ b/powerline/renderer.py @@ -251,7 +251,7 @@ class Renderer(object): 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, output_width=False, segment_info=None, matcher_info=None): + def render(self, mode=None, width=None, side=None, line=0, output_raw=False, output_width=False, segment_info=None, matcher_info=None, hl_args=None): '''Render all segments. When a width is provided, low-priority segments are dropped one at @@ -286,6 +286,11 @@ class Renderer(object): :param matcher_info: Matcher information. Is processed in :py:meth:`get_segment_info` method. + :param dict hl_args: + Additional arguments to pass on the :py:meth:`hl` and + :py:meth`hlstyle` methods. They are ignored in the default + implementation, but renderer-specific overrides can make use of + them as run-time "configuration" information. ''' theme = self.get_theme(matcher_info) return self.do_render( @@ -297,6 +302,7 @@ class Renderer(object): output_width=output_width, segment_info=self.get_segment_info(segment_info, mode), theme=theme, + hl_args=hl_args ) def compute_divider_widths(self, theme): @@ -324,7 +330,7 @@ class Renderer(object): :return: Results of joining these segments. ''' - def do_render(self, mode, width, side, line, output_raw, output_width, segment_info, theme): + def do_render(self, mode, width, side, line, output_raw, output_width, segment_info, theme, hl_args): '''Like Renderer.render(), but accept theme in place of matcher_info ''' segments = list(theme.get_segments(side, line, segment_info, mode)) @@ -333,14 +339,16 @@ class Renderer(object): self._prepare_segments(segments, output_width or width) + hl_args = hl_args or dict() + if not width: # 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(self.hl_join([ segment['_rendered_hl'] - for segment in self._render_segments(theme, segments) - ]) + self.hlstyle(), segments, current_width, output_raw, output_width) + for segment in self._render_segments(theme, segments, hl_args) + ]) + self.hlstyle(**hl_args), segments, current_width, output_raw, output_width) divider_widths = self.compute_divider_widths(theme) @@ -394,10 +402,10 @@ class Renderer(object): rendered_highlighted = self.hl_join([ segment['_rendered_hl'] - for segment in self._render_segments(theme, segments) + for segment in self._render_segments(theme, segments, hl_args) ]) if rendered_highlighted: - rendered_highlighted += self.hlstyle() + rendered_highlighted += self.hlstyle(**hl_args) return construct_returned_value(rendered_highlighted, segments, current_width, output_raw, output_width) @@ -470,7 +478,7 @@ class Renderer(object): ret += segment_len return ret - def _render_segments(self, theme, segments, render_highlighted=True): + def _render_segments(self, theme, segments, hl_args, render_highlighted=True): '''Internal segment rendering method. This method loops through the segment array and compares the @@ -527,6 +535,10 @@ class Renderer(object): contents_highlighted = '' draw_divider = segment['draw_' + divider_type + '_divider'] + segment_hl_args = {} + segment_hl_args.update(segment['highlight']) + segment_hl_args.update(hl_args) + # XXX Make sure self.hl() calls are called in the same order # segments are displayed. This is needed for Vim renderer to work. if draw_divider: @@ -546,14 +558,14 @@ class Renderer(object): if side == 'left': if render_highlighted: - contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight']) - divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False) + contents_highlighted = self.hl(self.escape(contents_raw), **segment_hl_args) + divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False, **hl_args) segment['_rendered_raw'] = contents_raw + divider_raw segment['_rendered_hl'] = contents_highlighted + divider_highlighted else: if render_highlighted: - divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False) - contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight']) + divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False, **hl_args) + contents_highlighted = self.hl(self.escape(contents_raw), **segment_hl_args) segment['_rendered_raw'] = divider_raw + contents_raw segment['_rendered_hl'] = divider_highlighted + contents_highlighted else: @@ -562,7 +574,7 @@ class Renderer(object): else: contents_raw = contents_raw + outer_padding - contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight']) + contents_highlighted = self.hl(self.escape(contents_raw), **segment_hl_args) segment['_rendered_raw'] = contents_raw segment['_rendered_hl'] = contents_highlighted prev_segment = segment @@ -576,7 +588,7 @@ class Renderer(object): ''' return string.translate(self.character_translations) - def hlstyle(fg=None, bg=None, attrs=None): + def hlstyle(fg=None, bg=None, attrs=None, **kwargs): '''Output highlight style string. Assuming highlighted string looks like ``{style}{contents}`` this method @@ -585,10 +597,10 @@ class Renderer(object): ''' raise NotImplementedError - def hl(self, contents, fg=None, bg=None, attrs=None): + def hl(self, contents, fg=None, bg=None, attrs=None, **kwargs): '''Output highlighted chunk. This implementation just outputs :py:meth:`hlstyle` joined with ``contents``. ''' - return self.hlstyle(fg, bg, attrs) + (contents or '') + return self.hlstyle(fg, bg, attrs, **kwargs) + (contents or '') diff --git a/powerline/renderers/i3bar.py b/powerline/renderers/i3bar.py index 020a93d8..3eab61fa 100644 --- a/powerline/renderers/i3bar.py +++ b/powerline/renderers/i3bar.py @@ -17,7 +17,7 @@ class I3barRenderer(Renderer): # We don’t need to explicitly reset attributes, so skip those calls return '' - def hl(self, contents, fg=None, bg=None, attrs=None): + def hl(self, contents, fg=None, bg=None, attrs=None, **kwargs): segment = { 'full_text': contents, 'separator': False, diff --git a/powerline/renderers/ipython/since_5.py b/powerline/renderers/ipython/since_5.py index 8a26da72..88c7625e 100644 --- a/powerline/renderers/ipython/since_5.py +++ b/powerline/renderers/ipython/since_5.py @@ -90,7 +90,7 @@ class IPythonPygmentsRenderer(IPythonRenderer): def hl_join(segments): return reduce(operator.iadd, segments, []) - def hl(self, contents, fg=None, bg=None, attrs=None): + def hl(self, contents, fg=None, bg=None, attrs=None, **kwargs): '''Output highlighted chunk. This implementation outputs a list containing a single pair diff --git a/powerline/renderers/lemonbar.py b/powerline/renderers/lemonbar.py index f378f235..8156807b 100644 --- a/powerline/renderers/lemonbar.py +++ b/powerline/renderers/lemonbar.py @@ -21,7 +21,7 @@ class LemonbarRenderer(Renderer): # We don’t need to explicitly reset attributes, so skip those calls return '' - def hl(self, contents, fg=None, bg=None, attrs=None): + def hl(self, contents, fg=None, bg=None, attrs=None, **kwargs): text = '' if fg is not None: diff --git a/powerline/renderers/pango_markup.py b/powerline/renderers/pango_markup.py index 1b7d624b..3c1a6755 100644 --- a/powerline/renderers/pango_markup.py +++ b/powerline/renderers/pango_markup.py @@ -15,7 +15,7 @@ class PangoMarkupRenderer(Renderer): # We don’t need to explicitly reset attributes, so skip those calls return '' - def hl(self, contents, fg=None, bg=None, attrs=None): + def hl(self, contents, fg=None, bg=None, attrs=None, **kwargs): '''Highlight a segment.''' awesome_attr = [] if fg is not None: diff --git a/powerline/renderers/shell/__init__.py b/powerline/renderers/shell/__init__.py index ebb05019..d7dbf96b 100644 --- a/powerline/renderers/shell/__init__.py +++ b/powerline/renderers/shell/__init__.py @@ -105,7 +105,7 @@ class ShellRenderer(PromptRenderer): 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): + def hlstyle(self, fg=None, bg=None, attrs=None, escape=True, **kwargs): '''Highlight a segment. If an argument is None, the argument is ignored. If an argument is @@ -162,7 +162,7 @@ class ShellRenderer(PromptRenderer): r = '\033Ptmux;' + r.replace('\033', '\033\033') + '\033\\' elif self.screen_escape: r = '\033P' + r.replace('\033', '\033\033') + '\033\\' - return self.escape_hl_start + r + self.escape_hl_end + return self.escape_hl_start + r + self.escape_hl_end if escape else r def get_theme(self, matcher_info): if not matcher_info: diff --git a/powerline/renderers/shell/bash.py b/powerline/renderers/shell/bash.py index 783bd501..5ccf206b 100644 --- a/powerline/renderers/shell/bash.py +++ b/powerline/renderers/shell/bash.py @@ -6,13 +6,91 @@ from powerline.renderers.shell import ShellRenderer class BashPromptRenderer(ShellRenderer): '''Powerline bash prompt segment renderer.''' - escape_hl_start = '\[' - escape_hl_end = '\]' + escape_hl_start = '\\[' + escape_hl_end = '\\]' character_translations = ShellRenderer.character_translations.copy() character_translations[ord('$')] = '\\$' character_translations[ord('`')] = '\\`' character_translations[ord('\\')] = '\\\\' + def do_render(self, side, line, width, output_width, output_raw, hl_args, **kwargs): + + # we are rendering the normal left prompt + if side == 'left' and line == 0 and width is not None: + + # we need left prompt's width to render the raw spacer + output_width = output_width or output_raw + + left = super(BashPromptRenderer, self).do_render( + side=side, + line=line, + output_width=output_width, + width=width, + output_raw=output_raw, + hl_args=hl_args, + **kwargs + ) + left_rendered = left[0] if output_width else left + + # we don't escape color sequences in the right prompt so we can do escaping as a whole + if hl_args: + hl_args = hl_args.copy() + hl_args.update({'escape': False}) + else: + hl_args = {'escape': False} + + right = super(BashPromptRenderer, self).do_render( + side='right', + line=line, + output_width=True, + width=width, + output_raw=output_raw, + hl_args=hl_args, + **kwargs + ) + + ret = [] + if right[-1] > 0: + # if the right prompt is not empty we embed it in the left prompt + # it must be escaped as a whole so readline doesn't see it + ret.append(''.join(( + left_rendered, + self.escape_hl_start, + '\033[s', # save the cursor position + '\033[{0}C'.format(width), # move to the right edge of the terminal + '\033[{0}D'.format(right[-1] - 1), # move back to the right prompt position + right[0], + '\033[u', # restore the cursor position + self.escape_hl_end + ))) + if output_raw: + ret.append(''.join(( + left[1], + ' ' * (width - left[-1] - right[-1]), + right[1] + ))) + else: + ret.append(left_rendered) + if output_raw: + ret.append(left[1]) + if output_width: + ret.append(left[-1]) + if len(ret) == 1: + return ret[0] + else: + return ret + + else: + return super(BashPromptRenderer, self).do_render( + side=side, + line=line, + width=width, + output_width=output_width, + output_raw=output_raw, + hl_args=hl_args, + **kwargs + ) + renderer = BashPromptRenderer diff --git a/powerline/renderers/tmux.py b/powerline/renderers/tmux.py index 74066fab..fc3282a1 100644 --- a/powerline/renderers/tmux.py +++ b/powerline/renderers/tmux.py @@ -38,7 +38,7 @@ class TmuxRenderer(Renderer): width = 10 return super(TmuxRenderer, self).render(width=width, segment_info=segment_info, **kwargs) - def hlstyle(self, fg=None, bg=None, attrs=None): + def hlstyle(self, fg=None, bg=None, attrs=None, **kwargs): '''Highlight a segment.''' # We don’t need to explicitly reset attributes, so skip those calls if not attrs and not bg and not fg: diff --git a/powerline/renderers/vim.py b/powerline/renderers/vim.py index 281177ce..a92d51c2 100644 --- a/powerline/renderers/vim.py +++ b/powerline/renderers/vim.py @@ -123,7 +123,7 @@ class VimRenderer(Renderer): def reset_highlight(self): self.hl_groups.clear() - def hlstyle(self, fg=None, bg=None, attrs=None): + def hlstyle(self, fg=None, bg=None, attrs=None, **kwargs): '''Highlight a segment. If an argument is None, the argument is ignored. If an argument is diff --git a/tests/test_shells/inputs/bash b/tests/test_shells/inputs/bash index 1b68b6f6..b0d14785 100644 --- a/tests/test_shells/inputs/bash +++ b/tests/test_shells/inputs/bash @@ -4,6 +4,7 @@ set_theme_option() { set_theme() { export POWERLINE_CONFIG_OVERRIDES="ext.shell.theme=$1" } +set_theme_option default.segment_data.hostname.args.only_if_ssh false set_theme_option default_leftonly.segment_data.hostname.args.only_if_ssh false ABOVE_LEFT='[{ "left": [ @@ -40,7 +41,9 @@ VIRTUAL_ENV= bgscript.sh & waitpid.sh false kill `cat pid` ; sleep 1s +set_theme_option default.segment_data.hostname.display false set_theme_option default_leftonly.segment_data.hostname.display false +set_theme_option default.segment_data.user.display false set_theme_option default_leftonly.segment_data.user.display false echo ' abc @@ -56,14 +59,15 @@ cd ../'$(echo)' cd ../'`echo`' cd ../'«Unicode!»' (exit 42)|(exit 43) -set_theme_option default_leftonly.segments.above "$ABOVE_LEFT" +set_theme default +set_theme_option default.segments.above "$ABOVE_LEFT" export DISPLAYED_ENV_VAR=foo unset DISPLAYED_ENV_VAR -set_theme_option default_leftonly.segments.above "$ABOVE_FULL" +set_theme_option default.segments.above "$ABOVE_FULL" export DISPLAYED_ENV_VAR=foo unset DISPLAYED_ENV_VAR -set_theme_option default_leftonly.segments.above -set_theme_option default_leftonly.dividers.left.hard \$ABC +set_theme_option default.segments.above +set_theme_option default.dividers.left.hard \$ABC false true is the last line exit diff --git a/tests/test_shells/outputs/bash.daemon.ok b/tests/test_shells/outputs/bash.daemon.ok index 89907c83..7b8cd6cb 100644 --- a/tests/test_shells/outputs/bash.daemon.ok +++ b/tests/test_shells/outputs/bash.daemon.ok @@ -7,7 +7,9 @@   HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false   HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1s [1]+ Terminated bgscript.sh +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  set_theme_option default.segment_data.hostname.display false   HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.hostname.display false + USER   BRANCH  …  tmp  shell  3rd  set_theme_option default.segment_data.user.display false  USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.user.display false   BRANCH  …  tmp  shell  3rd  echo '                                    abc @@ -27,16 +29,17 @@ def   BRANCH  …  shell  3rd  $(echo)  cd ../'`echo`'   BRANCH  …  shell  3rd  `echo`  cd ../'«Unicode!»'   BRANCH  …  shell  3rd  «Unicode!»  (exit 42)|(exit 43) -  BRANCH  …  shell  3rd  «Unicode!»  42  43  set_theme_option default_leftonly.segments.above "$ABOVE_LEFT" -  BRANCH  …  shell  3rd  «Unicode!»  export DISPLAYED_ENV_VAR=foo +  BRANCH  …  shell  3rd  «Unicode!»  42  43  set_theme default + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.segments.above "$ABOVE_LEFT" + …  shell  3rd  «Unicode!»     BRANCH export DISPLAYED_ENV_VAR=foo  foo   -  BRANCH  …  shell  3rd  «Unicode!»  unset DISPLAYED_ENV_VAR -  BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.segments.above "$ABOVE_FULL" + …  shell  3rd  «Unicode!»     BRANCH unset DISPLAYED_ENV_VAR + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.segments.above "$ABOVE_FULL"                                                                                                                                                                                                                                                                                                              -  BRANCH  …  shell  3rd  «Unicode!»  export DISPLAYED_ENV_VAR=foo + …  shell  3rd  «Unicode!»     BRANCH export DISPLAYED_ENV_VAR=foo                                                                                                                                                                                                                                                                                                        foo  -  BRANCH  …  shell  3rd  «Unicode!»  unset DISPLAYED_ENV_VAR + …  shell  3rd  «Unicode!»     BRANCH unset DISPLAYED_ENV_VAR                                                                                                                                                                                                                                                                                                              -  BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.segments.above -  BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.dividers.left.hard \$ABC -  BRANCH $ABC…  shell  3rd  «Unicode!» $ABCfalse + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.segments.above + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.dividers.left.hard \$ABC + …  shell  3rd  «Unicode!» $ABC   BRANCH false diff --git a/tests/test_shells/outputs/bash.nodaemon.ok b/tests/test_shells/outputs/bash.nodaemon.ok index c65dcc14..458c1a18 100644 --- a/tests/test_shells/outputs/bash.nodaemon.ok +++ b/tests/test_shells/outputs/bash.nodaemon.ok @@ -7,7 +7,9 @@   HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false   HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1s [1]+ Terminated bgscript.sh +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  set_theme_option default.segment_data.hostname.display false   HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.hostname.display false + USER   BRANCH  …  tmp  shell  3rd  set_theme_option default.segment_data.user.display false  USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.user.display false   BRANCH  …  tmp  shell  3rd  echo '    abc @@ -27,16 +29,17 @@ def   BRANCH  …  shell  3rd  $(echo)  cd ../'`echo`'   BRANCH  …  shell  3rd  `echo`  cd ../'«Unicode!»'   BRANCH  …  shell  3rd  «Unicode!»  (exit 42)|(exit 43) -  BRANCH  …  shell  3rd  «Unicode!»  42  43  set_theme_option default_leftonly.segments.above "$ABOVE_LEFT" -  BRANCH  …  shell  3rd  «Unicode!»  export DISPLAYED_ENV_VAR=foo +  BRANCH  …  shell  3rd  «Unicode!»  42  43  set_theme default + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.segments.above "$ABOVE_LEFT" + …  shell  3rd  «Unicode!»     BRANCH export DISPLAYED_ENV_VAR=foo  foo   -  BRANCH  …  shell  3rd  «Unicode!»  unset DISPLAYED_ENV_VAR -  BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.segments.above "$ABOVE_FULL" + …  shell  3rd  «Unicode!»     BRANCH unset DISPLAYED_ENV_VAR + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.segments.above "$ABOVE_FULL"                                                                                                                                                                                                                                                                                                              -  BRANCH  …  shell  3rd  «Unicode!»  export DISPLAYED_ENV_VAR=foo + …  shell  3rd  «Unicode!»     BRANCH export DISPLAYED_ENV_VAR=foo                                                                                                                                                                                                                                                                                                        foo  -  BRANCH  …  shell  3rd  «Unicode!»  unset DISPLAYED_ENV_VAR + …  shell  3rd  «Unicode!»     BRANCH unset DISPLAYED_ENV_VAR                                                                                                                                                                                                                                                                                                              -  BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.segments.above -  BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.dividers.left.hard \$ABC -  BRANCH $ABC…  shell  3rd  «Unicode!» $ABCfalse + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.segments.above + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.dividers.left.hard \$ABC + …  shell  3rd  «Unicode!» $ABC   BRANCH false