1*d68f33bcSAndroid Build Coastguard Worker# Copyright 2016 The Android Open Source Project 2*d68f33bcSAndroid Build Coastguard Worker# 3*d68f33bcSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 4*d68f33bcSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 5*d68f33bcSAndroid Build Coastguard Worker# You may obtain a copy of the License at 6*d68f33bcSAndroid Build Coastguard Worker# 7*d68f33bcSAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 8*d68f33bcSAndroid Build Coastguard Worker# 9*d68f33bcSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 10*d68f33bcSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 11*d68f33bcSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*d68f33bcSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 13*d68f33bcSAndroid Build Coastguard Worker# limitations under the License. 14*d68f33bcSAndroid Build Coastguard Worker 15*d68f33bcSAndroid Build Coastguard Worker"""Terminal utilities 16*d68f33bcSAndroid Build Coastguard Worker 17*d68f33bcSAndroid Build Coastguard WorkerThis module handles terminal interaction including ANSI color codes. 18*d68f33bcSAndroid Build Coastguard Worker""" 19*d68f33bcSAndroid Build Coastguard Worker 20*d68f33bcSAndroid Build Coastguard Workerimport os 21*d68f33bcSAndroid Build Coastguard Workerimport sys 22*d68f33bcSAndroid Build Coastguard Workerfrom typing import List, Optional 23*d68f33bcSAndroid Build Coastguard Worker 24*d68f33bcSAndroid Build Coastguard Worker_path = os.path.realpath(__file__ + '/../..') 25*d68f33bcSAndroid Build Coastguard Workerif sys.path[0] != _path: 26*d68f33bcSAndroid Build Coastguard Worker sys.path.insert(0, _path) 27*d68f33bcSAndroid Build Coastguard Workerdel _path 28*d68f33bcSAndroid Build Coastguard Worker 29*d68f33bcSAndroid Build Coastguard Worker# pylint: disable=wrong-import-position 30*d68f33bcSAndroid Build Coastguard Workerimport rh.shell 31*d68f33bcSAndroid Build Coastguard Worker 32*d68f33bcSAndroid Build Coastguard Worker 33*d68f33bcSAndroid Build Coastguard Worker# This will erase all content in the current line after the cursor. This is 34*d68f33bcSAndroid Build Coastguard Worker# useful for partial updates & progress messages as the terminal can display 35*d68f33bcSAndroid Build Coastguard Worker# it better. 36*d68f33bcSAndroid Build Coastguard WorkerCSI_ERASE_LINE_AFTER = '\x1b[K' 37*d68f33bcSAndroid Build Coastguard Worker 38*d68f33bcSAndroid Build Coastguard Worker 39*d68f33bcSAndroid Build Coastguard Workerclass Color(object): 40*d68f33bcSAndroid Build Coastguard Worker """Conditionally wraps text in ANSI color escape sequences.""" 41*d68f33bcSAndroid Build Coastguard Worker 42*d68f33bcSAndroid Build Coastguard Worker BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) 43*d68f33bcSAndroid Build Coastguard Worker BOLD = -1 44*d68f33bcSAndroid Build Coastguard Worker COLOR_START = '\033[1;%dm' 45*d68f33bcSAndroid Build Coastguard Worker BOLD_START = '\033[1m' 46*d68f33bcSAndroid Build Coastguard Worker RESET = '\033[m' 47*d68f33bcSAndroid Build Coastguard Worker 48*d68f33bcSAndroid Build Coastguard Worker def __init__(self, enabled=None): 49*d68f33bcSAndroid Build Coastguard Worker """Create a new Color object, optionally disabling color output. 50*d68f33bcSAndroid Build Coastguard Worker 51*d68f33bcSAndroid Build Coastguard Worker Args: 52*d68f33bcSAndroid Build Coastguard Worker enabled: True if color output should be enabled. If False then this 53*d68f33bcSAndroid Build Coastguard Worker class will not add color codes at all. 54*d68f33bcSAndroid Build Coastguard Worker """ 55*d68f33bcSAndroid Build Coastguard Worker self._enabled = enabled 56*d68f33bcSAndroid Build Coastguard Worker 57*d68f33bcSAndroid Build Coastguard Worker def start(self, color): 58*d68f33bcSAndroid Build Coastguard Worker """Returns a start color code. 59*d68f33bcSAndroid Build Coastguard Worker 60*d68f33bcSAndroid Build Coastguard Worker Args: 61*d68f33bcSAndroid Build Coastguard Worker color: Color to use, e.g. BLACK, RED, etc... 62*d68f33bcSAndroid Build Coastguard Worker 63*d68f33bcSAndroid Build Coastguard Worker Returns: 64*d68f33bcSAndroid Build Coastguard Worker If color is enabled, returns an ANSI sequence to start the given 65*d68f33bcSAndroid Build Coastguard Worker color, otherwise returns empty string 66*d68f33bcSAndroid Build Coastguard Worker """ 67*d68f33bcSAndroid Build Coastguard Worker if self.enabled: 68*d68f33bcSAndroid Build Coastguard Worker return self.COLOR_START % (color + 30) 69*d68f33bcSAndroid Build Coastguard Worker return '' 70*d68f33bcSAndroid Build Coastguard Worker 71*d68f33bcSAndroid Build Coastguard Worker def stop(self): 72*d68f33bcSAndroid Build Coastguard Worker """Returns a stop color code. 73*d68f33bcSAndroid Build Coastguard Worker 74*d68f33bcSAndroid Build Coastguard Worker Returns: 75*d68f33bcSAndroid Build Coastguard Worker If color is enabled, returns an ANSI color reset sequence, otherwise 76*d68f33bcSAndroid Build Coastguard Worker returns empty string 77*d68f33bcSAndroid Build Coastguard Worker """ 78*d68f33bcSAndroid Build Coastguard Worker if self.enabled: 79*d68f33bcSAndroid Build Coastguard Worker return self.RESET 80*d68f33bcSAndroid Build Coastguard Worker return '' 81*d68f33bcSAndroid Build Coastguard Worker 82*d68f33bcSAndroid Build Coastguard Worker def color(self, color, text): 83*d68f33bcSAndroid Build Coastguard Worker """Returns text with conditionally added color escape sequences. 84*d68f33bcSAndroid Build Coastguard Worker 85*d68f33bcSAndroid Build Coastguard Worker Args: 86*d68f33bcSAndroid Build Coastguard Worker color: Text color -- one of the color constants defined in this class. 87*d68f33bcSAndroid Build Coastguard Worker text: The text to color. 88*d68f33bcSAndroid Build Coastguard Worker 89*d68f33bcSAndroid Build Coastguard Worker Returns: 90*d68f33bcSAndroid Build Coastguard Worker If self._enabled is False, returns the original text. If it's True, 91*d68f33bcSAndroid Build Coastguard Worker returns text with color escape sequences based on the value of color. 92*d68f33bcSAndroid Build Coastguard Worker """ 93*d68f33bcSAndroid Build Coastguard Worker if not self.enabled: 94*d68f33bcSAndroid Build Coastguard Worker return text 95*d68f33bcSAndroid Build Coastguard Worker if color == self.BOLD: 96*d68f33bcSAndroid Build Coastguard Worker start = self.BOLD_START 97*d68f33bcSAndroid Build Coastguard Worker else: 98*d68f33bcSAndroid Build Coastguard Worker start = self.COLOR_START % (color + 30) 99*d68f33bcSAndroid Build Coastguard Worker return start + text + self.RESET 100*d68f33bcSAndroid Build Coastguard Worker 101*d68f33bcSAndroid Build Coastguard Worker @property 102*d68f33bcSAndroid Build Coastguard Worker def enabled(self): 103*d68f33bcSAndroid Build Coastguard Worker """See if the colorization is enabled.""" 104*d68f33bcSAndroid Build Coastguard Worker if self._enabled is None: 105*d68f33bcSAndroid Build Coastguard Worker if 'NOCOLOR' in os.environ: 106*d68f33bcSAndroid Build Coastguard Worker self._enabled = not rh.shell.boolean_shell_value( 107*d68f33bcSAndroid Build Coastguard Worker os.environ['NOCOLOR'], False) 108*d68f33bcSAndroid Build Coastguard Worker else: 109*d68f33bcSAndroid Build Coastguard Worker self._enabled = sys.stderr.isatty() 110*d68f33bcSAndroid Build Coastguard Worker return self._enabled 111*d68f33bcSAndroid Build Coastguard Worker 112*d68f33bcSAndroid Build Coastguard Worker 113*d68f33bcSAndroid Build Coastguard Workerdef print_status_line(line, print_newline=False): 114*d68f33bcSAndroid Build Coastguard Worker """Clears the current terminal line, and prints |line|. 115*d68f33bcSAndroid Build Coastguard Worker 116*d68f33bcSAndroid Build Coastguard Worker Args: 117*d68f33bcSAndroid Build Coastguard Worker line: String to print. 118*d68f33bcSAndroid Build Coastguard Worker print_newline: Print a newline at the end, if sys.stderr is a TTY. 119*d68f33bcSAndroid Build Coastguard Worker """ 120*d68f33bcSAndroid Build Coastguard Worker if sys.stderr.isatty(): 121*d68f33bcSAndroid Build Coastguard Worker output = '\r' + line + CSI_ERASE_LINE_AFTER 122*d68f33bcSAndroid Build Coastguard Worker if print_newline: 123*d68f33bcSAndroid Build Coastguard Worker output += '\n' 124*d68f33bcSAndroid Build Coastguard Worker else: 125*d68f33bcSAndroid Build Coastguard Worker output = line + '\n' 126*d68f33bcSAndroid Build Coastguard Worker 127*d68f33bcSAndroid Build Coastguard Worker sys.stderr.write(output) 128*d68f33bcSAndroid Build Coastguard Worker sys.stderr.flush() 129*d68f33bcSAndroid Build Coastguard Worker 130*d68f33bcSAndroid Build Coastguard Worker 131*d68f33bcSAndroid Build Coastguard Workerdef str_prompt( 132*d68f33bcSAndroid Build Coastguard Worker prompt: str, 133*d68f33bcSAndroid Build Coastguard Worker choices: List[str], 134*d68f33bcSAndroid Build Coastguard Worker lower: bool = True, 135*d68f33bcSAndroid Build Coastguard Worker) -> Optional[str]: 136*d68f33bcSAndroid Build Coastguard Worker """Helper function for processing user input. 137*d68f33bcSAndroid Build Coastguard Worker 138*d68f33bcSAndroid Build Coastguard Worker Args: 139*d68f33bcSAndroid Build Coastguard Worker prompt: The question to present to the user. 140*d68f33bcSAndroid Build Coastguard Worker lower: Whether to lowercase the response. 141*d68f33bcSAndroid Build Coastguard Worker 142*d68f33bcSAndroid Build Coastguard Worker Returns: 143*d68f33bcSAndroid Build Coastguard Worker The string the user entered, or None if EOF (e.g. Ctrl+D). 144*d68f33bcSAndroid Build Coastguard Worker """ 145*d68f33bcSAndroid Build Coastguard Worker prompt = f'{prompt} ({"/".join(choices)})? ' 146*d68f33bcSAndroid Build Coastguard Worker try: 147*d68f33bcSAndroid Build Coastguard Worker result = input(prompt) 148*d68f33bcSAndroid Build Coastguard Worker return result.lower() if lower else result 149*d68f33bcSAndroid Build Coastguard Worker except EOFError: 150*d68f33bcSAndroid Build Coastguard Worker # If the user hits Ctrl+D, or stdin is disabled, use the default. 151*d68f33bcSAndroid Build Coastguard Worker print() 152*d68f33bcSAndroid Build Coastguard Worker return None 153*d68f33bcSAndroid Build Coastguard Worker except KeyboardInterrupt: 154*d68f33bcSAndroid Build Coastguard Worker # If the user hits Ctrl+C, just exit the process. 155*d68f33bcSAndroid Build Coastguard Worker print() 156*d68f33bcSAndroid Build Coastguard Worker raise 157*d68f33bcSAndroid Build Coastguard Worker 158*d68f33bcSAndroid Build Coastguard Worker 159*d68f33bcSAndroid Build Coastguard Workerdef boolean_prompt(prompt='Do you want to continue?', default=True, 160*d68f33bcSAndroid Build Coastguard Worker true_value='yes', false_value='no', prolog=None): 161*d68f33bcSAndroid Build Coastguard Worker """Helper function for processing boolean choice prompts. 162*d68f33bcSAndroid Build Coastguard Worker 163*d68f33bcSAndroid Build Coastguard Worker Args: 164*d68f33bcSAndroid Build Coastguard Worker prompt: The question to present to the user. 165*d68f33bcSAndroid Build Coastguard Worker default: Boolean to return if the user just presses enter. 166*d68f33bcSAndroid Build Coastguard Worker true_value: The text to display that represents a True returned. 167*d68f33bcSAndroid Build Coastguard Worker false_value: The text to display that represents a False returned. 168*d68f33bcSAndroid Build Coastguard Worker prolog: The text to display before prompt. 169*d68f33bcSAndroid Build Coastguard Worker 170*d68f33bcSAndroid Build Coastguard Worker Returns: 171*d68f33bcSAndroid Build Coastguard Worker True or False. 172*d68f33bcSAndroid Build Coastguard Worker """ 173*d68f33bcSAndroid Build Coastguard Worker true_value, false_value = true_value.lower(), false_value.lower() 174*d68f33bcSAndroid Build Coastguard Worker true_text, false_text = true_value, false_value 175*d68f33bcSAndroid Build Coastguard Worker if true_value == false_value: 176*d68f33bcSAndroid Build Coastguard Worker raise ValueError( 177*d68f33bcSAndroid Build Coastguard Worker f'true_value and false_value must differ: got {true_value!r}') 178*d68f33bcSAndroid Build Coastguard Worker 179*d68f33bcSAndroid Build Coastguard Worker if default: 180*d68f33bcSAndroid Build Coastguard Worker true_text = true_text[0].upper() + true_text[1:] 181*d68f33bcSAndroid Build Coastguard Worker else: 182*d68f33bcSAndroid Build Coastguard Worker false_text = false_text[0].upper() + false_text[1:] 183*d68f33bcSAndroid Build Coastguard Worker 184*d68f33bcSAndroid Build Coastguard Worker if prolog: 185*d68f33bcSAndroid Build Coastguard Worker prompt = f'\n{prolog}\n{prompt}' 186*d68f33bcSAndroid Build Coastguard Worker prompt = '\n' + prompt 187*d68f33bcSAndroid Build Coastguard Worker 188*d68f33bcSAndroid Build Coastguard Worker while True: 189*d68f33bcSAndroid Build Coastguard Worker response = str_prompt(prompt, choices=(true_text, false_text)) 190*d68f33bcSAndroid Build Coastguard Worker if not response: 191*d68f33bcSAndroid Build Coastguard Worker return default 192*d68f33bcSAndroid Build Coastguard Worker if true_value.startswith(response): 193*d68f33bcSAndroid Build Coastguard Worker if not false_value.startswith(response): 194*d68f33bcSAndroid Build Coastguard Worker return True 195*d68f33bcSAndroid Build Coastguard Worker # common prefix between the two... 196*d68f33bcSAndroid Build Coastguard Worker elif false_value.startswith(response): 197*d68f33bcSAndroid Build Coastguard Worker return False 198