Merge pull request #696 from scopatz/rprompt

right prompt implementation
This commit is contained in:
Gil Forsyth 2016-02-23 19:29:40 -05:00
commit 76fcd989ee
11 changed files with 603 additions and 14 deletions

View file

@ -23,6 +23,8 @@ Current Developments
* ``?`` and ``??`` operator output now has colored titles, like in IPython. * ``?`` and ``??`` operator output now has colored titles, like in IPython.
* ``??`` will syntax highlight source code if pygments is available. * ``??`` will syntax highlight source code if pygments is available.
* Python mode output is now syntax highlighted if pygments is available. * Python mode output is now syntax highlighted if pygments is available.
* New ``$RIGHT_PROMPT`` environment variable for displaying right-aligned
text in prompt-toolkit shell.
**Changed:** **Changed:**

View file

@ -875,6 +875,7 @@ keyword arguments, which will be replaced automatically:
snail@home:~ >> # so does that! snail@home:~ >> # so does that!
By default, the following variables are available for use: By default, the following variables are available for use:
* ``user``: The username of the current user * ``user``: The username of the current user
* ``hostname``: The name of the host computer * ``hostname``: The name of the host computer
* ``cwd``: The current working directory * ``cwd``: The current working directory
@ -895,13 +896,35 @@ By default, the following variables are available for use:
You can also color your prompt easily by inserting keywords such as ``{GREEN}`` You can also color your prompt easily by inserting keywords such as ``{GREEN}``
or ``{BOLD_BLUE}``. Colors have the form shown below: or ``{BOLD_BLUE}``. Colors have the form shown below:
* ``(QUALIFIER\_)COLORNAME``: Inserts an ANSI color code * ``NO_COLOR``: Resets any previously used color codes
* ``COLORNAME`` can be any of: ``BLACK``, ``RED``, ``GREEN``, ``YELLOW``, * ``COLORNAME``: Inserts a color code for the following basic colors,
``BLUE``, ``PURPLE``, ``CYAN``, or ``WHITE`` which come in regular (dark) and intense (light) forms:
* ``QUALIFIER`` is optional and can be any of: ``BOLD``, ``UNDERLINE``,
``BACKGROUND``, ``INTENSE``, ``BOLD_INTENSE``, or - ``BLACK`` or ``INTENSE_BLACK``
``BACKGROUND_INTENSE`` - ``RED`` or ``INTENSE_RED``
* ``NO_COLOR``: Resets any previously used color codes - ``GREEN`` or ``INTENSE_GREEN``
- ``YELLOW`` or ``INTENSE_YELLOW``
- ``BLUE`` or ``INTENSE_BLUE``
- ``PURPLE`` or ``INTENSE_PURPLE``
- ``CYAN`` or ``INTENSE_CYAN``
- ``WHITE`` or ``INTENSE_WHITE``
* ``#HEX``: A ``#`` before a len-3 or len-6 hex code will use that
hex color, or the nearest approximation that that is supported by
the shell and terminal. For example, ``#fff`` and ``#fafad2`` are
both valid.
* ``BACKGROUND_`` may be added to the begining of a color name or hex
color to set a background color. For example, ``BACKGROUND_INTENSE_RED``
and ``BACKGROUND_#123456`` can both be used.
* ``bg#HEX`` or ``BG#HEX`` are shortcuts for setting a background hex color.
Thus you can set ``bg#0012ab`` or the uppercase version.
* ``BOLD_`` is a prefix qualifier that may be used with any foreground color.
For example, ``BOLD_RED`` and ``BOLD_#112233`` are OK!
* ``UNDERLINE_`` is a prefix qualifier that also may be used with any
foreground color. For example, ``UNDERLINE_GREEN``.
* Or any other combination of qualifiers, such as
``BOLD_UNDERLINE_INTENSE_BLACK``, which is the most metal color you
can use!
You can make use of additional variables beyond these by adding them to the You can make use of additional variables beyond these by adding them to the
``FORMATTER_DICT`` environment variable. The values in this dictionary ``FORMATTER_DICT`` environment variable. The values in this dictionary

