Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/azure-cli-core/azure/cli/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def __init__(self, **kwargs):
from azure.cli.core.cloud import get_active_cloud
from azure.cli.core.commands.transform import register_global_transforms
from azure.cli.core._session import ACCOUNT, CONFIG, SESSION, INDEX, VERSIONS
from azure.cli.core.style import format_styled_text
from azure.cli.core.util import handle_version_update
from azure.cli.core.commands.query_examples import register_global_query_examples_argument

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

self.progress_controller = None

if not self.enable_color:
format_styled_text.theme = 'none'

_configure_knack()

def refresh_request_id(self):
Expand All @@ -104,17 +108,18 @@ def get_cli_version(self):

def show_version(self):
from azure.cli.core.util import get_az_version_string, show_updates
from azure.cli.core.commands.constants import (SURVEY_PROMPT, SURVEY_PROMPT_COLOR,
UX_SURVEY_PROMPT, UX_SURVEY_PROMPT_COLOR)
from azure.cli.core.commands.constants import SURVEY_PROMPT_STYLED, UX_SURVEY_PROMPT_STYLED
from azure.cli.core.style import print_styled_text

ver_string, updates_available_components = get_az_version_string()
print(ver_string)
show_updates(updates_available_components)

show_link = self.config.getboolean('output', 'show_survey_link', True)
if show_link:
print('\n' + (SURVEY_PROMPT_COLOR if self.enable_color else SURVEY_PROMPT))
print(UX_SURVEY_PROMPT_COLOR if self.enable_color else UX_SURVEY_PROMPT)
print_styled_text()
print_styled_text(SURVEY_PROMPT_STYLED)
print_styled_text(UX_SURVEY_PROMPT_STYLED)

def exception_handler(self, ex): # pylint: disable=no-self-use
from azure.cli.core.util import handle_exception
Expand Down
8 changes: 4 additions & 4 deletions src/azure-cli-core/azure/cli/core/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import argparse

from azure.cli.core.commands import ExtensionCommandSource
from azure.cli.core.commands.constants import (SURVEY_PROMPT, SURVEY_PROMPT_COLOR,
UX_SURVEY_PROMPT, UX_SURVEY_PROMPT_COLOR)

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

def get_examples(self, command, parser, is_group):
"""Get examples of a certain command from the help file.
Expand Down
16 changes: 10 additions & 6 deletions src/azure-cli-core/azure/cli/core/commands/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from colorama import Fore, Style
from azure.cli.core.style import Style
from knack.parser import ARGPARSE_SUPPORTED_KWARGS


Expand Down Expand Up @@ -33,9 +33,13 @@
BLOCKED_MODS = ['context', 'shell', 'documentdb', 'component']

SURVEY_PROMPT = 'Please let us know how we are doing: https://aka.ms/azureclihats'
SURVEY_PROMPT_COLOR = Fore.YELLOW + Style.BRIGHT + 'Please let us know how we are doing: ' + Fore.BLUE + \
'https://aka.ms/azureclihats' + Style.RESET_ALL
SURVEY_PROMPT_STYLED = [
(Style.PRIMARY, 'Please let us know how we are doing: '),
(Style.HYPERLINK, 'https://aka.ms/azureclihats'),
]

UX_SURVEY_PROMPT = 'and let us know if you\'re interested in trying out our newest features: https://aka.ms/CLIUXstudy'
UX_SURVEY_PROMPT_COLOR = Fore.YELLOW + Style.BRIGHT + \
'and let us know if you\'re interested in trying out our newest features: ' \
+ Fore.BLUE + 'https://aka.ms/CLIUXstudy' + Style.RESET_ALL
UX_SURVEY_PROMPT_STYLED = [
(Style.PRIMARY, 'and let us know if you\'re interested in trying out our newest features: '),
(Style.HYPERLINK, 'https://aka.ms/CLIUXstudy'),
]
140 changes: 127 additions & 13 deletions src/azure-cli-core/azure/cli/core/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
For a complete demo, see `src/azure-cli/azure/cli/command_modules/util/custom.py` and run `az demo style`.
"""

import os
import sys
from enum import Enum

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


