From c7e7160ceb7a682771f2132478a30e6256d707b6 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 20 Jan 2016 21:47:24 +0100 Subject: [PATCH] Refactoring of the prompt_toolkit.styles. --- docs/pages/building_prompts.rst | 62 ++++--- examples/bottom-toolbar.py | 2 +- examples/clock-input.py | 4 +- examples/colored-prompt.py | 10 +- examples/full-screen-layout.py | 3 +- examples/get-multiline-input.py | 3 +- examples/html-input.py | 1 + examples/regular-language.py | 3 +- prompt_toolkit/shortcuts.py | 6 +- prompt_toolkit/styles.py | 230 ------------------------- prompt_toolkit/styles/__init__.py | 20 +++ prompt_toolkit/styles/base.py | 86 +++++++++ prompt_toolkit/styles/defaults.py | 79 +++++++++ prompt_toolkit/styles/from_dict.py | 138 +++++++++++++++ prompt_toolkit/styles/from_pygments.py | 74 ++++++++ tests/run_tests.py | 1 + tests/style_tests.py | 41 +++++ 17 files changed, 499 insertions(+), 264 deletions(-) delete mode 100644 prompt_toolkit/styles.py create mode 100644 prompt_toolkit/styles/__init__.py create mode 100644 prompt_toolkit/styles/base.py create mode 100644 prompt_toolkit/styles/defaults.py create mode 100644 prompt_toolkit/styles/from_dict.py create mode 100644 prompt_toolkit/styles/from_pygments.py create mode 100644 tests/style_tests.py diff --git a/docs/pages/building_prompts.rst b/docs/pages/building_prompts.rst index 578a8cc8e..304e62a6d 100644 --- a/docs/pages/building_prompts.rst +++ b/docs/pages/building_prompts.rst @@ -67,34 +67,55 @@ Colors The colors for syntax highlighting are defined by a :class:`~prompt_toolkit.styles.Style` instance. By default, a neutral built-in style is used, but any style instance can be passed to the -:func:`~prompt_toolkit.shortcuts.prompt` function. All Pygments style classes -can be used as well, when they are wrapped in a -:class:`~prompt_toolkit.styles.PygmentsStyle`. +:func:`~prompt_toolkit.shortcuts.prompt` function. A simple way to create a +style, is by using the :class:`~prompt_toolkit.styles.style_from_dict` +function: + +.. code:: python + + from prompt_toolkit.shortcuts import prompt + from prompt_toolkit.styles import style_from_dict + + our_style = style_from_dict({ + Token.Comment: '#888888 bold', + Token.Keyword: '#ff88ff bold', + }) + + text = prompt('Enter HTML: ', lexer=PygmentsLexer(HtmlLexer), + style=our_style) + + +The style dictionary is very similar to the Pygments ``styles`` dictionary, +with a few differences: + +- The `roman`, `sans`, `mono` and `border` options are not ignored. +- The style has a few additions: `blink`, `noblink`, `reverse` and `noreverse`. +- Colors can be in the `#ff0000` format, but they can be one of the built-in + ANSI color names as well. In that case, they map directly to the 16 color + palette of the terminal. + +Using a Pygments style +^^^^^^^^^^^^^^^^^^^^^^ + +All Pygments style classes can be used as well, when they are wrapped through +:func:`~prompt_toolkit.styles.style_from_pygments`. Suppose we'd like to use a Pygments style, for instance -``pygments.styles.tango.TangoStyle``. That works when we wrap it inside -:class:`~prompt_toolkit.styles.PygmentsStyle`, but we would still miss some -``prompt_toolkit`` specific styling, like the highlighting of selected text and -the styling of the completion menus. Because of that, we recommend to use the -:meth:`~prompt_toolkit.styles.PygmentsStyle.from_defaults` method to generate a -a :class:`~prompt_toolkit.styles.Style` instance. +``pygments.styles.tango.TangoStyle``, that is possible like this: Creating a custom style could be done like this: .. code:: python from prompt_toolkit.shortcuts import prompt - from prompt_toolkit.styles import PygmentsStyle + from prompt_toolkit.styles import style_from_pygments - from pygments.style import Style from pygments.styles.tango import TangoStyle - our_style = PygmentsStyle.from_defaults( - pygments_style_cls=TangoStyle, - style_dict={ - Token.Comment: '#888888 bold', - Token.Keyword: '#ff88ff bold', - }) + our_style = style_from_pygments(TangoStyle, { + Token.Comment: '#888888 bold', + Token.Keyword: '#ff88ff bold', + }) text = prompt('Enter HTML: ', lexer=PygmentsLexer(HtmlLexer), style=our_style) @@ -112,10 +133,9 @@ Each token is a Pygments token and can be styled individually. .. code:: python from prompt_toolkit.shortcuts import prompt - from pygments.style import Style - from prompt_toolkit.styles import PygmentsStyle + from prompt_toolkit.styles import style_from_dict - example_style = PygmentsStyle.from_defaults({ + example_style = style_from_dict({ # User input. Token: '#ff0066', @@ -159,7 +179,7 @@ is simple with the :func:`~prompt_toolkit.shortcuts.print_tokens` function. .. code:: python # Create a stylesheet. - style = PygmentsStyle.from_defaults(style_dict={ + style = style_from_dict({ Token.Hello: '#ff0066', Token.World: '#44ff44 italic', }) diff --git a/examples/bottom-toolbar.py b/examples/bottom-toolbar.py index f5d1e7ee6..6a9c7822c 100755 --- a/examples/bottom-toolbar.py +++ b/examples/bottom-toolbar.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals from prompt_toolkit import prompt from prompt_toolkit.styles import PygmentsStyle -from pygments.token import Token +from prompt_toolkit.token import Token test_style = PygmentsStyle.from_defaults({ diff --git a/examples/clock-input.py b/examples/clock-input.py index afd978d5f..8d2ff24e0 100755 --- a/examples/clock-input.py +++ b/examples/clock-input.py @@ -3,14 +3,14 @@ Example of a 'dynamic' prompt. On that shows the current time in the prompt. """ from __future__ import unicode_literals -from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.application import Application +from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.layout import Window from prompt_toolkit.layout.controls import BufferControl from prompt_toolkit.layout.processors import BeforeInput from prompt_toolkit.shortcuts import create_eventloop +from prompt_toolkit.token import Token from prompt_toolkit.utils import Callback -from pygments.token import Token import datetime import time diff --git a/examples/colored-prompt.py b/examples/colored-prompt.py index 2d64e7c6f..881726e61 100755 --- a/examples/colored-prompt.py +++ b/examples/colored-prompt.py @@ -5,11 +5,11 @@ from __future__ import unicode_literals from prompt_toolkit import prompt -from prompt_toolkit.styles import PygmentsStyle -from pygments.token import Token +from prompt_toolkit.styles import style_from_dict +from prompt_toolkit.token import Token -example_style = PygmentsStyle.from_defaults(style_dict={ +example_style = style_from_dict({ # User input. Token: '#ff0066', @@ -20,6 +20,10 @@ Token.Pound: '#00aa00', Token.Host: '#000088 bg:#aaaaff', Token.Path: '#884444 underline', + + # Make a selection reverse/underlined. + # (Use Control-Space to select.) + Token.SelectedText: 'reverse underline', }) diff --git a/examples/full-screen-layout.py b/examples/full-screen-layout.py index 8b91f3494..5fb571afe 100755 --- a/examples/full-screen-layout.py +++ b/examples/full-screen-layout.py @@ -17,8 +17,7 @@ from prompt_toolkit.layout.controls import BufferControl, FillControl, TokenListControl from prompt_toolkit.layout.dimension import LayoutDimension as D from prompt_toolkit.shortcuts import create_eventloop - -from pygments.token import Token +from prompt_toolkit.token import Token # 1. First we create the layout diff --git a/examples/get-multiline-input.py b/examples/get-multiline-input.py index 44d11e943..489c4b568 100755 --- a/examples/get-multiline-input.py +++ b/examples/get-multiline-input.py @@ -1,7 +1,8 @@ #!/usr/bin/env python from __future__ import unicode_literals from prompt_toolkit import prompt -from pygments.token import Token +from prompt_toolkit.token import Token + def continuation_tokens(cli, width): " The continuation: display dots before all the following lines. " diff --git a/examples/html-input.py b/examples/html-input.py index 678652ac3..ef2772c77 100755 --- a/examples/html-input.py +++ b/examples/html-input.py @@ -1,6 +1,7 @@ #!/usr/bin/env python """ Simple example of a syntax-highlighted HTML input line. +(This requires Pygments to be installed.) """ from __future__ import unicode_literals from pygments.lexers import HtmlLexer diff --git a/examples/regular-language.py b/examples/regular-language.py index e56d6780e..fa7abb612 100755 --- a/examples/regular-language.py +++ b/examples/regular-language.py @@ -21,8 +21,7 @@ from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer from prompt_toolkit.layout.lexers import SimpleLexer from prompt_toolkit.styles import PygmentsStyle - -from pygments.token import Token +from prompt_toolkit.token import Token import math diff --git a/prompt_toolkit/shortcuts.py b/prompt_toolkit/shortcuts.py index 93a150eaf..450258071 100644 --- a/prompt_toolkit/shortcuts.py +++ b/prompt_toolkit/shortcuts.py @@ -54,8 +54,10 @@ try: from pygments.lexer import Lexer as pygments_Lexer + from pygments.style import Style as pygments_Style except ImportError: pygments_Lexer = None + pygments_Style = None if is_windows(): from .terminal.win32_output import Win32Output @@ -213,7 +215,7 @@ def create_prompt_layout(message='', lexer=None, is_password=False, # class is given, turn it into a PygmentsLexer. (Important for # backwards-compatibility.) try: - if pygments_Lexer and issubclass(lexer, pygments.lexer.Lexer): + if pygments_Lexer and issubclass(lexer, pygments_Lexer): lexer = PygmentsLexer(lexer) except TypeError: # Happens when lexer is `None` or an instance of something else. pass @@ -418,7 +420,7 @@ def create_prompt_application( # Accept Pygments styles as well for backwards compatibility. try: - if issubclass(style, pygments.style.Style): + if pygments_Style and issubclass(style, pygments_Style): style = PygmentsStyle(style) except TypeError: # Happens when style is `None` or an instance of something else. pass diff --git a/prompt_toolkit/styles.py b/prompt_toolkit/styles.py deleted file mode 100644 index 0564509b4..000000000 --- a/prompt_toolkit/styles.py +++ /dev/null @@ -1,230 +0,0 @@ -""" -Styling for prompt_toolkit applications. - -Pygments needs to be installed for usage of ``PygmentsStyle``. -""" -from __future__ import unicode_literals -from abc import ABCMeta, abstractmethod -from collections import namedtuple -from six import with_metaclass - -from .token import Token - -# Following imports are only needed when a ``PygmentsStyle`` class is used. -try: - from pygments.style import Style as pygments_Style - from pygments.styles.default import DefaultStyle as pygments_DefaultStyle -except ImportError: - pygments_Style = None - pygments_DefaultStyle = None - - -__all__ = ( - 'Style', - 'Attrs', - 'DynamicStyle', - 'PygmentsStyle', - - 'DEFAULT_STYLE_EXTENSIONS', - 'DEFAULT_STYLE', -) - - -#: Style attributes. -Attrs = namedtuple('Attrs', 'color bgcolor bold underline italic blink reverse') -""" -:param color: Hexadecimal string. E.g. '000000' -:param bgcolor: Hexadecimal string. E.g. 'ffffff' -:param bold: Boolean -:param underline: Boolean -:param italic: Boolean -:param blink: Boolean -:param reverse: Boolean -""" - -_default_attrs = Attrs(color=None, bgcolor=None, bold=False, underline=False, - italic=False, blink=False, reverse=False) - -#: ``Attrs.bgcolor/fgcolor`` can be in either 'ffffff' format, or can be any of -#: the following in case we want to take colors from the 8/16 color palette. -#: Usually, in that case, the terminal application allows to configure the RGB -#: values for these names. -ANSI_COLOR_NAMES = [ - 'black', 'white', 'default', - - # Low intensity. - 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'gray', - - # High intensity. (Not supported everywhere.) - 'dark-gray', 'bright-red', 'bright-green', 'bright-yellow', 'bright-blue', - 'bright-magenta', 'bright-cyan', -] - - -class Style(with_metaclass(ABCMeta, object)): - """ - Abstract base class for prompt_toolkit styles. - """ - @abstractmethod - def get_attrs_for_token(self, token): - """ - Return :class:`.Attrs` for the given token. - """ - - @abstractmethod - def invalidation_hash(self): - """ - Invalidation hash for the style. When this changes over time, the - renderer knows that something in the style changed, and that everything - has to be redrawn. - """ - - -class DynamicStyle(Style): - """ - Style class that can dynamically returns an other Style. - - :param get_style: Callable that returns a :class:`.Style` instance. - """ - def __init__(self, get_style): - self.get_style = get_style - - def get_attrs_for_token(self, token): - style = self.get_style() - assert isinstance(style, Style) - - return style.get_attrs_for_token(token) - - def invalidation_hash(self): - return self.get_style().invalidation_hash() - - -class PygmentsStyle(Style): - """ - Adaptor for using Pygments styles as a :class:`.Style`. - - :param pygments_style_cls: Pygments ``Style`` class. - """ - def __init__(self, pygments_style_cls): - assert issubclass(pygments_style_cls, pygments_Style) - self.pygments_style_cls = pygments_style_cls - self._token_to_attrs_dict = None - - def get_attrs_for_token(self, token): - try: - style = self.pygments_style_cls.style_for_token(token) - return Attrs(color=style['color'], - bgcolor=style['bgcolor'], - bold=style.get('bold', False), - underline=style.get('underline', False), - italic=style.get('italic', False), - blink=False, - reverse=False) - - except KeyError: - return _default_attrs - - def invalidation_hash(self): - return id(self.pygments_style_cls) - - @classmethod - def from_defaults(cls, style_dict=None, - pygments_style_cls=pygments_DefaultStyle, - include_extensions=True): - """ - Shortcut to create a :class:`.PygmentsStyle` instance from a Pygments - dictionary and a style class. - - :param style_dict: Dictionary for this style. `{Token: style}`. - :param pygments_style_cls: Pygments style class to start from. - :param include_extensions: (`bool`) Include prompt_toolkit extensions. - """ - assert style_dict is None or isinstance(style_dict, dict) - assert pygments_style_cls is None or issubclass(pygments_style_cls, pygments_Style) - - class _CustomStyle(pygments_DefaultStyle): - background_color = None - styles = {} - - if pygments_style_cls is not None: - styles.update(pygments_style_cls.styles) - - if include_extensions: - styles.update(DEFAULT_STYLE_EXTENSIONS) - - if style_dict is not None: - styles.update(style_dict) - - return cls(_CustomStyle) - - -#: Styling of prompt-toolkit specific tokens, that are not know by the default -#: Pygments style. -DEFAULT_STYLE_EXTENSIONS = { - # Highlighting of search matches in document. - Token.SearchMatch: '#000000 bg:#888888', - Token.SearchMatch.Current: '#ffffff bg:#aa8888 underline', - - # Highlighting of select text in document. - Token.SelectedText: '#ffffff bg:#666666', - - # Highlighting of matching brackets. - Token.MatchingBracket: 'bg:#aaaaff #000000', - - # Line numbers. - Token.LineNumber: '#888888', - Token.LineNumber.Current: 'bold', - - # Default prompt. - Token.Prompt: 'bold', - Token.Prompt.Arg: 'noinherit', - Token.Prompt.Search: 'noinherit', - Token.Prompt.Search.Text: 'bold', - - # Search toolbar. - Token.Toolbar.Search: 'bold', - Token.Toolbar.Search.Text: 'nobold', - - # System toolbar - Token.Toolbar.System: 'bold', - - # "arg" toolbar. - Token.Toolbar.Arg: 'bold', - Token.Toolbar.Arg.Text: 'nobold', - - # Validation toolbar. - Token.Toolbar.Validation: 'bg:#550000 #ffffff', - Token.WindowTooSmall: 'bg:#550000 #ffffff', - - # Completions toolbar. - Token.Toolbar.Completions: 'bg:#bbbbbb #000000', - Token.Toolbar.Completions.Arrow: 'bg:#bbbbbb #000000 bold', - Token.Toolbar.Completions.Completion: 'bg:#bbbbbb #000000', - Token.Toolbar.Completions.Completion.Current: 'bg:#444444 #ffffff', - - # Completions menu. - Token.Menu.Completions.Completion: 'bg:#bbbbbb #000000', - Token.Menu.Completions.Completion.Current: 'bg:#888888 #ffffff', - Token.Menu.Completions.Meta: 'bg:#999999 #000000', - Token.Menu.Completions.Meta.Current: 'bg:#aaaaaa #000000', - Token.Menu.Completions.MultiColumnMeta: 'bg:#aaaaaa #000000', - Token.Menu.Completions.ProgressBar: 'bg:#aaaaaa', - Token.Menu.Completions.ProgressButton: 'bg:#000000', - - # Scrollbars. - Token.Scrollbar: 'bg:#444444', - Token.Scrollbar.Button: 'bg:#888888', - Token.Scrollbar.Arrow: 'bg:#222222 #ffffff', - - # Auto suggestion text. - Token.AutoSuggestion: '#666666', - - # When Control-C has been pressed. Grayed. - Token.Aborted: '#888888', -} - -default_style_extensions = DEFAULT_STYLE_EXTENSIONS # Old name. - - -#: The default built-in style. -DEFAULT_STYLE = PygmentsStyle.from_defaults() diff --git a/prompt_toolkit/styles/__init__.py b/prompt_toolkit/styles/__init__.py new file mode 100644 index 000000000..667eb1a73 --- /dev/null +++ b/prompt_toolkit/styles/__init__.py @@ -0,0 +1,20 @@ +""" +Styling for prompt_toolkit applications. +""" +from __future__ import unicode_literals + +from .base import * +from .defaults import * +from .from_dict import * +from .from_pygments import * + + +#: The default built-in style. +#: (For backwards compatibility, when Pygments is installed, this includes the +#: default Pygments style.) +try: + import pygments +except ImportError: + DEFAULT_STYLE = style_from_dict(DEFAULT_STYLE_EXTENSIONS) +else: + DEFAULT_STYLE = style_from_pygments() diff --git a/prompt_toolkit/styles/base.py b/prompt_toolkit/styles/base.py new file mode 100644 index 000000000..31428b965 --- /dev/null +++ b/prompt_toolkit/styles/base.py @@ -0,0 +1,86 @@ +""" +The base classes for the styling. +""" +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from collections import namedtuple +from six import with_metaclass + +__all__ = ( + 'Attrs', + 'DEFAULT_ATTRS', + 'ANSI_COLOR_NAMES', + 'Style', + 'DynamicStyle', +) + + +#: Style attributes. +Attrs = namedtuple('Attrs', 'color bgcolor bold underline italic blink reverse') +""" +:param color: Hexadecimal string. E.g. '000000' or Ansi color name: e.g. 'magenta' +:param bgcolor: Hexadecimal string. E.g. 'ffffff' or Ansi color name: e.g. 'magenta' +:param bold: Boolean +:param underline: Boolean +:param italic: Boolean +:param blink: Boolean +:param reverse: Boolean +""" + +#: The default `Attrs`. +DEFAULT_ATTRS = Attrs(color=None, bgcolor=None, bold=False, underline=False, + italic=False, blink=False, reverse=False) + + +#: ``Attrs.bgcolor/fgcolor`` can be in either 'ffffff' format, or can be any of +#: the following in case we want to take colors from the 8/16 color palette. +#: Usually, in that case, the terminal application allows to configure the RGB +#: values for these names. +ANSI_COLOR_NAMES = [ + 'black', 'white', 'default', + + # Low intensity. + 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'gray', + + # High intensity. (Not supported everywhere.) + 'dark-gray', 'bright-red', 'bright-green', 'bright-yellow', 'bright-blue', + 'bright-magenta', 'bright-cyan', +] + + +class Style(with_metaclass(ABCMeta, object)): + """ + Abstract base class for prompt_toolkit styles. + """ + @abstractmethod + def get_attrs_for_token(self, token): + """ + Return :class:`.Attrs` for the given token. + """ + + @abstractmethod + def invalidation_hash(self): + """ + Invalidation hash for the style. When this changes over time, the + renderer knows that something in the style changed, and that everything + has to be redrawn. + """ + + +class DynamicStyle(Style): + """ + Style class that can dynamically returns an other Style. + + :param get_style: Callable that returns a :class:`.Style` instance. + """ + def __init__(self, get_style): + self.get_style = get_style + + def get_attrs_for_token(self, token): + style = self.get_style() + assert isinstance(style, Style) + + return style.get_attrs_for_token(token) + + def invalidation_hash(self): + return self.get_style().invalidation_hash() diff --git a/prompt_toolkit/styles/defaults.py b/prompt_toolkit/styles/defaults.py new file mode 100644 index 000000000..0cfc449bf --- /dev/null +++ b/prompt_toolkit/styles/defaults.py @@ -0,0 +1,79 @@ +""" +The default styling. +""" +from __future__ import unicode_literals + +from prompt_toolkit.token import Token + +__all__ = ( + 'DEFAULT_STYLE_EXTENSIONS', + 'default_style_extensions', +) + + +#: Styling of prompt-toolkit specific tokens, that are not know by the default +#: Pygments style. +DEFAULT_STYLE_EXTENSIONS = { + # Highlighting of search matches in document. + Token.SearchMatch: '#000000 bg:#888888', + Token.SearchMatch.Current: '#ffffff bg:#aa8888 underline', + + # Highlighting of select text in document. + Token.SelectedText: '#ffffff bg:#666666', + + # Highlighting of matching brackets. + Token.MatchingBracket: 'bg:#aaaaff #000000', + + # Line numbers. + Token.LineNumber: '#888888', + Token.LineNumber.Current: 'bold', + + # Default prompt. + Token.Prompt: 'bold', + Token.Prompt.Arg: 'noinherit', + Token.Prompt.Search: 'noinherit', + Token.Prompt.Search.Text: 'bold', + + # Search toolbar. + Token.Toolbar.Search: 'bold', + Token.Toolbar.Search.Text: 'nobold', + + # System toolbar + Token.Toolbar.System: 'bold', + + # "arg" toolbar. + Token.Toolbar.Arg: 'bold', + Token.Toolbar.Arg.Text: 'nobold', + + # Validation toolbar. + Token.Toolbar.Validation: 'bg:#550000 #ffffff', + Token.WindowTooSmall: 'bg:#550000 #ffffff', + + # Completions toolbar. + Token.Toolbar.Completions: 'bg:#bbbbbb #000000', + Token.Toolbar.Completions.Arrow: 'bg:#bbbbbb #000000 bold', + Token.Toolbar.Completions.Completion: 'bg:#bbbbbb #000000', + Token.Toolbar.Completions.Completion.Current: 'bg:#444444 #ffffff', + + # Completions menu. + Token.Menu.Completions.Completion: 'bg:#bbbbbb #000000', + Token.Menu.Completions.Completion.Current: 'bg:#888888 #ffffff', + Token.Menu.Completions.Meta: 'bg:#999999 #000000', + Token.Menu.Completions.Meta.Current: 'bg:#aaaaaa #000000', + Token.Menu.Completions.MultiColumnMeta: 'bg:#aaaaaa #000000', + Token.Menu.Completions.ProgressBar: 'bg:#aaaaaa', + Token.Menu.Completions.ProgressButton: 'bg:#000000', + + # Scrollbars. + Token.Scrollbar: 'bg:#444444', + Token.Scrollbar.Button: 'bg:#888888', + Token.Scrollbar.Arrow: 'bg:#222222 #ffffff', + + # Auto suggestion text. + Token.AutoSuggestion: '#666666', + + # When Control-C has been pressed. Grayed. + Token.Aborted: '#888888', +} + +default_style_extensions = DEFAULT_STYLE_EXTENSIONS # Old name. diff --git a/prompt_toolkit/styles/from_dict.py b/prompt_toolkit/styles/from_dict.py new file mode 100644 index 000000000..009dbcf64 --- /dev/null +++ b/prompt_toolkit/styles/from_dict.py @@ -0,0 +1,138 @@ +""" +Tool for creating styles from a dictionary. + +This is very similar to the Pygments style dictionary, with some additions: +- Support for reverse and blink. +- Support for ANSI color names. (These will map directly to the 16 terminal + colors.) +""" +from .base import Style, DEFAULT_ATTRS, ANSI_COLOR_NAMES +from .defaults import DEFAULT_STYLE_EXTENSIONS + +__all__ = ( + 'style_from_dict', +) + + +def _colorformat(text): + """ + Parse/validate color format. + + Like in Pygments, but also support the ANSI color names. + (These will map to the colors of the 16 color palette.) + """ + if text[0:1] == '#': + col = text[1:] + if len(col) == 6: + return col + elif len(col) == 3: + return col[0]*2 + col[1]*2 + col[2]*2 + elif text == '' or text in ANSI_COLOR_NAMES: + return text + + raise ValueError('Wrong color format %r' % text) + + +def style_from_dict(style_dict, include_defaults=True): + """ + Create a ``Style`` instance from a dictionary. + + The dictionary is equivalent to the ``Style.styles`` dictionary from + pygments, with a few additions: it supports 'reverse' and 'blink'. + + Usage:: + + style_from_dict({ + Token: '#ff0000 bold underline', + Token.Title: 'blink', + Token.SomethingElse: 'reverse', + }) + + :param include_defaults: Include the defaults (built-in) styling for + selected text, etc...) + """ + assert isinstance(style_dict, dict) + + if include_defaults: + s2 = {} + s2.update(DEFAULT_STYLE_EXTENSIONS) + s2.update(style_dict) + style_dict = s2 + + # Expand token inheritance and turn style description into Attrs. + token_to_attrs = {} + + # (Loop through the tokens in order. Sorting makes sure that + # we process the parent first.) + for ttype, styledef in sorted(style_dict.items()): + # Start from parent Attrs or default Attrs. + attrs = DEFAULT_ATTRS + + if 'noinherit' not in styledef: + for i in range(1, len(ttype) + 1): + try: + attrs = token_to_attrs[ttype[:-i]] + except KeyError: + pass + else: + break + + # Now update with the given attributes. + for part in styledef.split(): + if part == 'noinherit': + pass + elif part == 'bold': + attrs = attrs._replace(bold=True) + elif part == 'nobold': + attrs = attrs._replace(bold=False) + elif part == 'italic': + attrs = attrs._replace(italic=True) + elif part == 'noitalic': + attrs = attrs._replace(italic=False) + elif part == 'underline': + attrs = attrs._replace(underline=True) + elif part == 'nounderline': + attrs = attrs._replace(underline=False) + + # prompt_toolkit extensions. Not in Pygments. + elif part == 'blink': + attrs = attrs._replace(blink=True) + elif part == 'noblink': + attrs = attrs._replace(blink=False) + elif part == 'reverse': + attrs = attrs._replace(reverse=True) + elif part == 'noreverse': + attrs = attrs._replace(reverse=False) + + # Pygments properties that we ignore. + elif part in ('roman', 'sans', 'mono'): + pass + elif part.startswith('border:'): + pass + + # Colors. + + elif part.startswith('bg:'): + attrs = attrs._replace(bgcolor=_colorformat(part[3:])) + else: + attrs = attrs._replace(color=_colorformat(part)) + + token_to_attrs[ttype] = attrs + + return _StyleFromDict(token_to_attrs) + + +class _StyleFromDict(Style): + """ + Turn a dictionary that maps `Token` to `Attrs` into a style class. + + :param token_to_attrs: Dictionary that maps `Token` to `Attrs`. + """ + def __init__(self, token_to_attrs): + self.token_to_attrs = token_to_attrs + + def get_attrs_for_token(self, token): + return self.token_to_attrs.get(token, DEFAULT_ATTRS) + + def invalidation_hash(self): + return id(self.token_to_attrs) diff --git a/prompt_toolkit/styles/from_pygments.py b/prompt_toolkit/styles/from_pygments.py new file mode 100644 index 000000000..da3c12287 --- /dev/null +++ b/prompt_toolkit/styles/from_pygments.py @@ -0,0 +1,74 @@ +""" +Adaptor for building prompt_toolkit styles, starting from a Pygments style. + +Usage:: + + from pygments.styles.tango import TangoStyle + style = style_from_pygments(pygments_style_cls=TangoStyle) +""" +from __future__ import unicode_literals + +from .base import Style, Attrs +from .defaults import DEFAULT_STYLE_EXTENSIONS +from .from_dict import style_from_dict + +__all__ = ( + 'PygmentsStyle', + 'style_from_pygments', +) + + +# Following imports are only needed when a ``PygmentsStyle`` class is used. +try: + from pygments.style import Style as pygments_Style + from pygments.styles.default import DefaultStyle as pygments_DefaultStyle +except ImportError: + pygments_Style = None + pygments_DefaultStyle = None + + +def style_from_pygments(style_cls=pygments_DefaultStyle, + style_dict=None, + include_defaults=True): + """ + Shortcut to create a :class:`.Style` instance from a Pygments style class + and a style dictionary. + + :param style_cls: Pygments style class to start from. + :param style_dict: Dictionary for this style. `{Token: style}`. + :param include_defaults: (`bool`) Include prompt_toolkit extensions. + """ + assert style_dict is None or isinstance(style_dict, dict) + assert style_cls is None or issubclass(style_cls, pygments_Style) + + styles_dict = {} + + if style_cls is not None: + styles_dict.update(style_cls.styles) + + if style_dict is not None: + styles_dict.update(style_dict) + + return style_from_dict(styles_dict, include_defaults=include_defaults) + + +class PygmentsStyle(Style): + " Deprecated. " + def __new__(cls, pygments_style_cls): + assert issubclass(pygments_style_cls, pygments_Style) + return style_from_dict(pygments_style_cls.styles) + + def invalidation_hash(self): + pass + + @classmethod + def from_defaults(cls, style_dict=None, + pygments_style_cls=pygments_DefaultStyle, + include_extensions=True): + " Deprecated. " + return style_from_pygments( + style_cls=pygments_style_cls, + style_dict=style_dict, + include_defaults=include_extensions) + + diff --git a/tests/run_tests.py b/tests/run_tests.py index 82a17eb20..5338dd512 100755 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -10,6 +10,7 @@ from layout_tests import * from contrib_tests import * from utils_tests import * +from style_tests import * import unittest diff --git a/tests/style_tests.py b/tests/style_tests.py new file mode 100644 index 000000000..7814cae3e --- /dev/null +++ b/tests/style_tests.py @@ -0,0 +1,41 @@ +from __future__ import unicode_literals +from prompt_toolkit.styles import Attrs, style_from_dict +from prompt_toolkit.token import Token + +import unittest + + +class StyleFromDictTest(unittest.TestCase): + def test_style_from_dict(self): + style = style_from_dict({ + Token.A: '#ff0000 bold underline italic', + Token.B: 'bg:#00ff00 blink reverse', + }) + + expected = Attrs(color='ff0000', bgcolor=None, bold=True, + underline=True, italic=True, blink=False, reverse=False) + assert style.get_attrs_for_token(Token.A) == expected + + expected = Attrs(color=None, bgcolor='00ff00', bold=False, + underline=False, italic=False, blink=True, reverse=True) + assert style.get_attrs_for_token(Token.B) == expected + + def test_style_inheritance(self): + style = style_from_dict({ + Token: '#ff0000', + Token.A.B.C: 'bold', + Token.A.B.C.D: 'red', + Token.A.B.C.D.E: 'noinherit blink' + }) + + expected = Attrs(color='ff0000', bgcolor=None, bold=True, + underline=False, italic=False, blink=False, reverse=False) + assert style.get_attrs_for_token(Token.A.B.C) == expected + + expected = Attrs(color='red', bgcolor=None, bold=True, + underline=False, italic=False, blink=False, reverse=False) + assert style.get_attrs_for_token(Token.A.B.C.D) == expected + + expected = Attrs(color=None, bgcolor=None, bold=False, + underline=False, italic=False, blink=True, reverse=False) + assert style.get_attrs_for_token(Token.A.B.C.D.E) == expected