feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,843 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
r"""A module for console attributes, special characters and functions.
The target architectures {linux, macos, windows} support inline encoding for
all attributes except color. Windows requires win32 calls to manipulate the
console color state.
Usage:
# Get the console attribute state.
out = log.out
con = console_attr.GetConsoleAttr(out=out)
# Get the ISO 8879:1986//ENTITIES Box and Line Drawing characters.
box = con.GetBoxLineCharacters()
# Print an X inside a box.
out.write(box.dr)
out.write(box.h)
out.write(box.dl)
out.write('\n')
out.write(box.v)
out.write('X')
out.write(box.v)
out.write('\n')
out.write(box.ur)
out.write(box.h)
out.write(box.ul)
out.write('\n')
# Print the bullet characters.
for c in con.GetBullets():
out.write(c)
out.write('\n')
# Print FAIL in red.
out.write('Epic ')
con.Colorize('FAIL', 'red')
out.write(', my first.')
# Print italic and bold text.
bold = con.GetFontCode(bold=True)
italic = con.GetFontCode(italic=True)
normal = con.GetFontCode()
out.write('This is {bold}bold{normal}, this is {italic}italic{normal},'
' and this is normal.\n'.format(bold=bold, italic=italic,
normal=normal))
# Read one character from stdin with echo disabled.
c = con.GetRawKey()
if c is None:
print 'EOF\n'
# Return the display width of a string that may contain FontCode() chars.
display_width = con.DisplayWidth(string)
# Reset the memoized state.
con = console_attr.ResetConsoleAttr()
# Print the console width and height in characters.
width, height = con.GetTermSize()
print 'width={width}, height={height}'.format(width=width, height=height)
# Colorize table data cells.
fail = console_attr.Colorizer('FAIL', 'red')
pass = console_attr.Colorizer('PASS', 'green')
cells = ['label', fail, 'more text', pass, 'end']
for cell in cells;
if isinstance(cell, console_attr.Colorizer):
cell.Render()
else:
out.write(cell)
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import locale
import os
import sys
import unicodedata
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import console_attr_os
from googlecloudsdk.core.console.style import text
from googlecloudsdk.core.util import encoding as encoding_util
import six
# TODO(b/123522546): Unify this logic with console.style.mappings
class BoxLineCharacters(object):
"""Box/line drawing characters.
The element names are from ISO 8879:1986//ENTITIES Box and Line Drawing//EN:
http://www.w3.org/2003/entities/iso8879doc/isobox.html
"""
class BoxLineCharactersUnicode(BoxLineCharacters):
"""unicode Box/line drawing characters (cp437 compatible unicode)."""
dl = ''
dr = ''
h = ''
hd = ''
hu = ''
ul = ''
ur = ''
v = ''
vh = ''
vl = ''
vr = ''
d_dl = ''
d_dr = ''
d_h = ''
d_hd = ''
d_hu = ''
d_ul = ''
d_ur = ''
d_v = ''
d_vh = ''
d_vl = ''
d_vr = ''
class BoxLineCharactersAscii(BoxLineCharacters):
"""ASCII Box/line drawing characters."""
dl = '+'
dr = '+'
h = '-'
hd = '+'
hu = '+'
ul = '+'
ur = '+'
v = '|'
vh = '+'
vl = '+'
vr = '+'
d_dl = '#'
d_dr = '#'
d_h = '='
d_hd = '#'
d_hu = '#'
d_ul = '#'
d_ur = '#'
d_v = '#'
d_vh = '#'
d_vl = '#'
d_vr = '#'
class BoxLineCharactersScreenReader(BoxLineCharactersAscii):
dl = ' '
dr = ' '
hd = ' '
hu = ' '
ul = ' '
ur = ' '
vh = ' '
vl = ' '
vr = ' '
class ProgressTrackerSymbols(object):
"""Characters used by progress trackers."""
class ProgressTrackerSymbolsUnicode(ProgressTrackerSymbols):
"""Characters used by progress trackers."""
@property
def spin_marks(self):
return ['', '', '', '', '', '']
success = text.TypedText([''], text_type=text.TextTypes.PT_SUCCESS)
failed = text.TypedText(['X'], text_type=text.TextTypes.PT_FAILURE)
interrupted = '-'
not_started = '.'
prefix_length = 2
class ProgressTrackerSymbolsAscii(ProgressTrackerSymbols):
"""Characters used by progress trackers."""
@property
def spin_marks(self):
return ['|', '/', '-', '\\',]
success = 'OK'
failed = 'X'
interrupted = '-'
not_started = '.'
prefix_length = 3
class ConsoleAttr(object):
"""Console attribute and special drawing characters and functions accessor.
Use GetConsoleAttr() to get a global ConsoleAttr object shared by all callers.
Use ConsoleAttr() for abstracting multiple consoles.
If _out is not associated with a console, or if the console properties cannot
be determined, the default behavior is ASCII art with no attributes.
Attributes:
_ANSI_COLOR: The ANSI color control sequence dict.
_ANSI_COLOR_RESET: The ANSI color reset control sequence string.
_csi: The ANSI Control Sequence indicator string, '' if not supported.
_encoding: The character encoding.
ascii: ASCII art. This is the default.
utf8: UTF-8 unicode.
win: Windows code page 437.
_font_bold: The ANSI bold font embellishment code string.
_font_italic: The ANSI italic font embellishment code string.
_get_raw_key: A function that reads one keypress from stdin with no echo.
_out: The console output file stream.
_term: TERM environment variable value.
_term_size: The terminal (x, y) dimensions in characters.
"""
_CONSOLE_ATTR_STATE = None
_ANSI_COLOR = {
'red': '31;1m',
'yellow': '33;1m',
'green': '32m',
'blue': '34;1m'
}
_ANSI_COLOR_RESET = '39;0m'
_BULLETS_UNICODE = ('', '', '', '', '', '')
_BULLETS_WINDOWS = ('', '', '', 'Φ', '·') # cp437 compatible unicode
_BULLETS_ASCII = ('o', '*', '+', '-')
def __init__(self, encoding=None, term=None, suppress_output=False):
"""Constructor.
Args:
encoding: Encoding override.
ascii -- ASCII art. This is the default.
utf8 -- UTF-8 unicode.
win -- Windows code page 437.
term: Terminal override. Replaces the value of ENV['TERM'].
suppress_output: True to create a ConsoleAttr that doesn't want to output
anything.
"""
# Normalize the encoding name.
if not encoding:
encoding = self._GetConsoleEncoding()
elif encoding == 'win':
encoding = 'cp437'
self._encoding = encoding or 'ascii'
if suppress_output:
self._term = ''
elif term:
self._term = term
else:
self._term = encoding_util.GetEncodedValue(os.environ, 'TERM', '').lower()
# ANSI "standard" attributes.
if self.SupportsAnsi():
# Select Graphic Rendition paramaters from
# http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
# Italic '3' would be nice here but its not widely supported.
self._csi = '\x1b['
self._font_bold = '1'
self._font_italic = '4'
else:
self._csi = None
self._font_bold = ''
self._font_italic = ''
# Encoded character attributes.
is_screen_reader = properties.VALUES.accessibility.screen_reader.GetBool()
if self._encoding == 'utf-8' and not is_screen_reader:
self._box_line_characters = BoxLineCharactersUnicode()
self._bullets = self._BULLETS_UNICODE
self._progress_tracker_symbols = ProgressTrackerSymbolsUnicode()
elif self._encoding == 'cp437' and not is_screen_reader:
self._box_line_characters = BoxLineCharactersUnicode()
self._bullets = self._BULLETS_WINDOWS
# Windows does not suport the unicode characters used for the spinner.
self._progress_tracker_symbols = ProgressTrackerSymbolsAscii()
else:
self._box_line_characters = BoxLineCharactersAscii()
if is_screen_reader:
self._box_line_characters = BoxLineCharactersScreenReader()
self._bullets = self._BULLETS_ASCII
self._progress_tracker_symbols = ProgressTrackerSymbolsAscii()
# OS specific attributes.
self._get_raw_key = [console_attr_os.GetRawKeyFunction()]
self._term_size = (
(0, 0) if suppress_output else console_attr_os.GetTermSize())
self._display_width_cache = {}
def _GetConsoleEncoding(self):
"""Gets the encoding as declared by the stdout stream.
Returns:
str, The encoding name or None if it could not be determined.
"""
console_encoding = getattr(sys.stdout, 'encoding', None)
if not console_encoding:
return None
console_encoding = console_encoding.lower()
if 'utf-8' in console_encoding:
# use ascii for windows code page 1252
locale_encoding = locale.getpreferredencoding()
if locale_encoding and 'cp1252' in locale_encoding:
return None
return 'utf-8'
elif 'cp437' in console_encoding:
return 'cp437'
elif 'cp1252' in console_encoding:
return None
return None
def Colorize(self, string, color, justify=None):
"""Generates a colorized string, optionally justified.
Args:
string: The string to write.
color: The color name -- must be in _ANSI_COLOR.
justify: The justification function, no justification if None. For
example, justify=lambda s: s.center(10)
Returns:
str, The colorized string that can be printed to the console.
"""
if justify:
string = justify(string)
if self._csi and color in self._ANSI_COLOR:
return '{csi}{color_code}{string}{csi}{reset_code}'.format(
csi=self._csi,
color_code=self._ANSI_COLOR[color],
reset_code=self._ANSI_COLOR_RESET,
string=string)
return string
def ConvertOutputToUnicode(self, buf):
"""Converts a console output string buf to unicode.
Mainly used for testing. Allows test comparisons in unicode while ensuring
that unicode => encoding => unicode works.
Args:
buf: The console output string to convert.
Returns:
The console output string buf converted to unicode.
"""
if isinstance(buf, six.text_type):
buf = buf.encode(self._encoding)
return six.text_type(buf, self._encoding, 'replace')
def GetBoxLineCharacters(self):
"""Returns the box/line drawing characters object.
The element names are from ISO 8879:1986//ENTITIES Box and Line Drawing//EN:
http://www.w3.org/2003/entities/iso8879doc/isobox.html
Returns:
A BoxLineCharacters object for the console output device.
"""
return self._box_line_characters
def GetBullets(self):
"""Returns the bullet characters list.
Use the list elements in order for best appearance in nested bullet lists,
wrapping back to the first element for deep nesting. The list size depends
on the console implementation.
Returns:
A tuple of bullet characters.
"""
return self._bullets
def GetProgressTrackerSymbols(self):
"""Returns the progress tracker characters object.
Returns:
A ProgressTrackerSymbols object for the console output device.
"""
return self._progress_tracker_symbols
def GetControlSequenceIndicator(self):
"""Returns the control sequence indicator string.
Returns:
The conrol sequence indicator string or None if control sequences are not
supported.
"""
return self._csi
def GetControlSequenceLen(self, buf):
"""Returns the control sequence length at the beginning of buf.
Used in display width computations. Control sequences have display width 0.
Args:
buf: The string to check for a control sequence.
Returns:
The conrol sequence length at the beginning of buf or 0 if buf does not
start with a control sequence.
"""
if not self._csi or not buf.startswith(self._csi):
return 0
n = 0
for c in buf:
n += 1
if c.isalpha():
break
return n
def GetEncoding(self):
"""Returns the current encoding."""
return self._encoding
def GetFontCode(self, bold=False, italic=False):
"""Returns a font code string for 0 or more embellishments.
GetFontCode() with no args returns the default font code string.
Args:
bold: True for bold embellishment.
italic: True for italic embellishment.
Returns:
The font code string for the requested embellishments. Write this string
to the console output to control the font settings.
"""
if not self._csi:
return ''
codes = []
if bold:
codes.append(self._font_bold)
if italic:
codes.append(self._font_italic)
return '{csi}{codes}m'.format(csi=self._csi, codes=';'.join(codes))
def Emphasize(self, s, bold=True, italic=False):
"""Returns a string emphasized."""
if self._csi:
s = s.replace(
self._csi + self._ANSI_COLOR_RESET,
self._csi + self._ANSI_COLOR_RESET + self.GetFontCode(bold, italic))
return ('{start}' + s + '{end}').format(
start=self.GetFontCode(bold, italic),
end=self.GetFontCode())
def GetRawKey(self):
"""Reads one key press from stdin with no echo.
Returns:
The key name, None for EOF, <KEY-*> for function keys, otherwise a
character.
"""
return self._get_raw_key[0]()
def GetTermIdentifier(self):
"""Returns the TERM envrionment variable for the console.
Returns:
str: A str that describes the console's text capabilities
"""
return self._term
def GetTermSize(self):
"""Returns the terminal (x, y) dimensions in characters.
Returns:
(x, y): A tuple of the terminal x and y dimensions.
"""
return self._term_size
def DisplayWidth(self, buf):
"""Returns the display width of buf, handling unicode and ANSI controls.
Args:
buf: The string to count from.
Returns:
The display width of buf, handling unicode and ANSI controls.
"""
if not isinstance(buf, six.string_types):
# Handle non-string objects like Colorizer().
return len(buf)
cached = self._display_width_cache.get(buf, None)
if cached is not None:
return cached
width = 0
max_width = 0
i = 0
while i < len(buf):
if self._csi and buf[i:].startswith(self._csi):
i += self.GetControlSequenceLen(buf[i:])
elif buf[i] == '\n':
# A newline incidates the start of a new line.
# Newline characters have 0 width.
max_width = max(width, max_width)
width = 0
i += 1
else:
width += GetCharacterDisplayWidth(buf[i])
i += 1
max_width = max(width, max_width)
self._display_width_cache[buf] = max_width
return max_width
def SplitIntoNormalAndControl(self, buf):
"""Returns a list of (normal_string, control_sequence) tuples from buf.
Args:
buf: The input string containing one or more control sequences
interspersed with normal strings.
Returns:
A list of (normal_string, control_sequence) tuples.
"""
if not self._csi or not buf:
return [(buf, '')]
seq = []
i = 0
while i < len(buf):
c = buf.find(self._csi, i)
if c < 0:
seq.append((buf[i:], ''))
break
normal = buf[i:c]
i = c + self.GetControlSequenceLen(buf[c:])
seq.append((normal, buf[c:i]))
return seq
def SplitLine(self, line, width):
"""Splits line into width length chunks.
Args:
line: The line to split.
width: The width of each chunk except the last which could be smaller than
width.
Returns:
A list of chunks, all but the last with display width == width.
"""
lines = []
chunk = ''
w = 0
keep = False
for normal, control in self.SplitIntoNormalAndControl(line):
keep = True
while True:
n = width - w
w += len(normal)
if w <= width:
break
lines.append(chunk + normal[:n])
chunk = ''
keep = False
w = 0
normal = normal[n:]
chunk += normal + control
if chunk or keep:
lines.append(chunk)
return lines
def SupportsAnsi(self):
"""Indicates whether the terminal appears to support ANSI escape sequences.
Returns:
bool: True if ANSI seems to be supported; False otherwise.
"""
if console_attr_os.ForceEnableAnsi():
return True
return (self._encoding != 'ascii' and
('screen' in self._term or 'xterm' in self._term))
class Colorizer(object):
"""Resource string colorizer.
Attributes:
_con: ConsoleAttr object.
_color: Color name.
_string: The string to colorize.
_justify: The justification function, no justification if None. For example,
justify=lambda s: s.center(10)
"""
def __init__(self, string, color, justify=None):
"""Constructor.
Args:
string: The string to colorize.
color: Color name used to index ConsoleAttr._ANSI_COLOR.
justify: The justification function, no justification if None. For
example, justify=lambda s: s.center(10)
"""
self._con = GetConsoleAttr()
self._color = color
self._string = string
self._justify = justify
def __eq__(self, other):
return self._string == six.text_type(other)
def __ne__(self, other):
return not self == other
def __gt__(self, other):
return self._string > six.text_type(other)
def __lt__(self, other):
return self._string < six.text_type(other)
def __ge__(self, other):
return not self < other
def __le__(self, other):
return not self > other
def __len__(self):
return self._con.DisplayWidth(self._string)
def __str__(self):
return self._string
def Render(self, stream, justify=None):
"""Renders the string as self._color on the console.
Args:
stream: The stream to render the string to. The stream given here *must*
have the same encoding as sys.stdout for this to work properly.
justify: The justification function, self._justify if None.
"""
stream.write(
self._con.Colorize(self._string, self._color, justify or self._justify))
def GetConsoleAttr(encoding=None, term=None, reset=False):
"""Gets the console attribute state.
If this is the first call or reset is True or encoding is not None and does
not match the current encoding or out is not None and does not match the
current out then the state is (re)initialized. Otherwise the current state
is returned.
This call associates the out file stream with the console. All console related
output should go to the same stream.
Args:
encoding: Encoding override.
ascii -- ASCII. This is the default.
utf8 -- UTF-8 unicode.
win -- Windows code page 437.
term: Terminal override. Replaces the value of ENV['TERM'].
reset: Force re-initialization if True.
Returns:
The global ConsoleAttr state object.
"""
attr = ConsoleAttr._CONSOLE_ATTR_STATE # pylint: disable=protected-access
if not reset:
if not attr:
reset = True
elif encoding and encoding != attr.GetEncoding():
reset = True
if reset:
attr = ConsoleAttr(encoding=encoding, term=term)
ConsoleAttr._CONSOLE_ATTR_STATE = attr # pylint: disable=protected-access
return attr
def ResetConsoleAttr(encoding=None):
"""Resets the console attribute state to the console default.
Args:
encoding: Reset to this encoding instead of the default.
ascii -- ASCII. This is the default.
utf8 -- UTF-8 unicode.
win -- Windows code page 437.
Returns:
The global ConsoleAttr state object.
"""
return GetConsoleAttr(encoding=encoding, reset=True)
def GetCharacterDisplayWidth(char):
"""Returns the monospaced terminal display width of char.
Assumptions:
- monospaced display
- ambiguous or unknown chars default to width 1
- ASCII control char width is 1 => don't use this for control chars
Args:
char: The character to determine the display width of.
Returns:
The monospaced terminal display width of char: either 0, 1, or 2.
"""
if not isinstance(char, six.text_type):
# Non-unicode chars have width 1. Don't use this function on control chars.
return 1
# Normalize to avoid special cases.
char = unicodedata.normalize('NFC', char)
if unicodedata.combining(char) != 0:
# Modifies the previous character and does not move the cursor.
return 0
elif unicodedata.category(char) == 'Cf':
# Unprintable formatting char.
return 0
elif unicodedata.east_asian_width(char) in 'FW':
# Fullwidth or Wide chars take 2 character positions.
return 2
else:
# Don't use this function on control chars.
return 1
def SafeText(data, encoding=None, escape=True):
br"""Converts the data to a text string compatible with the given encoding.
This works the same way as Decode() below except it guarantees that any
characters in the resulting text string can be re-encoded using the given
encoding (or GetConsoleAttr().GetEncoding() if None is given). This means
that the string will be safe to print to sys.stdout (for example) without
getting codec exceptions if the user's terminal doesn't support the encoding
used by the source of the text.
Args:
data: Any bytes, string, or object that has str() or unicode() methods.
encoding: The encoding name to ensure compatibility with. Defaults to
GetConsoleAttr().GetEncoding().
escape: Replace unencodable characters with a \uXXXX or \xXX equivalent if
True. Otherwise replace unencodable characters with an appropriate unknown
character, '?' for ASCII, and the unicode unknown replacement character
\uFFFE for unicode.
Returns:
A text string representation of the data, but modified to remove any
characters that would result in an encoding exception with the target
encoding. In the worst case, with escape=False, it will contain only ?
characters.
"""
if data is None:
return 'None'
encoding = encoding or GetConsoleAttr().GetEncoding()
string = encoding_util.Decode(data, encoding=encoding)
try:
# No change needed if the string encodes to the output encoding.
string.encode(encoding)
return string
except UnicodeError:
# The string does not encode to the output encoding. Encode it with error
# handling then convert it back into a text string (which will be
# guaranteed to only contain characters that can be encoded later.
return (string
.encode(encoding, 'backslashreplace' if escape else 'replace')
.decode(encoding))
def EncodeToBytes(data):
r"""Encode data to bytes.
The primary use case is for base64/mime style 7-bit ascii encoding where the
encoder input must be bytes. "safe" means that the conversion always returns
bytes and will not raise codec exceptions.
If data is text then an 8-bit ascii encoding is attempted, then the console
encoding, and finally utf-8.
Args:
data: Any bytes, string, or object that has str() or unicode() methods.
Returns:
A bytes string representation of the data.
"""
if data is None:
return b''
if isinstance(data, bytes):
# Already bytes - our work is done.
return data
# Coerce to text that will be converted to bytes.
s = six.text_type(data)
try:
# Assume the text can be directly converted to bytes (8-bit ascii).
return s.encode('iso-8859-1')
except UnicodeEncodeError:
pass
try:
# Try the output encoding.
return s.encode(GetConsoleAttr().GetEncoding())
except UnicodeEncodeError:
pass
# Punt to utf-8.
return s.encode('utf-8')
def Decode(data, encoding=None):
"""Converts the given string, bytes, or object to a text string.
Args:
data: Any bytes, string, or object that has str() or unicode() methods.
encoding: A suggesting encoding used to decode. If this encoding doesn't
work, other defaults are tried. Defaults to
GetConsoleAttr().GetEncoding().
Returns:
A text string representation of the data.
"""
encoding = encoding or GetConsoleAttr().GetEncoding()
return encoding_util.Decode(data, encoding=encoding)

View File

@@ -0,0 +1,283 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""OS specific console_attr helper functions."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
import sys
from googlecloudsdk.core.util import encoding
from googlecloudsdk.core.util import platforms
def GetTermSize():
"""Gets the terminal x and y dimensions in characters.
_GetTermSize*() helper functions taken from:
http://stackoverflow.com/questions/263890/
Returns:
(columns, lines): A tuple containing the terminal x and y dimensions.
"""
xy = None
# Believe the first helper that doesn't bail.
for get_terminal_size in (_GetTermSizePosix,
_GetTermSizeWindows,
_GetTermSizeEnvironment,
_GetTermSizeTput):
try:
xy = get_terminal_size()
if xy:
break
except: # pylint: disable=bare-except
pass
return xy or (80, 24)
def _GetTermSizePosix():
"""Returns the Posix terminal x and y dimensions."""
# pylint: disable=g-import-not-at-top
import fcntl
# pylint: disable=g-import-not-at-top
import struct
# pylint: disable=g-import-not-at-top
import termios
def _GetXY(fd):
"""Returns the terminal (x,y) size for fd.
Args:
fd: The terminal file descriptor.
Returns:
The terminal (x,y) size for fd or None on error.
"""
try:
# This magic incantation converts a struct from ioctl(2) containing two
# binary shorts to a (rows, columns) int tuple.
rc = struct.unpack(b'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'junk'))
return (rc[1], rc[0]) if rc else None
except: # pylint: disable=bare-except
return None
xy = _GetXY(0) or _GetXY(1) or _GetXY(2)
if not xy:
fd = None
try:
fd = os.open(os.ctermid(), os.O_RDONLY)
xy = _GetXY(fd)
except: # pylint: disable=bare-except
xy = None
finally:
if fd is not None:
os.close(fd)
return xy
def _GetTermSizeWindows():
"""Returns the Windows terminal x and y dimensions."""
# pylint:disable=g-import-not-at-top
import struct
# pylint: disable=g-import-not-at-top
from ctypes import create_string_buffer
# pylint:disable=g-import-not-at-top
from ctypes import windll
# stdin handle is -10
# stdout handle is -11
# stderr handle is -12
h = windll.kernel32.GetStdHandle(-12)
csbi = create_string_buffer(22)
if not windll.kernel32.GetConsoleScreenBufferInfo(h, csbi):
return None
(unused_bufx, unused_bufy, unused_curx, unused_cury, unused_wattr,
left, top, right, bottom,
unused_maxx, unused_maxy) = struct.unpack(b'hhhhHhhhhhh', csbi.raw)
x = right - left + 1
y = bottom - top + 1
return (x, y)
def _GetTermSizeEnvironment():
"""Returns the terminal x and y dimensions from the environment."""
return (int(os.environ['COLUMNS']), int(os.environ['LINES']))
def _GetTermSizeTput():
"""Returns the terminal x and y dimemsions from tput(1)."""
import subprocess # pylint: disable=g-import-not-at-top
output = encoding.Decode(subprocess.check_output(['tput', 'cols'],
stderr=subprocess.STDOUT))
cols = int(output)
output = encoding.Decode(subprocess.check_output(['tput', 'lines'],
stderr=subprocess.STDOUT))
rows = int(output)
return (cols, rows)
_ANSI_CSI = '\x1b' # ANSI control sequence indicator (ESC)
_CONTROL_D = '\x04' # unix EOF (^D)
_CONTROL_Z = '\x1a' # Windows EOF (^Z)
_WINDOWS_CSI_1 = '\x00' # Windows control sequence indicator #1
_WINDOWS_CSI_2 = '\xe0' # Windows control sequence indicator #2
def GetRawKeyFunction():
"""Returns a function that reads one keypress from stdin with no echo.
Returns:
A function that reads one keypress from stdin with no echo or a function
that always returns None if stdin does not support it.
"""
# Believe the first helper that doesn't bail.
for get_raw_key_function in (_GetRawKeyFunctionPosix,
_GetRawKeyFunctionWindows):
try:
return get_raw_key_function()
except: # pylint: disable=bare-except
pass
return lambda: None
def _GetRawKeyFunctionPosix():
"""_GetRawKeyFunction helper using Posix APIs."""
# pylint: disable=g-import-not-at-top
import tty
# pylint: disable=g-import-not-at-top
import termios
def _GetRawKeyPosix():
"""Reads and returns one keypress from stdin, no echo, using Posix APIs.
Returns:
The key name, None for EOF, <*> for function keys, otherwise a
character.
"""
ansi_to_key = {
'A': '<UP-ARROW>',
'B': '<DOWN-ARROW>',
'D': '<LEFT-ARROW>',
'C': '<RIGHT-ARROW>',
'5': '<PAGE-UP>',
'6': '<PAGE-DOWN>',
'H': '<HOME>',
'F': '<END>',
'M': '<DOWN-ARROW>',
'S': '<PAGE-UP>',
'T': '<PAGE-DOWN>',
}
# Flush pending output. sys.stdin.read() would do this, but it's explicitly
# bypassed in _GetKeyChar().
sys.stdout.flush()
fd = sys.stdin.fileno()
def _GetKeyChar():
return encoding.Decode(os.read(fd, 1))
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
c = _GetKeyChar()
if c == _ANSI_CSI:
c = _GetKeyChar()
while True:
if c == _ANSI_CSI:
return c
if c.isalpha():
break
prev_c = c
c = _GetKeyChar()
if c == '~':
c = prev_c
break
return ansi_to_key.get(c, '')
except: # pylint:disable=bare-except
c = None
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return None if c in (_CONTROL_D, _CONTROL_Z) else c
return _GetRawKeyPosix
def _GetRawKeyFunctionWindows():
"""_GetRawKeyFunction helper using Windows APIs."""
# pylint: disable=g-import-not-at-top
import msvcrt
def _GetRawKeyWindows():
"""Reads and returns one keypress from stdin, no echo, using Windows APIs.
Returns:
The key name, None for EOF, <*> for function keys, otherwise a
character.
"""
windows_to_key = {
'H': '<UP-ARROW>',
'P': '<DOWN-ARROW>',
'K': '<LEFT-ARROW>',
'M': '<RIGHT-ARROW>',
'I': '<PAGE-UP>',
'Q': '<PAGE-DOWN>',
'G': '<HOME>',
'O': '<END>',
}
# Flush pending output. sys.stdin.read() would do this it's explicitly
# bypassed in _GetKeyChar().
sys.stdout.flush()
def _GetKeyChar():
return encoding.Decode(msvcrt.getch())
c = _GetKeyChar()
# Special function key is a two character sequence; return the second char.
if c in (_WINDOWS_CSI_1, _WINDOWS_CSI_2):
return windows_to_key.get(_GetKeyChar(), '')
return None if c in (_CONTROL_D, _CONTROL_Z) else c
return _GetRawKeyWindows
def ForceEnableAnsi():
"""Attempts to enable virtual terminal processing on Windows.
Returns:
bool: True if ANSI support is now active; False otherwise.
"""
if platforms.OperatingSystem.Current() != platforms.OperatingSystem.WINDOWS:
return False
try:
import ctypes # pylint:disable=g-import-not-at-top
enable_virtual_terminal_processing = 0x0004
h = ctypes.windll.kernel32.GetStdHandle(-11) # stdout handle is -11
old_mode = ctypes.wintypes.DWORD()
if ctypes.windll.kernel32.GetConsoleMode(h, ctypes.byref(old_mode)):
if ctypes.windll.kernel32.SetConsoleMode(
h, old_mode.value | enable_virtual_terminal_processing):
return True
except (OSError, AttributeError):
pass # If we cannot force ANSI, we should simply return False
return False

View File

@@ -0,0 +1,299 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Simple console pager."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
import sys
from googlecloudsdk.core.console import console_attr
class Pager(object):
"""A simple console text pager.
This pager requires the entire contents to be available. The contents are
written one page of lines at a time. The prompt is written after each page of
lines. A one character response is expected. See HELP_TEXT below for more
info.
The contents are written as is. For example, ANSI control codes will be in
effect. This is different from pagers like more(1) which is ANSI control code
agnostic and miscalculates line lengths, and less(1) which displays control
character names by default.
Attributes:
_attr: The current ConsoleAttr handle.
_clear: A string that clears the prompt when written to _out.
_contents: The entire contents of the text lines to page.
_height: The terminal height in characters.
_out: The output stream, log.out (effectively) if None.
_prompt: The page break prompt.
_search_direction: The search direction command, n:forward, N:reverse.
_search_pattern: The current forward/reverse search compiled RE.
_width: The termonal width in characters.
"""
HELP_TEXT = """
Simple pager commands:
b, ^B, <PAGE-UP>, <LEFT-ARROW>
Back one page.
f, ^F, <SPACE>, <PAGE-DOWN>, <RIGHT-ARROW>
Forward one page. Does not quit if there are no more lines.
g, <HOME>
Back to the first page.
<number>g
Go to <number> lines from the top.
G, <END>
Forward to the last page.
<number>G
Go to <number> lines from the bottom.
h
Print pager command help.
j, +, <DOWN-ARROW>
Forward one line.
k, -, <UP-ARROW>
Back one line.
/pattern
Forward search for pattern.
?pattern
Backward search for pattern.
n
Repeat current search.
N
Repeat current search in the opposite direction.
q, Q, ^C, ^D, ^Z
Quit return to the caller.
any other character
Prompt again.
Hit any key to continue:"""
PREV_POS_NXT_REPRINT = -1, -1
def __init__(self, contents, out=None, prompt=None):
"""Constructor.
Args:
contents: The entire contents of the text lines to page.
out: The output stream, log.out (effectively) if None.
prompt: The page break prompt, a defalt prompt is used if None..
"""
self._contents = contents
self._out = out or sys.stdout
self._search_pattern = None
self._search_direction = None
# prev_pos, prev_next values to force reprint
self.prev_pos, self.prev_nxt = self.PREV_POS_NXT_REPRINT
# Initialize the console attributes.
self._attr = console_attr.GetConsoleAttr()
self._width, self._height = self._attr.GetTermSize()
# Initialize the prompt and the prompt clear string.
if not prompt:
prompt = '{bold}--({{percent}}%)--{normal}'.format(
bold=self._attr.GetFontCode(bold=True),
normal=self._attr.GetFontCode())
self._clear = '\r{0}\r'.format(' ' * (self._attr.DisplayWidth(prompt) - 6))
self._prompt = prompt
# Initialize a list of lines with long lines split into separate display
# lines.
self._lines = []
for line in contents.splitlines():
self._lines += self._attr.SplitLine(line, self._width)
def _Write(self, s):
"""Mockable helper that writes s to self._out."""
self._out.write(s)
def _GetSearchCommand(self, c):
"""Consumes a search command and returns the equivalent pager command.
The search pattern is an RE that is pre-compiled and cached for subsequent
/<newline>, ?<newline>, n, or N commands.
Args:
c: The search command char.
Returns:
The pager command char.
"""
self._Write(c)
buf = ''
while True:
p = self._attr.GetRawKey()
if p in (None, '\n', '\r') or len(p) != 1:
break
self._Write(p)
buf += p
self._Write('\r' + ' ' * len(buf) + '\r')
if buf:
try:
self._search_pattern = re.compile(buf)
except re.error:
# Silently ignore pattern errors.
self._search_pattern = None
return ''
self._search_direction = 'n' if c == '/' else 'N'
return 'n'
def _Help(self):
"""Print command help and wait for any character to continue."""
clear = self._height - (len(self.HELP_TEXT) -
len(self.HELP_TEXT.replace('\n', '')))
if clear > 0:
self._Write('\n' * clear)
self._Write(self.HELP_TEXT)
self._attr.GetRawKey()
self._Write('\n')
def Run(self):
"""Run the pager."""
# No paging if the contents are small enough.
if len(self._lines) <= self._height:
self._Write(self._contents)
return
# We will not always reset previous values.
reset_prev_values = True
# Save room for the prompt at the bottom of the page.
self._height -= 1
# Loop over all the pages.
pos = 0
while pos < len(self._lines):
# Write a page of lines.
nxt = pos + self._height
if nxt > len(self._lines):
nxt = len(self._lines)
pos = nxt - self._height
# Checks if the starting position is in between the current printed lines
# so we don't need to reprint all the lines.
if self.prev_pos < pos < self.prev_nxt:
# we start where the previous page ended.
self._Write('\n'.join(self._lines[self.prev_nxt:nxt]) + '\n')
elif pos != self.prev_pos and nxt != self.prev_nxt:
self._Write('\n'.join(self._lines[pos:nxt]) + '\n')
# Handle the prompt response.
percent = self._prompt.format(percent=100 * nxt // len(self._lines))
digits = ''
while True:
# We want to reset prev values if we just exited out of the while loop
if reset_prev_values:
self.prev_pos, self.prev_nxt = pos, nxt
reset_prev_values = False
self._Write(percent)
c = self._attr.GetRawKey()
self._Write(self._clear)
# Parse the command.
if c in (None, # EOF.
'q', # Quit.
'Q', # Quit.
'\x03', # ^C (unix & windows terminal interrupt)
'\x1b', # ESC.
):
# Quit.
return
elif c in ('/', '?'):
c = self._GetSearchCommand(c)
elif c.isdigit():
# Collect digits for operation count.
digits += c
continue
# Set the optional command count.
if digits:
count = int(digits)
digits = ''
else:
count = 0
# Finally commit to command c.
if c in ('<PAGE-UP>', '<LEFT-ARROW>', 'b', '\x02'):
# Previous page.
nxt = pos - self._height
if nxt < 0:
nxt = 0
elif c in ('<PAGE-DOWN>', '<RIGHT-ARROW>', 'f', '\x06', ' '):
# Next page.
if nxt >= len(self._lines):
continue
nxt = pos + self._height
if nxt >= len(self._lines):
nxt = pos
elif c in ('<HOME>', 'g'):
# First page.
nxt = count - 1
if nxt > len(self._lines) - self._height:
nxt = len(self._lines) - self._height
if nxt < 0:
nxt = 0
elif c in ('<END>', 'G'):
# Last page.
nxt = len(self._lines) - count
if nxt > len(self._lines) - self._height:
nxt = len(self._lines) - self._height
if nxt < 0:
nxt = 0
elif c == 'h':
self._Help()
# Special case when we want to reprint the previous display.
self.prev_pos, self.prev_nxt = self.PREV_POS_NXT_REPRINT
nxt = pos
break
elif c in ('<DOWN-ARROW>', 'j', '+', '\n', '\r'):
# Next line.
if nxt >= len(self._lines):
continue
nxt = pos + 1
if nxt >= len(self._lines):
nxt = pos
elif c in ('<UP-ARROW>', 'k', '-'):
# Previous line.
nxt = pos - 1
if nxt < 0:
nxt = 0
elif c in ('n', 'N'):
# Next pattern match search.
if not self._search_pattern:
continue
nxt = pos
i = pos
direction = 1 if c == self._search_direction else -1
while True:
i += direction
if i < 0 or i >= len(self._lines):
break
if self._search_pattern.search(self._lines[i]):
nxt = i
break
else:
# Silently ignore everything else.
continue
if nxt != pos:
# We will exit the while loop because position changed so we can reset
# prev values.
reset_prev_values = True
break
pos = nxt

View File

@@ -0,0 +1,530 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
r"""Multiline output for Cloud SDK.
This module contains a set of classes that are useful for managing console
output that can be updated that spans multiple lines.
Currently only SimpleSuffixConsoleOutput is offered which only supports
updating the last added message. SimpleSuffixConsoleOutput is basically a
collection of semantically distinct messages to be outputted to the console.
These messages all have a suffix, and SimpleSuffixConsoleOutput supports
updating the suffix of the last added message. Calling UpdateConsole on
a SimpleSuffixConsoleOutput will update these messages and any changes
to the console.
Example usage:
# Example for a simple spinner
spinner = ['|', '/', '-', '\\']
num_spinner_marks = len(spinner)
# Define a ConsoleOutput message
output = SimpleSuffixConsoleOutput(sys.stderr)
# Add the message you want to be displayed for the spinner and update the
# console to show the message.
message = sscm.AddMessage('Instance is being created...')
output.UpdateConsole()
> Instance is being created
# Start the spinner by updating the message and then updating the console.
for i in range(20):
output.UpdateMessage(message, spinner[i % num_spinner_marks])
output.UpdateConsole()
time.sleep(0.1)
> Instance is being created...|
> Instance is being created.../
> ...
output.UpdateMessage(message, 'done\n')
output.UpdateConsole()
> Instance is being created...done
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import abc
import threading
from googlecloudsdk.core.console import console_attr
import six
INDENTATION_WIDTH = 2
class ConsoleOutput(six.with_metaclass(abc.ABCMeta, object)):
"""Manages the printing and formatting of multiline console output.
It is up to implementations of this metaclass to determine how different
messages will added to the output.
"""
def UpdateConsole(self):
"""Updates the console output to show any updated or added messages."""
pass
class SimpleSuffixConsoleOutput(ConsoleOutput):
r"""A simple, suffix-only implementation of ConsoleOutput.
In this context, simple means that only updating the last line is supported.
This means that this is supported in all ASCII environments as it only relies
on carriage returns ('\r') for modifying output. Suffix-only means that only
modifying the ending of messages is supported, either via a
detail_message_callback or by modifying the suffix of a SuffixConsoleMessage.
"""
def __init__(self, stream):
"""Constructor.
Args:
stream: The output stream to write to.
"""
self._stream = stream
self._messages = []
self._last_print_index = 0
self._lock = threading.Lock()
super(SimpleSuffixConsoleOutput, self).__init__()
def AddMessage(self, message, detail_message_callback=None,
indentation_level=0):
"""Adds a SuffixConsoleMessage to the SimpleSuffixConsoleOutput object.
Args:
message: str, The message that will be displayed.
detail_message_callback: func() -> str, A no argument function that will
be called and the result will be appended to the message on each call
to UpdateConsole.
indentation_level: int, The indentation level of the message. Each
indentation is represented by two spaces.
Returns:
SuffixConsoleMessage, a message object that can be used to dynamically
change the printed message.
"""
with self._lock:
return self._AddMessage(
message,
detail_message_callback=detail_message_callback,
indentation_level=indentation_level)
def _AddMessage(self, message, detail_message_callback=None,
indentation_level=0):
console_message = SuffixConsoleMessage(
message,
self._stream,
detail_message_callback=detail_message_callback,
indentation_level=indentation_level)
self._messages.append(console_message)
return console_message
def UpdateMessage(self, message, new_suffix):
"""Updates the suffix of the given SuffixConsoleMessage."""
if not message:
raise ValueError('A message must be passed.')
if message not in self._messages:
raise ValueError(
'The given message does not belong to this output object.')
if self._messages and message != self._messages[-1]:
raise ValueError('Only the last added message can be updated.')
with self._lock:
message._UpdateSuffix(new_suffix) # pylint: disable=protected-access
def UpdateConsole(self):
with self._lock:
self._UpdateConsole()
def _UpdateConsole(self):
"""Updates the console output to show any updated or added messages."""
if self._messages:
# Check if there have been new messages added
if self._last_print_index < (len(self._messages) - 1):
# Print all the new messages starting at the last message printed
# and separate them with newlines.
for message in self._messages[self._last_print_index:-1]:
message.Print()
self._stream.write('\n')
# Update last print index
self._last_print_index = len(self._messages) - 1
self._messages[self._last_print_index].Print()
# TODO(b/123531304): Support text with escape codes.
class SuffixConsoleMessage(object):
"""A suffix-only implementation of ConsoleMessage."""
def __init__(self, message, stream, suffix='',
detail_message_callback=None, indentation_level=0):
"""Constructor.
Args:
message: str, the message that this object represents.
stream: The output stream to write to.
suffix: str, The suffix that will be appended to the very end of the
message.
detail_message_callback: func() -> str, A no argument function that will
be called and the result will be added after the message and before the
suffix on every call to Print().
indentation_level: int, The indentation level of the message. Each
indentation is represented by two spaces.
"""
self._stream = stream
self._message = message
self._suffix = suffix
# TODO(b/111592003): May be better to get this on demand.
# TODO(b/112460253): On terminals that don't automatically line wrap, use
# the entire console width.
# Some terminals will move the cursor to the next line once console_width
# characters have been written. So for now we need to use 1 less than the
# actual console width to prevent automatic wrapping leading to improper
# text formatting.
self._console_width = console_attr.ConsoleAttr().GetTermSize()[0] - 1
if self._console_width < 0:
self._console_width = 0
self._detail_message_callback = detail_message_callback
self._level = indentation_level
# Private attributes used for printing.
self._no_output = False
if (self._console_width - (INDENTATION_WIDTH * indentation_level)) <= 0:
# The indentation won't fit into the width of the console. In this case
# just don't output. This should be rare and better than failing the
# command.
self._no_output = True
self._num_lines = 0
self._lines = []
self._has_printed = False
def _UpdateSuffix(self, suffix):
"""Updates the suffix for this message."""
if not isinstance(suffix, six.string_types):
raise TypeError('expected a string or other character buffer object')
self._suffix = suffix
def Print(self, print_all=False):
"""Prints out the message to the console.
The implementation of this function assumes that when called, the
cursor position of the terminal is on the same line as the last line
that this function printed (and nothing more). The exception for this is if
this is the first time that print is being called on this message or if
print_all is True. The implementation should also return the cursor to
the last line of the printed message. The cursor position in this case
should be at the end of printed text to avoid text being overwritten.
Args:
print_all: bool, if the entire message should be printed instead of just
updating the message.
"""
if self._console_width == 0 or self._no_output:
# This can happen if we're on a pseudo-TTY or if the indentation level
# cannot be supported; return to prevent the process from being
# unresponsive.
return
message = self.GetMessage()
if not message:
# No message, so don't go through the effort of printing.
return
# This is the first time we're printing, so set up some variables.
if not self._has_printed or print_all:
self._has_printed = True
# Clear the current line so that our output is as we expect.
self._ClearLine()
self._lines = self._SplitMessageIntoLines(message)
self._num_lines = len(self._lines)
# Since this is the first print, write out the entire message.
for line in self._lines:
self._WriteLine(line)
return
new_lines = self._SplitMessageIntoLines(message)
new_num_lines = len(new_lines)
if new_num_lines < self._num_lines:
# This means the callback or suffix created shorter message and the
# number of lines shrank. The best thing we can do here is just output
# a new line and reprint everything.
self._stream.write('\n')
for line in new_lines:
self._WriteLine(line)
else:
# Here there are a greater or equal amount of lines. However, we do not
# know if lines are equivalent. We first need to check if n-1 lines have
# not changed.
matching_lines = self._GetNumMatchingLines(new_lines)
if self._num_lines - matching_lines <= 1:
# All the lines up the last printed line are the same, so we can just
# update the current line and print out any new lines.
lines_to_print = new_num_lines - self._num_lines + 1
self._ClearLine()
for line in new_lines[-1 * lines_to_print:]:
self._WriteLine(line)
else:
# This (potentially multiline) message has changed on a previous line.
# No choice but to declare bankruptcy and output a new line and reprint
# lines.
self._stream.write('\n')
for line in new_lines:
self._WriteLine(line)
# Update saved state
self._lines = new_lines
self._num_lines = new_num_lines
def GetMessage(self):
if self._detail_message_callback:
detail_message = self._detail_message_callback()
if detail_message:
return self._message + detail_message + self._suffix
return self._message + self._suffix
@property
def effective_width(self):
"""The effective width when the indentation level is considered."""
return self._console_width - (INDENTATION_WIDTH * self._level)
def _GetNumMatchingLines(self, new_lines):
matching_lines = 0
for i in range(min(len(new_lines), self._num_lines)):
if new_lines[i] != self._lines[i]:
break
matching_lines += 1
return matching_lines
def _SplitMessageIntoLines(self, message):
"""Converts message into a list of strs, each representing a line."""
lines = []
pos = 0
# Add check for width being less than indentation
while pos < len(message):
lines.append(message[pos:pos+self.effective_width])
pos += self.effective_width
if pos < len(message):
# Explicit newline is useful for testing.
lines[-1] += '\n'
return lines
def _ClearLine(self):
self._stream.write('\r{}\r'.format(' ' * self._console_width))
def _WriteLine(self, line):
self._stream.write(self._level * INDENTATION_WIDTH * ' ' + line)
self._stream.flush()
class MultilineConsoleOutput(ConsoleOutput):
r"""An implementation of ConsoleOutput which supports multiline updates.
This means all messages can be updated and actually have their output
be updated on the terminal. The main difference between this class and
the simple suffix version is that updates here are updates to the entire
message as this provides more flexibility.
This class accepts messages containing ANSI escape codes. The width
calculations will be handled correctly currently only in this class.
"""
def __init__(self, stream):
"""Constructor.
Args:
stream: The output stream to write to.
"""
self._stream = stream
self._messages = []
self._last_print_index = 0
self._lock = threading.Lock()
self._last_total_lines = 0
self._may_have_update = False
super(MultilineConsoleOutput, self).__init__()
def AddMessage(self, message, indentation_level=0):
"""Adds a MultilineConsoleMessage to the MultilineConsoleOutput object.
Args:
message: str, The message that will be displayed.
indentation_level: int, The indentation level of the message. Each
indentation is represented by two spaces.
Returns:
MultilineConsoleMessage, a message object that can be used to dynamically
change the printed message.
"""
with self._lock:
return self._AddMessage(
message,
indentation_level=indentation_level)
def _AddMessage(self, message, indentation_level=0):
self._may_have_update = True
console_message = MultilineConsoleMessage(
message,
self._stream,
indentation_level=indentation_level)
self._messages.append(console_message)
return console_message
def UpdateMessage(self, message, new_message):
"""Updates the message of the given MultilineConsoleMessage."""
if not message:
raise ValueError('A message must be passed.')
if message not in self._messages:
raise ValueError(
'The given message does not belong to this output object.')
with self._lock:
message._UpdateMessage(new_message) # pylint: disable=protected-access
self._may_have_update = True
def UpdateConsole(self):
with self._lock:
self._UpdateConsole()
def _GetAnsiCursorUpSequence(self, num_lines):
"""Returns an ANSI control sequences that moves the cursor up num_lines."""
return '\x1b[{}A'.format(num_lines)
def _UpdateConsole(self):
"""Updates the console output to show any updated or added messages."""
if not self._may_have_update:
return
# Reset at the start so if gcloud exits, the cursor is in the proper place.
# We need to track the number of outputted lines of the last update because
# new messages may have been added so it can't be computed from _messages.
if self._last_total_lines:
self._stream.write(self._GetAnsiCursorUpSequence(self._last_total_lines))
total_lines = 0
force_print_rest = False
for message in self._messages:
num_lines = message.num_lines
total_lines += num_lines
if message.has_update or force_print_rest:
force_print_rest |= message.num_lines_changed
message.Print()
else:
# Move onto next message
self._stream.write('\n' * num_lines)
self._last_total_lines = total_lines
self._may_have_update = False
class MultilineConsoleMessage(object):
"""A multiline implementation of ConsoleMessage."""
def __init__(self, message, stream, indentation_level=0):
"""Constructor.
Args:
message: str, the message that this object represents.
stream: The output stream to write to.
indentation_level: int, The indentation level of the message. Each
indentation is represented by two spaces.
"""
self._stream = stream
# Some terminals will move the cursor to the next line once console_width
# characters have been written. So for now we need to use 1 less than the
# actual console width to prevent automatic wrapping leading to improper
# text formatting.
self._console_attr = console_attr.GetConsoleAttr()
self._console_width = self._console_attr.GetTermSize()[0] - 1
if self._console_width < 0:
self._console_width = 0
self._level = indentation_level
# Private attributes used for printing.
self._no_output = False
if (self._console_width - (INDENTATION_WIDTH * indentation_level)) <= 0:
# The indentation won't fit into the width of the console. In this case
# just don't output. This should be rare and better than failing the
# command.
self._no_output = True
self._message = None
self._lines = []
self._has_update = False
self._num_lines_changed = False
self._UpdateMessage(message)
@property
def lines(self):
return self._lines
@property
def num_lines(self):
return len(self._lines)
@property
def has_update(self):
return self._has_update
@property
def num_lines_changed(self):
return self._num_lines_changed
def _UpdateMessage(self, new_message):
"""Updates the message for this Message object."""
if not isinstance(new_message, six.string_types):
raise TypeError('expected a string or other character buffer object')
if new_message != self._message:
self._message = new_message
if self._no_output:
return
num_old_lines = len(self._lines)
self._lines = self._SplitMessageIntoLines(self._message)
self._has_update = True
self._num_lines_changed = num_old_lines != len(self._lines)
def _SplitMessageIntoLines(self, message):
"""Converts message into a list of strs, each representing a line."""
lines = self._console_attr.SplitLine(message, self.effective_width)
for i in range(len(lines)):
lines[i] += '\n'
return lines
def Print(self):
"""Prints out the message to the console.
The implementation of this function assumes that when called, the
cursor position of the terminal is where the message should start printing.
"""
if self._no_output:
return
for line in self._lines:
self._ClearLine()
self._WriteLine(line)
self._has_update = False
@property
def effective_width(self):
"""The effective width when the indentation level is considered."""
return self._console_width - (INDENTATION_WIDTH * self._level)
def _ClearLine(self):
self._stream.write('\r{}\r'.format(' ' * self._console_width))
def _WriteLine(self, line):
self._stream.write(self._level * INDENTATION_WIDTH * ' ' + line)

