1414For a complete demo, see `src/azure-cli/azure/cli/command_modules/util/custom.py` and run `az demo style`.
1515"""
1616
17+ import os
1718import sys
1819from 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