Merge pull request #3933 from dyuri/ptk_support_for_themes

PTK support for custom themes (second try)
This commit is contained in:
Anthony Scopatz 2020-11-09 22:09:06 -06:00 committed by GitHub
commit 996892aec8
Failed to generate hash of commit
10 changed files with 285 additions and 79 deletions

1
.gitignore vendored
View file

@ -83,3 +83,4 @@ venv/
# mypy
.dmypy.json
.mypy_cache

View file

@ -0,0 +1,31 @@
**Added:**
* PTK style rules can be defined in custom styles using the ``Token.PTK`` token prefix.
For example ``custom_style["Token.PTK.CompletionMenu.Completion.Current"] = "bg:#ff0000 #fff"`` sets the ``completion-menu.completion.current`` PTK style to white on red.
* Added new environment variable ``XONSH_STYLE_OVERRIDES``. It's a dictionary containing pygments/ptk style definitions that overrides the styles defined by ``XONSH_COLOR_STYLE``.
For example::
$XONSH_STYLE_OVERRIDES["Token.Literal.String.Single"] = "#00ff00" # green 'strings' (pygments)
$XONSH_STYLE_OVERRIDES["completion-menu"] = "bg:#ffff00 #000" # black on yellow completion (ptk)
$XONSH_STYLE_OVERRIDES["Token.PTK.CompletionMenu.Completion.Current"] = "bg:#ff0000 #fff" # current completion is white on red (ptk via pygments)
**Changed:**
* <news item>
**Deprecated:**
* ``PTK_STYLE_OVERRIDES`` has been deprecated, its function replaced by ``XONSH_STYLE_OVERRIDES``
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -147,12 +147,23 @@ def test_ansi_color_name_to_escape_code_for_all_styles(color, style):
[
("test1", {}, {}),
("test2", {"Color.RED": "#ff0000"}, {"RED": "38;5;196"}),
("test3", {"BOLD_RED": "bold #ff0000"}, {"BOLD_RED": "1;38;5;196"}),
("test3", {"Token.Color.RED": "#ff0000"}, {"RED": "38;5;196"}),
("test4", {"BOLD_RED": "bold #ff0000"}, {"BOLD_RED": "1;38;5;196"}),
(
"test4",
"test5",
{"INTENSE_RED": "italic underline bg:#ff0000 #ff0000"},
{"INTENSE_RED": "3;4;48;5;196;38;5;196"},
),
(
"test6",
{"INTENSE_GREEN": "reverse blink hidden bg:#ff0000 #ff0000"},
{"INTENSE_GREEN": "7;5;8;48;5;196;38;5;196"},
),
(
"test6",
{"INTENSE_BLUE": "noreverse noblink nohidden bg:#ff0000 #ff0000"},
{"INTENSE_BLUE": "27;25;28;48;5;196;38;5;196"},
),
],
)
def test_register_custom_ansi_style(name, styles, refrules):

View file

@ -361,6 +361,11 @@ def test_colorize_file_ca(xonsh_builtins_LS_COLORS, monkeypatch):
{"Literal.String.Single": "#ff0000"},
{Token.Literal.String.Single: "#ff0000"},
), # short str key
(
"test5",
{"completion-menu.completion.current": "#00ff00"},
{Token.PTK.CompletionMenu.Completion.Current: "#00ff00"},
), # ptk style
],
)
def test_register_custom_pygments_style(name, styles, refrules):

View file

