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 customizationcyclopts.help.PlainFormatter- Plain text formatter for accessibilitycyclopts.help.PanelSpec- Panel appearance specificationcyclopts.help.TableSpec- Table styling specificationcyclopts.help.ColumnSpec- Column definition and renderingcyclopts.help.protocols.HelpFormatter- Protocol for custom formatters
See also:
Help - General help system documentation
Groups - Organizing parameters into groups