Skip to content

Commit ee2c9bb

Browse files
committed
Added rich_utils module which is the foundation code for supporting Rich in the rest of cmd2.
1 parent beed0d1 commit ee2c9bb

File tree

1 file changed

+127
-0
lines changed

1 file changed

+127
-0
lines changed

cmd2/rich_utils.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""Provides common utilities to support Rich in cmd2 applications."""
2+
3+
from collections.abc import Mapping
4+
from enum import Enum
5+
from typing import (
6+
IO,
7+
Any,
8+
Optional,
9+
)
10+
11+
from rich.console import Console
12+
from rich.style import (
13+
Style,
14+
StyleType,
15+
)
16+
from rich.theme import Theme
17+
from rich_argparse import RichHelpFormatter
18+
19+
20+
class AllowStyle(Enum):
21+
"""Values for ``cmd2.rich_utils.allow_style``."""
22+
23+
ALWAYS = 'Always' # Always output ANSI style sequences
24+
NEVER = 'Never' # Remove ANSI style sequences from all output
25+
TERMINAL = 'Terminal' # Remove ANSI style sequences if the output is not going to the terminal
26+
27+
def __str__(self) -> str:
28+
"""Return value instead of enum name for printing in cmd2's set command."""
29+
return str(self.value)
30+
31+
def __repr__(self) -> str:
32+
"""Return quoted value instead of enum description for printing in cmd2's set command."""
33+
return repr(self.value)
34+
35+
36+
# Controls when ANSI style sequences are allowed in output
37+
allow_style = AllowStyle.TERMINAL
38+
39+
# Default styles for cmd2
40+
DEFAULT_CMD2_STYLES: dict[str, StyleType] = {
41+
"cmd2.success": Style(color="green"),
42+
"cmd2.warning": Style(color="bright_yellow"),
43+
"cmd2.error": Style(color="bright_red"),
44+
"cmd2.help_header": Style(color="bright_green", bold=True),
45+
}
46+
47+
# Include default styles from RichHelpFormatter
48+
DEFAULT_CMD2_STYLES.update(RichHelpFormatter.styles.copy())
49+
50+
51+
class Cmd2Theme(Theme):
52+
"""Rich theme class used by Cmd2Console."""
53+
54+
def __init__(self, styles: Optional[Mapping[str, StyleType]] = None, inherit: bool = True) -> None:
55+
"""Cmd2Theme initializer.
56+
57+
:param styles: optional mapping of style names on to styles.
58+
Defaults to None for a theme with no styles.
59+
:param inherit: Inherit default styles. Defaults to True.
60+
"""
61+
cmd2_styles = DEFAULT_CMD2_STYLES.copy() if inherit else {}
62+
if styles is not None:
63+
cmd2_styles.update(styles)
64+
65+
super().__init__(cmd2_styles, inherit=inherit)
66+
67+
68+
# Current Rich theme used by Cmd2Console
69+
THEME: Cmd2Theme = Cmd2Theme()
70+
71+
72+
def set_theme(new_theme: Cmd2Theme) -> None:
73+
"""Set the Rich theme used by Cmd2Console and rich-argparse.
74+
75+
:param new_theme: new theme to use.
76+
"""
77+
global THEME # noqa: PLW0603
78+
THEME = new_theme
79+
80+
# Make sure the new theme has all style names included in a Cmd2Theme.
81+
missing_names = Cmd2Theme().styles.keys() - THEME.styles.keys()
82+
for name in missing_names:
83+
THEME.styles[name] = Style()
84+
85+
# Update rich-argparse styles
86+
for name in RichHelpFormatter.styles.keys() & THEME.styles.keys():
87+
RichHelpFormatter.styles[name] = THEME.styles[name]
88+
89+
90+
class Cmd2Console(Console):
91+
"""Rich console with characteristics appropriate for cmd2 applications."""
92+
93+
def __init__(self, file: IO[str]) -> None:
94+
"""Cmd2Console initializer.
95+
96+
:param file: a file object where the console should write to
97+
"""
98+
kwargs: dict[str, Any] = {}
99+
if allow_style == AllowStyle.ALWAYS:
100+
kwargs["force_terminal"] = True
101+
102+
# Turn off interactive mode if dest is not actually a terminal which supports it
103+
tmp_console = Console(file=file)
104+
kwargs["force_interactive"] = tmp_console.is_interactive
105+
elif allow_style == AllowStyle.NEVER:
106+
kwargs["force_terminal"] = False
107+
108+
# Turn off automatic markup, emoji, and highlight rendering at the console level.
109+
# You can still enable these in Console.print() calls.
110+
super().__init__(
111+
file=file,
112+
tab_size=4,
113+
markup=False,
114+
emoji=False,
115+
highlight=False,
116+
theme=THEME,
117+
**kwargs,
118+
)
119+
120+
def on_broken_pipe(self) -> None:
121+
"""Override which raises BrokenPipeError instead of SystemExit."""
122+
import contextlib
123+
124+
with contextlib.suppress(SystemExit):
125+
super().on_broken_pipe()
126+
127+
raise BrokenPipeError

0 commit comments

Comments
 (0)