/ archive / python / kamaji / colors.py
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