- Fixed Bootstrap bg-dark class to use better contrasting color - Added comprehensive text-muted contrast fixes for various contexts - Improved dark theme colors for better accessibility - Fixed CSS inheritance issues for code elements in dark contexts - All color choices meet WCAG AA contrast requirements
399 lines
10 KiB
Python
399 lines
10 KiB
Python
"""
|
||
UnitForge Color Utility for Python
|
||
Centralized ANSI color definitions and utility functions for consistent terminal output
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
from typing import Optional
|
||
|
||
|
||
class Colors:
|
||
"""ANSI color codes and formatting constants."""
|
||
|
||
# Basic colors
|
||
RED = "\033[0;31m"
|
||
GREEN = "\033[0;32m"
|
||
YELLOW = "\033[1;33m"
|
||
BLUE = "\033[0;34m"
|
||
PURPLE = "\033[0;35m"
|
||
CYAN = "\033[0;36m"
|
||
WHITE = "\033[0;37m"
|
||
GRAY = "\033[0;90m"
|
||
|
||
# Bright colors
|
||
BRIGHT_RED = "\033[1;31m"
|
||
BRIGHT_GREEN = "\033[1;32m"
|
||
BRIGHT_YELLOW = "\033[1;33m"
|
||
BRIGHT_BLUE = "\033[1;34m"
|
||
BRIGHT_PURPLE = "\033[1;35m"
|
||
BRIGHT_CYAN = "\033[1;36m"
|
||
BRIGHT_WHITE = "\033[1;37m"
|
||
|
||
# Background colors
|
||
BG_RED = "\033[41m"
|
||
BG_GREEN = "\033[42m"
|
||
BG_YELLOW = "\033[43m"
|
||
BG_BLUE = "\033[44m"
|
||
BG_PURPLE = "\033[45m"
|
||
BG_CYAN = "\033[46m"
|
||
BG_WHITE = "\033[47m"
|
||
|
||
# Text formatting
|
||
BOLD = "\033[1m"
|
||
DIM = "\033[2m"
|
||
ITALIC = "\033[3m"
|
||
UNDERLINE = "\033[4m"
|
||
BLINK = "\033[5m"
|
||
REVERSE = "\033[7m"
|
||
STRIKETHROUGH = "\033[9m"
|
||
|
||
# Reset
|
||
NC = "\033[0m" # No Color
|
||
RESET = "\033[0m"
|
||
|
||
# Status symbols
|
||
INFO_SYMBOL = "ℹ"
|
||
SUCCESS_SYMBOL = "✓"
|
||
WARNING_SYMBOL = "⚠"
|
||
ERROR_SYMBOL = "✗"
|
||
DEBUG_SYMBOL = "🐛"
|
||
|
||
|
||
def supports_color() -> bool:
|
||
"""
|
||
Check if the terminal supports colors.
|
||
|
||
Returns:
|
||
bool: True if colors are supported, False otherwise
|
||
"""
|
||
# Check if we're in a terminal
|
||
if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
|
||
return False
|
||
|
||
# Check environment variables
|
||
if os.environ.get("NO_COLOR"):
|
||
return False
|
||
|
||
if os.environ.get("FORCE_COLOR"):
|
||
return True
|
||
|
||
# Check TERM environment variable
|
||
term = os.environ.get("TERM", "")
|
||
if term == "dumb":
|
||
return False
|
||
|
||
return True
|
||
|
||
|
||
# Initialize colors based on support
|
||
if supports_color():
|
||
c = Colors()
|
||
else:
|
||
# Create a class with empty strings for all colors
|
||
class NoColors:
|
||
def __getattr__(self, name):
|
||
return ""
|
||
|
||
c = NoColors()
|
||
|
||
|
||
def color_text(text: str, color: str) -> str:
|
||
"""
|
||
Apply color to text.
|
||
|
||
Args:
|
||
text: The text to colorize
|
||
color: The ANSI color code
|
||
|
||
Returns:
|
||
str: Colored text with reset at the end
|
||
"""
|
||
if not supports_color():
|
||
return text
|
||
return f"{color}{text}{c.NC}"
|
||
|
||
|
||
def red(text: str) -> str:
|
||
"""Return red colored text."""
|
||
return color_text(text, c.RED)
|
||
|
||
|
||
def green(text: str) -> str:
|
||
"""Return green colored text."""
|
||
return color_text(text, c.GREEN)
|
||
|
||
|
||
def yellow(text: str) -> str:
|
||
"""Return yellow colored text."""
|
||
return color_text(text, c.YELLOW)
|
||
|
||
|
||
def blue(text: str) -> str:
|
||
"""Return blue colored text."""
|
||
return color_text(text, c.BLUE)
|
||
|
||
|
||
def purple(text: str) -> str:
|
||
"""Return purple colored text."""
|
||
return color_text(text, c.PURPLE)
|
||
|
||
|
||
def cyan(text: str) -> str:
|
||
"""Return cyan colored text."""
|
||
return color_text(text, c.CYAN)
|
||
|
||
|
||
def white(text: str) -> str:
|
||
"""Return white colored text."""
|
||
return color_text(text, c.WHITE)
|
||
|
||
|
||
def gray(text: str) -> str:
|
||
"""Return gray colored text."""
|
||
return color_text(text, c.GRAY)
|
||
|
||
|
||
def bright_red(text: str) -> str:
|
||
"""Return bright red colored text."""
|
||
return color_text(text, c.BRIGHT_RED)
|
||
|
||
|
||
def bright_green(text: str) -> str:
|
||
"""Return bright green colored text."""
|
||
return color_text(text, c.BRIGHT_GREEN)
|
||
|
||
|
||
def bright_yellow(text: str) -> str:
|
||
"""Return bright yellow colored text."""
|
||
return color_text(text, c.BRIGHT_YELLOW)
|
||
|
||
|
||
def bright_blue(text: str) -> str:
|
||
"""Return bright blue colored text."""
|
||
return color_text(text, c.BRIGHT_BLUE)
|
||
|
||
|
||
def bright_purple(text: str) -> str:
|
||
"""Return bright purple colored text."""
|
||
return color_text(text, c.BRIGHT_PURPLE)
|
||
|
||
|
||
def bright_cyan(text: str) -> str:
|
||
"""Return bright cyan colored text."""
|
||
return color_text(text, c.BRIGHT_CYAN)
|
||
|
||
|
||
def bright_white(text: str) -> str:
|
||
"""Return bright white colored text."""
|
||
return color_text(text, c.BRIGHT_WHITE)
|
||
|
||
|
||
def bold(text: str) -> str:
|
||
"""Return bold text."""
|
||
return color_text(text, c.BOLD)
|
||
|
||
|
||
def dim(text: str) -> str:
|
||
"""Return dim text."""
|
||
return color_text(text, c.DIM)
|
||
|
||
|
||
def italic(text: str) -> str:
|
||
"""Return italic text."""
|
||
return color_text(text, c.ITALIC)
|
||
|
||
|
||
def underline(text: str) -> str:
|
||
"""Return underlined text."""
|
||
return color_text(text, c.UNDERLINE)
|
||
|
||
|
||
def info(message: str, file=None) -> None:
|
||
"""Print an info message."""
|
||
output = f"{c.BLUE}{c.INFO_SYMBOL}{c.NC} {message}"
|
||
print(output, file=file)
|
||
|
||
|
||
def success(message: str, file=None) -> None:
|
||
"""Print a success message."""
|
||
output = f"{c.GREEN}{c.SUCCESS_SYMBOL}{c.NC} {message}"
|
||
print(output, file=file)
|
||
|
||
|
||
def warning(message: str, file=None) -> None:
|
||
"""Print a warning message."""
|
||
output = f"{c.YELLOW}{c.WARNING_SYMBOL}{c.NC} {message}"
|
||
print(output, file=file)
|
||
|
||
|
||
def error(message: str, file=None) -> None:
|
||
"""Print an error message."""
|
||
output = f"{c.RED}{c.ERROR_SYMBOL}{c.NC} {message}"
|
||
print(output, file=file or sys.stderr)
|
||
|
||
|
||
def debug(message: str, file=None) -> None:
|
||
"""Print a debug message (only if DEBUG environment variable is set)."""
|
||
if os.environ.get("DEBUG"):
|
||
output = f"{c.GRAY}{c.DEBUG_SYMBOL}{c.NC} {message}"
|
||
print(output, file=file or sys.stderr)
|
||
|
||
|
||
def header(text: str, file=None) -> None:
|
||
"""Print a header with underline."""
|
||
print(file=file)
|
||
print(f"{c.BOLD}{c.BLUE}{text}{c.NC}", file=file)
|
||
print(f"{c.BLUE}{'=' * len(text)}{c.NC}", file=file)
|
||
|
||
|
||
def subheader(text: str, file=None) -> None:
|
||
"""Print a subheader with underline."""
|
||
print(file=file)
|
||
print(f"{c.BOLD}{text}{c.NC}", file=file)
|
||
print("-" * len(text), file=file)
|
||
|
||
|
||
def step(current: int, total: int, description: str, file=None) -> None:
|
||
"""Print a step indicator."""
|
||
output = f"{c.CYAN}[{current}/{total}]{c.NC} {c.BOLD}{description}{c.NC}"
|
||
print(output, file=file)
|
||
|
||
|
||
def status(status_type: str, message: str, file=None) -> None:
|
||
"""
|
||
Print a status message with appropriate formatting.
|
||
|
||
Args:
|
||
status_type: Type of status (ok, fail, warn, info, skip)
|
||
message: The status message
|
||
file: Output file (defaults to stdout, stderr for errors)
|
||
"""
|
||
status_map = {
|
||
"ok": (f"{c.GREEN}[ OK ]{c.NC}", None),
|
||
"success": (f"{c.GREEN}[ OK ]{c.NC}", None),
|
||
"done": (f"{c.GREEN}[ OK ]{c.NC}", None),
|
||
"fail": (f"{c.RED}[ FAIL ]{c.NC}", sys.stderr),
|
||
"error": (f"{c.RED}[ FAIL ]{c.NC}", sys.stderr),
|
||
"failed": (f"{c.RED}[ FAIL ]{c.NC}", sys.stderr),
|
||
"warn": (f"{c.YELLOW}[ WARN ]{c.NC}", None),
|
||
"warning": (f"{c.YELLOW}[ WARN ]{c.NC}", None),
|
||
"info": (f"{c.BLUE}[ INFO ]{c.NC}", None),
|
||
"skip": (f"{c.GRAY}[ SKIP ]{c.NC}", None),
|
||
"skipped": (f"{c.GRAY}[ SKIP ]{c.NC}", None),
|
||
}
|
||
|
||
status_prefix, default_file = status_map.get(
|
||
status_type.lower(), (f"{c.WHITE}[ ]{c.NC}", None)
|
||
)
|
||
output_file = file or default_file
|
||
|
||
print(f"{status_prefix} {message}", file=output_file)
|
||
|
||
|
||
def box_header(text: str, width: int = 50, file=None) -> None:
|
||
"""Print a fancy box header."""
|
||
padding = (width - len(text) - 2) // 2
|
||
print(f"{c.BLUE}╔{'═' * (width - 2)}╗{c.NC}", file=file)
|
||
print(
|
||
f"{c.BLUE}║{c.NC}{' ' * padding}{c.BOLD}{text}{c.NC}"
|
||
f"{' ' * padding}{c.BLUE}║{c.NC}",
|
||
file=file,
|
||
)
|
||
print(f"{c.BLUE}╚{'═' * (width - 2)}╝{c.NC}", file=file)
|
||
|
||
|
||
def box_message(
|
||
text: str, color: Optional[str] = None, width: int = 50, file=None
|
||
) -> None:
|
||
"""Print a message in a box."""
|
||
box_color = color or c.BLUE
|
||
padding = (width - len(text) - 2) // 2
|
||
print(f"{box_color}┌{'─' * (width - 2)}┐{c.NC}", file=file)
|
||
print(
|
||
f"{box_color}│{c.NC}{' ' * padding}{text}{' ' * padding}{box_color}│{c.NC}",
|
||
file=file,
|
||
)
|
||
print(f"{box_color}└{'─' * (width - 2)}┘{c.NC}", file=file)
|
||
|
||
|
||
def progress_bar(current: int, total: int, width: int = 40, file=None) -> None:
|
||
"""Print a progress bar."""
|
||
percent = current * 100 // total
|
||
filled = current * width // total
|
||
empty = width - filled
|
||
|
||
bar = f"{c.BLUE}[{'█' * filled}{'░' * empty}] {percent}% ({current}/{total}){c.NC}"
|
||
print(f"\r{bar}", end="", file=file, flush=True)
|
||
|
||
if current == total:
|
||
print(file=file) # New line when complete
|
||
|
||
|
||
class ColorizedFormatter:
|
||
"""A context manager for colorized output."""
|
||
|
||
def __init__(self, color: str):
|
||
self.color = color
|
||
|
||
def __enter__(self):
|
||
if supports_color():
|
||
print(self.color, end="")
|
||
return self
|
||
|
||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
if supports_color():
|
||
print(c.NC, end="")
|
||
|
||
|
||
# Example usage and testing
|
||
if __name__ == "__main__":
|
||
print("\nUnitForge Color Utility Test")
|
||
print("=" * 30)
|
||
|
||
# Test basic colors
|
||
print("\nBasic Colors:")
|
||
print(red("Red text"))
|
||
print(green("Green text"))
|
||
print(yellow("Yellow text"))
|
||
print(blue("Blue text"))
|
||
print(purple("Purple text"))
|
||
print(cyan("Cyan text"))
|
||
|
||
# Test status functions
|
||
print("\nStatus Messages:")
|
||
info("This is an info message")
|
||
success("This is a success message")
|
||
warning("This is a warning message")
|
||
error("This is an error message")
|
||
debug("This is a debug message")
|
||
|
||
# Test headers
|
||
header("Main Header")
|
||
subheader("Sub Header")
|
||
|
||
# Test status indicators
|
||
print("\nStatus Indicators:")
|
||
status("ok", "Operation successful")
|
||
status("fail", "Operation failed")
|
||
status("warn", "Operation completed with warnings")
|
||
status("info", "Information message")
|
||
status("skip", "Operation skipped")
|
||
|
||
# Test boxes
|
||
print("\nBox Examples:")
|
||
box_header("UnitForge Test", 30)
|
||
box_message("Success!", c.GREEN, 30)
|
||
|
||
# Test progress
|
||
print("\nProgress Example:")
|
||
import time
|
||
|
||
for i in range(11):
|
||
progress_bar(i, 10, 30)
|
||
if i < 10:
|
||
time.sleep(0.1)
|
||
|
||
print("\nColor support:", "Yes" if supports_color() else "No")
|
||
print("Test completed!")
|