THEME = {
# Theme that doesn't contain any style
THEME_NONE = {}

# Theme to be used on a dark-themed terminal
THEME_DARK = {
# Style to ANSI escape sequence mapping
# https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
Style.PRIMARY: Fore.LIGHTWHITE_EX,
Style.PRIMARY: Fore.RESET,
Style.SECONDARY: Fore.LIGHTBLACK_EX, # may use WHITE, but will lose contrast to LIGHTWHITE_EX
Style.IMPORTANT: Fore.LIGHTMAGENTA_EX,
Style.ACTION: Fore.LIGHTBLUE_EX,
Expand All @@ -46,29 +51,138 @@ class Style(str, Enum):
Style.WARNING: Fore.LIGHTYELLOW_EX,
}

# Theme to be used on a light-themed terminal
THEME_LIGHT = {
Style.PRIMARY: Fore.RESET,
Style.SECONDARY: Fore.LIGHTBLACK_EX,
Style.IMPORTANT: Fore.MAGENTA,
Style.ACTION: Fore.BLUE,
Style.HYPERLINK: Fore.CYAN,
Style.ERROR: Fore.RED,
Style.SUCCESS: Fore.GREEN,
Style.WARNING: Fore.YELLOW,
}


class Theme(str, Enum):
DARK = 'dark'
LIGHT = 'light'
NONE = 'none'


THEME_DEFINITIONS = {
Theme.NONE: THEME_NONE,
Theme.DARK: THEME_DARK,
Theme.LIGHT: THEME_LIGHT
}

# Blue and bright blue is not visible under the default theme of powershell.exe
POWERSHELL_COLOR_REPLACEMENT = {
Fore.BLUE: Fore.RESET,
Fore.LIGHTBLUE_EX: Fore.RESET
}


def print_styled_text(*styled_text_objects, file=None, **kwargs):
"""
Print styled text. This function wraps the built-in function `print`, additional arguments can be sent
via keyword arguments.

:param styled_text_objects: The input text objects. See format_styled_text for formats of each object.
:param file: The file to print the styled text. The default target is sys.stderr.
"""
formatted_list = [format_styled_text(obj) for obj in styled_text_objects]
# Always fetch the latest sys.stderr in case it has been wrapped by colorama.
print(*formatted_list, file=file or sys.stderr, **kwargs)

def print_styled_text(styled, file=sys.stderr):
formatted = format_styled_text(styled)
print(formatted, file=file)

def format_styled_text(styled_text, theme=None):
"""Format styled text. Dark theme used by default. Available themes are 'dark', 'light', 'none'.

To change theme for all invocations of this function, set `format_styled_text.theme`.
To change theme for one invocation, set parameter `theme`.

:param styled_text: Can be in these formats:
- text
- (style, text)
- [(style, text), ...]
:param theme: The theme used to format text. Can be theme name str, `Theme` Enum or dict.
"""
if theme is None:
theme = getattr(format_styled_text, "theme", THEME_DARK)

# Convert str to the theme dict
if isinstance(theme, str):
try:
theme = THEME_DEFINITIONS[theme]
except KeyError:
from azure.cli.core.azclierror import CLIInternalError
raise CLIInternalError("Invalid theme. Supported themes: none, dark, light")

# Cache the value of is_legacy_powershell
if not hasattr(format_styled_text, "_is_legacy_powershell"):
from azure.cli.core.util import get_parent_proc_name
is_legacy_powershell = not is_modern_terminal() and get_parent_proc_name() == "powershell.exe"
setattr(format_styled_text, "_is_legacy_powershell", is_legacy_powershell)
is_legacy_powershell = getattr(format_styled_text, "_is_legacy_powershell")

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

# A str as PRIMARY text
if isinstance(styled_text, str):
styled_text = [(Style.PRIMARY, styled_text)]

# A tuple
if isinstance(styled_text, tuple):
styled_text = [styled_text]

for text in styled_text:
# str can also be indexed, bypassing IndexError, so explicitly check if the type is tuple
if not (isinstance(text, tuple) and len(text) == 2):
from azure.cli.core.azclierror import CLIInternalError
raise CLIInternalError("Invalid styled text. It should be a list of 2-element tuples.")

style = text[0]
if style not in THEME:
from azure.cli.core.azclierror import CLIInternalError
raise CLIInternalError("Invalid style. Only use pre-defined style in Style enum.")

formatted_parts.append(THEME[text[0]] + text[1])
style, raw_text = text

if theme is THEME_NONE:
formatted_parts.append(raw_text)
else:
try:
escape_seq = theme[style]
except KeyError:
from azure.cli.core.azclierror import CLIInternalError
raise CLIInternalError("Invalid style. Only use pre-defined style in Style enum.")
# Replace blue in powershell.exe
if is_legacy_powershell and escape_seq in POWERSHELL_COLOR_REPLACEMENT:
escape_seq = POWERSHELL_COLOR_REPLACEMENT[escape_seq]
formatted_parts.append(escape_seq + raw_text)

# Reset control sequence
formatted_parts.append(Fore.RESET)
if theme is not THEME_NONE:
formatted_parts.append(Fore.RESET)
return ''.join(formatted_parts)


def _is_modern_terminal():
# Windows Terminal: https://github.com/microsoft/terminal/issues/1040
if 'WT_SESSION' in os.environ:
return True
# VS Code: https://github.com/microsoft/vscode/pull/30346
if os.environ.get('TERM_PROGRAM', '').lower() == 'vscode':
return True
return False


def is_modern_terminal():
"""Detect whether the current terminal is a modern terminal that supports Unicode and
Console Virtual Terminal Sequences.

Currently, these terminals can be detected:
- Windows Terminal
- VS Code terminal
"""
# This function wraps _is_modern_terminal and use a function-level cache to save the result.
if not hasattr(is_modern_terminal, "return_value"):
setattr(is_modern_terminal, "return_value", _is_modern_terminal())
return getattr(is_modern_terminal, "return_value")
Loading