@ -21,6 +21,23 @@ from xonsh.color_tools import (
from xonsh.tools import FORMATTER
# pygments modifier to ANSI escape code mapping
_PART_STYLE_CODE_MAPPING = {
"bold": "1",
"nobold": "21",
"italic": "3",
"noitalic": "23",
"underline": "4",
"nounderline": "24",
"blink": "5",
"noblink": "25",
"reverse": "7",
"noreverse": "27",
"hidden": "8",
"nohidden": "28",
}
def _ensure_color_map(style="default", cmap=None):
if cmap is not None:
pass
@ -146,6 +163,9 @@ def ansi_partial_color_format(template, style="default", cmap=None, hide=False):
def _ansi_partial_color_format_main(template, style="default", cmap=None, hide=False):
cmap = _ensure_color_map(style=style, cmap=cmap)
overrides = builtins.__xonsh__.env["XONSH_STYLE_OVERRIDES"]
if overrides:
cmap.update(_style_dict_to_ansi(overrides))
esc = ("\001" if hide else "") + "\033["
m = "m" + ("\002" if hide else "")
bopen = "{"
@ -1090,12 +1110,8 @@ def _pygments_to_ansi_style(style):
ansi_style_list = []
parts = style.split(" ")
for part in parts:
if part == "bold":
ansi_style_list.append("1")
elif part == "italic":
ansi_style_list.append("3")
elif part == "underline":
ansi_style_list.append("4")
if part in _PART_STYLE_CODE_MAPPING:
ansi_style_list.append(_PART_STYLE_CODE_MAPPING[part])
elif part[:3] == "bg:":
ansi_style_list.append("48;5;" + rgb2short(part[3:])[0])
else:
@ -1104,6 +1120,18 @@ def _pygments_to_ansi_style(style):
return ";".join(ansi_style_list)
def _style_dict_to_ansi(styles):
"""Converts pygments like style dict to ANSI rules"""
ansi_style = {}
for token, style in styles.items():
token = str(token) # convert pygments token to str
parts = token.split(".")
if len(parts) == 1 or parts[-2] == "Color":
ansi_style[parts[-1]] = _pygments_to_ansi_style(style)
return ansi_style
def register_custom_ansi_style(name, styles, base="default"):
"""Register custom ANSI style.
@ -1118,11 +1146,7 @@ def register_custom_ansi_style(name, styles, base="default"):
"""
base_style = ANSI_STYLES[base].copy()
for token, style in styles.items():
token = str(token) # convert pygments token to str
parts = token.split(".")
if len(parts) == 1 or parts[-2] == "Color":
base_style[parts[-1]] = _pygments_to_ansi_style(style)
base_style.update(_style_dict_to_ansi(styles))
ANSI_STYLES[name] = base_style

View file

@ -30,8 +30,6 @@ from xonsh.platform import (
os_environ,
)
from xonsh.style_tools import PTK2_STYLE
from xonsh.tools import (
always_true,
always_false,
@ -1230,8 +1228,8 @@ def DEFAULT_VARS():
is_str_str_dict,
to_str_str_dict,
dict_to_str,
dict(PTK2_STYLE),
"A dictionary containing custom prompt_toolkit style definitions.",
{},
"A dictionary containing custom prompt_toolkit style definitions. (deprecated)",
),
"PUSHD_MINUS": Var(
is_bool,
@ -1731,6 +1729,18 @@ def DEFAULT_VARS():
"Whether or not to store the ``stdout`` and ``stderr`` streams in the "
"history files.",
),
"XONSH_STYLE_OVERRIDES": Var(
is_str_str_dict,
to_str_str_dict,
dict_to_str,
{},
"A dictionary containing custom prompt_toolkit/pygments style definitions.\n"
"The following style definitions are supported:\n\n"
" - ``pygments.token.Token`` - ``$XONSH_STYLE_OVERRIDES[Token.Keyword] = '#ff0000'``\n"
" - pygments token name (string) - ``$XONSH_STYLE_OVERRIDES['Token.Keyword'] = '#ff0000'``\n"
" - ptk style name (string) - ``$XONSH_STYLE_OVERRIDES['pygments.keyword'] = '#ff0000'``\n\n"
"(The rules above are all have the same effect.)",
),
"XONSH_TRACE_SUBPROC": Var(
is_bool,
to_bool,

View file

@ -10,7 +10,7 @@ from xonsh.events import events
from xonsh.base_shell import BaseShell
from xonsh.ptk_shell.formatter import PTKPromptFormatter
from xonsh.shell import transform_command
from xonsh.tools import print_exception, carriage_return
from xonsh.tools import print_exception, print_warning, carriage_return
from xonsh.platform import HAS_PYGMENTS, ON_WINDOWS, ON_POSIX
from xonsh.style_tools import partial_color_tokenize, _TokenType, DEFAULT_STYLE_DICT
from xonsh.lazyimps import pygments, pyghooks, winutils
@ -33,13 +33,11 @@ from prompt_toolkit.shortcuts import CompleteStyle
from prompt_toolkit.shortcuts.prompt import PromptSession
from prompt_toolkit.formatted_text import PygmentsTokens, to_formatted_text
from prompt_toolkit.styles import merge_styles, Style
from prompt_toolkit.styles.pygments import (
style_from_pygments_cls,
style_from_pygments_dict,
)
from prompt_toolkit.styles.pygments import pygments_token_to_classname
ANSI_OSC_PATTERN = re.compile("\x1b].*?\007")
CAPITAL_PATTERN = re.compile(r"([a-z])([A-Z])")
Token = _TokenType()
events.transmogrify("on_ptk_create", "LoadEvent")
@ -92,6 +90,46 @@ def remove_ansi_osc(prompt):
return prompt, osc_tokens
def _pygments_token_to_classname(token):
"""Converts pygments Tokens, token names (strings) to PTK style names."""
if token and isinstance(token, str):
# if starts with non capital letter => leave it as it is
if token[0].islower():
return token
# if starts with capital letter => pygments token name
if token.startswith("Token."):
token = token[6:]
# short colors - all caps
if token == token.upper():
token = "color." + token
return "pygments." + token.lower()
return pygments_token_to_classname(token)
def _style_from_pygments_dict(pygments_dict):
"""Custom implementation of ``style_from_pygments_dict`` that supports PTK specific
(``Token.PTK``) styles.
"""
pygments_style = []
for token, style in pygments_dict.items():
# if ``Token.PTK`` then add it as "native" PTK style too
if str(token).startswith("Token.PTK"):
key = CAPITAL_PATTERN.sub(r"\1-\2", str(token)[10:]).lower()
pygments_style.append((key, style))
pygments_style.append((_pygments_token_to_classname(token), style))
return Style(pygments_style)
def _style_from_pygments_cls(pygments_cls):
"""Custom implementation of ``style_from_pygments_cls`` that supports PTK specific
(``Token.PTK``) styles.
"""
return _style_from_pygments_dict(pygments_cls.styles)
class PromptToolkitShell(BaseShell):
"""The xonsh shell for prompt_toolkit v2 and later."""
@ -112,6 +150,7 @@ class PromptToolkitShell(BaseShell):
self.prompt_formatter = PTKPromptFormatter(self.prompter)
self.pt_completer = PromptToolkitCompleter(self.completer, self.ctx, self)
self.key_bindings = load_xonsh_bindings()
self._overrides_deprecation_warning_shown = False
# Store original `_history_matches` in case we need to restore it
self._history_matches_orig = self.prompter.default_buffer._history_matches
@ -202,22 +241,53 @@ class PromptToolkitShell(BaseShell):
if env.get("COLOR_INPUT"):
events.on_timingprobe.fire(name="on_pre_prompt_style")
style_overrides_env = env.get("PTK_STYLE_OVERRIDES", {}).copy()
if (
len(style_overrides_env) > 0
and not self._overrides_deprecation_warning_shown
):
print_warning(
"$PTK_STYLE_OVERRIDES is deprecated, use $XONSH_STYLE_OVERRIDES instead!"
)
self._overrides_deprecation_warning_shown = True
style_overrides_env.update(env.get("XONSH_STYLE_OVERRIDES", {}))
if HAS_PYGMENTS:
prompt_args["lexer"] = PygmentsLexer(pyghooks.XonshLexer)
style = style_from_pygments_cls(pyghooks.xonsh_style_proxy(self.styler))
style = _style_from_pygments_cls(
pyghooks.xonsh_style_proxy(self.styler)
)
if len(self.styler.non_pygments_rules) > 0:
try:
style = merge_styles(
[
style,
_style_from_pygments_dict(
self.styler.non_pygments_rules
),
]
)
except (AttributeError, TypeError, ValueError) as style_exception:
print_warning(
f"Error applying style override!\n{style_exception}\n"
)
else:
style = style_from_pygments_dict(DEFAULT_STYLE_DICT)
style = _style_from_pygments_dict(DEFAULT_STYLE_DICT)
if len(style_overrides_env) > 0:
try:
style = merge_styles(
[style, _style_from_pygments_dict(style_overrides_env)]
)
except (AttributeError, TypeError, ValueError) as style_exception:
print_warning(
f"Error applying style override!\n{style_exception}\n"
)
prompt_args["style"] = style
events.on_timingprobe.fire(name="on_post_prompt_style")
style_overrides_env = env.get("PTK_STYLE_OVERRIDES")
if style_overrides_env:
try:
style_overrides = Style.from_dict(style_overrides_env)
prompt_args["style"] = merge_styles([style, style_overrides])
except (AttributeError, TypeError, ValueError):
print_exception()
if env["ENABLE_ASYNC_PROMPT"]:
# once the prompt is done, update it in background as each future is completed
prompt_args["pre_run"] = self.prompt_formatter.start_update
@ -363,7 +433,9 @@ class PromptToolkitShell(BaseShell):
tokens = partial_color_tokenize(string)
if force_string and HAS_PYGMENTS:
env = builtins.__xonsh__.env
style_overrides_env = env.get("XONSH_STYLE_OVERRIDES", {})
self.styler.style_name = env.get("XONSH_COLOR_STYLE")
self.styler.override(style_overrides_env)
proxy_style = pyghooks.xonsh_style_proxy(self.styler)
formatter = pyghooks.XonshTerminal256Formatter(style=proxy_style)
s = pygments.format(tokens, formatter)
@ -382,14 +454,21 @@ class PromptToolkitShell(BaseShell):
# assume this is a list of (Token, str) tuples and just print
tokens = string
tokens = PygmentsTokens(tokens)
env = builtins.__xonsh__.env
style_overrides_env = env.get("XONSH_STYLE_OVERRIDES", {})
if HAS_PYGMENTS:
env = builtins.__xonsh__.env
self.styler.style_name = env.get("XONSH_COLOR_STYLE")
proxy_style = style_from_pygments_cls(
self.styler.override(style_overrides_env)
proxy_style = _style_from_pygments_cls(
pyghooks.xonsh_style_proxy(self.styler)
)
else:
proxy_style = style_from_pygments_dict(DEFAULT_STYLE_DICT)
proxy_style = merge_styles(
[
_style_from_pygments_dict(DEFAULT_STYLE_DICT),
_style_from_pygments_dict(style_overrides_env),
]
)
ptk_print(
tokens, style=proxy_style, end=end, include_default_pygments_style=False
)

View file

@ -52,7 +52,7 @@ from xonsh.color_tools import (
iscolor,
warn_deprecated_no_color,
)
from xonsh.style_tools import norm_name
from xonsh.style_tools import norm_name, DEFAULT_STYLE_DICT
from xonsh.lazyimps import terminal256, html
from xonsh.platform import (
os_environ,
@ -72,6 +72,26 @@ from xonsh.events import events
Color = Token.Color # alias to new color token namespace
# style rules that are not supported by pygments are stored here
NON_PYGMENTS_RULES: tp.Dict[str, tp.Dict[str, str]] = {}
# style modifiers not handled by pygments (but supported by ptk)
PTK_SPECIFIC_VALUES = frozenset(
{"reverse", "noreverse", "hidden", "nohidden", "blink", "noblink"}
)
# Generate fallback style dict from non-pygments styles
# (Let pygments handle the defaults where it can)
FALLBACK_STYLE_DICT = LazyObject(
lambda: {
token: value
for token, value in DEFAULT_STYLE_DICT.items()
if str(token).startswith("Token.PTK")
},
globals(),
"FALLBACK_STYLE_DICT",
)
def color_by_name(name, fg=None, bg=None):
"""Converts a color name to a color token, foreground name,
@ -234,7 +254,7 @@ def color_token_by_name(xc: tuple, styles=None) -> _TokenType:
tokName += "__" + xc[1]
token = getattr(Color, norm_name(tokName))
if token not in styles:
if token not in styles or not styles[token]:
styles[token] = pc
return token
@ -382,8 +402,13 @@ class XonshStyle(Style):
self.background_color = style_obj.background_color
except (ImportError, pygments.util.ClassNotFound):
self._smap = XONSH_BASE_STYLE.copy()
compound = CompoundColorMap(ChainMap(self.trap, cmap, PTK_STYLE, self._smap))
self.styles = ChainMap(self.trap, cmap, PTK_STYLE, self._smap, compound)
compound = CompoundColorMap(
ChainMap(self.trap, cmap, self._smap, FALLBACK_STYLE_DICT)
)
self.styles = ChainMap(
self.trap, cmap, self._smap, FALLBACK_STYLE_DICT, compound
)
self._style_name = value
for file_type, xonsh_color in builtins.__xonsh__.env.get(
@ -399,6 +424,13 @@ class XonshStyle(Style):
def style_name(self):
self._style_name = ""
@property
def non_pygments_rules(self):
return NON_PYGMENTS_RULES.get(self.style_name, {})
def override(self, style_dict):
self.trap.update(_tokenize_style_dict(style_dict))
def enhance_colors_for_cmd_exe(self):
""" Enhance colors when using cmd.exe on windows.
When using the default style all blue and dark red colors
@ -438,24 +470,56 @@ def xonsh_style_proxy(styler):
return XonshStyleProxy
def _ptk_specific_style_value(style_value):
"""Checks if the given value is PTK style specific"""
for ptk_spec in PTK_SPECIFIC_VALUES:
if ptk_spec in style_value:
return True
return False
def _format_ptk_style_name(name):
"""Format PTK style name to be able to include it in a pygments style"""
parts = name.split("-")
return "".join(part.capitalize() for part in parts)
def _get_token_by_name(name):
"""Get pygments token object by its string representation."""
if not isinstance(name, str):
return name
token = Token
parts = name.split(".")
# PTK - all lowercase
if parts[0] == parts[0].lower():
parts = ["PTK"] + [_format_ptk_style_name(part) for part in parts]
# color name
if len(parts) == 1:
parts = ["Color"] + parts
return color_token_by_name((name,))
if parts[0] == "Token":
parts = parts[1:]
while len(parts):
while len(parts) > 0:
token = getattr(token, parts[0])
parts = parts[1:]
return token
def _tokenize_style_dict(styles):
"""Converts possible string keys in style dicts to Tokens"""
return {
_get_token_by_name(token): value
for token, value in styles.items()
if not _ptk_specific_style_value(value)
}
def register_custom_pygments_style(
name, styles, highlight_color=None, background_color=None, base="default"
):
@ -481,11 +545,15 @@ def register_custom_pygments_style(
base_style = get_style_by_name(base)
custom_styles = base_style.styles.copy()
for token, value in styles.items():
if isinstance(token, str):
token = _get_token_by_name(token)
for token, value in _tokenize_style_dict(styles).items():
custom_styles[token] = value
non_pygments_rules = {
token: value
for token, value in styles.items()
if _ptk_specific_style_value(value)
}
style = type(
f"Custom{name}Style",
(Style,),
@ -510,26 +578,12 @@ def register_custom_pygments_style(
cmap[token] = custom_styles[token]
STYLES[name] = cmap
if len(non_pygments_rules) > 0:
NON_PYGMENTS_RULES[name] = non_pygments_rules
return style
PTK_STYLE = LazyObject(
lambda: {
Token.Menu.Completions: "bg:ansigray ansiblack",
Token.Menu.Completions.Completion: "",
Token.Menu.Completions.Completion.Current: "bg:ansibrightblack ansiwhite",
Token.Scrollbar: "bg:ansibrightblack",
Token.Scrollbar.Button: "bg:ansiblack",
Token.Scrollbar.Arrow: "bg:ansiblack ansiwhite bold",
Token.AutoSuggestion: "ansibrightblack",
Token.Aborted: "ansibrightblack",
},
globals(),
"PTK_STYLE",
)
XONSH_BASE_STYLE = LazyObject(
lambda: {
Whitespace: "ansigray",

View file

@ -633,7 +633,9 @@ class ReadlineShell(BaseShell, cmd.Cmd):
else:
# assume this is a list of (Token, str) tuples and format it
env = builtins.__xonsh__.env
style_overrides_env = env.get("XONSH_STYLE_OVERRIDES", {})
self.styler.style_name = env.get("XONSH_COLOR_STYLE")
self.styler.override(style_overrides_env)
style_proxy = pyghooks.xonsh_style_proxy(self.styler)
formatter = pyghooks.XonshTerminal256Formatter(style=style_proxy)
s = pygments.format(string, formatter).rstrip()

View file

@ -181,8 +181,6 @@ DEFAULT_STYLE_DICT = LazyObject(
lambda: "",
{
Token: "",
Token.Aborted: "ansibrightblack",
Token.AutoSuggestion: "ansibrightblack",
Token.Color.BACKGROUND_BLACK: "bg:ansiblack",
Token.Color.BACKGROUND_BLUE: "bg:ansiblue",
Token.Color.BACKGROUND_CYAN: "bg:ansicyan",
@ -314,9 +312,6 @@ DEFAULT_STYLE_DICT = LazyObject(
Token.Literal.String.Regex: "ansimagenta",
Token.Literal.String.Single: "",
Token.Literal.String.Symbol: "ansiyellow",
Token.Menu.Completions: "bg:ansigray ansiblack",
Token.Menu.Completions.Completion: "",
Token.Menu.Completions.Completion.Current: "bg:ansibrightblack ansiwhite",
Token.Name: "",
Token.Name.Attribute: "ansibrightyellow",
Token.Name.Builtin: "ansigreen",
@ -342,24 +337,18 @@ DEFAULT_STYLE_DICT = LazyObject(
Token.Operator.Word: "bold ansimagenta",
Token.Other: "",
Token.Punctuation: "",
Token.Scrollbar: "bg:ansibrightblack",
Token.Scrollbar.Arrow: "bg:ansiblack ansiwhite bold",
Token.Scrollbar.Button: "bg:ansiblack",
Token.Text: "",
Token.Text.Whitespace: "ansigray",
Token.PTK.Aborting: "ansibrightblack",
Token.PTK.AutoSuggestion: "ansibrightblack",
Token.PTK.CompletionMenu: "bg:ansigray ansiblack",
Token.PTK.CompletionMenu.Completion: "",
Token.PTK.CompletionMenu.Completion.Current: "bg:ansibrightblack ansiwhite",
Token.PTK.Scrollbar.Arrow: "bg:ansiblack ansiwhite bold",
Token.PTK.Scrollbar.Background: "bg:ansibrightblack",
Token.PTK.Scrollbar.Button: "bg:ansiblack",
},
),
globals(),
"DEFAULT_STYLE_DICT",
)
PTK2_STYLE = {
"completion-menu": "bg:ansigray ansiblack",
"completion-menu.completion": "",
"completion-menu.completion.current": "bg:ansibrightblack ansiwhite",
"scrollbar.background": "bg:ansibrightblack",
"scrollbar.arrow": "bg:ansiblack ansiwhite bold",
"scrollbar.button": "bg:ansiblack",
"auto-suggestion": "ansibrightblack",
"aborting": "ansibrightblack",
}