Skip to content

Rule Reference

Bases: Gradient

A Rich Rule that supports gradients via the Gradient base class.

Parameters:

Name Type Description Default
title Optional[str]

Optional title text placed within the rule.

required
title_style StyleType

Style applied to the title text after gradients.

'bold'
colors Optional[Sequence[ColorType]]

Foreground gradient color stops.

None
bg_colors Optional[Sequence[ColorType]]

Background gradient color stops.

None
rainbow bool

If True, generate a rainbow regardless of colors.

False
hues int

Number of hues when generating default spectrum.

17
thickness int

0..3 selects line character style.

1
style StyleType

Base style for the rule line (merged with gradients).

''
end str

Trailing characters after the rule (default newline).

'\n'
align AlignMethod

Alignment for the rule within the available width.

'center'
Source code in src/rich_gradient/rule.py
class Rule(Gradient):
    """A Rich Rule that supports gradients via the Gradient base class.

    Args:
        title: Optional title text placed within the rule.
        title_style: Style applied to the title text after gradients.
        colors: Foreground gradient color stops.
        bg_colors: Background gradient color stops.
        rainbow: If True, generate a rainbow regardless of colors.
        hues: Number of hues when generating default spectrum.
        thickness: 0..3 selects line character style.
        style: Base style for the rule line (merged with gradients).
        end: Trailing characters after the rule (default newline).
        align: Alignment for the rule within the available width.
    """

    def __init__(
        self,
        title: Optional[str],
        title_style: StyleType = "bold",
        colors: Optional[Sequence[ColorType]] = None,
        bg_colors: Optional[Sequence[ColorType]] = None,
        *,
        rainbow: bool = False,
        hues: int = 17,
        thickness: int = 1,
        characters: Optional[str] = None,
        style: StyleType = "",
        end: str = "\n",
        align: AlignMethod = "center",
        console: Optional[Console] = None,
    ) -> None:
        self.title = title or ""
        self.title_style = title_style
        self.thickness = thickness
        self.characters = characters or CHARACTER_MAP.get(thickness, CHARACTER_MAP[2])

        # Build the underlying Rich Rule renderable
        base_rule = RichRule(
            title=self.title,
            characters=self.characters,
            style=style,
            end=end,
            align=align,
        )

        try:
            if self.title:
                highlight_words = {self.title: self.title_style}
            else:
                highlight_words = None

            # Validate provided color names against runtime config to ensure
            # clearly invalid names (e.g., 'bad') are rejected. We accept
            # hex values and rgb(...) forms, and for plain names consult the
            # runtime config color keys (case-insensitive) as accepted names.
            if colors is not None:
                known_names = {
                    k.lower() for k in dict(getattr(config, "colors", {}) or {}).keys()
                }
                for c in colors:
                    if isinstance(c, str):
                        s = c.strip()
                        if s.startswith("#") or s.lower().startswith(("rgb(", "rgba(")):
                            # hex or rgb forms are allowed; parsing will validate further
                            continue
                        # Plain name: must exist in runtime config (case-insensitive)
                        if s.lower() not in known_names:
                            raise ValueError(f"Invalid color name: {s}")

            super().__init__(
                base_rule,
                colors=list(colors) if colors is not None else None,
                bg_colors=list(bg_colors) if bg_colors is not None else None,
                console=console,
                hues=hues,
                rainbow=rainbow,
                vertical_justify="middle",
                highlight_words=highlight_words,
            )
        except Exception as err:
            # Normalize any parsing/validation error into ValueError for the
            # public Rule API so callers receive a consistent exception type
            # for invalid color inputs.
            raise ValueError(f"Invalid color provided: {err}") from err

    @property
    def thickness(self) -> int:
        """Get the thickness of the Rule."""
        for thickness, char in CHARACTER_MAP.items():
            if char == getattr(self, "_rule_char", CHARACTER_MAP[2]):
                return thickness
        return 2  # Default

    @thickness.setter
    def thickness(self, value: int) -> None:
        """Set the thickness of the Rule.
        Args:
            value: Thickness as an integer (0-3) or the corresponding character.
        Raises:
            ValueError: If the value is not a valid thickness or character."""
        if isinstance(value, int) and 0 <= value <= 3:
            self._thickness = value
            self._characters = CHARACTER_MAP[value]
            return
        raise ValueError(
            "thickness string must be one of the following characters: "
            + ", ".join(CHARACTER_MAP.values())
        )

    @property
    def characters(self) -> str:
        """Get the character used for the rule line."""
        if self._characters:
            return self._characters

        # Validate thickness
        if not isinstance(self.thickness, int):
            raise TypeError(
                f"thickness must be an integer, recieved {type(self.thickness).__name__}"
            )
        if 0 <= self.thickness <= 3:
            raise ValueError("thickness must be an integer between 0 and 3 (inclusive)")

        return CHARACTER_MAP.get(self.thickness, CHARACTER_MAP[2])

    @characters.setter
    def characters(self, value: str) -> None:
        """Set the character used for the rule line."""
        if not isinstance(value, str) or len(value) != 1:
            raise ValueError("characters must be a single character string")
        self._characters = value

    @property
    def title(self) -> Optional[TextType]:
        """Get the title of the Rule."""
        return self._title or None

    @title.setter
    def title(self, value: Optional[TextType]) -> None:
        """Set the title of the Rule."""
        if value is not None and not isinstance(value, (str, RichText, Text)):
            raise TypeError(
                f"title must be str, RichText, or Text, got {type(value).__name__}"
            )
        self._title = value

    @property
    def title_style(self) -> Optional[StyleType]:
        """Get the title style of the Rule's title."""
        return self._title_style or None

    @title_style.setter
    def title_style(self, value: Optional[StyleType]) -> None:
        """Set the title style of the Rule's title."""
        if value is not None and not isinstance(value, (str, Style)):
            raise TypeError(
                f"title_style must be str or Style, got {type(value).__name__}"
            )
        self._title_style = Style.parse(str(value)) if value is not None else None

    def __rich_console__(
        self, console: Console, options: ConsoleOptions
    ) -> RenderResult:
        """Render the rule using the underlying RichRule at full width and
        apply gradient coloring using Gradient utilities.

        This overrides the base Gradient alignment wrapper to ensure the
        RichRule expands to the console width (so the line isn't collapsed
        to a single character when aligned/padded externally).
        """
        # Render underlying content directly (no Align wrapper)
        content = self.renderables[0] if self.renderables else ""
        width = options.max_width

        lines = console.render_lines(content, options, pad=True, new_lines=False)
        for line_index, segments in enumerate(lines):
            highlight_map = None
            if self._highlight_rules:
                line_text = "".join(segment.text for segment in segments)
                highlight_map = self._build_highlight_map(line_text)
            column = 0
            char_index = 0
            for seg in segments:
                text = seg.text
                base_style = seg.style or Style()
                cluster = ""
                cluster_width = 0
                cluster_indices: list[int] = []
                for character in text:
                    current_index = char_index
                    char_index += 1
                    character_width = get_character_cell_size(character)
                    if character_width <= 0:
                        cluster += character
                        cluster_indices.append(current_index)
                        continue
                    if cluster:
                        style = self._get_style_at_position(
                            column - cluster_width, cluster_width, width
                        )
                        merged_style = self._merge_styles(base_style, style)
                        merged_style = self._apply_highlight_style(
                            merged_style, highlight_map, cluster_indices
                        )
                        yield Segment(cluster, merged_style)
                        cluster = ""
                        cluster_width = 0
                        cluster_indices = []
                    cluster = character
                    cluster_width = character_width
                    cluster_indices = [current_index]
                    column += character_width
                if cluster:
                    style = self._get_style_at_position(
                        column - cluster_width, cluster_width, width
                    )
                    merged_style = self._merge_styles(base_style, style)
                    merged_style = self._apply_highlight_style(
                        merged_style, highlight_map, cluster_indices
                    )
                    yield Segment(cluster, merged_style)
            if line_index < len(lines) - 1:
                yield Segment.line()
        # Ensure a trailing newline after the rule so following content appears below
        yield Segment.line()

