"""
CLI utilities for modern terminal output.
This module provides color-coded output and error/warning tracking
for command-line tools.
"""
import os
import sys
import shutil
import subprocess
import locale
from pathlib import Path
from typing import Optional
class Colors:
"""ANSI color codes for terminal output."""
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
BLACK = "\033[30m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
BRIGHT_RED = "\033[91m"
BRIGHT_GREEN = "\033[92m"
BRIGHT_YELLOW = "\033[93m"
BRIGHT_BLUE = "\033[94m"
BRIGHT_MAGENTA = "\033[95m"
BRIGHT_CYAN = "\033[96m"
BRIGHT_WHITE = "\033[97m"
BG_RED = "\033[41m"
BG_GREEN = "\033[42m"
BG_YELLOW = "\033[43m"
BG_BLUE = "\033[44m"
class Console:
"""Manages CLI output with colors and error/warning tracking."""
UNICODE_SYMBOLS = {
'warning': '⚠',
'error': '✗',
'success': '✓',
'header': '▶',
'step': '•',
'file': '📄',
}
ASCII_SYMBOLS = {
'warning': '[!]',
'error': '[x]',
'success': '[ok]',
'header': '>',
'step': '*',
'file': '[f]',
}
def __init__(self, use_colors: Optional[bool] = None):
if use_colors is None:
self.use_colors = 'NO_COLOR' not in os.environ and sys.stdout.isatty()
else:
self.use_colors = use_colors and sys.stdout.isatty()
self.use_unicode = self._detect_unicode_support()
self.warning_count = 0
self.error_count = 0
def info(self, message: str) -> None:
"""Print informational message."""
print(message)
def success(self, message: str) -> None:
"""Print success message in green."""
print(self._colorize(message, Colors.BRIGHT_GREEN))
def warning(self, message: str) -> None:
"""Print warning message in yellow."""
self.warning_count += 1
symbol = self._get_symbol('warning')
print(self._colorize(f"{symbol} {message}", Colors.BRIGHT_YELLOW), file=sys.stderr)
def error(self, message: str) -> None:
"""Print error message in red."""
self.error_count += 1
symbol = self._get_symbol('error')
print(self._colorize(f"{symbol} {message}", Colors.BRIGHT_RED), file=sys.stderr)
def header(self, message: str) -> None:
"""Print section header in cyan with bold."""
symbol = self._get_symbol('header')
print(f"{self._colorize(symbol, Colors.BRIGHT_CYAN)} {self._colorize(message, Colors.BOLD + Colors.BRIGHT_CYAN)}")
def step(self, message: str) -> None:
"""Print step message in blue."""
symbol = self._get_symbol('step')
print(f" {self._colorize(symbol, Colors.BRIGHT_BLUE)} {message}")
def file(self, message: str) -> None:
"""Print file-related message in dim."""
symbol = self._get_symbol('file')
print(f" {self._colorize(symbol, Colors.DIM)} {message}")
def summary(self) -> None:
"""Print execution summary."""
print()
success_symbol = self._get_symbol('success')
if self.error_count == 0 and self.warning_count == 0:
self.success(f"{success_symbol} Generation completed successfully!")
elif self.error_count == 0:
self.warning(f"Generation completed with {self.warning_count} warning(s)")
else:
self.error(f"Generation failed with {self.error_count} error(s) and {self.warning_count} warning(s)")
def _detect_unicode_support(self) -> bool:
"""Detect if the terminal supports Unicode output.
Returns:
True if Unicode is supported, False otherwise (use ASCII fallback)
"""
if not sys.stdout.isatty():
return False
term = os.environ.get('TERM', '')
if term == 'dumb' or term == '':
return False
try:
encoding = sys.stdout.encoding
if encoding is None:
return False
if encoding.lower().startswith('utf'):
return True
try:
loc = locale.getpreferredencoding(False)
if loc and loc.lower().startswith('utf'):
return True
except Exception:
pass
lang = os.environ.get('LANG', '')
if 'utf' in lang.lower() or 'UTF' in lang:
return True
return False
except Exception:
return False
def _get_symbol(self, symbol_name: str) -> str:
"""Get appropriate symbol based on Unicode support.
Args:
symbol_name: Key from UNICODE_SYMBOLS/ASCII_SYMBOLS dict
Returns:
Unicode or ASCII symbol depending on terminal support
"""
if self.use_unicode:
return self.UNICODE_SYMBOLS.get(symbol_name, '')
return self.ASCII_SYMBOLS.get(symbol_name, '')
def _colorize(self, text: str, color: str) -> str:
"""Apply color to text if colors are enabled."""
if self.use_colors:
return f"{color}{text}{Colors.RESET}"
return text
def find_clang_format() -> Optional[str]:
"""Find clang-format executable in the system.
Returns:
Path to clang-format if found, None otherwise
"""
for name in ["clang-format", "clang-format-18", "clang-format-17", "clang-format-16", "clang-format-15"]:
path = shutil.which(name)
if path:
return path
return None
def format_file_with_clang_format(file_path: Path, clang_format_path: str, console: Console) -> bool:
"""Format a file using clang-format.
Args:
file_path: Path to the file to format
clang_format_path: Path to clang-format executable
console: Console instance for output
Returns:
True if formatting succeeded, False otherwise
"""
try:
result = subprocess.run(
[clang_format_path, "-i", str(file_path)],
capture_output=True,
text=True,
)
if result.returncode == 0:
console.success(f"Formatted {file_path.name} with clang-format")
return True
else:
console.warning(f"clang-format failed for {file_path}: {result.stderr}")
return False
except Exception as e:
console.warning(f"Failed to run clang-format: {e}")
return False