1
0

cli_ui.py 2.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. """CLI UI helpers (ANSI colors, spinner, consistent formatting).
  2. Kept dependency-free to avoid pulling extra packages into the MVP.
  3. """
  4. from __future__ import annotations
  5. import os
  6. import sys
  7. import threading
  8. import time
  9. from typing import Iterable
  10. RESET = "\x1b[0m"
  11. PRIMARY = "\x1b[38;2;120;200;255m"
  12. ACCENT = "\x1b[38;2;150;140;255m"
  13. INFO = "\x1b[38;2;120;120;120m"
  14. WARN = "\x1b[38;2;255;190;120m"
  15. ERROR = "\x1b[38;2;255;120;120m"
  16. def supports_ansi() -> bool:
  17. if os.getenv("NO_COLOR"):
  18. return False
  19. return sys.stdout.isatty()
  20. def c(text: str, color: str) -> str:
  21. if not supports_ansi():
  22. return text
  23. return f"{color}{text}{RESET}"
  24. def hr(char: str = "=", width: int = 80) -> str:
  25. return char * width
  26. def clamp_text(text: str, limit: int = 40_000) -> str:
  27. if text is None:
  28. return ""
  29. if len(text) <= limit:
  30. return text
  31. return text[:limit] + f"\n\n...<truncated {len(text) - limit} chars>"
  32. def log_tool_event(name: str, message: str) -> None:
  33. """LangChain-demo-like tool logging."""
  34. header = f"⏺ {name}"
  35. print(c(header, ACCENT))
  36. lines: Iterable[str] = message.splitlines() if message else [""]
  37. for line in lines:
  38. print(f" ⎿ {line}")
  39. class Spinner:
  40. """Simple terminal spinner."""
  41. def __init__(self, label: str = "Thinking…") -> None:
  42. self.label = label
  43. self.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
  44. self._index = 0
  45. self._running = False
  46. self._thread: threading.Thread | None = None
  47. def start(self) -> None:
  48. if not supports_ansi() or self._running:
  49. return
  50. self._running = True
  51. def _run() -> None:
  52. while self._running:
  53. frame = self.frames[self._index % len(self.frames)]
  54. self._index += 1
  55. print(c(f"{frame} {self.label}", INFO), end="\r", flush=True)
  56. time.sleep(0.08)
  57. self._thread = threading.Thread(target=_run, daemon=True)
  58. self._thread.start()
  59. def stop(self) -> None:
  60. if not self._running:
  61. return
  62. self._running = False
  63. if supports_ansi():
  64. print("\r\x1b[2K", end="", flush=True)