Revert "feat(cli): skin-aware light/dark theme mode with terminal auto-detection"
This reverts commit a1c81360a5.
This commit is contained in:
1
cli.py
1
cli.py
@@ -219,7 +219,6 @@ def load_cli_config() -> Dict[str, Any]:
|
|||||||
"streaming": False,
|
"streaming": False,
|
||||||
|
|
||||||
"skin": "default",
|
"skin": "default",
|
||||||
"theme_mode": "auto",
|
|
||||||
},
|
},
|
||||||
"clarify": {
|
"clarify": {
|
||||||
"timeout": 120, # Seconds to wait for a clarify answer before auto-proceeding
|
"timeout": 120, # Seconds to wait for a clarify answer before auto-proceeding
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Shared ANSI color utilities for Hermes CLI modules."""
|
"""Shared ANSI color utilities for Hermes CLI modules."""
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
@@ -21,123 +20,3 @@ def color(text: str, *codes) -> str:
|
|||||||
if not sys.stdout.isatty():
|
if not sys.stdout.isatty():
|
||||||
return text
|
return text
|
||||||
return "".join(codes) + text + Colors.RESET
|
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,
|
"streaming": False,
|
||||||
"show_cost": False, # Show $ cost in the status bar (off by default)
|
"show_cost": False, # Show $ cost in the status bar (off by default)
|
||||||
"skin": "default",
|
"skin": "default",
|
||||||
"theme_mode": "auto",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
# Privacy settings
|
# Privacy settings
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ class SkinConfig:
|
|||||||
name: str
|
name: str
|
||||||
description: str = ""
|
description: str = ""
|
||||||
colors: Dict[str, str] = field(default_factory=dict)
|
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)
|
spinner: Dict[str, Any] = field(default_factory=dict)
|
||||||
branding: Dict[str, str] = field(default_factory=dict)
|
branding: Dict[str, str] = field(default_factory=dict)
|
||||||
tool_prefix: str = "┊"
|
tool_prefix: str = "┊"
|
||||||
@@ -123,12 +122,7 @@ class SkinConfig:
|
|||||||
banner_hero: str = "" # Rich-markup hero art (replaces HERMES_CADUCEUS)
|
banner_hero: str = "" # Rich-markup hero art (replaces HERMES_CADUCEUS)
|
||||||
|
|
||||||
def get_color(self, key: str, fallback: str = "") -> str:
|
def get_color(self, key: str, fallback: str = "") -> str:
|
||||||
"""Get a color value with fallback.
|
"""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]
|
|
||||||
return self.colors.get(key, fallback)
|
return self.colors.get(key, fallback)
|
||||||
|
|
||||||
def get_spinner_list(self, key: str) -> List[str]:
|
def get_spinner_list(self, key: str) -> List[str]:
|
||||||
@@ -174,21 +168,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
|||||||
"session_label": "#DAA520",
|
"session_label": "#DAA520",
|
||||||
"session_border": "#8B8682",
|
"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": {
|
"spinner": {
|
||||||
# Empty = use hardcoded defaults in display.py
|
# Empty = use hardcoded defaults in display.py
|
||||||
},
|
},
|
||||||
@@ -222,21 +201,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
|||||||
"session_label": "#C7A96B",
|
"session_label": "#C7A96B",
|
||||||
"session_border": "#6E584B",
|
"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": {
|
"spinner": {
|
||||||
"waiting_faces": ["(⚔)", "(⛨)", "(▲)", "(<>)", "(/)"],
|
"waiting_faces": ["(⚔)", "(⛨)", "(▲)", "(<>)", "(/)"],
|
||||||
"thinking_faces": ["(⚔)", "(⛨)", "(▲)", "(⌁)", "(<>)"],
|
"thinking_faces": ["(⚔)", "(⛨)", "(▲)", "(⌁)", "(<>)"],
|
||||||
@@ -301,22 +265,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
|||||||
"session_label": "#888888",
|
"session_label": "#888888",
|
||||||
"session_border": "#555555",
|
"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": {},
|
"spinner": {},
|
||||||
"branding": {
|
"branding": {
|
||||||
"agent_name": "Hermes Agent",
|
"agent_name": "Hermes Agent",
|
||||||
@@ -348,21 +296,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
|||||||
"session_label": "#7eb8f6",
|
"session_label": "#7eb8f6",
|
||||||
"session_border": "#4b5563",
|
"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": {},
|
"spinner": {},
|
||||||
"branding": {
|
"branding": {
|
||||||
"agent_name": "Hermes Agent",
|
"agent_name": "Hermes Agent",
|
||||||
@@ -394,21 +327,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
|||||||
"session_label": "#A9DFFF",
|
"session_label": "#A9DFFF",
|
||||||
"session_border": "#496884",
|
"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": {
|
"spinner": {
|
||||||
"waiting_faces": ["(≈)", "(Ψ)", "(∿)", "(◌)", "(◠)"],
|
"waiting_faces": ["(≈)", "(Ψ)", "(∿)", "(◌)", "(◠)"],
|
||||||
"thinking_faces": ["(Ψ)", "(∿)", "(≈)", "(⌁)", "(◌)"],
|
"thinking_faces": ["(Ψ)", "(∿)", "(≈)", "(⌁)", "(◌)"],
|
||||||
@@ -473,23 +391,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
|||||||
"session_label": "#919191",
|
"session_label": "#919191",
|
||||||
"session_border": "#656565",
|
"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": {
|
"spinner": {
|
||||||
"waiting_faces": ["(◉)", "(◌)", "(◬)", "(⬤)", "(::)"],
|
"waiting_faces": ["(◉)", "(◌)", "(◬)", "(⬤)", "(::)"],
|
||||||
"thinking_faces": ["(◉)", "(◬)", "(◌)", "(○)", "(●)"],
|
"thinking_faces": ["(◉)", "(◬)", "(◌)", "(○)", "(●)"],
|
||||||
@@ -555,21 +456,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
|||||||
"session_label": "#FFD39A",
|
"session_label": "#FFD39A",
|
||||||
"session_border": "#6C4724",
|
"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": {
|
"spinner": {
|
||||||
"waiting_faces": ["(✦)", "(▲)", "(◇)", "(<>)", "(🔥)"],
|
"waiting_faces": ["(✦)", "(▲)", "(◇)", "(<>)", "(🔥)"],
|
||||||
"thinking_faces": ["(✦)", "(▲)", "(◇)", "(⌁)", "(🔥)"],
|
"thinking_faces": ["(✦)", "(▲)", "(◇)", "(⌁)", "(🔥)"],
|
||||||
@@ -623,8 +509,6 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
|
|||||||
|
|
||||||
_active_skin: Optional[SkinConfig] = None
|
_active_skin: Optional[SkinConfig] = None
|
||||||
_active_skin_name: str = "default"
|
_active_skin_name: str = "default"
|
||||||
_theme_mode: str = "auto"
|
|
||||||
_resolved_theme_mode: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
def _skins_dir() -> Path:
|
def _skins_dir() -> Path:
|
||||||
@@ -652,8 +536,6 @@ def _build_skin_config(data: Dict[str, Any]) -> SkinConfig:
|
|||||||
default = _BUILTIN_SKINS["default"]
|
default = _BUILTIN_SKINS["default"]
|
||||||
colors = dict(default.get("colors", {}))
|
colors = dict(default.get("colors", {}))
|
||||||
colors.update(data.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 = dict(default.get("spinner", {}))
|
||||||
spinner.update(data.get("spinner", {}))
|
spinner.update(data.get("spinner", {}))
|
||||||
branding = dict(default.get("branding", {}))
|
branding = dict(default.get("branding", {}))
|
||||||
@@ -663,7 +545,6 @@ def _build_skin_config(data: Dict[str, Any]) -> SkinConfig:
|
|||||||
name=data.get("name", "unknown"),
|
name=data.get("name", "unknown"),
|
||||||
description=data.get("description", ""),
|
description=data.get("description", ""),
|
||||||
colors=colors,
|
colors=colors,
|
||||||
colors_light=colors_light,
|
|
||||||
spinner=spinner,
|
spinner=spinner,
|
||||||
branding=branding,
|
branding=branding,
|
||||||
tool_prefix=data.get("tool_prefix", default.get("tool_prefix", "┊")),
|
tool_prefix=data.get("tool_prefix", default.get("tool_prefix", "┊")),
|
||||||
@@ -744,39 +625,6 @@ def get_active_skin_name() -> str:
|
|||||||
return _active_skin_name
|
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:
|
def init_skin_from_config(config: dict) -> None:
|
||||||
"""Initialize the active skin from CLI config at startup.
|
"""Initialize the active skin from CLI config at startup.
|
||||||
|
|
||||||
@@ -789,13 +637,6 @@ def init_skin_from_config(config: dict) -> None:
|
|||||||
else:
|
else:
|
||||||
set_active_skin("default")
|
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
|
# 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")
|
warn = skin.get_color("ui_warn", "#FF8C00")
|
||||||
error = skin.get_color("ui_error", "#FF6B6B")
|
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 {
|
return {
|
||||||
"input-area": prompt,
|
"input-area": prompt,
|
||||||
"placeholder": f"{dim} italic",
|
"placeholder": f"{dim} italic",
|
||||||
@@ -865,11 +698,11 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
|
|||||||
"hint": f"{dim} italic",
|
"hint": f"{dim} italic",
|
||||||
"input-rule": input_rule,
|
"input-rule": input_rule,
|
||||||
"image-badge": f"{label} bold",
|
"image-badge": f"{label} bold",
|
||||||
"completion-menu": f"{menu_bg} {text}",
|
"completion-menu": f"bg:#1a1a2e {text}",
|
||||||
"completion-menu.completion": f"{menu_bg} {text}",
|
"completion-menu.completion": f"bg:#1a1a2e {text}",
|
||||||
"completion-menu.completion.current": f"{menu_sel_bg} {title}",
|
"completion-menu.completion.current": f"bg:#333355 {title}",
|
||||||
"completion-menu.meta.completion": f"{menu_bg} {dim}",
|
"completion-menu.meta.completion": f"bg:#1a1a2e {dim}",
|
||||||
"completion-menu.meta.completion.current": f"{menu_sel_bg} {label}",
|
"completion-menu.meta.completion.current": f"bg:#333355 {label}",
|
||||||
"clarify-border": input_rule,
|
"clarify-border": input_rule,
|
||||||
"clarify-title": f"{title} bold",
|
"clarify-title": f"{title} bold",
|
||||||
"clarify-question": f"{text} bold",
|
"clarify-question": f"{text} bold",
|
||||||
|
|||||||
@@ -13,13 +13,9 @@ def reset_skin_state():
|
|||||||
from hermes_cli import skin_engine
|
from hermes_cli import skin_engine
|
||||||
skin_engine._active_skin = None
|
skin_engine._active_skin = None
|
||||||
skin_engine._active_skin_name = "default"
|
skin_engine._active_skin_name = "default"
|
||||||
skin_engine._theme_mode = "auto"
|
|
||||||
skin_engine._resolved_theme_mode = None
|
|
||||||
yield
|
yield
|
||||||
skin_engine._active_skin = None
|
skin_engine._active_skin = None
|
||||||
skin_engine._active_skin_name = "default"
|
skin_engine._active_skin_name = "default"
|
||||||
skin_engine._theme_mode = "auto"
|
|
||||||
skin_engine._resolved_theme_mode = None
|
|
||||||
|
|
||||||
|
|
||||||
class TestSkinConfig:
|
class TestSkinConfig:
|
||||||
@@ -316,65 +312,3 @@ class TestCliBrandingHelpers:
|
|||||||
assert overrides["clarify-title"] == f"{skin.get_color('banner_title')} bold"
|
assert overrides["clarify-title"] == f"{skin.get_color('banner_title')} bold"
|
||||||
assert overrides["sudo-prompt"] == f"{skin.get_color('ui_error')} bold"
|
assert overrides["sudo-prompt"] == f"{skin.get_color('ui_error')} bold"
|
||||||
assert overrides["approval-title"] == f"{skin.get_color('ui_warn')} bold"
|
assert overrides["approval-title"] == f"{skin.get_color('ui_warn')} bold"
|
||||||
|
|
||||||
|
|
||||||
class TestThemeMode:
|
|
||||||
def test_get_theme_mode_defaults_to_dark_on_unknown(self):
|
|
||||||
from hermes_cli.skin_engine import get_theme_mode, set_theme_mode
|
|
||||||
|
|
||||||
set_theme_mode("auto")
|
|
||||||
# In a test env, detection returns "unknown" → defaults to "dark"
|
|
||||||
with patch("hermes_cli.colors.detect_terminal_background", return_value="unknown"):
|
|
||||||
from hermes_cli import skin_engine
|
|
||||||
skin_engine._resolved_theme_mode = None # force re-detection
|
|
||||||
assert get_theme_mode() == "dark"
|
|
||||||
|
|
||||||
def test_set_theme_mode_light(self):
|
|
||||||
from hermes_cli.skin_engine import get_theme_mode, set_theme_mode
|
|
||||||
|
|
||||||
set_theme_mode("light")
|
|
||||||
assert get_theme_mode() == "light"
|
|
||||||
|
|
||||||
def test_set_theme_mode_dark(self):
|
|
||||||
from hermes_cli.skin_engine import get_theme_mode, set_theme_mode
|
|
||||||
|
|
||||||
set_theme_mode("dark")
|
|
||||||
assert get_theme_mode() == "dark"
|
|
||||||
|
|
||||||
def test_get_color_respects_light_mode(self):
|
|
||||||
from hermes_cli.skin_engine import SkinConfig, set_theme_mode
|
|
||||||
|
|
||||||
skin = SkinConfig(
|
|
||||||
name="test",
|
|
||||||
colors={"banner_title": "#FFD700", "prompt": "#FFF8DC"},
|
|
||||||
colors_light={"banner_title": "#6B4C00"},
|
|
||||||
)
|
|
||||||
set_theme_mode("light")
|
|
||||||
assert skin.get_color("banner_title") == "#6B4C00"
|
|
||||||
# Key not in colors_light falls back to colors
|
|
||||||
assert skin.get_color("prompt") == "#FFF8DC"
|
|
||||||
|
|
||||||
def test_get_color_falls_back_in_dark_mode(self):
|
|
||||||
from hermes_cli.skin_engine import SkinConfig, set_theme_mode
|
|
||||||
|
|
||||||
skin = SkinConfig(
|
|
||||||
name="test",
|
|
||||||
colors={"banner_title": "#FFD700", "prompt": "#FFF8DC"},
|
|
||||||
colors_light={"banner_title": "#6B4C00"},
|
|
||||||
)
|
|
||||||
set_theme_mode("dark")
|
|
||||||
assert skin.get_color("banner_title") == "#FFD700"
|
|
||||||
assert skin.get_color("prompt") == "#FFF8DC"
|
|
||||||
|
|
||||||
def test_init_skin_from_config_reads_theme_mode(self):
|
|
||||||
from hermes_cli.skin_engine import init_skin_from_config, get_theme_mode_setting
|
|
||||||
|
|
||||||
init_skin_from_config({"display": {"skin": "default", "theme_mode": "light"}})
|
|
||||||
assert get_theme_mode_setting() == "light"
|
|
||||||
|
|
||||||
def test_builtin_skins_have_colors_light(self):
|
|
||||||
from hermes_cli.skin_engine import _BUILTIN_SKINS, _build_skin_config
|
|
||||||
|
|
||||||
for name, data in _BUILTIN_SKINS.items():
|
|
||||||
skin = _build_skin_config(data)
|
|
||||||
assert len(skin.colors_light) > 0, f"Skin '{name}' has empty colors_light"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user