characters property writable

Get the character used for the rule line.

thickness property writable

Get the thickness of the Rule.

title property writable

Get the title of the Rule.

title_style property writable

Get the title style of the Rule's title.

__rich_console__(console, options)

Render the rule using the underlying RichRule at full width and apply gradient coloring using Gradient utilities.

This overrides the base Gradient alignment wrapper to ensure the RichRule expands to the console width (so the line isn't collapsed to a single character when aligned/padded externally).

Source code in src/rich_gradient/rule.py
def __rich_console__(
    self, console: Console, options: ConsoleOptions
) -> RenderResult:
    """Render the rule using the underlying RichRule at full width and
    apply gradient coloring using Gradient utilities.

    This overrides the base Gradient alignment wrapper to ensure the
    RichRule expands to the console width (so the line isn't collapsed
    to a single character when aligned/padded externally).
    """
    # Render underlying content directly (no Align wrapper)
    content = self.renderables[0] if self.renderables else ""
    width = options.max_width

    lines = console.render_lines(content, options, pad=True, new_lines=False)
    for line_index, segments in enumerate(lines):
        highlight_map = None
        if self._highlight_rules:
            line_text = "".join(segment.text for segment in segments)
            highlight_map = self._build_highlight_map(line_text)
        column = 0
        char_index = 0
        for seg in segments:
            text = seg.text
            base_style = seg.style or Style()
            cluster = ""
            cluster_width = 0
            cluster_indices: list[int] = []
            for character in text:
                current_index = char_index
                char_index += 1
                character_width = get_character_cell_size(character)
                if character_width <= 0:
                    cluster += character
                    cluster_indices.append(current_index)
                    continue
                if cluster:
                    style = self._get_style_at_position(
                        column - cluster_width, cluster_width, width
                    )
                    merged_style = self._merge_styles(base_style, style)
                    merged_style = self._apply_highlight_style(
                        merged_style, highlight_map, cluster_indices
                    )
                    yield Segment(cluster, merged_style)
                    cluster = ""
                    cluster_width = 0
                    cluster_indices = []
                cluster = character
                cluster_width = character_width
                cluster_indices = [current_index]
                column += character_width
            if cluster:
                style = self._get_style_at_position(
                    column - cluster_width, cluster_width, width
                )
                merged_style = self._merge_styles(base_style, style)
                merged_style = self._apply_highlight_style(
                    merged_style, highlight_map, cluster_indices
                )
                yield Segment(cluster, merged_style)
        if line_index < len(lines) - 1:
            yield Segment.line()
    # Ensure a trailing newline after the rule so following content appears below
    yield Segment.line()