xref: /aosp_15_r20/tools/repohooks/rh/terminal.py (revision d68f33bc6fb0cc2476107c2af0573a2f5a63dfc1)
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