View File

@@ -0,0 +1,245 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Prompt completion support module."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
import sys
from googlecloudsdk.core.console import console_attr
from six.moves import range # pylint: disable=redefined-builtin
def _IntegerCeilingDivide(numerator, denominator):
"""returns numerator/denominator rounded up if there is any remainder."""
return -(-numerator // denominator)
def _TransposeListToRows(all_items, width=80, height=40, pad=' ', bold=None,
normal=None):
"""Returns padded newline terminated column-wise list for items.
Used by PromptCompleter to pretty print the possible completions for TAB-TAB.
Args:
all_items: [str], The ordered list of all items to transpose.
width: int, The total display width in characters.
height: int, The total display height in lines.
pad: str, String inserted before each column.
bold: str, The bold font highlight control sequence.
normal: str, The normal font highlight control sequence.
Returns:
[str], A padded newline terminated list of colum-wise rows for the ordered
items list. The return value is a single list, not a list of row lists.
Convert the return value to a printable string by ''.join(return_value).
The first "row" is preceded by a newline and all rows start with the pad.
"""
def _Dimensions(items):
"""Returns the transpose dimensions for items."""
longest_item_len = max(len(x) for x in items)
column_count = int(width / (len(pad) + longest_item_len)) or 1
row_count = _IntegerCeilingDivide(len(items), column_count)
return longest_item_len, column_count, row_count
def _TrimAndAnnotate(item, longest_item_len):
"""Truncates and appends '*' if len(item) > longest_item_len."""
if len(item) <= longest_item_len:
return item
return item[:longest_item_len] + '*'
def _Highlight(item, longest_item_len, difference_index, bold, normal):
"""Highlights the different part of the completion and left justfies."""
length = len(item)
if length > difference_index:
item = (item[:difference_index] + bold +
item[difference_index] + normal +
item[difference_index+1:])
return item + (longest_item_len - length) * ' '
# Trim the items list until row_count <= height.
items = set(all_items)
longest_item_len, column_count, row_count = _Dimensions(items)
while row_count > height and longest_item_len > 3:
items = {_TrimAndAnnotate(x, longest_item_len - 2) for x in all_items}
longest_item_len, column_count, row_count = _Dimensions(items)
items = sorted(items)
# Highlight the start of the differences.
if bold:
difference_index = len(os.path.commonprefix(items))
items = [_Highlight(x, longest_item_len, difference_index, bold, normal)
for x in items]
# Do the column-wise transpose with padding and newlines included.
row_data = ['\n']
row_index = 0
while row_index < row_count:
column_index = row_index
for _ in range(column_count):
if column_index >= len(items):
break
row_data.append(pad)
row_data.append(items[column_index])
column_index += row_count
row_data.append('\n')
row_index += 1
return row_data
def _PrefixMatches(prefix, possible_matches):
"""Returns the subset of possible_matches that start with prefix.
Args:
prefix: str, The prefix to match.
possible_matches: [str], The list of possible matching strings.
Returns:
[str], The subset of possible_matches that start with prefix.
"""
return [x for x in possible_matches if x.startswith(prefix)]
class PromptCompleter(object):
"""Prompt + input + completion.
Yes, this is a roll-your own implementation.
Yes, readline is that bad:
linux: is unaware of the prompt even though it overrise raw_input()
macos: different implementation than linux, and more brokener
windows: didn't even try to implement
"""
_CONTROL_C = '\x03'
_DELETE = '\x7f'
def __init__(self, prompt, choices=None, out=None, width=None, height=None,
pad=' '):
"""Constructor.
Args:
prompt: str or None, The prompt string.
choices: callable or list, A callable with no arguments that returns the
list of all choices, or the list of choices.
out: stream, The output stream, sys.stderr by default.
width: int, The total display width in characters.
height: int, The total display height in lines.
pad: str, String inserted before each column.
"""
self._prompt = prompt
self._choices = choices
self._out = out or sys.stderr
self._attr = console_attr.ConsoleAttr()
term_width, term_height = self._attr.GetTermSize()
if width is None:
width = 80
if width > term_width:
width = term_width
self._width = width
if height is None:
height = 40
if height > term_height:
height = term_height
self._height = height
self._pad = pad
def Input(self):
"""Reads and returns one line of user input with TAB complation."""
all_choices = None
matches = []
response = []
if self._prompt:
self._out.write(self._prompt)
c = None
# Loop on input characters read one at a time without echo.
while True:
previous_c = c # for detecting <TAB><TAB>.
c = self._attr.GetRawKey()
if c in (None, '\n', '\r', PromptCompleter._CONTROL_C) or len(c) != 1:
# End of the input line.
self._out.write('\n')
break
elif c in ('\b', PromptCompleter._DELETE):
# Delete the last response character and reset the matches list.
if response:
response.pop()
self._out.write('\b \b')
matches = all_choices
elif c == '\t':
# <TAB> kicks in completion.
response_prefix = ''.join(response)
if previous_c == c:
# <TAB><TAB> displays all possible completions.
matches = _PrefixMatches(response_prefix, matches)
if len(matches) > 1:
self._Display(response_prefix, matches)
else:
# <TAB> complete as much of the current response as possible.
if all_choices is None:
if callable(self._choices):
all_choices = self._choices()
else:
all_choices = self._choices
matches = all_choices
# Determine the longest prefix match and adjust the matches list.
matches = _PrefixMatches(response_prefix, matches)
response_prefix = ''.join(response)
common_prefix = os.path.commonprefix(matches)
# If the longest common prefix is longer than the response then the
# portion past the response prefix chars can be appended.
if len(common_prefix) > len(response):
# As long as we are adding chars to the response its safe to prune
# the matches list to the new common prefix.
matches = _PrefixMatches(common_prefix, matches)
self._out.write(common_prefix[len(response):])
response = list(common_prefix)
else:
# Echo and append all remaining chars to the response.
response.append(c)
self._out.write(c)
return ''.join(response)
def _Display(self, prefix, matches):
"""Displays the possible completions and redraws the prompt and response.
Args:
prefix: str, The current response.
matches: [str], The list of strings that start with prefix.
"""
row_data = _TransposeListToRows(
matches, width=self._width, height=self._height, pad=self._pad,
bold=self._attr.GetFontCode(bold=True), normal=self._attr.GetFontCode())
if self._prompt:
row_data.append(self._prompt)
row_data.append(prefix)
self._out.write(''.join(row_data))

View File

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains a list of colors and attributes available in ANSI."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import enum
_colors = {
'NO_COLOR': -1,
'BLACK': 0,
'RED': 1,
'GREEN': 2,
'YELLOW': 3,
'BLUE': 4,
'MAGENTA': 5,
'CYAN': 6,
'WHITE': 7,
'BRIGHT_BLACK': 8,
'BRIGHT_RED': 9,
'BRIGHT_GREEN': 10,
'BRIGHT_YELLOW': 11,
'BRIGHT_BLUE': 12,
'BRIGHT_MAGENTA': 13,
'BRIGHT_CYAN': 14,
'BRIGHT_WHITE': 15,
}
_colors.update(dict([('COLOR_{}'.format(i), i) for i in range(16, 256)]))
# ANSI Colors with the enum values being the color code. Pseudo enum class.
Colors = enum.Enum('Colors', _colors) # pylint: disable=invalid-name
class Attrs(enum.Enum):
"""ANSI text attributes with the enum values being the attributes code."""
BOLD = 1
ITALICS = 3
UNDERLINE = 4

View File

@@ -0,0 +1,158 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Mappings from TextTypes to TextAttributes used by the TextTypeParser."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import console_attr
from googlecloudsdk.core.console.style import ansi
from googlecloudsdk.core.console.style import text
class StyleMapping(object):
"""Mapping of TextTypes to TextAttributes."""
def __init__(self, mappings):
"""Creates a StyleMapping object to be used by a StyledLogger.
Args:
mappings: (dict[TextTypes, TextAttributes]), the mapping
to be used for this StyleMapping object.
"""
self.mappings = mappings
def __getitem__(self, key):
if key in self.mappings:
return self.mappings[key]
return None
STYLE_MAPPINGS_BASIC = StyleMapping({
text.TextTypes.RESOURCE_NAME: text.TextAttributes('[{}]'),
text.TextTypes.OUTPUT: text.TextAttributes('{}'),
text.TextTypes.USER_INPUT: text.TextAttributes('{}'),
text.TextTypes.URI: text.TextAttributes('{}'),
text.TextTypes.URL: text.TextAttributes('{}'),
text.TextTypes.COMMAND: text.TextAttributes('{}'),
text.TextTypes.INFO: text.TextAttributes('{}'),
text.TextTypes.PT_SUCCESS: text.TextAttributes('{}'),
text.TextTypes.PT_FAILURE: text.TextAttributes('{}'),
})
STYLE_MAPPINGS_ANSI = StyleMapping({
text.TextTypes.RESOURCE_NAME: text.TextAttributes(
'[{}]',
color=ansi.Colors.BLUE,
attrs=[]),
text.TextTypes.OUTPUT: text.TextAttributes(
'[{}]',
color=ansi.Colors.BLUE,
attrs=[]),
text.TextTypes.USER_INPUT: text.TextAttributes(
'{}',
color=ansi.Colors.CYAN,
attrs=[ansi.Attrs.BOLD]),
text.TextTypes.URI: text.TextAttributes(
'{}',
color=None,
attrs=[]),
text.TextTypes.URL: text.TextAttributes(
'{}',
color=None,
attrs=[ansi.Attrs.UNDERLINE]),
text.TextTypes.COMMAND: text.TextAttributes(
'{}',
color=ansi.Colors.GREEN,
attrs=[]),
text.TextTypes.INFO: text.TextAttributes(
'{}',
color=ansi.Colors.YELLOW,
attrs=[]),
text.TextTypes.PT_SUCCESS: text.TextAttributes(
'{}', color=ansi.Colors.GREEN),
text.TextTypes.PT_FAILURE: text.TextAttributes(
'{}', color=ansi.Colors.RED),
})
STYLE_MAPPINGS_ANSI_256 = StyleMapping({
text.TextTypes.RESOURCE_NAME: text.TextAttributes(
'[{}]',
color=ansi.Colors.COLOR_33,
attrs=[]),
text.TextTypes.OUTPUT: text.TextAttributes(
'[{}]',
color=ansi.Colors.COLOR_33,
attrs=[]),
text.TextTypes.USER_INPUT: text.TextAttributes(
'{}',
color=ansi.Colors.COLOR_81,
attrs=[ansi.Attrs.BOLD]),
text.TextTypes.URI: text.TextAttributes(
'{}',
color=None,
attrs=[]),
text.TextTypes.URL: text.TextAttributes(
'{}',
color=None,
attrs=[ansi.Attrs.UNDERLINE]),
text.TextTypes.COMMAND: text.TextAttributes(
'{}',
color=ansi.Colors.COLOR_34,
attrs=[]),
text.TextTypes.INFO: text.TextAttributes(
'{}',
color=ansi.Colors.COLOR_167,
attrs=[]),
text.TextTypes.PT_SUCCESS: text.TextAttributes(
'{}', color=ansi.Colors.GREEN),
text.TextTypes.PT_FAILURE: text.TextAttributes(
'{}', color=ansi.Colors.RED),
})
STYLE_MAPPINGS_TESTING = StyleMapping(dict([
(text_type, text.TextAttributes('[{{}}]({})'.format(text_type.name)))
for text_type in [
text.TextTypes.RESOURCE_NAME,
text.TextTypes.OUTPUT,
text.TextTypes.USER_INPUT,
text.TextTypes.URI,
text.TextTypes.URL,
text.TextTypes.COMMAND,
text.TextTypes.INFO,
text.TextTypes.PT_SUCCESS,
text.TextTypes.PT_FAILURE]]))
def GetStyleMappings(console_attributes=None):
"""Gets the style mappings based on the console and user properties."""
console_attributes = console_attributes or console_attr.GetConsoleAttr()
is_screen_reader = properties.VALUES.accessibility.screen_reader.GetBool()
if properties.VALUES.core.color_theme.Get() == 'testing':
return STYLE_MAPPINGS_TESTING
elif (not is_screen_reader and
console_attributes.SupportsAnsi() and
properties.VALUES.core.color_theme.Get() != 'off'):
if console_attributes._term == 'xterm-256color': # pylint: disable=protected-access
return STYLE_MAPPINGS_ANSI_256
else:
return STYLE_MAPPINGS_ANSI
else:
return STYLE_MAPPINGS_BASIC

View File

@@ -0,0 +1,175 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Stylized printing using ANSI codes utility module."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.core import properties
from googlecloudsdk.core.console.style import mappings
from googlecloudsdk.core.console.style import text
import six
class _StyleContext(object):
"""Contains style information used in recursive parsingin TypedTextParser."""
def __init__(self, color, attrs):
self.color = color
self.attrs = attrs
@classmethod
def FromTextAttributes(cls, text_attributes):
if not text_attributes:
return cls(None, [])
return cls(text_attributes.color, text_attributes.attrs or [])
def UpdateFromTextAttributes(self, text_attributes):
if not text_attributes:
return self
new_color = text_attributes.color or self.color
new_attrs = getattr(text_attributes, 'attrs', []) + self.attrs
# Return a new object so children attributes don't propagate back up.
return self.__class__(new_color, new_attrs)
class TypedTextParser(object):
"""Logger used to styled text to stderr."""
CSI = '\x1b[' # Control Sequence Introducer
SGR = 'm' # Select Graphic Rendition, acts as a terminator for the sequence
SET_FOREGROUND = '38;5;{}'
RESET = '39;0'
# Adding 0x20 to a given attr value changes it from turning the attribute on,
# to turning the attribute off.
ATTR_OFF = 0x20
def __init__(self, style_mappings, style_enabled):
"""Creates a styled logger used to print styled text to stdout.
Args:
style_mappings: (StyleMapping), A mapping from TextTypes to
mappings.TextAttributes used to stylize the output. If the map does
not contain a TextAttribute object, plain text will bef
logged.
style_enabled: (bool), whether logged text should be styled.
"""
self.style_mappings = style_mappings
self.style_enabled = style_enabled
def _GetAnsiSequenceForAttribute(self, text_attributes, style_context):
"""Returns the ANSI start and reset sequences for the text_attributes."""
style_sequence = ''
reset_sequence = ''
attrs = (set(getattr(style_context, 'attrs', [])) |
set(getattr(text_attributes, 'attrs', [])))
if attrs:
style_sequence += ';'.join(sorted([
six.text_type(attr.value) for attr in attrs]))
reset_sequence += ';'.join(
sorted([
six.text_type('%02x' % (attr.value + self.ATTR_OFF))
for attr in attrs
]))
color = (getattr(text_attributes, 'color', None) or
getattr(style_context, 'color', None))
if color:
if style_sequence:
style_sequence += ';'
style_sequence += self.SET_FOREGROUND.format(color.value)
if reset_sequence:
reset_sequence += ';'
reset_sequence += self.RESET
begin_style, end_style = '', ''
if style_sequence:
begin_style = self.CSI + style_sequence + self.SGR
if reset_sequence:
end_style = self.CSI + reset_sequence + self.SGR
return begin_style, end_style
def ParseTypedTextToString(self, typed_text, style_context=None,
stylize=True):
"""Parses a TypedText object into plain and ansi-annotated unicode.
The reason this returns both the plain and ansi-annotated strings is to
support file logging.
Args:
typed_text: mappings.TypedText, typed text to be converted to unicode.
style_context: _StyleContext, argument used for recursive calls
to preserve text attributes and colors. Recursive calls are made when a
TypedText object contains TypedText objects.
stylize: bool, Whether or not to stylize the string.
Returns:
str, the parsed text.
"""
if isinstance(typed_text, six.string_types):
return typed_text
stylize = stylize and self.style_enabled
parsed_chunks = []
text_attributes = self.style_mappings[typed_text.text_type]
begin_style, end_style = self._GetAnsiSequenceForAttribute(
text_attributes, style_context)
if style_context:
new_style_context = style_context.UpdateFromTextAttributes(
text_attributes)
else:
new_style_context = _StyleContext.FromTextAttributes(text_attributes)
for chunk in typed_text.texts:
if isinstance(chunk, text.TypedText):
parsed_chunks.append(self.ParseTypedTextToString(
chunk,
style_context=new_style_context,
stylize=stylize))
# For correctness, the style gets terminated at the end of the last
# text object. So we need to output the style code again.
if stylize:
parsed_chunks.append(begin_style)
else:
parsed_chunks.append(chunk)
parsed_text = ''.join(parsed_chunks)
if text_attributes and text_attributes.format_str:
parsed_text = text_attributes.format_str.format(parsed_text)
if stylize:
parsed_text = '{begin_style}{text}{end_style}'.format(
begin_style=begin_style,
text=parsed_text,
end_style=end_style)
return parsed_text
def _ColorsEnabled():
"""Returns true if colors should be enabled."""
style = properties.VALUES.core.interactive_ux_style.Get()
if any([
style == properties.VALUES.core.InteractiveUXStyles.OFF.name,
properties.VALUES.core.show_structured_logs.Get() != 'never',
properties.VALUES.core.disable_color.GetBool()]):
return False
return True
def GetTypedTextParser(enabled=True, style_mappings=None):
"""Returns a typed text parser, creating a new one if necessary."""
can_enable = _ColorsEnabled()
enabled = can_enable and enabled # Cannot force color printing.
style_mappings = style_mappings or mappings.GetStyleMappings()
return TypedTextParser(style_mappings, enabled)

View File

@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Semantic text objects that are used for styled outputting."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import enum
class TextAttributes(object):
"""Attributes to use to style text with."""
def __init__(self, format_str=None, color=None, attrs=None):
"""Defines a set of attributes for a piece of text.
Args:
format_str: (str), string that will be used to format the text
with. For example '[{}]', to enclose text in brackets.
color: (Colors), the color the text should be formatted with.
attrs: (Attrs), the attributes to apply to text.
"""
self._format_str = format_str
self._color = color
self._attrs = attrs or []
@property
def format_str(self):
return self._format_str
@property
def color(self):
return self._color
@property
def attrs(self):
return self._attrs
class TypedText(object):
"""Text with a semantic type that will be used for styling."""
def __init__(self, texts, text_type=None):
"""String of text and a corresponding type to use to style that text.
Args:
texts: (list[str]), list of strs or TypedText objects
that should be styled using text_type.
text_type: (TextTypes), the semantic type of the text that
will be used to style text.
"""
self.texts = texts
self.text_type = text_type
def __len__(self):
length = 0
for text in self.texts:
length += len(text)
return length
def __add__(self, other):
texts = [self, other]
return TypedText(texts)
def __radd__(self, other):
texts = [other, self]
return TypedText(texts)
class _TextTypes(enum.Enum):
"""Text types base class that defines base functionality."""
def __call__(self, *args):
"""Returns a TypedText object using this style."""
return TypedText(list(args), self)
class TextTypes(_TextTypes):
"""Defines text types that can be used for styling text."""
RESOURCE_NAME = 1
URL = 2
USER_INPUT = 3
COMMAND = 4
INFO = 5
URI = 6
OUTPUT = 7
PT_SUCCESS = 8
PT_FAILURE = 9