# Help Customization

Cyclopts provides extensive customization options for help screen appearance and formatting through the `help_formatter` parameter available on both `App` and `Group`. These parameters accept formatters that follow the `HelpFormatter` protocol.

## Setting Help Formatters

### App-Level Formatting

The `App` class accepts a `help_formatter` parameter that controls the default formatting for all help output:

    from cyclopts import App
    from cyclopts.help import DefaultFormatter, PlainFormatter

    # Use a built-in formatter by name: {"default", "plain"}
    app = App(help_formatter="plain")

    # Or pass a formatter instance with custom configuration
    app = App(
        help_formatter=DefaultFormatter(
            # Custom configuration options
        )
    )

    # Or use a completely custom formatter; see HelpFormatter protocol.
    app = App(help_formatter=MyCustomFormatter())

### Group-Level Formatting

Individual `Group` instances can have their own `help_formatter` that overrides the app-level default:

    from cyclopts import App, Group
    from cyclopts.help import DefaultFormatter, PanelSpec
    from rich.box import DOUBLE

    # Create a group with custom formatting
    advanced_group = Group(
        "Advanced Options",
        help_formatter=DefaultFormatter(
            panel_spec=PanelSpec(
                border_style="red",
                box=DOUBLE,
            )
        )
    )

    # The app can have a different default formatter
    app = App(help_formatter="plain")

    # Parameters in advanced_group will use the group's formatter,
    # while other parameters use the app's formatter

## Built-in Formatters

### DefaultFormatter

The `DefaultFormatter` is the default help formatter that uses Rich for beautiful terminal output with colors, borders, and structured layouts.

    from cyclopts import App

    # Explicitly use the default formatter (same as not specifying)
    app = App(help_formatter="default")

    @app.default
    def main(name: str, count: int = 1):
        """A simple greeting application.

        Parameters
        ----------
        name : str
            Person to greet.
        count : int
            Number of times to greet.
        """
        for _ in range(count):
            print(f"Hello, {name}!")

    if __name__ == "__main__":
        app()

Output:

    Usage: my-app [ARGS] [OPTIONS]

    A simple greeting application.

    ╭─ Commands ───────────────────────────────────────────────────────────────────╮
    │ --help -h  Display this message and exit.                                    │
    │ --version  Display application version.                                      │
    ╰──────────────────────────────────────────────────────────────────────────────╯
    ╭─ Parameters ─────────────────────────────────────────────────────────────────╮
    │ *  NAME --name    Person to greet. [required]                                │
    │    COUNT --count  Number of times to greet. [default: 1]                     │
    ╰──────────────────────────────────────────────────────────────────────────────╯

### PlainFormatter

The `PlainFormatter` provides accessibility-focused plain text output without colors or special characters, ideal for screen readers and simpler terminals.

    from cyclopts import App

    # Use plain text formatter for accessibility
    app = App(help_formatter="plain")

    @app.default
    def main(name: str, count: int = 1):
        """A simple greeting application.

        Parameters
        ----------
        name : str
            Person to greet.
        count : int
            Number of times to greet.
        """
        for _ in range(count):
            print(f"Hello, {name}!")

    if __name__ == "__main__":
        app()

Output:

    Usage: demo.py [ARGS] [OPTIONS]

    A simple greeting application.

    Commands:
    --help, -h: Display this message and exit.
    --version: Display application version.

    Parameters:
    NAME, --name: Person to greet.
    COUNT, --count: Number of times to greet.

## Basic Customization

The `DefaultFormatter` accepts several customization options through its initialization parameters.

### Panel Customization

The `PanelSpec` controls the outer panel appearance:

    from cyclopts import App
    from cyclopts.help import DefaultFormatter, PanelSpec
    from rich.box import DOUBLE

    app = App(
        help_formatter=DefaultFormatter(
            panel_spec=PanelSpec(
                box=DOUBLE,              # Use double-line borders
                border_style="blue",     # Blue border color
                padding=(1, 2),         # (vertical, horizontal) padding
                expand=True,            # Expand to full terminal width
            )
        )
    )

    @app.default
    def main(path: str, verbose: bool = False):
        """Process a file with custom panel styling."""
        print(f"Processing {path}")

    if __name__ == "__main__":
        app()

Output:

    Usage: demo.py [ARGS] [OPTIONS]

    Process a file with custom panel styling.

    ╔═ Commands ═══════════════════════════════════════════════════════════╗
    ║                                                                      ║
    ║  --help -h  Display this message and exit.                           ║
    ║  --version  Display application version.                             ║
    ║                                                                      ║
    ╚══════════════════════════════════════════════════════════════════════╝
    ╔═ Parameters ═════════════════════════════════════════════════════════╗
    ║                                                                      ║
    ║  *  PATH --path                     [required]                       ║
    ║     VERBOSE --verbose  [default: False]                              ║
    ║       --no-verbose                                                   ║
    ║                                                                      ║
    ╚══════════════════════════════════════════════════════════════════════╝

