"""Status indicator element for displaying status with colored indicator.
This module provides the StatusIndicator element for displaying a colored
circle/dot indicator with an optional label. Supports preset statuses
(error, warning, success, etc.) and custom user-defined statuses.
"""
from typing import TYPE_CHECKING, Any
from wijjit.elements.base import Element, ElementType
from wijjit.terminal.ansi import supports_unicode
if TYPE_CHECKING:
from wijjit.rendering.paint_context import PaintContext
# Default status presets: status_name -> (color_name, indicator_char_override)
# If indicator_char_override is None, use the indicator_style setting
DEFAULT_STATUSES: dict[str, tuple[str, str | None]] = {
"error": ("red", None),
"warning": ("yellow", None),
"success": ("green", None),
"disabled": ("gray", None),
"info": ("blue", None),
"pending": ("cyan", None),
"active": ("bright_green", None),
"inactive": ("dim", None),
}
# Unicode indicator characters
INDICATOR_FILLED = "\u25cf" # Black circle (filled)
INDICATOR_HOLLOW = "\u25cb" # White circle (hollow)
INDICATOR_SQUARE = "\u25a0" # Black square
INDICATOR_ASCII = "*" # ASCII fallback
[docs]
class StatusIndicator(Element):
"""Status indicator element displaying colored dot with label.
A status indicator shows a colored circle/dot with an optional label
to represent various states like success, error, warning, etc.
Supports preset statuses and custom user-defined statuses.
Parameters
----------
id : str, optional
Element identifier
status : str, optional
Status name matching preset or custom status (default: "info")
label : str, optional
Optional label displayed after indicator
custom_statuses : dict, optional
Custom status definitions mapping name to color string
or (color, indicator_char) tuple
indicator_style : {"filled", "hollow", "square", "ascii"}, optional
Indicator character style (default: "filled")
Attributes
----------
status : str
Current status name
label : str or None
Label text
indicator_style : str
Indicator visual style
Notes
-----
This is a display-only element and is not focusable.
Default statuses:
- error: red
- warning: yellow
- success: green
- disabled: gray
- info: blue
- pending: cyan
- active: bright_green
- inactive: dim
Custom statuses can be registered via:
- Constructor: custom_statuses={"custom": "magenta"}
- Runtime: indicator.register_status("custom", "magenta")
"""
[docs]
def __init__(
self,
id: str | None = None,
classes: str | list[str] | set[str] | None = None,
status: str = "info",
label: str | None = None,
custom_statuses: dict[str, str | tuple[str, str]] | None = None,
indicator_style: str = "filled",
) -> None:
super().__init__(id=id, classes=classes)
self.element_type = ElementType.DISPLAY
self.focusable = False # Display-only element
self._status = status
self.label = label
self.indicator_style = indicator_style
# Build status registry from defaults + custom
self._statuses: dict[str, tuple[str, str | None]] = dict(DEFAULT_STATUSES)
if custom_statuses:
for name, value in custom_statuses.items():
if isinstance(value, str):
# Just color, use default indicator
self._statuses[name] = (value, None)
else:
# (color, indicator) tuple
self._statuses[name] = value
@property
def status(self) -> str:
"""Get current status.
Returns
-------
str
Current status name
"""
return self._status
@status.setter
def status(self, value: str) -> None:
"""Set status.
Parameters
----------
value : str
Status name to set
"""
self._status = value
[docs]
def register_status(
self, name: str, color: str, indicator: str | None = None
) -> None:
"""Register a custom status at runtime.
Parameters
----------
name : str
Status name to register
color : str
Color for the indicator (e.g., "red", "bright_blue", "magenta")
indicator : str, optional
Custom indicator character (defaults to indicator_style setting)
"""
self._statuses[name] = (color, indicator)
def _get_indicator_char(self, use_unicode: bool) -> str:
"""Get indicator character based on style and unicode support.
Parameters
----------
use_unicode : bool
Whether unicode is supported
Returns
-------
str
Indicator character
"""
if not use_unicode or self.indicator_style == "ascii":
return INDICATOR_ASCII
if self.indicator_style == "hollow":
return INDICATOR_HOLLOW
elif self.indicator_style == "square":
return INDICATOR_SQUARE
else: # "filled" or default
return INDICATOR_FILLED
[docs]
def render_signature(self) -> Any:
"""Paint memo for the skip-unchanged fast path.
Captures the current status, indicator style, label, and the runtime
status registry (which drives the indicator color and character), plus
the style-affecting state. See
:meth:`wijjit.elements.base.Element.render_signature`.
"""
return (
"status_indicator",
self._status,
self.indicator_style,
self.label,
tuple(sorted(self._statuses.items())),
self._style_signature(),
)
[docs]
def render_to(self, ctx: "PaintContext") -> None:
"""Render status indicator to paint context.
Parameters
----------
ctx : PaintContext
Paint context with buffer, style resolver, and bounds
Theme styles:
This element uses the following theme style classes:
- ``status_indicator``: Base indicator style
- 'status_indicator.{status}': Status-specific style (e.g., status_indicator.error)
- ``status_indicator.label``: Label text style
"""
# Look up status definition
status_def = self._statuses.get(self._status)
if status_def:
color, custom_char = status_def
else:
# Unknown status - use gray as fallback
color = "gray"
custom_char = None
# Determine indicator character
use_unicode = supports_unicode()
if custom_char:
# Custom character specified in status def
indicator = custom_char
else:
indicator = self._get_indicator_char(use_unicode)
# Resolve styles
# Try status-specific style first, fall back to base
indicator_style = ctx.style_resolver.resolve_style(
self, f"status_indicator.{self._status}"
)
# If no specific style, use base with color override
if not indicator_style.fg_color:
indicator_style = ctx.style_resolver.resolve_style(self, "status_indicator")
label_style = ctx.style_resolver.resolve_style(self, "status_indicator.label")
x_offset = 0
# Render indicator with color
# Apply color from status definition
from wijjit.styling.style import Style
colored_style = Style(
fg_color=self._color_name_to_rgb(color),
bold=indicator_style.bold,
)
ctx.write_text(x_offset, 0, indicator, colored_style)
x_offset += 1
# Render label if present
if self.label:
ctx.write_text(x_offset, 0, f" {self.label}", label_style)
def _color_name_to_rgb(self, color_name: str) -> tuple[int, int, int] | None:
"""Convert color name to RGB tuple.
Parameters
----------
color_name : str
Color name like "red", "green", "bright_blue"
Returns
-------
tuple[int, int, int] or None
RGB color tuple or None if unknown
"""
# Basic color map
colors = {
"black": (0, 0, 0),
"red": (255, 0, 0),
"green": (0, 255, 0),
"yellow": (255, 255, 0),
"blue": (0, 0, 255),
"magenta": (255, 0, 255),
"cyan": (0, 255, 255),
"white": (255, 255, 255),
"gray": (128, 128, 128),
"grey": (128, 128, 128),
"dim": (96, 96, 96),
# Bright variants
"bright_red": (255, 100, 100),
"bright_green": (100, 255, 100),
"bright_yellow": (255, 255, 100),
"bright_blue": (100, 100, 255),
"bright_magenta": (255, 100, 255),
"bright_cyan": (100, 255, 255),
"bright_white": (255, 255, 255),
# Semantic colors
"orange": (255, 165, 0),
"purple": (128, 0, 128),
"pink": (255, 192, 203),
"brown": (139, 69, 19),
}
return colors.get(color_name.lower())
[docs]
def get_intrinsic_size(self) -> tuple[int, int]:
"""Get the intrinsic (preferred) size of the status indicator.
Returns
-------
tuple[int, int]
(width, height) based on indicator and label
"""
width = 1 # indicator
if self.label:
width += 1 + len(self.label) # space + label
return (width, 1)