|
| 1 | +"""Namespace styling with ANSI colors and emojis. |
| 2 | +
|
| 3 | +This module provides visual styling for memory namespaces using ANSI |
| 4 | +color codes and emojis. Colors are chosen based on psychological and |
| 5 | +philosophical associations with each namespace's purpose. |
| 6 | +
|
| 7 | +Color Psychology: |
| 8 | +- RED: Danger, urgency, stop (blockers) |
| 9 | +- BLUE: Trust, stability, authority (decisions) |
| 10 | +- GREEN: Growth, knowledge, freshness (learnings, inception) |
| 11 | +- CYAN: Forward movement, achievement (progress, elicitation) |
| 12 | +- YELLOW: Curiosity, discovery, attention (research) |
| 13 | +- MAGENTA: Creativity, patterns, wisdom (patterns, retrospective) |
| 14 | +- ORANGE: Evaluation, warmth, feedback (reviews) |
| 15 | +""" |
| 16 | + |
| 17 | +from __future__ import annotations |
| 18 | + |
| 19 | +from dataclasses import dataclass |
| 20 | +from enum import Enum |
| 21 | + |
| 22 | +__all__ = [ |
| 23 | + "NamespaceStyle", |
| 24 | + "get_style", |
| 25 | + "format_namespace", |
| 26 | + "format_marker", |
| 27 | + "STYLES", |
| 28 | +] |
| 29 | + |
| 30 | + |
| 31 | +class Color(Enum): |
| 32 | + """ANSI color codes for terminal output.""" |
| 33 | + |
| 34 | + # Standard colors |
| 35 | + RED = "\033[31m" |
| 36 | + GREEN = "\033[32m" |
| 37 | + YELLOW = "\033[33m" |
| 38 | + BLUE = "\033[34m" |
| 39 | + MAGENTA = "\033[35m" |
| 40 | + CYAN = "\033[36m" |
| 41 | + |
| 42 | + # Bright/bold colors |
| 43 | + BRIGHT_RED = "\033[91m" |
| 44 | + BRIGHT_GREEN = "\033[92m" |
| 45 | + BRIGHT_YELLOW = "\033[93m" |
| 46 | + BRIGHT_BLUE = "\033[94m" |
| 47 | + BRIGHT_MAGENTA = "\033[95m" |
| 48 | + BRIGHT_CYAN = "\033[96m" |
| 49 | + |
| 50 | + # Orange (via 256-color mode) |
| 51 | + ORANGE = "\033[38;5;208m" |
| 52 | + BRIGHT_ORANGE = "\033[38;5;214m" |
| 53 | + |
| 54 | + # Reset |
| 55 | + RESET = "\033[0m" |
| 56 | + |
| 57 | + # Bold modifier |
| 58 | + BOLD = "\033[1m" |
| 59 | + |
| 60 | + |
| 61 | +@dataclass(frozen=True) |
| 62 | +class NamespaceStyle: |
| 63 | + """Visual style for a namespace. |
| 64 | +
|
| 65 | + Attributes: |
| 66 | + namespace: The namespace identifier. |
| 67 | + emoji: Emoji representing the namespace. |
| 68 | + color: ANSI color code for the namespace. |
| 69 | + label: Human-readable label for the namespace. |
| 70 | + description: Brief description of namespace purpose. |
| 71 | + """ |
| 72 | + |
| 73 | + namespace: str |
| 74 | + emoji: str |
| 75 | + color: str |
| 76 | + label: str |
| 77 | + description: str |
| 78 | + |
| 79 | + def format_badge(self, text: str | None = None) -> str: |
| 80 | + """Format a colored badge for the namespace. |
| 81 | +
|
| 82 | + Args: |
| 83 | + text: Optional text to include after the badge. |
| 84 | + If None, just returns the emoji and label. |
| 85 | +
|
| 86 | + Returns: |
| 87 | + ANSI-colored string with emoji, label, and optional text. |
| 88 | + """ |
| 89 | + badge = f"{self.color}{self.emoji} {self.label}{Color.RESET.value}" |
| 90 | + if text: |
| 91 | + return f"{badge} {text}" |
| 92 | + return badge |
| 93 | + |
| 94 | + def format_marker(self, content: str) -> str: |
| 95 | + """Format a capture marker with styling. |
| 96 | +
|
| 97 | + Args: |
| 98 | + content: The marker content text. |
| 99 | +
|
| 100 | + Returns: |
| 101 | + Styled marker string like "[🛑 blocker] content". |
| 102 | + """ |
| 103 | + marker_name = self.namespace |
| 104 | + # Use shorter names for common markers |
| 105 | + if self.namespace == "learnings": |
| 106 | + marker_name = "learned" |
| 107 | + elif self.namespace == "decisions": |
| 108 | + marker_name = "decision" |
| 109 | + elif self.namespace == "blockers": |
| 110 | + marker_name = "blocker" |
| 111 | + |
| 112 | + return ( |
| 113 | + f"{self.color}[{self.emoji} {marker_name}]{Color.RESET.value} " |
| 114 | + f"{content}" |
| 115 | + ) |
| 116 | + |
| 117 | + def format_inline(self) -> str: |
| 118 | + """Format just the namespace with emoji and color. |
| 119 | +
|
| 120 | + Returns: |
| 121 | + Colored namespace string like "🛑 blockers". |
| 122 | + """ |
| 123 | + return f"{self.color}{self.emoji} {self.namespace}{Color.RESET.value}" |
| 124 | + |
| 125 | + |
| 126 | +# ============================================================================= |
| 127 | +# Namespace Style Definitions |
| 128 | +# ============================================================================= |
| 129 | + |
| 130 | +STYLES: dict[str, NamespaceStyle] = { |
| 131 | + # 🛑 BLOCKERS - Red for danger/stop/urgent |
| 132 | + "blockers": NamespaceStyle( |
| 133 | + namespace="blockers", |
| 134 | + emoji="🛑", |
| 135 | + color=Color.BRIGHT_RED.value, |
| 136 | + label="BLOCKER", |
| 137 | + description="Obstacles and impediments", |
| 138 | + ), |
| 139 | + # ⚖️ DECISIONS - Blue for trust/authority/stability |
| 140 | + "decisions": NamespaceStyle( |
| 141 | + namespace="decisions", |
| 142 | + emoji="⚖️", |
| 143 | + color=Color.BRIGHT_BLUE.value, |
| 144 | + label="DECISION", |
| 145 | + description="Architecture decisions and choices", |
| 146 | + ), |
| 147 | + # 💡 LEARNINGS - Green for growth/knowledge/insight |
| 148 | + "learnings": NamespaceStyle( |
| 149 | + namespace="learnings", |
| 150 | + emoji="💡", |
| 151 | + color=Color.BRIGHT_GREEN.value, |
| 152 | + label="LEARNED", |
| 153 | + description="Technical insights and discoveries", |
| 154 | + ), |
| 155 | + # 🚀 PROGRESS - Cyan for forward movement/achievement |
| 156 | + "progress": NamespaceStyle( |
| 157 | + namespace="progress", |
| 158 | + emoji="🚀", |
| 159 | + color=Color.BRIGHT_CYAN.value, |
| 160 | + label="PROGRESS", |
| 161 | + description="Task completions and milestones", |
| 162 | + ), |
| 163 | + # 🧩 PATTERNS - Magenta for creativity/abstraction/wisdom |
| 164 | + "patterns": NamespaceStyle( |
| 165 | + namespace="patterns", |
| 166 | + emoji="🧩", |
| 167 | + color=Color.BRIGHT_MAGENTA.value, |
| 168 | + label="PATTERN", |
| 169 | + description="Cross-project generalizations", |
| 170 | + ), |
| 171 | + # 🔍 RESEARCH - Yellow for curiosity/discovery/illumination |
| 172 | + "research": NamespaceStyle( |
| 173 | + namespace="research", |
| 174 | + emoji="🔍", |
| 175 | + color=Color.BRIGHT_YELLOW.value, |
| 176 | + label="RESEARCH", |
| 177 | + description="External findings and evaluations", |
| 178 | + ), |
| 179 | + # 👁️ REVIEWS - Orange for evaluation/scrutiny/feedback |
| 180 | + "reviews": NamespaceStyle( |
| 181 | + namespace="reviews", |
| 182 | + emoji="👁️", |
| 183 | + color=Color.ORANGE.value, |
| 184 | + label="REVIEW", |
| 185 | + description="Code review findings", |
| 186 | + ), |
| 187 | + # 🔄 RETROSPECTIVE - Magenta for reflection/introspection |
| 188 | + "retrospective": NamespaceStyle( |
| 189 | + namespace="retrospective", |
| 190 | + emoji="🔄", |
| 191 | + color=Color.MAGENTA.value, |
| 192 | + label="RETRO", |
| 193 | + description="Post-mortem reflections", |
| 194 | + ), |
| 195 | + # 🌱 INCEPTION - Light green for beginnings/new growth |
| 196 | + "inception": NamespaceStyle( |
| 197 | + namespace="inception", |
| 198 | + emoji="🌱", |
| 199 | + color=Color.GREEN.value, |
| 200 | + label="INCEPTION", |
| 201 | + description="Problem statements and scope", |
| 202 | + ), |
| 203 | + # 💬 ELICITATION - Light cyan for communication/dialogue |
| 204 | + "elicitation": NamespaceStyle( |
| 205 | + namespace="elicitation", |
| 206 | + emoji="💬", |
| 207 | + color=Color.CYAN.value, |
| 208 | + label="ELICIT", |
| 209 | + description="Requirements clarifications", |
| 210 | + ), |
| 211 | +} |
| 212 | + |
| 213 | +# Default style for unknown namespaces |
| 214 | +DEFAULT_STYLE = NamespaceStyle( |
| 215 | + namespace="memory", |
| 216 | + emoji="📝", |
| 217 | + color=Color.RESET.value, |
| 218 | + label="MEMORY", |
| 219 | + description="General memory", |
| 220 | +) |
| 221 | + |
| 222 | + |
| 223 | +# ============================================================================= |
| 224 | +# Public API |
| 225 | +# ============================================================================= |
| 226 | + |
| 227 | + |
| 228 | +def get_style(namespace: str) -> NamespaceStyle: |
| 229 | + """Get the style for a namespace. |
| 230 | +
|
| 231 | + Args: |
| 232 | + namespace: The namespace identifier. |
| 233 | +
|
| 234 | + Returns: |
| 235 | + NamespaceStyle for the namespace, or DEFAULT_STYLE if unknown. |
| 236 | + """ |
| 237 | + return STYLES.get(namespace, DEFAULT_STYLE) |
| 238 | + |
| 239 | + |
| 240 | +def format_namespace(namespace: str, with_emoji: bool = True) -> str: |
| 241 | + """Format a namespace with color and optional emoji. |
| 242 | +
|
| 243 | + Args: |
| 244 | + namespace: The namespace identifier. |
| 245 | + with_emoji: Whether to include the emoji (default True). |
| 246 | +
|
| 247 | + Returns: |
| 248 | + Colored namespace string. |
| 249 | + """ |
| 250 | + style = get_style(namespace) |
| 251 | + if with_emoji: |
| 252 | + return style.format_inline() |
| 253 | + return f"{style.color}{namespace}{Color.RESET.value}" |
| 254 | + |
| 255 | + |
| 256 | +def format_marker(namespace: str, content: str) -> str: |
| 257 | + """Format a capture marker with full styling. |
| 258 | +
|
| 259 | + Args: |
| 260 | + namespace: The namespace identifier. |
| 261 | + content: The marker content text. |
| 262 | +
|
| 263 | + Returns: |
| 264 | + Fully styled marker string. |
| 265 | +
|
| 266 | + Example: |
| 267 | + >>> format_marker("blockers", "Database connection timeout") |
| 268 | + '[🛑 blocker] Database connection timeout' # with red coloring |
| 269 | + """ |
| 270 | + style = get_style(namespace) |
| 271 | + return style.format_marker(content) |
| 272 | + |
| 273 | + |
| 274 | +def format_memory_header( |
| 275 | + namespace: str, |
| 276 | + memory_id: str, |
| 277 | + timestamp: str | None = None, |
| 278 | +) -> str: |
| 279 | + """Format a memory header for display. |
| 280 | +
|
| 281 | + Args: |
| 282 | + namespace: The namespace identifier. |
| 283 | + memory_id: The memory ID. |
| 284 | + timestamp: Optional ISO timestamp. |
| 285 | +
|
| 286 | + Returns: |
| 287 | + Formatted header string with color and emoji. |
| 288 | + """ |
| 289 | + style = get_style(namespace) |
| 290 | + header = f"{style.color}{style.emoji} [{style.label}]{Color.RESET.value}" |
| 291 | + |
| 292 | + # Add dimmed ID |
| 293 | + dim = "\033[2m" |
| 294 | + header += f" {dim}{memory_id}{Color.RESET.value}" |
| 295 | + |
| 296 | + if timestamp: |
| 297 | + header += f" {dim}({timestamp}){Color.RESET.value}" |
| 298 | + |
| 299 | + return header |
| 300 | + |
| 301 | + |
| 302 | +# ============================================================================= |
| 303 | +# Marker Reference for Guidance Templates |
| 304 | +# ============================================================================= |
| 305 | + |
| 306 | + |
| 307 | +def get_marker_reference_styled() -> str: |
| 308 | + """Get a styled marker reference for guidance templates. |
| 309 | +
|
| 310 | + Returns: |
| 311 | + Multi-line string showing all markers with colors and emojis. |
| 312 | + """ |
| 313 | + lines = ["**Capture Markers:**", ""] |
| 314 | + |
| 315 | + # Primary markers (most commonly used) |
| 316 | + primary = ["decisions", "learnings", "blockers", "progress"] |
| 317 | + lines.append("*Primary:*") |
| 318 | + for ns in primary: |
| 319 | + style = STYLES[ns] |
| 320 | + marker_name = ns |
| 321 | + if ns == "learnings": |
| 322 | + marker_name = "learned" |
| 323 | + elif ns == "decisions": |
| 324 | + marker_name = "decision" |
| 325 | + elif ns == "blockers": |
| 326 | + marker_name = "blocker" |
| 327 | + lines.append(f" `[{style.emoji} {marker_name}]` - {style.description}") |
| 328 | + |
| 329 | + lines.append("") |
| 330 | + lines.append("*Additional:*") |
| 331 | + |
| 332 | + # Additional markers |
| 333 | + additional = ["research", "patterns", "reviews", "retrospective"] |
| 334 | + for ns in additional: |
| 335 | + style = STYLES[ns] |
| 336 | + lines.append(f" `[{style.emoji} {ns}]` - {style.description}") |
| 337 | + |
| 338 | + return "\n".join(lines) |
0 commit comments