diff --git a/src/azure-cli-core/azure/cli/core/style.py b/src/azure-cli-core/azure/cli/core/style.py new file mode 100644 index 00000000000..4369ed84e8b --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/style.py @@ -0,0 +1,74 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +""" +Support styled output. + +Currently, only color is supported, underline/bold/italic may be supported in the future. + +Design spec: +https://devdivdesignguide.azurewebsites.net/command-line-interface/color-guidelines-for-command-line-interface/ + +For a complete demo, see `src/azure-cli/azure/cli/command_modules/util/custom.py` and run `az demo style`. +""" + +import sys +from enum import Enum + +from colorama import Fore + + +class Style(str, Enum): + PRIMARY = "primary" + SECONDARY = "secondary" + IMPORTANT = "important" + ACTION = "action" # name TBD + HYPERLINK = "hyperlink" + # Message colors + ERROR = "error" + SUCCESS = "success" + WARNING = "warning" + + +THEME = { + # Style to ANSI escape sequence mapping + # https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences + Style.PRIMARY: Fore.LIGHTWHITE_EX, + 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, +} + + +def print_styled_text(styled, file=sys.stderr): + formatted = format_styled_text(styled) + print(formatted, file=file) + + +def format_styled_text(styled_text): + # https://python-prompt-toolkit.readthedocs.io/en/stable/pages/printing_text.html#style-text-tuples + formatted_parts = [] + + 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]) + + # Reset control sequence + formatted_parts.append(Fore.RESET) + return ''.join(formatted_parts) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_style.py b/src/azure-cli-core/azure/cli/core/tests/test_style.py new file mode 100644 index 00000000000..37bbb97be00 --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/tests/test_style.py @@ -0,0 +1,48 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import unittest + + +class TestStyle(unittest.TestCase): + + def test_format_styled_text(self): + from azure.cli.core.style import Style, format_styled_text + styled_text = [ + (Style.PRIMARY, "Bright White: Primary text color\n"), + (Style.SECONDARY, "White: Secondary text color\n"), + (Style.IMPORTANT, "Bright Magenta: Important text color\n"), + (Style.ACTION, "Bright Blue: Commands, parameters, and system inputs\n"), + (Style.HYPERLINK, "Bright Cyan: Hyperlink\n"), + (Style.ERROR, "Bright Red: Error message indicator\n"), + (Style.SUCCESS, "Bright Green: Success message indicator\n"), + (Style.WARNING, "Bright Yellow: Warning message indicator\n"), + ] + formatted = format_styled_text(styled_text) + excepted = """\x1b[97mBright White: Primary text color +\x1b[90mWhite: 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""" + self.assertEqual(formatted, excepted) + + # Test invalid style + from azure.cli.core.azclierror import CLIInternalError + with self.assertRaisesRegex(CLIInternalError, "Invalid style."): + format_styled_text([("invalid_style", "dummy text",)]) + + # Test invalid styled style + with self.assertRaisesRegex(CLIInternalError, "Invalid styled text."): + format_styled_text([(Style.PRIMARY,)]) + with self.assertRaisesRegex(CLIInternalError, "Invalid styled text."): + format_styled_text(["dummy text"]) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/azure-cli/azure/cli/command_modules/util/_help.py b/src/azure-cli/azure/cli/command_modules/util/_help.py index 62dc572c457..08a9d506e74 100644 --- a/src/azure-cli/azure/cli/command_modules/util/_help.py +++ b/src/azure-cli/azure/cli/command_modules/util/_help.py @@ -45,3 +45,13 @@ type: command short-summary: Upgrade Azure CLI and extensions """ + +helps['demo'] = """ +type: group +short-summary: Demos for designing, developing and demonstrating Azure CLI. +""" + +helps['demo style'] = """ +type: command +short-summary: A demo showing supported text styles. +""" diff --git a/src/azure-cli/azure/cli/command_modules/util/commands.py b/src/azure-cli/azure/cli/command_modules/util/commands.py index b3a36e0dd77..05b6615ba47 100644 --- a/src/azure-cli/azure/cli/command_modules/util/commands.py +++ b/src/azure-cli/azure/cli/command_modules/util/commands.py @@ -14,3 +14,6 @@ def load_command_table(self, _): with self.command_group('') as g: g.custom_command('upgrade', 'upgrade_version', is_preview=True) + + with self.command_group('demo', deprecate_info=g.deprecate(hide=True)) as g: + g.custom_command('style', 'demo_style') diff --git a/src/azure-cli/azure/cli/command_modules/util/custom.py b/src/azure-cli/azure/cli/command_modules/util/custom.py index eb207f3fd57..293b2a08a21 100644 --- a/src/azure-cli/azure/cli/command_modules/util/custom.py +++ b/src/azure-cli/azure/cli/command_modules/util/custom.py @@ -170,3 +170,77 @@ def upgrade_version(cmd, update_all=None, yes=None): # pylint: disable=too-many "More details in https://docs.microsoft.com/cli/azure/update-azure-cli#automatic-update" logger.warning("Upgrade finished.%s", "" if cmd.cli_ctx.config.getboolean('auto-upgrade', 'enable', False) else auto_upgrade_msg) + + +def demo_style(cmd): # pylint: disable=unused-argument + from azure.cli.core.style import Style, print_styled_text + print("[available styles]\n") + styled_text = [ + (Style.PRIMARY, "Bright White: Primary text color\n"), + (Style.SECONDARY, "White: Secondary text color\n"), + (Style.IMPORTANT, "Bright Magenta: Important text color\n"), + (Style.ACTION, "Bright Blue: Commands, parameters, and system inputs\n"), + (Style.HYPERLINK, "Bright Cyan: Hyperlink\n"), + (Style.ERROR, "Bright Red: Error message indicator\n"), + (Style.SUCCESS, "Bright Green: Success message indicator\n"), + (Style.WARNING, "Bright Yellow: Warning message indicator\n"), + ] + print_styled_text(styled_text) + + print("[interactive]\n") + # NOTE! Unicode character ⦾ ⦿ will most likely not be displayed correctly + styled_text = [ + (Style.ACTION, "?"), + (Style.PRIMARY, " Select a SKU for your app:\n"), + (Style.PRIMARY, "⦾ Free "), + (Style.SECONDARY, "Dev/Test workloads: 1 GB memory, 60 minutes/day compute\n"), + (Style.PRIMARY, "⦾ Basic "), + (Style.SECONDARY, "Dev/Test workloads: 1.75 GB memory, monthly charges apply\n"), + (Style.PRIMARY, "⦾ Standard "), + (Style.SECONDARY, "Production workloads: 1.75 GB memory, monthly charges apply\n"), + (Style.ACTION, "⦿ Premium "), + (Style.SECONDARY, "Production workloads: 3.5 GB memory, monthly charges apply\n"), + ] + print_styled_text(styled_text) + + print("[progress report]\n") + # NOTE! Unicode character ✓ will most likely not be displayed correctly + styled_text = [ + (Style.SUCCESS, '(✓) Done: '), + (Style.PRIMARY, "Creating a resource group for myfancyapp\n"), + (Style.SUCCESS, '(✓) Done: '), + (Style.PRIMARY, "Creating an App Service Plan for myfancyappplan on a "), + (Style.IMPORTANT, "premium instance"), + (Style.PRIMARY, " that has a "), + (Style.IMPORTANT, "monthly charge"), + (Style.PRIMARY, "\n"), + (Style.SUCCESS, '(✓) Done: '), + (Style.PRIMARY, "Creating a webapp named myfancyapp\n"), + ] + print_styled_text(styled_text) + + print("[error handing]\n") + styled_text = [ + (Style.ERROR, "ERROR: Command not found: az storage create\n"), + (Style.PRIMARY, "TRY\n"), + (Style.ACTION, "az storage account create --name"), + (Style.PRIMARY, " mystorageaccount "), + (Style.ACTION, "--resource-group"), + (Style.PRIMARY, " MyResourceGroup\n"), + (Style.SECONDARY, "Create a storage account. For more detail, see "), + (Style.HYPERLINK, "https://docs.microsoft.com/en-us/azure/storage/common/storage-account-create?" + "tabs=azure-cli#create-a-storage-account-1"), + (Style.SECONDARY, "\n"), + ] + print_styled_text(styled_text) + + print("[post-output hint]\n") + styled_text = [ + (Style.PRIMARY, "The default subscription is "), + (Style.IMPORTANT, "AzureSDKTest (0b1f6471-1bf0-4dda-aec3-cb9272f09590)"), + (Style.PRIMARY, ". To switch to another subscription, run "), + (Style.ACTION, "az account set --subscription"), + (Style.PRIMARY, " \n"), + (Style.WARNING, "WARNING: The subscription has been disabled!") + ] + print_styled_text(styled_text)