### Table Customization

The `TableSpec` controls the table styling within panels:

    from cyclopts import App
    from cyclopts.help import DefaultFormatter, TableSpec

    app = App(
        help_formatter=DefaultFormatter(
            table_spec=TableSpec(
                show_header=True,  # Show column headers
                show_lines=True,  # Show lines between rows
                show_edge=False,  # Remove outer table border
                border_style="green",  # Green table elements
                padding=(0, 2, 0, 0),  # Extra right padding
                box=SQUARE,  # otherwise we won't see the lines
            )
        )
    )

    @app.default
    def main(path: str, verbose: bool = False):
        """Process a file with custom table styling."""
        print(f"Processing {path}")

    if __name__ == "__main__":
        app()

Output:

    Usage: test_table_custom.py [ARGS] [OPTIONS]

    Process a file with custom table styling.

    ╭─ Commands ───────────────────────────────────────────────────────────────────╮
    │ Command    │Description                                                      │
    │ ───────────┼──────────────────────────────────────────────────────────────── │
    │ --help -h  │Display this message and exit.                                   │
    │ ───────────┼──────────────────────────────────────────────────────────────── │
    │ --version  │Display application version.                                     │
    ╰──────────────────────────────────────────────────────────────────────────────╯
    ╭─ Parameters ─────────────────────────────────────────────────────────────────╮
    │    │Option             │Description                                          │
    │ ───┼───────────────────┼──────────────────────────────────────────────────── │
    │ *  │PATH --path        │[required]                                           │
    │ ───┼───────────────────┼──────────────────────────────────────────────────── │
    │    │VERBOSE --verbose  │[default: False]                                     │
    │    │  --no-verbose     │                                                     │
    ╰──────────────────────────────────────────────────────────────────────────────╯

### Combining Customizations

You can combine both panel and table specifications:

    from cyclopts import App
    from cyclopts.help import DefaultFormatter, PanelSpec, TableSpec
    from rich.box import ROUNDED

    app = App(
        help_formatter=DefaultFormatter(
            panel_spec=PanelSpec(
                box=ROUNDED,
                border_style="cyan",
                padding=(0, 1),
            ),
            table_spec=TableSpec(
                show_header=False,
                show_lines=False,
                padding=(0, 1),
            )
        )
    )

    @app.default
    def main(path: str, verbose: bool = False):
        """Process a file with combined customizations."""
        print(f"Processing {path}")

    if __name__ == "__main__":
        app()

Output:

    Usage: my-app [ARGS] [OPTIONS]

    Process a file with combined customizations.

    ╭─ Commands ──────────────────────────────────────────────────────────╮
    │ --help -h  Display this message and exit.                           │
    │ --version  Display application version.                             │
    ╰─────────────────────────────────────────────────────────────────────╯
    ╭─ Parameters ────────────────────────────────────────────────────────╮
    │ *  PATH --path       [required]                                     │
    │    VERBOSE --verbose [default: False]                               │
    ╰─────────────────────────────────────────────────────────────────────╯

## Group-Level Formatting

Different parameter groups can have different formatting styles, allowing you to visually distinguish between different types of options:

    from cyclopts import App, Group, Parameter
    from cyclopts.help import DefaultFormatter, PanelSpec
    from rich.box import DOUBLE, MINIMAL
    from typing import Annotated

    # Create groups with different styles
    required_group = Group(
        "Required Options",
        help_formatter=DefaultFormatter(
            panel_spec=PanelSpec(
                box=DOUBLE,
                border_style="red bold",
            )
        )
    )

    optional_group = Group(
        "Optional Settings",
        help_formatter=DefaultFormatter(
            panel_spec=PanelSpec(
                box=MINIMAL,
                border_style="green",
            )
        )
    )

    app = App()

    @app.default
    def main(
        # Required parameters with red double border
        input_file: Annotated[str, Parameter(group=required_group)],
        output_dir: Annotated[str, Parameter(group=required_group)],

        # Optional parameters with green minimal border
        verbose: Annotated[bool, Parameter(group=optional_group)] = False,
        threads: Annotated[int, Parameter(group=optional_group)] = 4,
    ):
        """Process files with styled help groups."""
        print(f"Processing {input_file} -> {output_dir}")
        if verbose:
            print(f"Using {threads} threads")

    if __name__ == "__main__":
        app()

