Source code for pyfcstm.entry.simulate.display
"""
State display formatting for the simulation REPL.
This module provides the StateDisplay class for formatting state machine
information with ANSI color support for terminal output.
"""
import logging
import os
import sys
from typing import List, Tuple, Optional
[docs]
class StateDisplay:
"""
Formatter for displaying state machine information in the terminal.
This class handles formatting of current state, variables, and events
with ANSI color support. Colors are automatically disabled on terminals
that don't support them.
:ivar use_color: Whether to use ANSI color codes
:vartype use_color: bool
:ivar logger: Logger instance for log messages
:vartype logger: logging.Logger
"""
# ANSI color codes - compatible with both light and dark themes
COLORS = {
'reset': '\033[0m',
'bold': '\033[1m',
'blue': '\033[94m', # Blue - labels
'green': '\033[92m', # Green - success/normal state
'yellow': '\033[93m', # Yellow - variable names
'red': '\033[91m', # Red - errors
'cyan': '\033[96m', # Cyan - values
'gray': '\033[90m', # Gray - secondary info
}
[docs]
def __init__(self, use_color: bool = True, logger: Optional[logging.Logger] = None):
"""
Initialize the state display formatter.
:param use_color: Whether to use ANSI colors, defaults to True
:type use_color: bool, optional
:param logger: Logger instance for log messages, defaults to None
:type logger: logging.Logger, optional
"""
# Check if colors should be used
self.use_color = use_color and self._supports_color()
self.logger = logger
def _supports_color(self) -> bool:
"""
Detect if the terminal supports ANSI colors.
:return: True if colors are supported
:rtype: bool
"""
return (
hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() and
'TERM' in os.environ and os.environ['TERM'] != 'dumb'
)
def _colorize(self, text: str, color: str) -> str:
"""
Apply ANSI color to text.
:param text: The text to colorize
:type text: str
:param color: The color name from COLORS dict
:type color: str
:return: Colorized text or plain text if colors disabled
:rtype: str
"""
if not self.use_color:
return text
return f"{self.COLORS.get(color, '')}{text}{self.COLORS['reset']}"
[docs]
def format_current_state(self, runtime) -> str:
"""
Format current state and variable information.
:param runtime: The simulation runtime instance
:type runtime: SimulationRuntime
:return: Formatted state and variables display
:rtype: str
"""
lines = []
# Cycle count
cycle_label = self._colorize("Cycle:", 'blue')
cycle_value = self._colorize(str(runtime.cycle_count), 'cyan')
lines.append(f"{cycle_label} {cycle_value}")
# Current state
try:
if runtime.current_state:
state_text = '.'.join(runtime.current_state.path)
state_label = self._colorize("Current State:", 'blue')
state_value = self._colorize(state_text, 'green')
lines.append(f"{state_label} {state_value}")
else:
state_label = self._colorize("Current State:", 'blue')
state_value = self._colorize("(terminated)", 'red')
lines.append(f"{state_label} {state_value}")
except IndexError:
# Runtime has ended
state_label = self._colorize("Current State:", 'blue')
state_value = self._colorize("(terminated)", 'red')
lines.append(f"{state_label} {state_value}")
# Variables
if runtime.vars:
var_label = self._colorize("Variables:", 'blue')
lines.append(var_label)
for name, value in sorted(runtime.vars.items()):
name_colored = self._colorize(name, 'yellow')
value_colored = self._colorize(str(value), 'cyan')
lines.append(f" {name_colored} = {value_colored}")
return "\n".join(lines)
[docs]
def format_events(self, events: List[Tuple[str, Optional[str]]]) -> str:
"""
Format event list.
:param events: List of (full_path, short_name) tuples
:type events: List[Tuple[str, Optional[str]]]
:return: Formatted events display
:rtype: str
"""
if not events:
return self._colorize("No events available in current state", 'gray')
lines = []
lines.append(self._colorize("Available Events:", 'blue'))
for full_path, short_name in events:
if short_name:
full_colored = self._colorize(full_path, 'cyan')
short_colored = self._colorize(short_name, 'green')
lines.append(f" • {short_colored} ({full_colored})")
else:
event_colored = self._colorize(full_path, 'green')
lines.append(f" • {event_colored}")
return "\n".join(lines)
[docs]
def log(self, message: str, level: str = "info"):
"""
Output log message using the configured logger.
This method delegates to the logger instance configured during initialization.
If no logger is configured, the message is silently ignored.
:param message: The log message
:type message: str
:param level: Log level (debug, info, warning, error), defaults to "info"
:type level: str, optional
"""
if self.logger is None:
return
level_map = {
'debug': logging.DEBUG,
'info': logging.INFO,
'warning': logging.WARNING,
'error': logging.ERROR,
}
log_level = level_map.get(level, logging.INFO)
self.logger.log(log_level, message)
[docs]
def format_table(self, headers: list, rows: list, var_names: list = None) -> str:
"""
Format data as a centered table with colors.
:param headers: List of column headers
:type headers: list
:param rows: List of row data (each row is a list)
:type rows: list
:param var_names: List of variable names for coloring, defaults to None
:type var_names: list, optional
:return: Formatted table string
:rtype: str
"""
if not rows:
return ""
var_names = var_names or []
# Calculate column widths (based on visible content)
col_widths = [len(str(h)) for h in headers]
for row in rows:
for i, cell in enumerate(row):
col_widths[i] = max(col_widths[i], len(str(cell)))
# Add padding
col_widths = [w + 2 for w in col_widths]
lines = []
# Format header row with colors
header_parts = []
for i, header in enumerate(headers):
width = col_widths[i]
# Determine color for header
if header == 'Cycle':
colored_header = self._colorize(header, 'blue')
elif header == 'State':
colored_header = self._colorize(header, 'blue')
elif header in var_names:
colored_header = self._colorize(header, 'yellow')
else:
colored_header = header
# Center the header
padding = width - len(header)
left_pad = padding // 2
right_pad = padding - left_pad
header_parts.append(' ' * left_pad + colored_header + ' ' * right_pad)
lines.append(''.join(header_parts))
# Format separator row
separator_parts = ['-' * w for w in col_widths]
lines.append(''.join(separator_parts))
# Format data rows with colors
for row in rows:
row_parts = []
for i, cell in enumerate(row):
width = col_widths[i]
cell_str = str(cell)
# Determine color for cell
colored_cell = cell_str
if i == 0: # Cycle column
if cell_str != '...':
try:
int(cell_str)
colored_cell = self._colorize(cell_str, 'cyan')
except ValueError:
pass
elif i == 1: # State column
if '.' in cell_str or cell_str == '(terminated)':
colored_cell = self._colorize(cell_str, 'green')
else: # Variable columns
if cell_str != '...':
try:
float(cell_str)
colored_cell = self._colorize(cell_str, 'cyan')
except ValueError:
pass
# Center the cell
padding = width - len(cell_str)
left_pad = padding // 2
right_pad = padding - left_pad
row_parts.append(' ' * left_pad + colored_cell + ' ' * right_pad)
lines.append(''.join(row_parts))
return '\n'.join(lines)