Files
osxphotos/osxphotos/cli/click_rich_echo.py
Rhet Turnbull 6c57fb2df9 Theme (#664)
* Initial theme manager, not yet done

* Added rich_theme_manager

* Updated rich-theme-manager

* Switched to rich_theme_manager for theme management

* Updated dependencies

* Added rich paging to subtopic help

* Fixed clone to clone only styles specified in cloned theme

* Added placeholder for help colors

* Updated config dir, help methods
2022-04-17 22:53:42 -07:00

269 lines
8.3 KiB
Python

"""click.echo replacement that supports rich text formatting"""
import inspect
import os
import typing as t
import click
from rich.console import Console
from rich.markdown import Markdown
from rich.theme import Theme
from .common import time_stamp
__all__ = [
"get_rich_console",
"get_rich_theme",
"rich_click_echo",
"rich_echo",
"rich_echo_error",
"rich_echo_via_pager",
"set_rich_console",
"set_rich_theme",
"set_rich_timestamp",
]
# TODO: this should really be a class instead of a module with a bunch of globals
# include emoji's in rich_echo_error output
ERROR_EMOJI = True
class _Console:
"""Store console object for rich output"""
def __init__(self):
self._console: t.Optional[Console] = None
@property
def console(self):
return self._console
@console.setter
def console(self, console: Console):
self._console = console
_console = _Console()
_theme = None
_timestamp = False
# set to 1 if running tests
OSXPHOTOS_IS_TESTING = bool(os.getenv("OSXPHOTOS_IS_TESTING", default=False))
def set_rich_console(console: Console) -> None:
"""Set the console object to use for rich_echo and rich_echo_via_pager"""
global _console
_console.console = console
def get_rich_console() -> Console:
"""Get console object
Returns:
Console object
"""
global _console
return _console.console
def set_rich_theme(theme: Theme) -> None:
"""Set the theme to use for rich_click_echo"""
global _theme
_theme = theme
def get_rich_theme() -> t.Optional[Theme]:
"""Get the theme to use for rich_click_echo"""
global _theme
return _theme
def set_rich_timestamp(timestamp: bool) -> None:
"""Set whether to print timestamp with rich_echo, rich_echo_error, and rich_click_error"""
global _timestamp
_timestamp = timestamp
def rich_echo(
message: t.Optional[t.Any] = None,
theme=None,
markdown=False,
highlight=False,
**kwargs: t.Any,
) -> None:
"""Echo text to the console with rich formatting.
Args:
message: The string or bytes to output. Other objects are converted to strings.
theme: optional rich.theme.Theme object to use for formatting
markdown: if True, interpret message as Markdown
highlight: if True, use automatic rich.print highlighting
kwargs: any extra arguments are passed to rich.console.Console.print() and click.echo
if kwargs contains 'file', 'nl', 'err', 'color', these are passed to click.echo,
all other values passed to rich.console.Console.print()
"""
# args for click.echo that may have been passed in kwargs
echo_args = {}
for arg in ("file", "nl", "err", "color"):
val = kwargs.pop(arg, None)
if val is not None:
echo_args[arg] = val
width = kwargs.pop("width", None)
if width is None and OSXPHOTOS_IS_TESTING:
# if not outputting to terminal, use a huge width to avoid wrapping
# otherwise tests fail
width = 10_000
console = get_rich_console() or Console(theme=theme, width=width)
if markdown:
message = Markdown(message)
# Markdown always adds a new line so disable unless explicitly specified
global _timestamp
if _timestamp:
message = time_stamp() + message
console.print(message, highlight=highlight, **kwargs)
def rich_echo_error(
message: t.Optional[t.Any] = None,
theme=None,
markdown=False,
highlight=False,
**kwargs: t.Any,
) -> None:
"""Echo text to the console with rich formatting and if stdout is redirected, echo to stderr
Args:
message: The string or bytes to output. Other objects are converted to strings.
theme: optional rich.theme.Theme object to use for formatting
markdown: if True, interpret message as Markdown
highlight: if True, use automatic rich.print highlighting
kwargs: any extra arguments are passed to rich.console.Console.print() and click.echo
if kwargs contains 'file', 'nl', 'err', 'color', these are passed to click.echo,
all other values passed to rich.console.Console.print()
"""
global ERROR_EMOJI
if ERROR_EMOJI:
if "[error]" in message:
message = f":cross_mark-emoji: {message}"
elif "[warning]" in message:
message = f":warning-emoji: {message}"
console = get_rich_console() or Console(theme=theme or get_rich_theme())
if not console.is_terminal:
# if stdout is redirected, echo to stderr
rich_click_echo(
message,
theme=theme or get_rich_theme(),
markdown=markdown,
highlight=highlight,
**kwargs,
err=True,
)
else:
rich_echo(
message,
theme=theme or get_rich_theme(),
markdown=markdown,
highlight=highlight,
**kwargs,
)
def rich_click_echo(
message: t.Optional[t.Any] = None,
theme=None,
markdown=False,
highlight=False,
**kwargs: t.Any,
) -> None:
"""Echo text to the console with rich formatting using click.echo
This is a wrapper around click.echo that supports rich text formatting.
Args:
message: The string or bytes to output. Other objects are converted to strings.
theme: optional rich.theme.Theme object to use for formatting
markdown: if True, interpret message as Markdown
highlight: if True, use automatic rich.print highlighting
kwargs: any extra arguments are passed to rich.console.Console.print() and click.echo
if kwargs contains 'file', 'nl', 'err', 'color', these are passed to click.echo,
all other values passed to rich.console.Console.print()
"""
# args for click.echo that may have been passed in kwargs
echo_args = {}
for arg in ("file", "nl", "err", "color"):
val = kwargs.pop(arg, None)
if val is not None:
echo_args[arg] = val
# click.echo will include "\n" so don't add it here unless specified
end = kwargs.pop("end", "")
if width := kwargs.pop("width", None) is None:
# if not outputting to terminal, use a huge width to avoid wrapping
# otherwise tests fail
temp_console = Console()
width = temp_console.width if temp_console.is_terminal else 10_000
console = Console(
force_terminal=True,
theme=theme or get_rich_theme(),
width=width,
)
if markdown:
message = Markdown(message)
# Markdown always adds a new line so disable unless explicitly specified
echo_args["nl"] = echo_args.get("nl") is True
global _timestamp
if _timestamp:
message = time_stamp() + message
with console.capture() as capture:
console.print(message, end=end, highlight=highlight, **kwargs)
click.echo(capture.get(), **echo_args)
def rich_echo_via_pager(
text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str],
theme: t.Optional[Theme] = None,
highlight=False,
markdown: bool = False,
**kwargs,
) -> None:
"""This function takes a text and shows it via an environment specific
pager on stdout.
Args:
text_or_generator: the text to page, or alternatively, a generator emitting the text to page.
theme: optional rich.theme.Theme object to use for formatting
markdown: if True, interpret message as Markdown
highlight: if True, use automatic rich.print highlighting
**kwargs: if "color" in kwargs, works the same as click.echo_via_pager(color=color)
otherwise any kwargs are passed to rich.Console.print()
"""
if inspect.isgeneratorfunction(text_or_generator):
text_or_generator = t.cast(t.Callable[[], t.Iterable[str]], text_or_generator)()
elif isinstance(text_or_generator, str):
text_or_generator = [text_or_generator]
else:
try:
text_or_generator = iter(text_or_generator)
except TypeError:
text_or_generator = [text_or_generator]
console = _console.console or Console(theme=theme)
color = kwargs.pop("color", True)
with console.pager(styles=color):
for x in text_or_generator:
if isinstance(x, str) and markdown:
x = Markdown(x)
console.print(x, highlight=highlight, **kwargs)