View file

@ -6,6 +6,7 @@ import glob
import builtins import builtins
import platform import platform
import subprocess import subprocess
from collections import defaultdict
from contextlib import contextmanager from contextlib import contextmanager
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
@ -23,10 +24,13 @@ ON_MAC = (platform.system() == 'Darwin')
def sp(cmd): def sp(cmd):
return subprocess.check_output(cmd, universal_newlines=True) return subprocess.check_output(cmd, universal_newlines=True)
class DummyStyler():
styles = defaultdict(None.__class__)
class DummyBaseShell(BaseShell): class DummyBaseShell(BaseShell):
def __init__(self): def __init__(self):
pass self.styler = DummyStyler()
class DummyShell: class DummyShell:

View file

@ -75,7 +75,7 @@ def source_foreign(args, stdin=None):
"""Sources a file written in a foreign shell language.""" """Sources a file written in a foreign shell language."""
parser = _ensure_source_foreign_parser() parser = _ensure_source_foreign_parser()
ns = parser.parse_args(args) ns = parser.parse_args(args)
if ns.prevcmd is not None: if ns.prevcmd is not None:
pass # don't change prevcmd if given explicitly pass # don't change prevcmd if given explicitly
elif os.path.isfile(ns.files_or_code[0]): elif os.path.isfile(ns.files_or_code[0]):
# we have filename to source # we have filename to source
@ -191,6 +191,11 @@ def vox(args, stdin=None):
vox = Vox() vox = Vox()
return vox(args, stdin=stdin) return vox(args, stdin=stdin)
def mpl(args, stdin=None):
"""Hooks to matplotlib"""
from xonsh.mplhooks import show
show()
DEFAULT_ALIASES = { DEFAULT_ALIASES = {
'cd': cd, 'cd': cd,
@ -212,6 +217,7 @@ DEFAULT_ALIASES = {
'replay': replay_main, 'replay': replay_main,
'!!': bang_bang, '!!': bang_bang,
'!n': bang_n, '!n': bang_n,
'mpl': mpl,
'trace': trace, 'trace': trace,
'timeit': timeit_alias, 'timeit': timeit_alias,
'xonfig': xonfig, 'xonfig': xonfig,

View file

@ -1,7 +1,10 @@
"""Tools for helping with ANSI color codes.""" """Tools for helping with ANSI color codes."""
import re
import string import string
from warnings import warn from warnings import warn
RE_BACKGROUND = re.compile('(bg|bg#|bghex|background)')
def partial_color_format(template, style='default', cmap=None, hide=False): def partial_color_format(template, style='default', cmap=None, hide=False):
"""Formats a template string but only with respect to the colors. """Formats a template string but only with respect to the colors.
Another template string is returned, with the color values filled in. Another template string is returned, with the color values filled in.
@ -42,8 +45,23 @@ def partial_color_format(template, style='default', cmap=None, hide=False):
toks = [] toks = []
for literal, field, spec, conv in formatter.parse(template): for literal, field, spec, conv in formatter.parse(template):
toks.append(literal) toks.append(literal)
if field in cmap: if field is None:
pass
elif field in cmap:
toks.extend([esc, cmap[field], m]) toks.extend([esc, cmap[field], m])
elif '#' in field:
field = field.lower()
pre, _, post = field.partition('#')
f_or_b = '38' if RE_BACKGROUND.search(pre) is None else '48'
rgb, _, post = post.partition('_')
c256, _ = rgb_to_256(rgb)
color = f_or_b + ';5;' + c256
mods = pre + '_' + post
if 'underline' in mods:
color = '4;' + color
if 'bold' in mods:
color = '1;' + color
toks.extend([esc, color, m])
elif field is not None: elif field is not None:
toks.append(bopen) toks.append(bopen)
toks.append(field) toks.append(field)
@ -56,6 +74,294 @@ def partial_color_format(template, style='default', cmap=None, hide=False):
toks.append(bclose) toks.append(bclose)
return ''.join(toks) return ''.join(toks)
RGB_256 = {
'000000': '16',
'00005f': '17',
'000080': '04',
'000087': '18',
'0000af': '19',
'0000d7': '20',
'0000ff': '21',
'005f00': '22',
'005f5f': '23',
'005f87': '24',
'005faf': '25',
'005fd7': '26',
'005fff': '27',
'008000': '02',
'008080': '06',
'008700': '28',
'00875f': '29',
'008787': '30',
'0087af': '31',
'0087d7': '32',
'0087ff': '33',
'00af00': '34',
'00af5f': '35',
'00af87': '36',
'00afaf': '37',
'00afd7': '38',
'00afff': '39',
'00d700': '40',
'00d75f': '41',
'00d787': '42',
'00d7af': '43',
'00d7d7': '44',
'00d7ff': '45',
'00ff00': '46',
'00ff5f': '47',
'00ff87': '48',
'00ffaf': '49',
'00ffd7': '50',
'00ffff': '51',
'080808': '232',
'121212': '233',
'1c1c1c': '234',
'262626': '235',
'303030': '236',
'3a3a3a': '237',
'444444': '238',
'4e4e4e': '239',
'585858': '240',
'5f0000': '52',
'5f005f': '53',
'5f0087': '54',
'5f00af': '55',
'5f00d7': '56',
'5f00ff': '57',
'5f5f00': '58',
'5f5f5f': '59',
'5f5f87': '60',
'5f5faf': '61',
'5f5fd7': '62',
'5f5fff': '63',
'5f8700': '64',
'5f875f': '65',
'5f8787': '66',
'5f87af': '67',
'5f87d7': '68',
'5f87ff': '69',
'5faf00': '70',
'5faf5f': '71',
'5faf87': '72',
'5fafaf': '73',
'5fafd7': '74',
'5fafff': '75',
'5fd700': '76',
'5fd75f': '77',
'5fd787': '78',
'5fd7af': '79',
'5fd7d7': '80',
'5fd7ff': '81',
'5fff00': '82',
'5fff5f': '83',
'5fff87': '84',
'5fffaf': '85',
'5fffd7': '86',
'5fffff': '87',
'626262': '241',
'6c6c6c': '242',
'767676': '243',
'800000': '01',
'800080': '05',
'808000': '03',
'808080': '244',
'870000': '88',
'87005f': '89',
'870087': '90',
'8700af': '91',
'8700d7': '92',
'8700ff': '93',
'875f00': '94',
'875f5f': '95',
'875f87': '96',
'875faf': '97',
'875fd7': '98',
'875fff': '99',
'878700': '100',
'87875f': '101',
'878787': '102',
'8787af': '103',
'8787d7': '104',
'8787ff': '105',
'87af00': '106',
'87af5f': '107',
'87af87': '108',
'87afaf': '109',
'87afd7': '110',
'87afff': '111',
'87d700': '112',
'87d75f': '113',
'87d787': '114',
'87d7af': '115',
'87d7d7': '116',
'87d7ff': '117',
'87ff00': '118',
'87ff5f': '119',
'87ff87': '120',
'87ffaf': '121',
'87ffd7': '122',
'87ffff': '123',
'8a8a8a': '245',
'949494': '246',
'9e9e9e': '247',
'a8a8a8': '248',
'af0000': '124',
'af005f': '125',
'af0087': '126',
'af00af': '127',
'af00d7': '128',
'af00ff': '129',
'af5f00': '130',
'af5f5f': '131',
'af5f87': '132',
'af5faf': '133',
'af5fd7': '134',
'af5fff': '135',
'af8700': '136',
'af875f': '137',
'af8787': '138',
'af87af': '139',
'af87d7': '140',
'af87ff': '141',
'afaf00': '142',
'afaf5f': '143',
'afaf87': '144',
'afafaf': '145',
'afafd7': '146',
'afafff': '147',
'afd700': '148',
'afd75f': '149',
'afd787': '150',
'afd7af': '151',
'afd7d7': '152',
'afd7ff': '153',
'afff00': '154',
'afff5f': '155',
'afff87': '156',
'afffaf': '157',
'afffd7': '158',
'afffff': '159',
'b2b2b2': '249',
'bcbcbc': '250',
'c0c0c0': '07',
'c6c6c6': '251',
'd0d0d0': '252',
'd70000': '160',
'd7005f': '161',
'd70087': '162',
'd700af': '163',
'd700d7': '164',
'd700ff': '165',
'd75f00': '166',
'd75f5f': '167',
'd75f87': '168',
'd75faf': '169',
'd75fd7': '170',
'd75fff': '171',
'd78700': '172',
'd7875f': '173',
'd78787': '174',
'd787af': '175',
'd787d7': '176',
'd787ff': '177',
'd7af00': '178',
'd7af5f': '179',
'd7af87': '180',
'd7afaf': '181',
'd7afd7': '182',
'd7afff': '183',
'd7d700': '184',
'd7d75f': '185',
'd7d787': '186',
'd7d7af': '187',
'd7d7d7': '188',
'd7d7ff': '189',
'd7ff00': '190',
'd7ff5f': '191',
'd7ff87': '192',
'd7ffaf': '193',
'd7ffd7': '194',
'd7ffff': '195',
'dadada': '253',
'e4e4e4': '254',
'eeeeee': '255',
'ff0000': '196',
'ff005f': '197',
'ff0087': '198',
'ff00af': '199',
'ff00d7': '200',
'ff00ff': '201',
'ff5f00': '202',
'ff5f5f': '203',
'ff5f87': '204',
'ff5faf': '205',
'ff5fd7': '206',
'ff5fff': '207',
'ff8700': '208',
'ff875f': '209',
'ff8787': '210',
'ff87af': '211',
'ff87d7': '212',
'ff87ff': '213',
'ffaf00': '214',
'ffaf5f': '215',
'ffaf87': '216',
'ffafaf': '217',
'ffafd7': '218',
'ffafff': '219',
'ffd700': '220',
'ffd75f': '221',
'ffd787': '222',
'ffd7af': '223',
'ffd7d7': '224',
'ffd7ff': '225',
'ffff00': '226',
'ffff5f': '227',
'ffff87': '228',
'ffffaf': '229',
'ffffd7': '230',
'ffffff': '231',
}
RE_RGB3 = re.compile(r'(.)(.)(.)')
RE_RGB6 = re.compile(r'(..)(..)(..)')
def rgb_to_ints(rgb):
"""Converts an RGB string into a tuple of ints."""
if len(rgb) == 6:
return tuple([int(h, 16) for h in RE_RGB6.split(rgb)[1:4]])
else:
return tuple([int(h*2, 16) for h in RE_RGB3.split(rgb)[1:4]])
def rgb_to_256(rgb):
"""Find the closest ANSI 256 approximation to the given RGB value.
Thanks to Micah Elliott (http://MicahElliott.com) for colortrans.py
"""
rgb = rgb.lstrip('#')
if len(rgb) == 0:
return '0', '000000'
incs = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff)
# Break 6-char RGB code into 3 integer vals.
parts = rgb_to_ints(rgb)
res = []
for part in parts:
i = 0
while i < len(incs)-1:
s, b = incs[i], incs[i+1] # smaller, bigger
if s <= part <= b:
s1 = abs(s - part)
b1 = abs(b - part)
if s1 < b1: closest = s
else: closest = b
res.append(closest)
break
i += 1
res = ''.join([('%02.x' % i) for i in res])
equiv = RGB_256[res]
return equiv, res
DEFAULT_STYLE = { DEFAULT_STYLE = {
# Reset # Reset

View file

@ -79,6 +79,7 @@ DEFAULT_ENSURERS = {
re.compile('\w*PATH$'): (is_env_path, str_to_env_path, env_path_to_str), re.compile('\w*PATH$'): (is_env_path, str_to_env_path, env_path_to_str),
'PATHEXT': (is_env_path, str_to_env_path, env_path_to_str), 'PATHEXT': (is_env_path, str_to_env_path, env_path_to_str),
'RAISE_SUBPROC_ERROR': (is_bool, to_bool, bool_to_str), 'RAISE_SUBPROC_ERROR': (is_bool, to_bool, bool_to_str),
'RIGHT_PROMPT': (is_string, ensure_string, ensure_string),
'TEEPTY_PIPE_DELAY': (is_float, float, str), 'TEEPTY_PIPE_DELAY': (is_float, float, str),
'XONSHRC': (is_env_path, str_to_env_path, env_path_to_str), 'XONSHRC': (is_env_path, str_to_env_path, env_path_to_str),
'XONSH_COLOR_STYLE': (is_string, ensure_string, ensure_string), 'XONSH_COLOR_STYLE': (is_string, ensure_string, ensure_string),
@ -181,6 +182,7 @@ DEFAULT_VALUES = {
'PUSHD_MINUS': False, 'PUSHD_MINUS': False,
'PUSHD_SILENT': False, 'PUSHD_SILENT': False,
'RAISE_SUBPROC_ERROR': False, 'RAISE_SUBPROC_ERROR': False,
'RIGHT_PROMPT': '',
'SHELL_TYPE': 'best', 'SHELL_TYPE': 'best',
'SUGGEST_COMMANDS': True, 'SUGGEST_COMMANDS': True,
'SUGGEST_MAX_NUM': 5, 'SUGGEST_MAX_NUM': 5,
@ -349,6 +351,10 @@ DEFAULT_DOCS = {
'This is most useful in xonsh scripts or modules where failures ' 'This is most useful in xonsh scripts or modules where failures '
'should cause an end to execution. This is less useful at a terminal.' 'should cause an end to execution. This is less useful at a terminal.'
'The error that is raised is a subprocess.CalledProcessError.'), 'The error that is raised is a subprocess.CalledProcessError.'),
'RIGHT_PROMPT': VarDocs('Template string for right-aligned text '
'at the prompt. This may be parameterized in the same way as '
'the $PROMPT variable. Currently, this is only available in the '
'prompt-toolkit shell.'),
'SHELL_TYPE': VarDocs( 'SHELL_TYPE': VarDocs(
'Which shell is used. Currently two base shell types are supported:\n\n' 'Which shell is used. Currently two base shell types are supported:\n\n'
" - 'readline' that is backed by Python's readline module\n" " - 'readline' that is backed by Python's readline module\n"

63
xonsh/mplhooks.py Normal file
View file

@ -0,0 +1,63 @@
"""Matplotlib hooks, for what its worth."""
import shutil
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from xonsh.tools import print_color
def figure_to_rgb_array(fig, width, height):
"""Converts figure to a numpy array of rgb values
Forked from http://www.icare.univ-lille1.fr/wiki/index.php/How_to_convert_a_matplotlib_figure_to_a_numpy_array_or_a_PIL_image
"""
w, h = fig.canvas.get_width_height()
dpi = fig.get_dpi()
fig.set_size_inches(width/dpi, height/dpi, forward=True)
width, height = fig.canvas.get_width_height()
ax = fig.gca()
ax.set_xticklabels([])
ax.set_yticklabels([])
fig.set_tight_layout(True)
fig.set_frameon(False)
fig.set_facecolor('w')
font_size = matplotlib.rcParams['font.size']
matplotlib.rcParams.update({'font.size': 1})
# Draw the renderer and get the RGB buffer from the figure
fig.canvas.draw()
buf = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8)
buf.shape = (height, width, 3)
# clean up and return
matplotlib.rcParams.update({'font.size': font_size})
return buf
def buf_to_color_str(buf):
"""Converts an RGB array to a xonsh color string."""
space = ' '
pix = '{{bg#{0:02x}{1:02x}{2:02x}}} '
pixels = []
for h in range(buf.shape[0]):
last = None
for w in range(buf.shape[1]):
rgb = buf[h,w]
if last is not None and (last == rgb).all():
pixels.append(space)
else:
pixels.append(pix.format(*rgb))
last = rgb
pixels.append('{NO_COLOR}\n')
pixels[-1] = pixels[-1].rstrip()
return ''.join(pixels)
def show():
fig = plt.gcf()
w, h = shutil.get_terminal_size()
h -= 1 # leave space for next prompt
buf = figure_to_rgb_array(fig, w, h)
s = buf_to_color_str(buf)
print_color(s)

View file

@ -58,11 +58,16 @@ class PromptToolkitShell(BaseShell):
multicolumn = (completions_display == 'multi') multicolumn = (completions_display == 'multi')
self.styler.style_name = env.get('XONSH_COLOR_STYLE') self.styler.style_name = env.get('XONSH_COLOR_STYLE')
completer = None if completions_display == 'none' else self.pt_completer completer = None if completions_display == 'none' else self.pt_completer
prompt_tokens = self.prompt_tokens(None)
get_prompt_tokens = lambda cli: prompt_tokens
rprompt_tokens = self.rprompt_tokens(None)
get_rprompt_tokens = lambda cli: rprompt_tokens
with self.prompter: with self.prompter:
line = self.prompter.prompt( line = self.prompter.prompt(
mouse_support=mouse_support, mouse_support=mouse_support,
auto_suggest=auto_suggest, auto_suggest=auto_suggest,
get_prompt_tokens=self.prompt_tokens, get_prompt_tokens=get_prompt_tokens,
get_rprompt_tokens=get_rprompt_tokens,
style=PygmentsStyle(xonsh_style_proxy(self.styler)), style=PygmentsStyle(xonsh_style_proxy(self.styler)),
completer=completer, completer=completer,
lexer=PygmentsLexer(XonshLexer), lexer=PygmentsLexer(XonshLexer),
@ -127,6 +132,20 @@ class PromptToolkitShell(BaseShell):
self.settitle() self.settitle()
return toks return toks
def rprompt_tokens(self, cli):
"""Returns a list of (token, str) tuples for the current right
prompt.
"""
p = builtins.__xonsh_env__.get('RIGHT_PROMPT')
if len(p) == 0:
return []
try:
p = partial_format_prompt(p)
except Exception: # pylint: disable=broad-except
print_exception()
toks = partial_color_tokenize(p)
return toks
def format_color(self, string, **kwargs): def format_color(self, string, **kwargs):
"""Formats a color string using Pygments. This, therefore, returns """Formats a color string using Pygments. This, therefore, returns
a list of (Token, str) tuples. a list of (Token, str) tuples.

View file

@ -4,6 +4,8 @@ from prompt_toolkit.utils import DummyContext
from prompt_toolkit.shortcuts import (create_prompt_application, from prompt_toolkit.shortcuts import (create_prompt_application,
create_eventloop, create_asyncio_eventloop, create_output) create_eventloop, create_asyncio_eventloop, create_output)
from xonsh.shell import prompt_toolkit_version_info
class Prompter(object): class Prompter(object):
def __init__(self, cli=None, *args, **kwargs): def __init__(self, cli=None, *args, **kwargs):
@ -18,6 +20,7 @@ class Prompter(object):
will be created when the prompt() method is called. will be created when the prompt() method is called.
""" """
self.cli = cli self.cli = cli
self.major_minor = prompt_toolkit_version_info()[:2]
def __enter__(self): def __enter__(self):
self.reset() self.reset()
@ -61,6 +64,8 @@ class Prompter(object):
# Create CommandLineInterface. # Create CommandLineInterface.
if self.cli is None: if self.cli is None:
if self.major_minor <= (0, 57):
kwargs.pop('get_rprompt_tokens', None)
cli = CommandLineInterface( cli = CommandLineInterface(
application=create_prompt_application(message, **kwargs), application=create_prompt_application(message, **kwargs),
eventloop=eventloop, eventloop=eventloop,

View file

@ -1,8 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Hooks for pygments syntax highlighting.""" """Hooks for pygments syntax highlighting."""
import re
import string import string
import builtins
from warnings import warn from warnings import warn
from collections import ChainMap from collections import ChainMap
from collections.abc import MutableMapping
from pygments.lexer import inherit, bygroups, using, this from pygments.lexer import inherit, bygroups, using, this
from pygments.token import (Keyword, Name, Comment, String, Error, Number, from pygments.token import (Keyword, Name, Comment, String, Error, Number,
@ -82,26 +85,131 @@ XonshSubprocLexer.tokens['root'] = [
Color = Token.Color # alias to new color token namespace Color = Token.Color # alias to new color token namespace
RE_BACKGROUND = re.compile('(BG#|BGHEX|BACKGROUND)')
def norm_name(name):
"""Normalizes a color name."""
return name.replace('#', 'HEX').replace('BGHEX', 'BACKGROUND_HEX')
def color_by_name(name, fg=None, bg=None):
"""Converts a color name to a color token, foreground name,
and background name. Will take into consideration current foreground
and background colors, if provided.
Parameters
----------
name : str
Color name.
fg : str, optional
Foreground color name.
bg : str, optional
Background color name.
Returns
-------
tok : Token
Pygments Token.Color subclass
fg : str or None
New computed foreground color name.
bg : str or None
New computed background color name.
"""
name = name.upper()
if name == 'NO_COLOR':
return Color.NO_COLOR, None, None
m = RE_BACKGROUND.search(name)
if m is None: # must be foreground color
fg = norm_name(name)
else:
bg = norm_name(name)
# assmble token
if fg is None and bg is None:
tokname = 'NO_COLOR'
elif fg is None:
tokname = bg
elif bg is None:
tokname = fg
else:
tokname = fg + '__' + bg
tok = getattr(Color, tokname)
return tok, fg, bg
def code_by_name(name, styles):
"""Converts a token name into a pygments-style color code.
Parameters
----------
name : str
Color token name.
styles : Mapping
Mapping for looking up non-hex colors
Returns
-------
code : str
Pygments style color code.
"""
fg, _, bg = name.lower().partition('__')
if fg.startswith('background_'):
fg, bg = bg, fg
codes = []
# foreground color
if len(fg) == 0:
pass
elif 'hex' in fg:
for p in fg.split('_'):
codes.append('#'+p[3:] if p.startswith('hex') else p)
else:
fgtok = getattr(Color, fg.upper())
if fgtok in styles:
codes.append(styles[fgtok])
else:
codes += fg.split('_')
# background color
if len(bg) == 0:
pass
elif bg.startswith('background_hex'):
codes.append('bg:#'+bg[14:])
else:
bgtok = getattr(Color, bg.upper())
if bgtok in styles:
codes.append(styles[bgtok])
else:
codes.append(bg.replace('background_', 'bg:'))
code = ' '.join(codes)
return code
def partial_color_tokenize(template): def partial_color_tokenize(template):
"""Toeknizes a template string containing colors. Will return a list """Toeknizes a template string containing colors. Will return a list
of tuples mapping the token to the string which has that color. of tuples mapping the token to the string which has that color.
These sub-strings maybe templates themselves. These sub-strings maybe templates themselves.
""" """
formatter = string.Formatter() formatter = string.Formatter()
if hasattr(builtins, '__xonsh_shell__'):
styles = __xonsh_shell__.shell.styler.styles
else:
styles = None
bopen = '{' bopen = '{'
bclose = '}' bclose = '}'
colon = ':' colon = ':'
expl = '!' expl = '!'
color = Color.NO_COLOR color = Color.NO_COLOR
fg = bg = None
value = '' value = ''
toks = [] toks = []
for literal, field, spec, conv in formatter.parse(template): for literal, field, spec, conv in formatter.parse(template):
if field in KNOWN_COLORS: if field is None:
value += literal value += literal
next_color = getattr(Color, field) elif field in KNOWN_COLORS or '#' in field:
value += literal
next_color, fg, bg = color_by_name(field, fg, bg)
if next_color is not color: if next_color is not color:
if len(value) > 0: if len(value) > 0:
toks.append((color, value)) toks.append((color, value))
if styles is not None:
styles[color] # ensure color is available
color = next_color color = next_color
value = '' value = ''
elif field is not None: elif field is not None:
@ -117,9 +225,49 @@ def partial_color_tokenize(template):
else: else:
value += literal value += literal
toks.append((color, value)) toks.append((color, value))
if styles is not None:
styles[color] # ensure color is available
return toks return toks
class CompoundColorMap(MutableMapping):
"""Looks up color tokes by name, potentailly generating the value
from the lookup.
"""
def __init__(self, styles, *args, **kwargs):
self.styles = styles
self.colors = dict(*args, **kwargs)
def __getitem__(self, key):
if key in self.colors:
return self.colors[key]
if key in self.styles:
value = self.styles[key]
self[key] = value
return value
if key is Color:
raise KeyError
pre, _, name = str(key).rpartition('.')
if pre != 'Token.Color':
raise KeyError
value = code_by_name(name, self.styles)
self[key] = value
return value
def __setitem__(self, key, value):
self.colors[key] = value
def __delitem__(self, key):
del self.colors[key]
def __iter__(self):
yield from self.colors.keys()
def __len__(self):
return len(self.colors)
class XonshStyle(Style): class XonshStyle(Style):
"""A xonsh pygments style that will dispatch to the correct color map """A xonsh pygments style that will dispatch to the correct color map
by using a ChainMap. The style_name property may be used to reset by using a ChainMap. The style_name property may be used to reset
@ -156,7 +304,8 @@ class XonshStyle(Style):
smap = get_style_by_name(value)().styles smap = get_style_by_name(value)().styles
except (ImportError, pygments.util.ClassNotFound): except (ImportError, pygments.util.ClassNotFound):
smap = XONSH_BASE_STYLE smap = XONSH_BASE_STYLE
self.styles = ChainMap(self.trap, cmap, PTK_STYLE, smap) compound = CompoundColorMap(ChainMap(self.trap, cmap, PTK_STYLE, smap))
self.styles = ChainMap(self.trap, cmap, PTK_STYLE, smap, compound)
self._style_name = value self._style_name = value
@style_name.deleter @style_name.deleter

View file

@ -33,6 +33,12 @@ def prompt_toolkit_version():
return getattr(prompt_toolkit, '__version__', '<0.57') return getattr(prompt_toolkit, '__version__', '<0.57')
def prompt_toolkit_version_info():
"""Gets the prompt toolkit version info tuple."""
v = prompt_toolkit_version().strip('<>+-=.')
return tuple(map(int, v.split('.')))
def best_shell_type(): def best_shell_type():
"""Gets the best shell type that is available""" """Gets the best shell type that is available"""
if ON_WINDOWS or is_prompt_toolkit_available(): if ON_WINDOWS or is_prompt_toolkit_available():