Output:

    Usage: test_group_formatting.py [ARGS] [OPTIONS]

    Process files with styled help groups.

    ╭─ Commands ───────────────────────────────────────────────────────────────────╮
    │ --help -h  Display this message and exit.                                    │
    │ --version  Display application version.                                      │
    ╰──────────────────────────────────────────────────────────────────────────────╯
       Optional Settings
      VERBOSE --verbose  [default: False]
        --no-verbose
      THREADS --threads  [default: 4]

    ╔═ Required Options ═══════════════════════════════════════════════════════════╗
    ║ *  INPUT-FILE --input-file  [required]                                       ║
    ║ *  OUTPUT-DIR --output-dir  [required]                                       ║
    ╚══════════════════════════════════════════════════════════════════════════════╝

## Custom Column Layout

For complete control over the help table layout, you can define custom columns using `ColumnSpec`:

    from cyclopts import App, Group, Parameter
    from cyclopts.help import DefaultFormatter, ColumnSpec, TableSpec
    from typing import Annotated

    # Define custom column renderers
    def names_renderer(entry):
        """Combine parameter names and shorts."""
        names = " ".join(entry.names) if entry.names else ""
        shorts = " ".join(entry.shorts) if entry.shorts else ""
        return f"{names} {shorts}".strip()

    def type_renderer(entry):
        """Show the parameter type."""
        from cyclopts.annotations import get_hint_name
        return get_hint_name(entry.type) if entry.type else ""

    # Create custom columns
    custom_group = Group(
        "Custom Layout",
        help_formatter=DefaultFormatter(
            table_spec=TableSpec(show_header=True),
            column_specs=(
                ColumnSpec(
                    renderer=lambda e: "★" if e.required else " ",
                    header="",
                    width=2,
                    style="yellow bold",
                ),
                ColumnSpec(
                    renderer=names_renderer,
                    header="Option",
                    style="cyan",
                    max_width=30,
                ),
                ColumnSpec(
                    renderer=type_renderer,
                    header="Type",
                    style="magenta",
                    justify="center",
                ),
                ColumnSpec(
                    renderer="description",  # Use attribute name
                    header="Description",
                    overflow="fold",
                ),
            )
        )
    )

    app = App()

    @app.default
    def main(
        input_path: Annotated[str, Parameter(group=custom_group, help="Input file path")],
        output_path: Annotated[str, Parameter(group=custom_group, help="Output file path")],
        count: Annotated[int, Parameter(group=custom_group, help="Number of iterations")] = 1,
    ):
        """Demo custom column layout."""
        print(f"Processing {input_path} -> {output_path} ({count} times)")

    if __name__ == "__main__":
        app()

Output:

    Usage: test_custom_column.py [ARGS] [OPTIONS]

    Demo custom column layout.

    ╭─ Commands ───────────────────────────────────────────────────────────────────╮
    │ --help -h  Display this message and exit.                                    │
    │ --version  Display application version.                                      │
    ╰──────────────────────────────────────────────────────────────────────────────╯
    ╭─ Custom Layout ──────────────────────────────────────────────────────────────╮
    │     Option                     Type  Description                             │
    │ ★   INPUT-PATH --input-path    str   Input file path                         │
    │ ★   OUTPUT-PATH --output-path  str   Output file path                        │
    │     COUNT --count              int   Number of iterations                    │
    ╰──────────────────────────────────────────────────────────────────────────────╯

### Dynamic Column Builders

For even more flexibility, you can create columns dynamically based on runtime conditions:

    from cyclopts import App, Parameter
    from cyclopts.help import DefaultFormatter, ColumnSpec
    from typing import Annotated

    def dynamic_columns(console, options, entries):
        """Build columns based on console width and entries."""
        columns = []

        # Only show required indicator if there are required params
        if any(e.required for e in entries):
            columns.append(ColumnSpec(
                renderer=lambda e: "*" if e.required else "",
                width=2,
                style="red",
            ))

        # Adjust name column width based on console size
        max_width = min(40, int(console.width * 0.3))
        columns.append(ColumnSpec(
            renderer=lambda e: " ".join(e.names + e.shorts),
            header="Option",
            max_width=max_width,
            style="cyan",
        ))

        # Always include description
        columns.append(ColumnSpec(
            renderer="description",
            header="Description",
            overflow="fold",
        ))

        return tuple(columns)

    app = App(
        help_formatter=DefaultFormatter(
            column_specs=dynamic_columns
        )
    )

    @app.default
    def main(
        input_file: str,
        output_file: str,
        verbose: bool = False,
    ):
        """Process files with dynamic columns."""
        print(f"Processing {input_file} -> {output_file}")

    if __name__ == "__main__":
        app()

