Skip to content

Panels - Advanced

Advanced

This document contains information that the majority of users will not need.

Additional details on default order

By default, unless explicitly ordered otherwise, command panels always come after options panels.

# /// script
# dependencies = ["rich-click>=1.9"]
# ///
import rich_click as click

# This will explicitly set commands to be above options
# Merely defining the command panel or option panel alone will not do this.
# They BOTH must be defined, and then commands must be set above options.
@click.group()
@click.command_panel("Commands")
@click.option_panel("Options")
def cli():
    """CLI help text"""
    pass


@cli.command()
def subcommand():
    pass

if __name__ == "__main__":
    cli()
Output

python panels_panel_order_explicit.py --help

There exists a config option commands_before_options (default False), which changes the default behavior so that commands come before options. When explicitly defining panels of multiple types with decorators (i.e. both option panels and command panels), this config option is ignored. So for example, the below code will set options above commands:

# /// script
# dependencies = ["rich-click>=1.9"]
# ///
import rich_click as click

# Even though `commands_before_options` is True,
# the panel order is being explicitly defined by the decorators,
# and thus the `commands_before_options` config option is ignored.
@click.group()
@click.option_panel("Options")
@click.command_panel("Commands")
@click.rich_config({"commands_before_options": True})
def cli():
    """CLI help text"""
    pass

@cli.command()
def subcommand():
    """Subcommand help text"""
    pass

if __name__ == "__main__":
    cli()
Output

python panels_panel_order_explicit_override.py --help

If you do not explicitly define panels, then the sort order behavior is more advanced. The sort order in all situations is deliberate and also thoroughly tested, but it's not worth going into detail about. In short, if you want to have full control over panel sorting, then you should define each panel!

Tables & Column Types

RichPanels consist of a rich.panel.Panel which contains inside of it a rich.table.Table.

For the inner table, the column_types are configurable. The selected column types determine what gets rendered, and the order of the column_types=[...] list determines the order in which they show in the table.

Supported RichOptionPanel column types:

  • "required"
  • "opt_primary"
  • "opt_secondary"
  • "opt_long"
  • "opt_short"
  • "opt_all"
  • "opt_all_metavar"
  • "opt_long_metavar"
  • "metavar"
  • "metavar_short"
  • "help"

Supported RichCommandPanel column types:

  • "name"
  • "aliases"
  • "name_with_aliases"
  • "help"

Below is an example showing how column types can be used:

# /// script
# dependencies = ["rich-click>=1.9"]
# ///
import rich_click as click

@click.command()
@click.option("--src", "-s", help="Source", type=click.STRING, panel="Main")
@click.option("--dest", "-d", help="Destination", type=click.STRING, panel="Main")
@click.option("--env", "-e", help="Environment", panel="Extra")
@click.option("--quiet/--no-quiet", "-q/-Q", help="Quiet logging", panel="Extra")
@click.help_option("--help", "-h", panel="Extra")
@click.option_panel("Main", column_types=["opt_short", "opt_long", "metavar", "help"])
@click.option_panel("Extra", column_types=["help", "metavar", "opt_primary", "opt_secondary"])
@click.rich_config({
    "delimiter_comma": ", ",
    "style_option_negative": "bold magenta",
    "style_switch_negative": "bold blue",
})
def move_item(src, dest, env, quiet):
    """Move an item from a src location to a dest location"""
    pass

if __name__ == "__main__":
    move_item()
Output

python panels_column_types.py --help

Help Text Sections

The "help" column type shows information such as deprecations, env var, default value, and the help text itself.

These sections-- whether they render at all, or in which order they render-- are all configurable via the config.

Supported Option help section types (configurable via options_table_help_sections):

  • "help"
  • "required"
  • "envvar"
  • "default"
  • "range"
  • "metavar"
  • "metavar_short"
  • "deprecated"

Supported Command help section types (configurable via commands_table_help_sections):

  • "help"
  • "aliases"
  • "deprecated"

A popular choice for extremely large CLIs is to remove the metavar column and append it to the help text. Below is an example that does this, as well as doing some additional reordering of the help text elements.

# /// script
# dependencies = ["rich-click>=1.9"]
# ///
import rich_click as click

@click.command()
@click.argument("src")
@click.argument("dest")
@click.option("--env",
              help="Environment",
              default="dev", show_default=True,
              envvar="ENV", show_envvar=True,
              required=True)
@click.option("--log-level",
              help="Log level",
              default="INFO", show_default=True,
              envvar="LOG_LEVEL", show_envvar=True,
              deprecated=True)
@click.option_panel("Arguments")
@click.option_panel("Options")
@click.rich_config({
    "options_table_column_types": ["opt_long", "opt_short", "help"],
    "options_table_help_sections": ["metavar", "required", "help", "default", "envvar"]
})
def move_item(src, dest, env, log_level):
    """Move an item from a src location to a dest location"""
    pass

if __name__ == "__main__":
    move_item()
Output

python panels_help_section_types.py --help

RichPanel().to_info_dict()

RichPanel objects support the .to_info_dict() method added in Click 8.0. Additionally, RichGroups will show any panels explicitly assigned when rendering its own info dict.

Note that both default panels and objects assigned by default do not render:

  • For a panel to show up in a RichGroup info dict's panels, it must be explicitly assigned to the group.
  • For an object to show up in a RichPanel info dict, it must be explicitly assigned to the panel. In practice what this means is: a panel which is explicitly defined but which acts as a default panel (e.g. @click.option_panel("Options")), and whose assigned objects are inferred, will not show any assigned objects in its info dict.

Custom RichPanel Classes

Warning

The RichPanel API may be unstable across minor versions, since it is a new concept that we are still trying to find the best API for. If you subclass RichPanel, you may want to pin your rich-click version to rich-click>=1.9,<1.10.

RichPanels can be subclassed for additional functionality, if you so choose:

import rich_click as click

# This is not the best example since you could also
# set panel_styles={"title_align": "center"} without subclassing.

class CustomOptionPanel(click.RichOptionPanel):

    def render(self, command, ctx, formatter):
        panel = super().render(command, ctx, formatter)
        panel.title_align = "center"
        return panel

@click.command()
@click.argument("src")
@click.argument("dest")
@click.option("--env")
@click.option("--log-level")
@click.option_panel("Arguments", cls=CustomOptionPanel)
@click.option_panel("Options", cls=CustomOptionPanel)
@click.rich_config()
def move_item(src, dest, env, log_level):
    """Move an item from a src location to a dest location"""
    pass

if __name__ == "__main__":
    move_item()
Output

python panels_subclass.py --help