Colored help¶
Extend Cloup’s own help formatter and theme to add colorization of:
Options
Choices
Metavars
Cli name
Sub-commands
Command aliases
Long and short options
Choices
Metavars
Environment variables
Defaults
Todo
Write examples and tutorial.
Why not use rich-click
?¶
rich-click
is a good project that aims to integrate Rich with Click. Like Click Extra, it provides a ready-to-use help formatter for Click.
But contrary to Click Extra, the help screen is rendered within a table, which takes the whole width of the terminal. This is not ideal if you try to print the output of a command somewhere else.
The typical use-case is users reporting issues on GitHub, and pasting the output of a command in the issue description. If the output is too wide, it will be akwardly wrapped, or adds a horizontal scrollbar to the page.
Without a table imposing a maximal width, the help screens from Click Extra will be rendered with the minimal width of the text, and will be more readable.
Hint
This is just a matter of preference, as nothing prevents you to use both rich-click
and Click Extra in the same project, and get the best from both.
color_option
¶
Todo
Write examples and tutorial.
help_option
¶
Todo
Write examples and tutorial.
Colors and styles¶
Here is a little CLI to demonstrate the rendering of colors and styles, based on cloup.styling.Style
:
from click import command
from click_extra import Color, style, Choice, option
from click_extra.tabulate import render_table
all_styles = [
"bold",
"dim",
"underline",
"overline",
"italic",
"blink",
"reverse",
"strikethrough",
]
all_colors = sorted(Color._dict.values())
@command
@option("--matrix", type=Choice(["colors", "styles"]))
def render_matrix(matrix):
table = []
if matrix == "colors":
table_headers = ["Foreground ↴ \ Background →"] + all_colors
for fg_color in all_colors:
line = [
style(fg_color, fg=fg_color)
]
for bg_color in all_colors:
line.append(
style(fg_color, fg=fg_color, bg=bg_color)
)
table.append(line)
elif matrix == "styles":
table_headers = ["Color ↴ \ Style →"] + all_styles
for color_name in all_colors:
line = [
style(color_name, fg=color_name)
]
for prop in all_styles:
line.append(
style(color_name, fg=color_name, **{prop: True})
)
table.append(line)
render_table(table, headers=table_headers)
$ render-matrix --matrix=colors
Foreground ↴ \ Background → black blue bright_black bright_blue bright_cyan bright_green bright_magenta bright_red bright_white bright_yellow cyan green magenta red reset white yellow
--------------------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- --------------
black black black black black black black black black black black black black black black black black black
blue blue blue blue blue blue blue blue blue blue blue blue blue blue blue blue blue blue
bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black
bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue
bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan
bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green
bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta
bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red
bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white
bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow
cyan cyan cyan cyan cyan cyan cyan cyan cyan cyan cyan cyan cyan cyan cyan cyan cyan cyan
green green green green green green green green green green green green green green green green green green
magenta magenta magenta magenta magenta magenta magenta magenta magenta magenta magenta magenta magenta magenta magenta magenta magenta magenta
red red red red red red red red red red red red red red red red red red
reset reset reset reset reset reset reset reset reset reset reset reset reset reset reset reset reset reset
white white white white white white white white white white white white white white white white white white
yellow yellow yellow yellow yellow yellow yellow yellow yellow yellow yellow yellow yellow yellow yellow yellow yellow yellow
$ render-matrix --matrix=styles
Color ↴ \ Style → bold dim underline overline italic blink reverse strikethrough
----------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- --------------
black black black black black black black black black
blue blue blue blue blue blue blue blue blue
bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black bright_black
bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue bright_blue
bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan bright_cyan
bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green bright_green
bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta bright_magenta
bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red bright_red
bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white bright_white
bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow bright_yellow
cyan cyan cyan cyan cyan cyan cyan cyan cyan
green green green green green green green green green
magenta magenta magenta magenta magenta magenta magenta magenta magenta
red red red red red red red red red
reset reset reset reset reset reset reset reset reset
white white white white white white white white white
yellow yellow yellow yellow yellow yellow yellow yellow yellow
Caution
The current rendering of colors and styles in this HTML documentation is not complete, and does not reflect the real output in a terminal.
That is because pygments-ansi-color
, the component we rely on to render ANSI code in Sphinx via Pygments, only supports a subset of the ANSI codes we use.
Tip
The code above is presented as a CLI, so you can copy and run it yourself in your environment, and see the output in your terminal. That way you can evaluate the real effect of these styles and colors for your end users.
click_extra.colorize
API¶
classDiagram ExtraOption <|-- ColorOption ExtraOption <|-- HelpOption HelpFormatter <|-- HelpExtraFormatter HelpTheme <|-- HelpExtraTheme
Helpers and utilities to apply ANSI coloring to terminal content.
- class click_extra.colorize.HelpExtraTheme(invoked_command=<function identity>, command_help=<function identity>, heading=<function identity>, constraint=<function identity>, section_help=<function identity>, col1=<function identity>, col2=<function identity>, alias=<function identity>, alias_secondary=None, epilog=<function identity>, critical=<function identity>, error=<function identity>, warning=<function identity>, info=<function identity>, debug=<function identity>, option=<function identity>, subcommand=<function identity>, choice=<function identity>, metavar=<function identity>, bracket=<function identity>, envvar=<function identity>, default=<function identity>, deprecated=<function identity>, search=<function identity>, success=<function identity>, subheading=<function identity>)[source]¶
Bases:
HelpTheme
Extends
cloup.HelpTheme
withlogging.levels
and extra properties.- subheading()¶
Non-canonical Click Extra properties. :rtype:
TypeVar
(T
)Note
Subheading is used for sub-sections, like in the help of mail-deduplicate.
Todo
Maybe this shouldn’t be in Click Extra because it is a legacy inheritance from one of my other project.
- with_(**kwargs)[source]¶
Derives a new theme from the current one, with some styles overridden.
Returns the same instance if the provided styles are the same as the current.
- Return type:
- static light()[source]¶
A theme assuming a light terminal background color. :rtype:
HelpExtraTheme
Todo
Tweak colors to make them more readable.
- click_extra.colorize.KO = '\x1b[31m✘\x1b[0m'¶
Pre-rendered UI-elements.
- click_extra.colorize.color_env_vars = {'CLICOLOR': True, 'CLICOLORS': True, 'CLICOLORS_FORCE': True, 'CLICOLOR_FORCE': True, 'COLOR': True, 'COLORS': True, 'FORCE_COLOR': True, 'FORCE_COLORS': True, 'NOCOLOR': False, 'NOCOLORS': False, 'NO_COLOR': False, 'NO_COLORS': False}¶
List of environment variables recognized as flags to switch color rendering on or off.
The key is the name of the variable and the boolean value the value to pass to
--color
option flag when encountered.
- class click_extra.colorize.ColorOption(param_decls=None, is_flag=True, default=True, is_eager=True, expose_value=False, help='Strip out all colors and all ANSI codes from output.', **kwargs)[source]¶
Bases:
ExtraOption
A pre-configured option that is adding a
--color
/--no-color
(aliased by--ansi
/--no-ansi
) option to keep or strip colors and ANSI codes from CLI output.This option is eager by default to allow for other eager options (like
--version
) to be rendered colorless.Todo
Should we switch to
--color=<auto|never|always>
as GNU tools does?Also see how the isatty property defaults with this option, and how it can be implemented in Python.
Todo
Support the TERM environment variable convention?
- class click_extra.colorize.HelpOption(param_decls=None, is_flag=True, expose_value=False, is_eager=True, help='Show this message and exit.', **kwargs)[source]¶
Bases:
ExtraOption
Like Click’s @help_option but made into a reusable class-based option.
Note
Keep implementation in sync with upstream for drop-in replacement compatibility.
Todo
Reuse Click’s
HelpOption
once this PR is merged: https://github.com/pallets/click/pull/2563Same defaults as Click’s @help_option but with
-h
short option.See: https://github.com/pallets/click/blob/d9af5cf/src/click/decorators.py#L563C23-L563C34
- static print_help(ctx, param, value)[source]¶
Prints help text and exits.
Exact same behavior as Click’s original @help_option callback, but forces the closing of the context before exiting.
- Return type:
- class click_extra.colorize.ExtraHelpColorsMixin[source]¶
Bases:
object
Adds extra-keywords highlighting to Click commands.
This mixin for
click.Command
-like classes intercepts the top-level helper- generation method to initialize the formatter with dynamic settings. This is implemented at this stage so we have access to the global context.
- click_extra.colorize.escape_for_help_screen(text)[source]¶
Prepares a string to be used in a regular expression for matches in help screen.
Applies re.escape, then accounts for long strings being wrapped on multiple lines and padded with spaces to fit the columnar layout.
- Return type:
It allows for: - additional number of optional blank characters (line-returns, spaces, tabs, …)
after a dash, as the help renderer is free to wrap strings after a dash.
a space to be replaced by any number of blank characters.
- class click_extra.colorize.HelpExtraFormatter(*args, **kwargs)[source]¶
Bases:
HelpFormatter
Extends Cloup’s custom HelpFormatter to highlights options, choices, metavars and default values.
This is being discussed for upstream integration at:
Forces theme to our default.
Also transform Cloup’s standard
HelpTheme
to our ownHelpExtraTheme
.- theme: HelpExtraTheme¶
- cli_names: set[str] = {}¶
- subcommands: set[str] = {}¶
- command_aliases: set[str] = {}¶
- long_options: set[str] = {}¶
- short_options: set[str] = {}¶
- choices: set[str] = {}¶
- metavars: set[str] = {}¶
- envvars: set[str] = {}¶
- defaults: set[str] = {}¶
- style_aliases = {'bracket_1': 'bracket', 'bracket_2': 'bracket', 'default_label': 'bracket', 'envvar_label': 'bracket', 'label_sep_1': 'bracket', 'label_sep_2': 'bracket', 'label_sep_3': 'bracket', 'long_option': 'option', 'range': 'bracket', 'required_label': 'bracket', 'short_option': 'option'}¶
Map regex’s group IDs to styles.
Most of the time, the style name is the same as the group ID. But some regular expression implementations requires us to work around group IDs limitations, like
bracket_1
andbracket_2
. In which case we use this mapping to apply back the canonical style to that regex-specific group ID.
- get_style_id(group_id)[source]¶
Get the style ID to apply to a group.
Return the style which has the same ID as the group, unless it is defined in the
style_aliases
mapping above.- Return type:
- width: int¶
- buffer: t.List[str]¶
- colorize_group(str_to_style, group_id)[source]¶
Colorize a string according to the style of the group ID.
- Return type:
- colorize(match)[source]¶
Colorize all groups with IDs in the provided matching result.
All groups without IDs are left as-is.
All groups are processed in the order they appear in the
match
object. Then all groups are concatenated to form the final string that is returned. :rtype:str
Caution
Implementation is a bit funky here because there is no way to iterate over both unnamed and named groups, in the order they appear in the regex, while keeping track of the group ID.
So we have to iterate over the list of matching strings and pick up the corresponding group ID along the way, from the
match.groupdict()
dictionary. This also means we assume that thematch.groupdict()
is returning an ordered dictionary. Which is supposed to be true as of Python 3.7.
- highlight_extra_keywords(help_text)[source]¶
Highlight extra keywords in help screens based on the theme.
It is based on regular expressions. While this is not a bullet-proof method, it is good enough. After all, help screens are not consumed by machine but are designed for humans. :rtype:
str
Danger
All the regular expressions below are designed to match its original string into a sequence of contiguous groups.
This means each part of the matching result must be encapsulated in a group. And subgroups are not allowed (unless their are explicitly set as non-matching with
(?:...)
prefix).Groups with a name must have a corresponding style.
- click_extra.colorize.highlight(content, substrings, styling_method, ignore_case=False)[source]¶
Highlights parts of the
string
that matchessubstrings
.Takes care of overlapping parts within the
string
.- Return type:
- ..todo:
Same as the
ignore_case
parameter, should we support case-folding? As in “Straße” => “Strasse”? Beware, it messes with string length and characters index…