Source code for validate_actions.cli_components.output_formatter
"""Output formatter interface and implementations for CLI validation results."""
from abc import ABC, abstractmethod
from pathlib import Path
from rich.console import Console
from rich.text import Text
from validate_actions.globals.problems import Problem, ProblemLevel
[docs]
class OutputFormatter(ABC):
"""Interface for formatting CLI output."""
[docs]
@abstractmethod
def format_file_header(self, file: Path) -> str:
"""Format header for a file being validated."""
pass
[docs]
@abstractmethod
def format_problem(self, problem: Problem) -> str:
"""Format a single problem for display."""
pass
[docs]
@abstractmethod
def format_no_problems(self) -> str:
"""Format message when no problems found."""
pass
[docs]
@abstractmethod
def format_summary(
self, total_errors: int, total_warnings: int, max_level: ProblemLevel
) -> str:
"""Format final summary of all validation results."""
pass
[docs]
class ColoredFormatter(OutputFormatter):
"""
Colored console output formatter.
Formats CLI output with ANSI color codes and consistent spacing.
Used as the default formatter for interactive terminal sessions.
"""
STYLE = {
ProblemLevel.NON: {"color_bold": "\033[1;92m", "color": "\033[92m", "sign": "✓"},
ProblemLevel.ERR: {"color_bold": "\033[1;31m", "color": "\033[31m", "sign": "✗"},
ProblemLevel.WAR: {"color_bold": "\033[1;33m", "color": "\033[33m", "sign": "⚠"},
}
DEF_STYLE = {
"format_end": "\033[0m",
"neutral": "\033[2m",
"underline": "\033[4m",
}
[docs]
def format_file_header(self, file: Path) -> str:
"""Format file header with underline."""
return f'\n{self.DEF_STYLE["underline"]}{file}{self.DEF_STYLE["format_end"]}'
[docs]
def format_problem(self, problem: Problem) -> str:
"""Format problem with colors and positioning."""
line = (
f' {self.DEF_STYLE["neutral"]}{problem.pos.line + 1}:{problem.pos.col + 1}'
f'{self.DEF_STYLE["format_end"]}'
)
line += max(20 - len(line), 0) * " "
level_info = self._get_level_info(problem.level)
line += f'{level_info["color"]}{level_info["name"]}{self.DEF_STYLE["format_end"]}'
line += max(38 - len(line), 0) * " "
line += problem.desc
if problem.rule:
line += f' {self.DEF_STYLE["neutral"]}({problem.rule}){self.DEF_STYLE["format_end"]}'
return line
[docs]
def format_no_problems(self) -> str:
"""Format success message when no problems found."""
return (
f' {self.DEF_STYLE["neutral"]}{self.STYLE[ProblemLevel.NON]["sign"]} '
f'All checks passed{self.DEF_STYLE["format_end"]}'
)
[docs]
def format_summary(
self, total_errors: int, total_warnings: int, max_level: ProblemLevel
) -> str:
"""Format colored summary with counts."""
style = self.STYLE[max_level]
total_problems = total_errors + total_warnings
return (
f'\n{style["color_bold"]}{style["sign"]} {total_problems} problems '
f'({total_errors} errors, {total_warnings} warnings){self.DEF_STYLE["format_end"]}\n'
)
def _get_level_info(self, level: ProblemLevel) -> dict:
"""Get color and name info for problem level."""
level_map = {
ProblemLevel.WAR: {"color": self.STYLE[ProblemLevel.WAR]["color"], "name": "warning"},
ProblemLevel.ERR: {"color": self.STYLE[ProblemLevel.ERR]["color"], "name": "error"},
ProblemLevel.NON: {"color": self.STYLE[ProblemLevel.NON]["color"], "name": "fixed"},
}
return level_map.get(level, {"color": "", "name": "unknown"})
[docs]
class RichFormatter(OutputFormatter):
"""
Modern Rich-based formatter with clean, minimalist styling.
Features:
- Clean typography with subtle colors
- Consistent spacing and alignment
- Modern icons and visual hierarchy
- Responsive layout that works in any terminal width
"""
def __init__(self):
"""Initialize with Rich console."""
self.console = Console()
# Modern color palette - subtle and professional
self.colors = {
ProblemLevel.NON: "bright_green",
ProblemLevel.ERR: "bright_red",
ProblemLevel.WAR: "yellow",
"muted": "bright_black",
"accent": "cyan",
"text": "white",
}
# Modern icons
self.icons = {
ProblemLevel.NON: "✓",
ProblemLevel.ERR: "✕",
ProblemLevel.WAR: "⚠",
"file": "📁",
}
[docs]
def format_file_header(self, file: Path) -> str:
"""Format clean file header with modern styling."""
# Truncate path to show only filename and at most 2 levels up
parts = file.parts
if len(parts) > 3: # More than 2 directories + filename
display_path = str(Path(*parts[-3:])) # Take last 2 dirs + filename
else:
display_path = str(file)
# Create file header with icon and clean typography
header_text = Text()
header_text.append(f"{self.icons['file']} ", style=self.colors["muted"])
header_text.append(display_path, style=f"bold {self.colors['accent']}")
# Use console to render to string
with self.console.capture() as capture:
self.console.print(header_text)
return f"\n{capture.get()}"
[docs]
def format_problem(self, problem: Problem) -> str:
"""Format problem with clean typography and smart alignment."""
# Build the line with Rich styling
line = Text()
# Position (muted, right-aligned in a fixed width)
position = f"{problem.pos.line + 1}:{problem.pos.col + 1}"
line.append(f"{position:>8} ", style=self.colors["muted"])
# Level indicator with icon
level_color = self.colors.get(problem.level, self.colors["text"])
level_name = self._get_level_name(problem.level)
icon = self.icons.get(problem.level, "•")
line.append(f"{icon} ", style=level_color)
line.append(f"{level_name:<7} ", style=f"{level_color}")
# Description
line.append(str(problem.desc), style=self.colors["text"])
# Rule name (muted, if present)
if problem.rule:
line.append(f" ({problem.rule})", style=self.colors["muted"])
# Render to string
with self.console.capture() as capture:
self.console.print(line)
return capture.get().rstrip()
[docs]
def format_no_problems(self) -> str:
"""Format success message with clean styling."""
success_text = Text()
success_text.append(
f" {self.icons[ProblemLevel.NON]} ", style=self.colors[ProblemLevel.NON]
)
success_text.append("All checks passed", style=f"bold {self.colors[ProblemLevel.NON]}")
with self.console.capture() as capture:
self.console.print(success_text)
return capture.get()
[docs]
def format_summary(
self, total_errors: int, total_warnings: int, max_level: ProblemLevel
) -> str:
"""Format modern summary with visual hierarchy."""
total_problems = total_errors + total_warnings
if total_problems == 0:
# Clean success summary
summary = Text()
summary.append(
f"\n{self.icons[ProblemLevel.NON]} ", style=self.colors[ProblemLevel.NON]
)
summary.append(
"Validation completed successfully", style=f"bold {self.colors[ProblemLevel.NON]}"
)
summary.append(" - no issues found\n", style=self.colors["muted"])
else:
# Create a clean summary table
summary = Text()
summary.append("\n")
# Main status line
icon = self.icons.get(max_level, "•")
color = self.colors.get(max_level, self.colors["text"])
summary.append(f"{icon} ", style=color)
summary.append("Validation completed", style=f"bold {color}")
issue_text = f" - {total_problems} issue"
if total_problems != 1:
issue_text += "s"
issue_text += " found"
summary.append(issue_text, style=color)
# Breakdown
if total_errors > 0:
summary.append(
f"\n {self.icons[ProblemLevel.ERR]} ", style=self.colors[ProblemLevel.ERR]
)
error_text = f"{total_errors} error"
if total_errors != 1:
error_text += "s"
summary.append(error_text, style=self.colors[ProblemLevel.ERR])
if total_warnings > 0:
summary.append(
f"\n {self.icons[ProblemLevel.WAR]} ", style=self.colors[ProblemLevel.WAR]
)
warning_text = f"{total_warnings} warning"
if total_warnings != 1:
warning_text += "s"
summary.append(warning_text, style=self.colors[ProblemLevel.WAR])
summary.append("\n")
# Render to string
with self.console.capture() as capture:
self.console.print(summary)
return capture.get()
def _get_level_name(self, level: ProblemLevel) -> str:
"""Get display name for problem level."""
level_names = {
ProblemLevel.WAR: "warning",
ProblemLevel.ERR: "error",
ProblemLevel.NON: "fixed",
}
return level_names.get(level, "unknown")