Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Misc.] Define theme for Cloud Shell #17283

Merged
merged 1 commit into from
Apr 20, 2021
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
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,5 @@
/src/azure-cli/azure/cli/command_modules/sql/ @jaredmoo @Juliehzl @evelyn-ys
/src/azure-cli/azure/cli/command_modules/storage/ @Juliehzl @jsntcy @zhoxing-ms @evelyn-ys
/src/azure-cli/azure/cli/command_modules/synapse/ @idear1203 @sunsw1994 @aim-for-better
/src/azure-cli/azure/cli/command_modules/util/ @jiasli @Juliehzl @zhoxing-ms
/src/azure-cli/azure/cli/command_modules/util/ @jiasli @Juliehzl @zhoxing-ms @evelyn-ys
/src/azure-cli/azure/cli/command_modules/vm/ @qwordy @houk-ms @yungezz
27 changes: 21 additions & 6 deletions src/azure-cli-core/azure/cli/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ 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 @@ -96,11 +95,7 @@ def __init__(self, **kwargs):

self.progress_controller = None

if self.enable_color:
theme = self.config.get('core', 'theme', fallback='dark')
else:
theme = 'none'
format_styled_text.theme = theme
self._configure_style()

def refresh_request_id(self):
"""Assign a new random GUID as x-ms-client-request-id
Expand Down Expand Up @@ -182,6 +177,26 @@ def save_local_context(self, parsed_args, argument_definitions, specified_argume
logger.warning('Your preference of %s now saved to local context. To learn more, type in `az '
'local-context --help`', ', '.join(args_str) + ' is' if len(args_str) == 1 else ' are')

def _configure_style(self):
from azure.cli.core.util import in_cloud_console
from azure.cli.core.style import format_styled_text, get_theme_dict, Style

# Configure Style
if self.enable_color:
theme = self.config.get('core', 'theme',
fallback="cloud-shell" if in_cloud_console() else "dark")

theme_dict = get_theme_dict(theme)

if theme_dict:
# If theme is used, also apply it to knack's logger
from knack.util import color_map
color_map['error'] = theme_dict[Style.ERROR]
color_map['warning'] = theme_dict[Style.WARNING]
else:
theme = 'none'
format_styled_text.theme = theme


class MainCommandsLoader(CLICommandsLoader):

Expand Down
136 changes: 77 additions & 59 deletions src/azure-cli-core/azure/cli/core/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@
Design spec:
https://devdivdesignguide.azurewebsites.net/command-line-interface/color-guidelines-for-command-line-interface/

Console Virtual Terminal Sequences:
https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#text-formatting

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

from colorama import Fore
from knack.log import get_logger
from knack.util import is_modern_terminal


logger = get_logger(__name__)


class Style(str, Enum):
Expand All @@ -33,53 +39,84 @@ class Style(str, Enum):
WARNING = "warning"


def _rgb_hex(rgb_hex: str):
"""
Convert RGB hex value to Control Sequences.
"""
template = '\x1b[38;2;{r};{g};{b}m'
if rgb_hex.startswith("#"):
rgb_hex = rgb_hex[1:]

rgb = {}
for i, c in enumerate(('r', 'g', 'b')):
value_str = rgb_hex[i * 2: i * 2 + 2]
value_int = int(value_str, 16)
rgb[c] = value_int

return template.format(**rgb)


DEFAULT = '\x1b[0m' # Default

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

# Theme to be used on a dark-themed terminal
# Theme to be used in 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.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,
Style.HYPERLINK: Fore.LIGHTCYAN_EX,
# Message colors
Style.ERROR: Fore.LIGHTRED_EX,
Style.SUCCESS: Fore.LIGHTGREEN_EX,
Style.WARNING: Fore.LIGHTYELLOW_EX,
Style.PRIMARY: DEFAULT,
Style.SECONDARY: '\x1b[90m', # Bright Foreground Black
Style.IMPORTANT: '\x1b[95m', # Bright Foreground Magenta
Style.ACTION: '\x1b[94m', # Bright Foreground Blue
Style.HYPERLINK: '\x1b[96m', # Bright Foreground Cyan
Style.ERROR: '\x1b[91m', # Bright Foreground Red
Style.SUCCESS: '\x1b[92m', # Bright Foreground Green
Style.WARNING: '\x1b[93m' # Bright Foreground Yellow
}

# Theme to be used on a light-themed terminal
# Theme to be used in 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,
Style.PRIMARY: DEFAULT,
Style.SECONDARY: '\x1b[90m', # Bright Foreground Black
Style.IMPORTANT: '\x1b[35m', # Foreground Magenta
Style.ACTION: '\x1b[34m', # Foreground Blue
Style.HYPERLINK: '\x1b[36m', # Foreground Cyan
Style.ERROR: '\x1b[31m', # Foreground Red
Style.SUCCESS: '\x1b[32m', # Foreground Green
Style.WARNING: '\x1b[33m' # Foreground Yellow
}

# Theme to be used in Cloud Shell
# Text and background's Contrast Ratio should be above 4.5:1
THEME_CLOUD_SHELL = {
Style.PRIMARY: _rgb_hex('#ffffff'),
Style.SECONDARY: _rgb_hex('#bcbcbc'),
Style.IMPORTANT: _rgb_hex('#f887ff'),
Style.ACTION: _rgb_hex('#6cb0ff'),
Style.HYPERLINK: _rgb_hex('#72d7d8'),
Style.ERROR: _rgb_hex('#f55d5c'),
Style.SUCCESS: _rgb_hex('#70d784'),
Style.WARNING: _rgb_hex('#fbd682'),
}


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


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

# 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
'\x1b[34m': DEFAULT, # Foreground Blue
'\x1b[94m': DEFAULT # Bright Foreground Blue
}


Expand Down Expand Up @@ -113,11 +150,7 @@ def format_styled_text(styled_text, theme=None):

# 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")
theme = get_theme_dict(theme)

# Cache the value of is_legacy_powershell
if not hasattr(format_styled_text, "_is_legacy_powershell"):
Expand Down Expand Up @@ -145,9 +178,7 @@ def format_styled_text(styled_text, theme=None):

style, raw_text = text

if theme is THEME_NONE:
formatted_parts.append(raw_text)
else:
if theme:
try:
escape_seq = theme[style]
except KeyError:
Expand All @@ -157,10 +188,12 @@ def format_styled_text(styled_text, theme=None):
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)
else:
formatted_parts.append(raw_text)

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


Expand Down Expand Up @@ -199,25 +232,10 @@ def highlight_command(raw_command):
return styled_command


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")
def get_theme_dict(theme: str):
try:
return THEME_DEFINITIONS[theme]
except KeyError as ex:
available_themes = ', '.join([m.value for m in Theme.__members__.values()]) # pylint: disable=no-member
logger.warning("Invalid theme %s. Supported themes: %s", ex, available_themes)
return None
27 changes: 12 additions & 15 deletions src/azure-cli-core/azure/cli/core/tests/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import unittest
from unittest import mock

from azure.cli.core.style import Style, Theme, format_styled_text, print_styled_text, _is_modern_terminal
from azure.cli.core.style import Style, Theme, format_styled_text, print_styled_text, _rgb_hex


class TestStyle(unittest.TestCase):
Expand All @@ -33,27 +33,27 @@ def test_format_styled_text(self):
(Style.WARNING, "Bright Yellow: Warning message indicator\n"),
]
formatted = format_styled_text(styled_text)
excepted = """\x1b[39mWhite: Primary text color
excepted = """\x1b[0mWhite: Primary text color
\x1b[90mBright Black: Secondary text color
\x1b[95mBright Magenta: Important text color
\x1b[94mBright Blue: Commands, parameters, and system inputs
\x1b[96mBright Cyan: Hyperlink
\x1b[91mBright Red: Error message indicator
\x1b[92mBright Green: Success message indicator
\x1b[93mBright Yellow: Warning message indicator
\x1b[39m"""
\x1b[0m"""
self.assertEqual(formatted, excepted)

# Test str input
styled_text = "Primary text color"
formatted = format_styled_text(styled_text)
excepted = "\x1b[39mPrimary text color\x1b[39m"
excepted = "\x1b[0mPrimary text color\x1b[0m"
self.assertEqual(formatted, excepted)

# Test tuple input
styled_text = (Style.PRIMARY, "Primary text color")
formatted = format_styled_text(styled_text)
excepted = "\x1b[39mPrimary text color\x1b[39m"
excepted = "\x1b[0mPrimary text color\x1b[0m"
self.assertEqual(formatted, excepted)

def test_format_styled_text_on_error(self):
Expand Down Expand Up @@ -111,9 +111,9 @@ def test_format_styled_text_theme(self):
(Style.SECONDARY, "Bright Black: Secondary text color\n"),
]
formatted = format_styled_text(styled_text)
excepted = """\x1b[39mWhite: Primary text color
excepted = """\x1b[0mWhite: Primary text color
\x1b[90mBright Black: Secondary text color
\x1b[39m"""
\x1b[0m"""
self.assertEqual(formatted, excepted)

# Color is turned off via param
Expand Down Expand Up @@ -156,16 +156,13 @@ def test_print_styled_text(self, mock_format_styled_text, mock_print):
print_styled_text("test text 1", "test text 2")
mock_print.assert_called_with("test text 1", "test text 2", file=sys.stderr)

def test_rgb_hex(self):

class TestUtils(unittest.TestCase):
result = _rgb_hex("#13A10E")
self.assertEqual(result, '\x1b[38;2;19;161;14m')

def test_is_modern_terminal(self):
with mock.patch.dict("os.environ", clear=True):
self.assertEqual(_is_modern_terminal(), False)
with mock.patch.dict("os.environ", WT_SESSION='c25cb945-246a-49e5-b37a-1e4b6671b916'):
self.assertEqual(_is_modern_terminal(), True)
with mock.patch.dict("os.environ", TERM_PROGRAM='vscode'):
self.assertEqual(_is_modern_terminal(), True)
result = _rgb_hex("3A96DD")
self.assertEqual(result, '\x1b[38;2;58;150;221m')


if __name__ == '__main__':
Expand Down
6 changes: 6 additions & 0 deletions src/azure-cli-core/azure/cli/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -1266,3 +1266,9 @@ def get_parent_proc_name():
parent_proc_name = _get_parent_proc_name()
setattr(get_parent_proc_name, "return_value", parent_proc_name)
return getattr(get_parent_proc_name, "return_value")


def is_modern_terminal():
"""In addition to knack.util.is_modern_terminal, detect Cloud Shell."""
import knack.util
return knack.util.is_modern_terminal() or in_cloud_console()
3 changes: 1 addition & 2 deletions src/azure-cli-core/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,10 @@
'azure-common~=1.1',
'azure-core==1.12.0',
'azure-mgmt-core>=1.2.0,<2.0.0',
'colorama~=0.4.1',
'cryptography>=3.2,<3.4',
'humanfriendly>=4.7,<10.0',
'jmespath',
'knack~=0.8.0',
'knack~=0.8.1',
'msal>=1.10.0,<2.0.0',
# Dependencies of the vendored subscription SDK
# https://github.com/Azure/azure-sdk-for-python/blob/ab12b048ddf676fe0ccec16b2167117f0609700d/sdk/resources/azure-mgmt-resource/setup.py#L82-L86
Expand Down
Loading