# Copyright 2016 The Android Open Source Project # # 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. """Terminal utilities This module handles terminal interaction including ANSI color codes. """ import os import sys from typing import List, Optional _path = os.path.realpath(__file__ + '/../..') if sys.path[0] != _path: sys.path.insert(0, _path) del _path # pylint: disable=wrong-import-position import rh.shell # This will erase all content in the current line after the cursor. This is # useful for partial updates & progress messages as the terminal can display # it better. CSI_ERASE_LINE_AFTER = '\x1b[K' class Color(object): """Conditionally wraps text in ANSI color escape sequences.""" BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) BOLD = -1 COLOR_START = '\033[1;%dm' BOLD_START = '\033[1m' RESET = '\033[m' def __init__(self, enabled=None): """Create a new Color object, optionally disabling color output. Args: enabled: True if color output should be enabled. If False then this class will not add color codes at all. """ self._enabled = enabled def start(self, color): """Returns a start color code. Args: color: Color to use, e.g. BLACK, RED, etc... Returns: If color is enabled, returns an ANSI sequence to start the given color, otherwise returns empty string """ if self.enabled: return self.COLOR_START % (color + 30) return '' def stop(self): """Returns a stop color code. Returns: If color is enabled, returns an ANSI color reset sequence, otherwise returns empty string """ if self.enabled: return self.RESET return '' def color(self, color, text): """Returns text with conditionally added color escape sequences. Args: color: Text color -- one of the color constants defined in this class. text: The text to color. Returns: If self._enabled is False, returns the original text. If it's True, returns text with color escape sequences based on the value of color. """ if not self.enabled: return text if color == self.BOLD: start = self.BOLD_START else: start = self.COLOR_START % (color + 30) return start + text + self.RESET @property def enabled(self): """See if the colorization is enabled.""" if self._enabled is None: if 'NOCOLOR' in os.environ: self._enabled = not rh.shell.boolean_shell_value( os.environ['NOCOLOR'], False) else: self._enabled = sys.stderr.isatty() return self._enabled def print_status_line(line, print_newline=False): """Clears the current terminal line, and prints |line|. Args: line: String to print. print_newline: Print a newline at the end, if sys.stderr is a TTY. """ if sys.stderr.isatty(): output = '\r' + line + CSI_ERASE_LINE_AFTER if print_newline: output += '\n' else: output = line + '\n' sys.stderr.write(output) sys.stderr.flush() def str_prompt( prompt: str, choices: List[str], lower: bool = True, ) -> Optional[str]: """Helper function for processing user input. Args: prompt: The question to present to the user. lower: Whether to lowercase the response. Returns: The string the user entered, or None if EOF (e.g. Ctrl+D). """ prompt = f'{prompt} ({"/".join(choices)})? ' try: result = input(prompt) return result.lower() if lower else result except EOFError: # If the user hits Ctrl+D, or stdin is disabled, use the default. print() return None except KeyboardInterrupt: # If the user hits Ctrl+C, just exit the process. print() raise def boolean_prompt(prompt='Do you want to continue?', default=True, true_value='yes', false_value='no', prolog=None): """Helper function for processing boolean choice prompts. Args: prompt: The question to present to the user. default: Boolean to return if the user just presses enter. true_value: The text to display that represents a True returned. false_value: The text to display that represents a False returned. prolog: The text to display before prompt. Returns: True or False. """ true_value, false_value = true_value.lower(), false_value.lower() true_text, false_text = true_value, false_value if true_value == false_value: raise ValueError( f'true_value and false_value must differ: got {true_value!r}') if default: true_text = true_text[0].upper() + true_text[1:] else: false_text = false_text[0].upper() + false_text[1:] if prolog: prompt = f'\n{prolog}\n{prompt}' prompt = '\n' + prompt while True: response = str_prompt(prompt, choices=(true_text, false_text)) if not response: return default if true_value.startswith(response): if not false_value.startswith(response): return True # common prefix between the two... elif false_value.startswith(response): return False