Revert "feat(cli): skin-aware light/dark theme mode with terminal auto-detection"
This reverts commit a1c81360a5.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
"""Shared ANSI color utilities for Hermes CLI modules."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
@@ -21,123 +20,3 @@ def color(text: str, *codes) -> str:
|
||||
if not sys.stdout.isatty():
|
||||
return text
|
||||
return "".join(codes) + text + Colors.RESET
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Terminal background detection (light vs dark)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _detect_via_colorfgbg() -> str:
|
||||
"""Check the COLORFGBG environment variable.
|
||||
|
||||
Some terminals (rxvt, xterm, iTerm2) set COLORFGBG to ``<fg>;<bg>``
|
||||
where bg >= 8 usually means a dark background.
|
||||
Returns "light", "dark", or "unknown".
|
||||
"""
|
||||
val = os.environ.get("COLORFGBG", "")
|
||||
if not val:
|
||||
return "unknown"
|
||||
parts = val.split(";")
|
||||
try:
|
||||
bg = int(parts[-1])
|
||||
except (ValueError, IndexError):
|
||||
return "unknown"
|
||||
# Standard terminal colors 0-6 are dark, 7+ are light.
|
||||
# bg < 7 → dark background; bg >= 7 → light background.
|
||||
if bg >= 7:
|
||||
return "light"
|
||||
return "dark"
|
||||
|
||||
|
||||
def _detect_via_macos_appearance() -> str:
|
||||
"""Check macOS AppleInterfaceStyle via ``defaults read``.
|
||||
|
||||
Returns "light", "dark", or "unknown".
|
||||
"""
|
||||
if sys.platform != "darwin":
|
||||
return "unknown"
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
["defaults", "read", "-g", "AppleInterfaceStyle"],
|
||||
capture_output=True, text=True, timeout=2,
|
||||
)
|
||||
if result.returncode == 0 and "dark" in result.stdout.lower():
|
||||
return "dark"
|
||||
# If the key doesn't exist, macOS is in light mode.
|
||||
return "light"
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _detect_via_osc11() -> str:
|
||||
"""Query the terminal background colour via the OSC 11 escape sequence.
|
||||
|
||||
Writes ``\\e]11;?\\a`` and reads the response to determine luminance.
|
||||
Only works when stdin/stdout are connected to a real TTY (not piped).
|
||||
Returns "light", "dark", or "unknown".
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
return "unknown"
|
||||
if not (sys.stdin.isatty() and sys.stdout.isatty()):
|
||||
return "unknown"
|
||||
try:
|
||||
import select
|
||||
import termios
|
||||
import tty
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
old_attrs = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
# Send OSC 11 query
|
||||
sys.stdout.write("\x1b]11;?\x07")
|
||||
sys.stdout.flush()
|
||||
# Wait briefly for response
|
||||
if not select.select([fd], [], [], 0.1)[0]:
|
||||
return "unknown"
|
||||
response = b""
|
||||
while select.select([fd], [], [], 0.05)[0]:
|
||||
response += os.read(fd, 128)
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)
|
||||
|
||||
# Parse response: \x1b]11;rgb:RRRR/GGGG/BBBB\x07 (or \x1b\\)
|
||||
text = response.decode("latin-1", errors="replace")
|
||||
if "rgb:" not in text:
|
||||
return "unknown"
|
||||
rgb_part = text.split("rgb:")[-1].split("\x07")[0].split("\x1b")[0]
|
||||
channels = rgb_part.split("/")
|
||||
if len(channels) < 3:
|
||||
return "unknown"
|
||||
# Each channel is 2 or 4 hex digits; normalise to 0-255
|
||||
vals = []
|
||||
for ch in channels[:3]:
|
||||
ch = ch.strip()
|
||||
if len(ch) <= 2:
|
||||
vals.append(int(ch, 16))
|
||||
else:
|
||||
vals.append(int(ch[:2], 16)) # take high byte
|
||||
# Perceived luminance (ITU-R BT.601)
|
||||
luminance = 0.299 * vals[0] + 0.587 * vals[1] + 0.114 * vals[2]
|
||||
return "light" if luminance > 128 else "dark"
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def detect_terminal_background() -> str:
|
||||
"""Detect whether the terminal has a light or dark background.
|
||||
|
||||
Tries three strategies in order:
|
||||
1. COLORFGBG environment variable
|
||||
2. macOS appearance setting
|
||||
3. OSC 11 escape sequence query
|
||||
|
||||
Returns "light", "dark", or "unknown" if detection fails.
|
||||
"""
|
||||
for detector in (_detect_via_colorfgbg, _detect_via_macos_appearance, _detect_via_osc11):
|
||||
result = detector()
|
||||
if result != "unknown":
|
||||
return result
|
||||
return "unknown"
|
||||
|
||||
@@ -236,7 +236,6 @@ DEFAULT_CONFIG = {
|
||||
"streaming": False,
|
||||
"show_cost": False, # Show $ cost in the status bar (off by default)
|
||||
"skin": "default",
|
||||
"theme_mode": "auto",
|
||||
},
|
||||
|
||||
# Privacy settings
|
||||
|
||||
@@ -114,7 +114,6 @@ class SkinConfig:
|
||||
name: str
|
||||
description: str = ""
|
||||
colors: Dict[str, str] = field(default_factory=dict)
|
||||
colors_light: Dict[str, str] = field(default_factory=dict)
|
||||
spinner: Dict[str, Any] = field(default_factory=dict)
|
||||
branding: Dict[str, str] = field(default_factory=dict)
|
||||
tool_prefix: str = "┊"
|
||||
@@ -123,12 +122,7 @@ class SkinConfig:
|
||||
banner_hero: str = "" # Rich-markup hero art (replaces HERMES_CADUCEUS)
|
||||
|
||||
def get_color(self, key: str, fallback: str = "") -> str:
|
||||
"""Get a color value with fallback.
|
||||
|
||||
In light theme mode, returns the light override if available.
|
||||
"""
|
||||
if get_theme_mode() == "light" and key in self.colors_light:
|
||||
return self.colors_light[key]
|
||||
"""Get a color value with fallback."""
|
||||
return self.colors.get(key, fallback)
|
||||
|
||||
def get_spinner_list(self, key: str) -> List[str]:
|
||||
@@ -174,21 +168,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
||||
"session_label": "#DAA520",
|
||||
"session_border": "#8B8682",
|
||||
},
|
||||
"colors_light": {
|
||||
"banner_border": "#7A5A00",
|
||||
"banner_title": "#6B4C00",
|
||||
"banner_accent": "#7A5500",
|
||||
"banner_dim": "#8B7355",
|
||||
"banner_text": "#3D2B00",
|
||||
"prompt": "#3D2B00",
|
||||
"ui_accent": "#7A5500",
|
||||
"ui_label": "#01579B",
|
||||
"ui_ok": "#1B5E20",
|
||||
"input_rule": "#7A5A00",
|
||||
"response_border": "#6B4C00",
|
||||
"session_label": "#5C4300",
|
||||
"session_border": "#8B7355",
|
||||
},
|
||||
"spinner": {
|
||||
# Empty = use hardcoded defaults in display.py
|
||||
},
|
||||
@@ -222,21 +201,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
||||
"session_label": "#C7A96B",
|
||||
"session_border": "#6E584B",
|
||||
},
|
||||
"colors_light": {
|
||||
"banner_border": "#6B1010",
|
||||
"banner_title": "#5C4300",
|
||||
"banner_accent": "#8B1A1A",
|
||||
"banner_dim": "#5C4030",
|
||||
"banner_text": "#3A1800",
|
||||
"prompt": "#3A1800",
|
||||
"ui_accent": "#8B1A1A",
|
||||
"ui_label": "#5C4300",
|
||||
"ui_ok": "#1B5E20",
|
||||
"input_rule": "#6B1010",
|
||||
"response_border": "#7A1515",
|
||||
"session_label": "#5C4300",
|
||||
"session_border": "#5C4A3A",
|
||||
},
|
||||
"spinner": {
|
||||
"waiting_faces": ["(⚔)", "(⛨)", "(▲)", "(<>)", "(/)"],
|
||||
"thinking_faces": ["(⚔)", "(⛨)", "(▲)", "(⌁)", "(<>)"],
|
||||
@@ -301,22 +265,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
||||
"session_label": "#888888",
|
||||
"session_border": "#555555",
|
||||
},
|
||||
"colors_light": {
|
||||
"banner_border": "#333333",
|
||||
"banner_title": "#222222",
|
||||
"banner_accent": "#333333",
|
||||
"banner_dim": "#555555",
|
||||
"banner_text": "#333333",
|
||||
"prompt": "#222222",
|
||||
"ui_accent": "#333333",
|
||||
"ui_label": "#444444",
|
||||
"ui_ok": "#444444",
|
||||
"ui_error": "#333333",
|
||||
"input_rule": "#333333",
|
||||
"response_border": "#444444",
|
||||
"session_label": "#444444",
|
||||
"session_border": "#666666",
|
||||
},
|
||||
"spinner": {},
|
||||
"branding": {
|
||||
"agent_name": "Hermes Agent",
|
||||
@@ -348,21 +296,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
||||
"session_label": "#7eb8f6",
|
||||
"session_border": "#4b5563",
|
||||
},
|
||||
"colors_light": {
|
||||
"banner_border": "#1A3A7A",
|
||||
"banner_title": "#1A3570",
|
||||
"banner_accent": "#1E4090",
|
||||
"banner_dim": "#3B4555",
|
||||
"banner_text": "#1A2A50",
|
||||
"prompt": "#1A2A50",
|
||||
"ui_accent": "#1A3570",
|
||||
"ui_label": "#1E3A80",
|
||||
"ui_ok": "#1B5E20",
|
||||
"input_rule": "#1A3A7A",
|
||||
"response_border": "#2A4FA0",
|
||||
"session_label": "#1A3570",
|
||||
"session_border": "#5A6070",
|
||||
},
|
||||
"spinner": {},
|
||||
"branding": {
|
||||
"agent_name": "Hermes Agent",
|
||||
@@ -394,21 +327,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
||||
"session_label": "#A9DFFF",
|
||||
"session_border": "#496884",
|
||||
},
|
||||
"colors_light": {
|
||||
"banner_border": "#0D3060",
|
||||
"banner_title": "#0D3060",
|
||||
"banner_accent": "#154080",
|
||||
"banner_dim": "#2A4565",
|
||||
"banner_text": "#0A2850",
|
||||
"prompt": "#0A2850",
|
||||
"ui_accent": "#0D3060",
|
||||
"ui_label": "#0D3060",
|
||||
"ui_ok": "#1B5E20",
|
||||
"input_rule": "#0D3060",
|
||||
"response_border": "#1A5090",
|
||||
"session_label": "#0D3060",
|
||||
"session_border": "#3A5575",
|
||||
},
|
||||
"spinner": {
|
||||
"waiting_faces": ["(≈)", "(Ψ)", "(∿)", "(◌)", "(◠)"],
|
||||
"thinking_faces": ["(Ψ)", "(∿)", "(≈)", "(⌁)", "(◌)"],
|
||||
@@ -473,23 +391,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
||||
"session_label": "#919191",
|
||||
"session_border": "#656565",
|
||||
},
|
||||
"colors_light": {
|
||||
"banner_border": "#666666",
|
||||
"banner_title": "#222222",
|
||||
"banner_accent": "#333333",
|
||||
"banner_dim": "#555555",
|
||||
"banner_text": "#333333",
|
||||
"prompt": "#222222",
|
||||
"ui_accent": "#333333",
|
||||
"ui_label": "#444444",
|
||||
"ui_ok": "#444444",
|
||||
"ui_error": "#333333",
|
||||
"ui_warn": "#444444",
|
||||
"input_rule": "#666666",
|
||||
"response_border": "#555555",
|
||||
"session_label": "#444444",
|
||||
"session_border": "#777777",
|
||||
},
|
||||
"spinner": {
|
||||
"waiting_faces": ["(◉)", "(◌)", "(◬)", "(⬤)", "(::)"],
|
||||
"thinking_faces": ["(◉)", "(◬)", "(◌)", "(○)", "(●)"],
|
||||
@@ -555,21 +456,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
||||
"session_label": "#FFD39A",
|
||||
"session_border": "#6C4724",
|
||||
},
|
||||
"colors_light": {
|
||||
"banner_border": "#7A3511",
|
||||
"banner_title": "#5C2D00",
|
||||
"banner_accent": "#8B4000",
|
||||
"banner_dim": "#5A3A1A",
|
||||
"banner_text": "#3A1E00",
|
||||
"prompt": "#3A1E00",
|
||||
"ui_accent": "#8B4000",
|
||||
"ui_label": "#5C2D00",
|
||||
"ui_ok": "#1B5E20",
|
||||
"input_rule": "#7A3511",
|
||||
"response_border": "#8B4513",
|
||||
"session_label": "#5C2D00",
|
||||
"session_border": "#6B5540",
|
||||
},
|
||||
"spinner": {
|
||||
"waiting_faces": ["(✦)", "(▲)", "(◇)", "(<>)", "(🔥)"],
|
||||
"thinking_faces": ["(✦)", "(▲)", "(◇)", "(⌁)", "(🔥)"],
|
||||
@@ -623,8 +509,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
||||
|
||||
_active_skin: Optional[SkinConfig] = None
|
||||
_active_skin_name: str = "default"
|
||||
_theme_mode: str = "auto"
|
||||
_resolved_theme_mode: Optional[str] = None
|
||||
|
||||
|
||||
def _skins_dir() -> Path:
|
||||
@@ -652,8 +536,6 @@ def _build_skin_config(data: Dict[str, Any]) -> SkinConfig:
|
||||
default = _BUILTIN_SKINS["default"]
|
||||
colors = dict(default.get("colors", {}))
|
||||
colors.update(data.get("colors", {}))
|
||||
colors_light = dict(default.get("colors_light", {}))
|
||||
colors_light.update(data.get("colors_light", {}))
|
||||
spinner = dict(default.get("spinner", {}))
|
||||
spinner.update(data.get("spinner", {}))
|
||||
branding = dict(default.get("branding", {}))
|
||||
@@ -663,7 +545,6 @@ def _build_skin_config(data: Dict[str, Any]) -> SkinConfig:
|
||||
name=data.get("name", "unknown"),
|
||||
description=data.get("description", ""),
|
||||
colors=colors,
|
||||
colors_light=colors_light,
|
||||
spinner=spinner,
|
||||
branding=branding,
|
||||
tool_prefix=data.get("tool_prefix", default.get("tool_prefix", "┊")),
|
||||
@@ -744,39 +625,6 @@ def get_active_skin_name() -> str:
|
||||
return _active_skin_name
|
||||
|
||||
|
||||
def get_theme_mode() -> str:
|
||||
"""Return the resolved theme mode: "light" or "dark".
|
||||
|
||||
When ``_theme_mode`` is ``"auto"``, detection is attempted once and cached.
|
||||
If detection returns ``"unknown"``, defaults to ``"dark"``.
|
||||
"""
|
||||
global _resolved_theme_mode
|
||||
if _theme_mode in ("light", "dark"):
|
||||
return _theme_mode
|
||||
# Auto mode — detect and cache
|
||||
if _resolved_theme_mode is None:
|
||||
try:
|
||||
from hermes_cli.colors import detect_terminal_background
|
||||
detected = detect_terminal_background()
|
||||
except Exception:
|
||||
detected = "unknown"
|
||||
_resolved_theme_mode = detected if detected in ("light", "dark") else "dark"
|
||||
return _resolved_theme_mode
|
||||
|
||||
|
||||
def set_theme_mode(mode: str) -> None:
|
||||
"""Set the theme mode to "light", "dark", or "auto"."""
|
||||
global _theme_mode, _resolved_theme_mode
|
||||
_theme_mode = mode
|
||||
# Reset cached detection so it re-runs on next get_theme_mode() if auto
|
||||
_resolved_theme_mode = None
|
||||
|
||||
|
||||
def get_theme_mode_setting() -> str:
|
||||
"""Return the raw theme mode setting (may be "auto", "light", or "dark")."""
|
||||
return _theme_mode
|
||||
|
||||
|
||||
def init_skin_from_config(config: dict) -> None:
|
||||
"""Initialize the active skin from CLI config at startup.
|
||||
|
||||
@@ -789,13 +637,6 @@ def init_skin_from_config(config: dict) -> None:
|
||||
else:
|
||||
set_active_skin("default")
|
||||
|
||||
# Theme mode
|
||||
theme_mode = display.get("theme_mode", "auto")
|
||||
if isinstance(theme_mode, str) and theme_mode.strip():
|
||||
set_theme_mode(theme_mode.strip())
|
||||
else:
|
||||
set_theme_mode("auto")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Convenience helpers for CLI modules
|
||||
@@ -849,14 +690,6 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
|
||||
warn = skin.get_color("ui_warn", "#FF8C00")
|
||||
error = skin.get_color("ui_error", "#FF6B6B")
|
||||
|
||||
# Use lighter background colours for completion menus in light mode
|
||||
if get_theme_mode() == "light":
|
||||
menu_bg = "bg:#e8e8e8"
|
||||
menu_sel_bg = "bg:#d0d0d0"
|
||||
else:
|
||||
menu_bg = "bg:#1a1a2e"
|
||||
menu_sel_bg = "bg:#333355"
|
||||
|
||||
return {
|
||||
"input-area": prompt,
|
||||
"placeholder": f"{dim} italic",
|
||||
@@ -865,11 +698,11 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
|
||||
"hint": f"{dim} italic",
|
||||
"input-rule": input_rule,
|
||||
"image-badge": f"{label} bold",
|
||||
"completion-menu": f"{menu_bg} {text}",
|
||||
"completion-menu.completion": f"{menu_bg} {text}",
|
||||
"completion-menu.completion.current": f"{menu_sel_bg} {title}",
|
||||
"completion-menu.meta.completion": f"{menu_bg} {dim}",
|
||||
"completion-menu.meta.completion.current": f"{menu_sel_bg} {label}",
|
||||
"completion-menu": f"bg:#1a1a2e {text}",
|
||||
"completion-menu.completion": f"bg:#1a1a2e {text}",
|
||||
"completion-menu.completion.current": f"bg:#333355 {title}",
|
||||
"completion-menu.meta.completion": f"bg:#1a1a2e {dim}",
|
||||
"completion-menu.meta.completion.current": f"bg:#333355 {label}",
|
||||
"clarify-border": input_rule,
|
||||
"clarify-title": f"{title} bold",
|
||||
"clarify-question": f"{text} bold",
|
||||
|
||||
Reference in New Issue
Block a user