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