Output (adjusts based on terminal width):

    Usage: test_dynamic_columns.py [ARGS] [OPTIONS]

    Process files with dynamic columns.

    ╭─ Commands ───────────────────────────────────────────────────────────────────╮
    │ Option     Description                                                       │
    │ --help -h  Display this message and exit.                                    │
    │ --version  Display application version.                                      │
    ╰──────────────────────────────────────────────────────────────────────────────╯
    ╭─ Parameters ─────────────────────────────────────────────────────────────────╮
    │     Option                    Description                                    │
    │ *   INPUT-FILE --input-file                                                  │
    │ *   OUTPUT-FILE                                                              │
    │     --output-file                                                            │
    │     VERBOSE --verbose                                                        │
    │     --no-verbose                                                             │
    ╰──────────────────────────────────────────────────────────────────────────────╯

## Creating Custom Formatters

For complete control, you can implement your own formatter by following the `HelpFormatter` protocol. The formatter methods receive the console and options first, followed by the content to render:

    from cyclopts import App
    from cyclopts.help import HelpPanel
    from rich.console import Console, ConsoleOptions
    from rich.table import Table
    from rich.panel import Panel

    class MyCustomFormatter:
        """A custom formatter with unique styling."""

        def __call__(self, console: Console, options: ConsoleOptions, panel: HelpPanel) -> None:
            """Render a help panel with custom styling."""
            if not panel.entries:
                return

            # Create a custom table
            table = Table(show_header=True, header_style="bold magenta")
            table.add_column("Option", style="cyan", no_wrap=True)
            table.add_column("Description", style="white")

            for entry in panel.entries:
                name = " ".join(entry.names + entry.shorts)
                # Extract plain text from description (handles InlineText, etc)
                desc = ""
                if entry.description:
                    if hasattr(entry.description, 'plain'):
                        desc = entry.description.plain
                    elif hasattr(entry.description, '__rich_console__'):
                        # Render to plain text without styles
                        with console.capture() as capture:
                            console.print(entry.description, end="")
                        desc = capture.get()
                    else:
                        desc = str(entry.description)
                table.add_row(name, desc)

            # Wrap in a custom panel
            panel_title = panel.title or "Options"
            styled_panel = Panel(
                table,
                title=f"[bold blue]{panel_title}[/bold blue]",
                border_style="blue",
            )

            console.print(styled_panel)

        def render_usage(self, console: Console, options: ConsoleOptions, usage) -> None:
            """Render the usage line."""
            if usage:
                console.print(f"[bold green]Usage:[/bold green] {usage}")

        def render_description(self, console: Console, options: ConsoleOptions, description) -> None:
            """Render the description."""
            if description:
                console.print(f"\n[italic]{description}[/italic]\n")

    # Use the custom formatter
    app = App(help_formatter=MyCustomFormatter())

    @app.default
    def main(input_file: str, output_file: str, verbose: bool = False):
        """Process files with custom formatter."""
        print(f"Processing {input_file} -> {output_file}")

    if __name__ == "__main__":
        app()

Output:

    Usage: test_custom_formatter.py [ARGS] [OPTIONS]

    Process files with custom formatter.

    ╭─ Commands ───────────────────────────────────────────────────────────────────╮
    │ ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓                               │
    │ ┃ Option    ┃ Description                    ┃                               │
    │ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩                               │
    │ │ --help -h │ Display this message and exit. │                               │
    │ │ --version │ Display application version.   │                               │
    │ └───────────┴────────────────────────────────┘                               │
    ╰──────────────────────────────────────────────────────────────────────────────╯
    ╭─ Parameters ─────────────────────────────────────────────────────────────────╮
    │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓                             │
    │ ┃ Option                         ┃ Description ┃                             │
    │ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩                             │
    │ │ INPUT-FILE --input-file        │             │                             │
    │ │ OUTPUT-FILE --output-file      │             │                             │
    │ │ VERBOSE --verbose --no-verbose │             │                             │
    │ └────────────────────────────────┴─────────────┘                             │
    ╰──────────────────────────────────────────────────────────────────────────────╯

## Reference

For complete API documentation of help formatting components, see:

- `cyclopts.help.DefaultFormatter` - Rich-based formatter with full customization

- `cyclopts.help.PlainFormatter` - Plain text formatter for accessibility

- `cyclopts.help.PanelSpec` - Panel appearance specification

- `cyclopts.help.TableSpec` - Table styling specification

- `cyclopts.help.ColumnSpec` - Column definition and rendering

- `cyclopts.help.protocols.HelpFormatter` - Protocol for custom formatters

See also:

- Help - General help system documentation

- Groups - Organizing parameters into groups