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 with logging.levels and extra properties.

critical()
Return type:

TypeVar(T)

error()
Return type:

TypeVar(T)

warning()
Return type:

TypeVar(T)

info()
Return type:

TypeVar(T)

debug()

Log levels from Python’s logging module.

Return type:

TypeVar(T)

option()
Return type:

TypeVar(T)

subcommand()
Return type:

TypeVar(T)

choice()
Return type:

TypeVar(T)

metavar()
Return type:

TypeVar(T)

bracket()
Return type:

TypeVar(T)

envvar()
Return type:

TypeVar(T)

default()
Return type:

TypeVar(T)

deprecated()
Return type:

TypeVar(T)

search()
Return type:

TypeVar(T)

success()

Click Extra new coloring properties.

Return type:

TypeVar(T)

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:

HelpExtraTheme

static dark()[source]

A theme assuming a dark terminal background color.

Return type:

HelpExtraTheme

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.

Source: https://github.com/pallets/click/issues/558

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.

static disable_colors(ctx, param, value)[source]

Callback disabling all coloring utilities.

Re-inspect the environment for existence of colorization flags to re-interpret the provided value.

Return type:

None

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/2563

Same 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:

None

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.

get_help_option(ctx)[source]

Returns our custom help option object instead of Click’s default one.

Return type:

Option | None

get_help(ctx)[source]

Replace default formatter by our own.

Return type:

str

format_help(ctx, formatter)[source]

Feed our custom formatter instance with the keywords to highlight.

Return type:

None

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:

str

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 own HelpExtraTheme.

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 and bracket_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:

str

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:

str

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 the match.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.

getvalue()[source]

Wrap original Click.HelpFormatter.getvalue() to force extra-colorization on rendering.

Return type:

str

click_extra.colorize.highlight(content, substrings, styling_method, ignore_case=False)[source]

Highlights parts of the string that matches substrings.

Takes care of overlapping parts within the string.

Return type:

str

..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…