xref: /aosp_15_r20/external/pigweed/pw_cli/py/pw_cli/status_reporter.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2023 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Attractive status output to the terminal (and other places if you want)."""
15
16import logging
17from typing import Callable
18
19from pw_cli.color import colors
20
21
22def _no_color(msg: str) -> str:
23    return msg
24
25
26def _split_lines(msg: str | list[str]) -> tuple[str, list[str]]:
27    """Turn a list of strings into a tuple of the first and list of rest."""
28    if isinstance(msg, str):
29        return (msg, [])
30
31    return (msg[0], msg[1:])
32
33
34class StatusReporter:
35    """Print user-friendly status reports to the terminal for CLI tools.
36
37    You can instead redirect these lines to logs without formatting by
38    substituting ``LoggingStatusReporter``. Consumers of this should be
39    designed to take any subclass and not make assumptions about where the
40    output will go. But the reason you would choose this over plain logging is
41    because you want to support pretty-printing to the terminal.
42
43    This is also "themable" in the sense that you can subclass this, override
44    the methods with whatever formatting you want, and supply the subclass to
45    anything that expects an instance of this.
46
47    Key:
48
49    - info: Plain ol' informational status.
50    - ok: Something was checked and it was okay.
51    - new: Something needed to be changed/updated and it was successfully.
52    - wrn: Warning, non-critical.
53    - err: Error, critical.
54
55    This doesn't expose the %-style string formatting that is used in idiomatic
56    Python logging, but this shouldn't be used for performance-critical logging
57    situations anyway.
58    """
59
60    def _report(  # pylint: disable=no-self-use
61        self,
62        msg: str | list[str],
63        color: Callable[[str], str],
64        char: str,
65        func: Callable,
66        silent: bool,
67    ) -> None:
68        """Actually print/log/whatever the status lines."""
69        first_line, rest_lines = _split_lines(msg)
70        first_line = color(f'{char} {first_line}')
71        spaces = ' ' * len(char)
72        rest_lines = [color(f'{spaces} {line}') for line in rest_lines]
73
74        if not silent:
75            for line in [first_line, *rest_lines]:
76                func(line)
77
78    def demo(self):
79        """Run this to see what your status reporter output looks like."""
80        self.info(
81            [
82                'FYI, here\'s some information:',
83                'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
84                'Donec condimentum metus molestie metus maximus ultricies '
85                'ac id dolor.',
86            ]
87        )
88        self.ok('This is okay, no changes needed.')
89        self.new('We changed some things successfully!')
90        self.wrn('Uh oh, you might want to be aware of this.')
91        self.err('This is bad! Things might be broken!')
92
93    def info(self, msg: str | list[str], silent: bool = False) -> None:
94        self._report(msg, _no_color, '\u2022', print, silent)
95
96    def ok(self, msg: str | list[str], silent: bool = False) -> None:
97        self._report(msg, colors().blue, '\u2713', print, silent)
98
99    def new(self, msg: str | list[str], silent: bool = False) -> None:
100        self._report(msg, colors().green, '\u2713', print, silent)
101
102    def wrn(self, msg: str | list[str], silent: bool = False) -> None:
103        self._report(msg, colors().yellow, '\u26A0\uFE0F ', print, silent)
104
105    def err(self, msg: str | list[str], silent: bool = False) -> None:
106        self._report(msg, colors().red, '\U0001F525', print, silent)
107
108
109class LoggingStatusReporter(StatusReporter):
110    """Print status lines to logs instead of to the terminal."""
111
112    def __init__(self, logger: logging.Logger) -> None:
113        self.logger = logger
114        super().__init__()
115
116    def _report(
117        self,
118        msg: str | list[str],
119        color: Callable[[str], str],
120        char: str,
121        func: Callable,
122        silent: bool,
123    ) -> None:
124        first_line, rest_lines = _split_lines(msg)
125
126        if not silent:
127            for line in [first_line, *rest_lines]:
128                func(line)
129
130    def info(self, msg: str | list[str], silent: bool = False) -> None:
131        self._report(msg, _no_color, '', self.logger.info, silent)
132
133    def ok(self, msg: str | list[str], silent: bool = False) -> None:
134        self._report(msg, _no_color, '', self.logger.info, silent)
135
136    def new(self, msg: str | list[str], silent: bool = False) -> None:
137        self._report(msg, _no_color, '', self.logger.info, silent)
138
139    def wrn(self, msg: str | list[str], silent: bool = False) -> None:
140        self._report(msg, _no_color, '', self.logger.warning, silent)
141
142    def err(self, msg: str | list[str], silent: bool = False) -> None:
143        self._report(msg, _no_color, '', self.logger.error, silent)
144