Skip to content

Commit 6f25505

Browse files
authored
{Style} Refine style framework (#16258)
1 parent 36bcdae commit 6f25505

File tree

11 files changed

+354
-53
lines changed

11 files changed

+354
-53
lines changed

src/azure-cli-core/azure/cli/core/__init__.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def __init__(self, **kwargs):
4949
from azure.cli.core.cloud import get_active_cloud
5050
from azure.cli.core.commands.transform import register_global_transforms
5151
from azure.cli.core._session import ACCOUNT, CONFIG, SESSION, INDEX, VERSIONS
52+
from azure.cli.core.style import format_styled_text
5253
from azure.cli.core.util import handle_version_update
5354
from azure.cli.core.commands.query_examples import register_global_query_examples_argument
5455

@@ -80,6 +81,9 @@ def __init__(self, **kwargs):
8081

8182
self.progress_controller = None
8283

84+
if not self.enable_color:
85+
format_styled_text.theme = 'none'
86+
8387
_configure_knack()
8488

8589
def refresh_request_id(self):
@@ -104,17 +108,18 @@ def get_cli_version(self):
104108

105109
def show_version(self):
106110
from azure.cli.core.util import get_az_version_string, show_updates
107-
from azure.cli.core.commands.constants import (SURVEY_PROMPT, SURVEY_PROMPT_COLOR,
108-
UX_SURVEY_PROMPT, UX_SURVEY_PROMPT_COLOR)
111+
from azure.cli.core.commands.constants import SURVEY_PROMPT_STYLED, UX_SURVEY_PROMPT_STYLED
112+
from azure.cli.core.style import print_styled_text
109113

110114
ver_string, updates_available_components = get_az_version_string()
111115
print(ver_string)
112116
show_updates(updates_available_components)
113117

114118
show_link = self.config.getboolean('output', 'show_survey_link', True)
115119
if show_link:
116-
print('\n' + (SURVEY_PROMPT_COLOR if self.enable_color else SURVEY_PROMPT))
117-
print(UX_SURVEY_PROMPT_COLOR if self.enable_color else UX_SURVEY_PROMPT)
120+
print_styled_text()
121+
print_styled_text(SURVEY_PROMPT_STYLED)
122+
print_styled_text(UX_SURVEY_PROMPT_STYLED)
118123

119124
def exception_handler(self, ex): # pylint: disable=no-self-use
120125
from azure.cli.core.util import handle_exception

src/azure-cli-core/azure/cli/core/_help.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
import argparse
88

99
from azure.cli.core.commands import ExtensionCommandSource
10-
from azure.cli.core.commands.constants import (SURVEY_PROMPT, SURVEY_PROMPT_COLOR,
11-
UX_SURVEY_PROMPT, UX_SURVEY_PROMPT_COLOR)
1210

1311
from knack.help import (HelpFile as KnackHelpFile, CommandHelpFile as KnackCommandHelpFile,
1412
GroupHelpFile as KnackGroupHelpFile, ArgumentGroupRegistry as KnackArgumentGroupRegistry,
@@ -178,10 +176,12 @@ def show_help(self, cli_name, nouns, parser, is_group):
178176
from azure.cli.core.util import show_updates_available
179177
show_updates_available(new_line_after=True)
180178
show_link = self.cli_ctx.config.getboolean('output', 'show_survey_link', True)
179+
from azure.cli.core.commands.constants import (SURVEY_PROMPT_STYLED, UX_SURVEY_PROMPT_STYLED)
180+
from azure.cli.core.style import print_styled_text
181181
if show_link:
182-
print(SURVEY_PROMPT_COLOR if self.cli_ctx.enable_color else SURVEY_PROMPT)
182+
print_styled_text(SURVEY_PROMPT_STYLED)
183183
if not nouns:
184-
print(UX_SURVEY_PROMPT_COLOR if self.cli_ctx.enable_color else UX_SURVEY_PROMPT)
184+
print_styled_text(UX_SURVEY_PROMPT_STYLED)
185185

186186
def get_examples(self, command, parser, is_group):
187187
"""Get examples of a certain command from the help file.

src/azure-cli-core/azure/cli/core/commands/constants.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Licensed under the MIT License. See License.txt in the project root for license information.
44
# --------------------------------------------------------------------------------------------
55

6-
from colorama import Fore, Style
6+
from azure.cli.core.style import Style
77
from knack.parser import ARGPARSE_SUPPORTED_KWARGS
88

99

@@ -33,9 +33,13 @@
3333
BLOCKED_MODS = ['context', 'shell', 'documentdb', 'component']
3434

3535
SURVEY_PROMPT = 'Please let us know how we are doing: https://aka.ms/azureclihats'
36-
SURVEY_PROMPT_COLOR = Fore.YELLOW + Style.BRIGHT + 'Please let us know how we are doing: ' + Fore.BLUE + \
37-
'https://aka.ms/azureclihats' + Style.RESET_ALL
36+
SURVEY_PROMPT_STYLED = [
37+
(Style.PRIMARY, 'Please let us know how we are doing: '),
38+
(Style.HYPERLINK, 'https://aka.ms/azureclihats'),
39+
]
40+
3841
UX_SURVEY_PROMPT = 'and let us know if you\'re interested in trying out our newest features: https://aka.ms/CLIUXstudy'
39-
UX_SURVEY_PROMPT_COLOR = Fore.YELLOW + Style.BRIGHT + \
40-
'and let us know if you\'re interested in trying out our newest features: ' \
41-
+ Fore.BLUE + 'https://aka.ms/CLIUXstudy' + Style.RESET_ALL
42+
UX_SURVEY_PROMPT_STYLED = [
43+
(Style.PRIMARY, 'and let us know if you\'re interested in trying out our newest features: '),
44+
(Style.HYPERLINK, 'https://aka.ms/CLIUXstudy'),
45+
]

src/azure-cli-core/azure/cli/core/style.py

Lines changed: 127 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
For a complete demo, see `src/azure-cli/azure/cli/command_modules/util/custom.py` and run `az demo style`.
1515
"""
1616

17+
import os
1718
import sys
1819
from enum import Enum
1920

@@ -32,10 +33,14 @@ class Style(str, Enum):
3233
WARNING = "warning"
3334

3435

35-
THEME = {
36+
# Theme that doesn't contain any style
37+
THEME_NONE = {}
38+
39+
# Theme to be used on a dark-themed terminal
40+
THEME_DARK = {
3641
# Style to ANSI escape sequence mapping
3742
# https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
38-
Style.PRIMARY: Fore.LIGHTWHITE_EX,
43+
Style.PRIMARY: Fore.RESET,
3944
Style.SECONDARY: Fore.LIGHTBLACK_EX, # may use WHITE, but will lose contrast to LIGHTWHITE_EX
4045
Style.IMPORTANT: Fore.LIGHTMAGENTA_EX,
4146
Style.ACTION: Fore.LIGHTBLUE_EX,
@@ -46,29 +51,138 @@ class Style(str, Enum):
4651
Style.WARNING: Fore.LIGHTYELLOW_EX,
4752
}
4853

54+
# Theme to be used on a light-themed terminal
55+
THEME_LIGHT = {
56+
Style.PRIMARY: Fore.RESET,
57+
Style.SECONDARY: Fore.LIGHTBLACK_EX,
58+
Style.IMPORTANT: Fore.MAGENTA,
59+
Style.ACTION: Fore.BLUE,
60+
Style.HYPERLINK: Fore.CYAN,
61+
Style.ERROR: Fore.RED,
62+
Style.SUCCESS: Fore.GREEN,
63+
Style.WARNING: Fore.YELLOW,
64+
}
65+
66+
67+
class Theme(str, Enum):
68+
DARK = 'dark'
69+
LIGHT = 'light'
70+
NONE = 'none'
71+
72+
73+
THEME_DEFINITIONS = {
74+
Theme.NONE: THEME_NONE,
75+
Theme.DARK: THEME_DARK,
76+
Theme.LIGHT: THEME_LIGHT
77+
}
78+
79+
# Blue and bright blue is not visible under the default theme of powershell.exe
80+
POWERSHELL_COLOR_REPLACEMENT = {
81+
Fore.BLUE: Fore.RESET,
82+
Fore.LIGHTBLUE_EX: Fore.RESET
83+
}
84+
85+
86+
def print_styled_text(*styled_text_objects, file=None, **kwargs):
87+
"""
88+
Print styled text. This function wraps the built-in function `print`, additional arguments can be sent
89+
via keyword arguments.
90+
91+
:param styled_text_objects: The input text objects. See format_styled_text for formats of each object.
92+
:param file: The file to print the styled text. The default target is sys.stderr.
93+
"""
94+
formatted_list = [format_styled_text(obj) for obj in styled_text_objects]
95+
# Always fetch the latest sys.stderr in case it has been wrapped by colorama.
96+
print(*formatted_list, file=file or sys.stderr, **kwargs)
4997

50-
def print_styled_text(styled, file=sys.stderr):
51-
formatted = format_styled_text(styled)
52-
print(formatted, file=file)
5398

99+
def format_styled_text(styled_text, theme=None):
100+
"""Format styled text. Dark theme used by default. Available themes are 'dark', 'light', 'none'.
101+
102+
To change theme for all invocations of this function, set `format_styled_text.theme`.
103+
To change theme for one invocation, set parameter `theme`.
104+
105+
:param styled_text: Can be in these formats:
106+
- text
107+
- (style, text)
108+
- [(style, text), ...]
109+
:param theme: The theme used to format text. Can be theme name str, `Theme` Enum or dict.
110+
"""
111+
if theme is None:
112+
theme = getattr(format_styled_text, "theme", THEME_DARK)
113+
114+
# Convert str to the theme dict
115+
if isinstance(theme, str):
116+
try:
117+
theme = THEME_DEFINITIONS[theme]
118+
except KeyError:
119+
from azure.cli.core.azclierror import CLIInternalError
120+
raise CLIInternalError("Invalid theme. Supported themes: none, dark, light")
121+
122+
# Cache the value of is_legacy_powershell
123+
if not hasattr(format_styled_text, "_is_legacy_powershell"):
124+
from azure.cli.core.util import get_parent_proc_name
125+
is_legacy_powershell = not is_modern_terminal() and get_parent_proc_name() == "powershell.exe"
126+
setattr(format_styled_text, "_is_legacy_powershell", is_legacy_powershell)
127+
is_legacy_powershell = getattr(format_styled_text, "_is_legacy_powershell")
54128

55-
def format_styled_text(styled_text):
56129
# https://python-prompt-toolkit.readthedocs.io/en/stable/pages/printing_text.html#style-text-tuples
57130
formatted_parts = []
58131

132+
# A str as PRIMARY text
133+
if isinstance(styled_text, str):
134+
styled_text = [(Style.PRIMARY, styled_text)]
135+
136+
# A tuple
137+
if isinstance(styled_text, tuple):
138+
styled_text = [styled_text]
139+
59140
for text in styled_text:
60141
# str can also be indexed, bypassing IndexError, so explicitly check if the type is tuple
61142
if not (isinstance(text, tuple) and len(text) == 2):
62143
from azure.cli.core.azclierror import CLIInternalError
63144
raise CLIInternalError("Invalid styled text. It should be a list of 2-element tuples.")
64145

65-
style = text[0]
66-
if style not in THEME:
67-
from azure.cli.core.azclierror import CLIInternalError
68-
raise CLIInternalError("Invalid style. Only use pre-defined style in Style enum.")
69-
70-
formatted_parts.append(THEME[text[0]] + text[1])
146+
style, raw_text = text
147+
148+
if theme is THEME_NONE:
149+
formatted_parts.append(raw_text)
150+
else:
151+
try:
152+
escape_seq = theme[style]
153+
except KeyError:
154+
from azure.cli.core.azclierror import CLIInternalError
155+
raise CLIInternalError("Invalid style. Only use pre-defined style in Style enum.")
156+
# Replace blue in powershell.exe
157+
if is_legacy_powershell and escape_seq in POWERSHELL_COLOR_REPLACEMENT:
158+
escape_seq = POWERSHELL_COLOR_REPLACEMENT[escape_seq]
159+
formatted_parts.append(escape_seq + raw_text)
71160

72161
# Reset control sequence
73-
formatted_parts.append(Fore.RESET)
162+
if theme is not THEME_NONE:
163+
formatted_parts.append(Fore.RESET)
74164
return ''.join(formatted_parts)
165+
166+
167+
def _is_modern_terminal():
168+
# Windows Terminal: https://github.com/microsoft/terminal/issues/1040
169+
if 'WT_SESSION' in os.environ:
170+
return True
171+
# VS Code: https://github.com/microsoft/vscode/pull/30346
172+
if os.environ.get('TERM_PROGRAM', '').lower() == 'vscode':
173+
return True
174+
return False
175+
176+
177+
def is_modern_terminal():
178+
"""Detect whether the current terminal is a modern terminal that supports Unicode and
179+
Console Virtual Terminal Sequences.
180+
181+
Currently, these terminals can be detected:
182+
- Windows Terminal
183+
- VS Code terminal
184+
"""
185+
# This function wraps _is_modern_terminal and use a function-level cache to save the result.
186+
if not hasattr(is_modern_terminal, "return_value"):
187+
setattr(is_modern_terminal, "return_value", _is_modern_terminal())
188+
return getattr(is_modern_terminal, "return_value")

0 commit comments

Comments
 (0)