colors.py
1 """ 2 Color utilities for Kamagi terminal output. 3 """ 4 5 import re 6 from typing import Optional 7 8 9 class Colors: 10 """ANSI color codes for terminal output.""" 11 12 # Basic colors 13 RESET = '\033[0m' 14 BOLD = '\033[1m' 15 DIM = '\033[2m' 16 ITALIC = '\033[3m' 17 UNDERLINE = '\033[4m' 18 19 # Foreground colors 20 BLACK = '\033[30m' 21 RED = '\033[31m' 22 GREEN = '\033[32m' 23 YELLOW = '\033[33m' 24 BLUE = '\033[34m' 25 MAGENTA = '\033[35m' 26 CYAN = '\033[36m' 27 WHITE = '\033[37m' 28 29 # Bright foreground colors 30 BRIGHT_BLACK = '\033[90m' 31 BRIGHT_RED = '\033[91m' 32 BRIGHT_GREEN = '\033[92m' 33 BRIGHT_YELLOW = '\033[93m' 34 BRIGHT_BLUE = '\033[94m' 35 BRIGHT_MAGENTA = '\033[95m' 36 BRIGHT_CYAN = '\033[96m' 37 BRIGHT_WHITE = '\033[97m' 38 39 # Background colors 40 BG_BLACK = '\033[40m' 41 BG_RED = '\033[41m' 42 BG_GREEN = '\033[42m' 43 BG_YELLOW = '\033[43m' 44 BG_BLUE = '\033[44m' 45 BG_MAGENTA = '\033[45m' 46 BG_CYAN = '\033[46m' 47 BG_WHITE = '\033[47m' 48 49 50 def colorize_response(text: str, enable_colors: bool = True) -> str: 51 """ 52 Add intelligent color coding to response text. 53 54 Args: 55 text: The text to colorize 56 enable_colors: Whether to enable colors 57 58 Returns: 59 Colorized text 60 """ 61 if not enable_colors: 62 return text 63 64 # Store original text 65 result = text 66 67 # Color code blocks (```...```) 68 def colorize_code_block(match): 69 code = match.group(2) 70 lang = match.group(1) if match.group(1) else '' 71 72 # Syntax highlight the code 73 highlighted = syntax_highlight(code, lang) 74 75 return f"{Colors.BG_BLACK}{Colors.BRIGHT_WHITE}```{lang}\n{highlighted}\n```{Colors.RESET}" 76 77 result = re.sub(r'```(\w*)\n(.*?)```', colorize_code_block, result, flags=re.DOTALL) 78 79 # Color inline code (`...`) 80 result = re.sub( 81 r'`([^`]+)`', 82 f'{Colors.CYAN}\\1{Colors.RESET}', 83 result 84 ) 85 86 # Color bold text (**...**) 87 result = re.sub( 88 r'\*\*([^\*]+)\*\*', 89 f'{Colors.BOLD}{Colors.YELLOW}\\1{Colors.RESET}', 90 result 91 ) 92 93 # Color italic text (*...*) 94 result = re.sub( 95 r'\*([^\*]+)\*', 96 f'{Colors.ITALIC}{Colors.BRIGHT_CYAN}\\1{Colors.RESET}', 97 result 98 ) 99 100 # Color headers (# Header) 101 result = re.sub( 102 r'^(#{1,6})\s+(.+)$', 103 f'{Colors.BOLD}{Colors.BRIGHT_MAGENTA}\\1 \\2{Colors.RESET}', 104 result, 105 flags=re.MULTILINE 106 ) 107 108 # Color bullet points 109 result = re.sub( 110 r'^(\s*[-*+])\s', 111 f'{Colors.BRIGHT_GREEN}\\1{Colors.RESET} ', 112 result, 113 flags=re.MULTILINE 114 ) 115 116 # Color numbered lists 117 result = re.sub( 118 r'^(\s*\d+\.)\s', 119 f'{Colors.BRIGHT_BLUE}\\1{Colors.RESET} ', 120 result, 121 flags=re.MULTILINE 122 ) 123 124 # Color URLs 125 result = re.sub( 126 r'(https?://[^\s]+)', 127 f'{Colors.UNDERLINE}{Colors.BLUE}\\1{Colors.RESET}', 128 result 129 ) 130 131 # Color file paths 132 result = re.sub( 133 r'([~/][\w/.-]+\.\w+)', 134 f'{Colors.BRIGHT_YELLOW}\\1{Colors.RESET}', 135 result 136 ) 137 138 # Color quoted text 139 result = re.sub( 140 r'^>\s+(.+)$', 141 f'{Colors.DIM}{Colors.ITALIC}> \\1{Colors.RESET}', 142 result, 143 flags=re.MULTILINE 144 ) 145 146 return result 147 148 149 def syntax_highlight(code: str, language: str = '') -> str: 150 """ 151 Basic syntax highlighting for code. 152 153 Args: 154 code: The code to highlight 155 language: Programming language 156 157 Returns: 158 Highlighted code 159 """ 160 result = code 161 162 # Python keywords 163 if language.lower() in ['python', 'py', '']: 164 keywords = [ 165 'def', 'class', 'if', 'else', 'elif', 'for', 'while', 'return', 166 'import', 'from', 'as', 'try', 'except', 'finally', 'with', 167 'async', 'await', 'yield', 'lambda', 'pass', 'break', 'continue', 168 'True', 'False', 'None', 'and', 'or', 'not', 'in', 'is' 169 ] 170 171 for keyword in keywords: 172 result = re.sub( 173 rf'\b({keyword})\b', 174 f'{Colors.BRIGHT_MAGENTA}\\1{Colors.RESET}', 175 result 176 ) 177 178 # JavaScript/TypeScript keywords 179 elif language.lower() in ['javascript', 'js', 'typescript', 'ts']: 180 keywords = [ 181 'function', 'const', 'let', 'var', 'if', 'else', 'for', 'while', 182 'return', 'class', 'import', 'export', 'from', 'async', 'await', 183 'try', 'catch', 'finally', 'true', 'false', 'null', 'undefined' 184 ] 185 186 for keyword in keywords: 187 result = re.sub( 188 rf'\b({keyword})\b', 189 f'{Colors.BRIGHT_MAGENTA}\\1{Colors.RESET}', 190 result 191 ) 192 193 # Color strings 194 result = re.sub( 195 r'(["\'])([^\1]*?)\1', 196 f'{Colors.GREEN}\\1\\2\\1{Colors.RESET}', 197 result 198 ) 199 200 # Color comments 201 result = re.sub( 202 r'(#.+)$', 203 f'{Colors.DIM}{Colors.BRIGHT_BLACK}\\1{Colors.RESET}', 204 result, 205 flags=re.MULTILINE 206 ) 207 208 result = re.sub( 209 r'(//.+)$', 210 f'{Colors.DIM}{Colors.BRIGHT_BLACK}\\1{Colors.RESET}', 211 result, 212 flags=re.MULTILINE 213 ) 214 215 # Color numbers 216 result = re.sub( 217 r'\b(\d+\.?\d*)\b', 218 f'{Colors.CYAN}\\1{Colors.RESET}', 219 result 220 ) 221 222 # Color function calls 223 result = re.sub( 224 r'\b(\w+)\s*\(', 225 f'{Colors.BRIGHT_BLUE}\\1{Colors.RESET}(', 226 result 227 ) 228 229 return result 230 231 232 def strip_colors(text: str) -> str: 233 """ 234 Remove all ANSI color codes from text. 235 236 Args: 237 text: Text with color codes 238 239 Returns: 240 Plain text without colors 241 """ 242 ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') 243 return ansi_escape.sub('', text) 244 245 246 def supports_color() -> bool: 247 """ 248 Check if terminal supports colors. 249 250 Returns: 251 True if colors are supported 252 """ 253 import sys 254 import os 255 256 # Check if stdout is a TTY 257 if not hasattr(sys.stdout, 'isatty') or not sys.stdout.isatty(): 258 return False 259 260 # Check TERM environment variable 261 term = os.environ.get('TERM', '') 262 if term in ['dumb', '']: 263 return False 264 265 # Check NO_COLOR environment variable 266 if os.environ.get('NO_COLOR'): 267 return False 268 269 return True