Files
unitforge/scripts/colors.py
William Valentin 860f60591c Fix contrast issues with text-muted and bg-dark classes
- 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
2025-09-14 14:58:35 -07:00

399 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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!")