feat(skins): add built-in daylight skin

This commit is contained in:
Liu Chongwei
2026-04-14 11:59:24 +08:00
committed by Teknium
parent a2ea237db2
commit bc93641c4f
5 changed files with 129 additions and 15 deletions

24
cli.py
View File

@@ -988,19 +988,19 @@ def _prune_orphaned_branches(repo_root: str) -> None:
# ANSI building blocks for conversation display
_ACCENT_ANSI_DEFAULT = "\033[1;38;2;255;215;0m" # True-color #FFD700 bold — fallback
_BOLD = "\033[1m"
_DIM = "\033[2m"
_RST = "\033[0m"
def _hex_to_ansi_bold(hex_color: str) -> str:
"""Convert a hex color like '#268bd2' to a bold true-color ANSI escape."""
def _hex_to_ansi(hex_color: str, *, bold: bool = False) -> str:
"""Convert a hex color like '#268bd2' to a true-color ANSI escape."""
try:
r = int(hex_color[1:3], 16)
g = int(hex_color[3:5], 16)
b = int(hex_color[5:7], 16)
return f"\033[1;38;2;{r};{g};{b}m"
prefix = "1;" if bold else ""
return f"\033[{prefix}38;2;{r};{g};{b}m"
except (ValueError, IndexError):
return _ACCENT_ANSI_DEFAULT
return _ACCENT_ANSI_DEFAULT if bold else "\033[38;2;184;134;11m"
class _SkinAwareAnsi:
@@ -1010,20 +1010,22 @@ class _SkinAwareAnsi:
force re-resolution after a ``/skin`` switch.
"""
def __init__(self, skin_key: str, fallback_hex: str = "#FFD700"):
def __init__(self, skin_key: str, fallback_hex: str = "#FFD700", *, bold: bool = False):
self._skin_key = skin_key
self._fallback_hex = fallback_hex
self._bold = bold
self._cached: str | None = None
def __str__(self) -> str:
if self._cached is None:
try:
from hermes_cli.skin_engine import get_active_skin
self._cached = _hex_to_ansi_bold(
get_active_skin().get_color(self._skin_key, self._fallback_hex)
self._cached = _hex_to_ansi(
get_active_skin().get_color(self._skin_key, self._fallback_hex),
bold=self._bold,
)
except Exception:
self._cached = _hex_to_ansi_bold(self._fallback_hex)
self._cached = _hex_to_ansi(self._fallback_hex, bold=self._bold)
return self._cached
def __add__(self, other: str) -> str:
@@ -1037,7 +1039,8 @@ class _SkinAwareAnsi:
self._cached = None
_ACCENT = _SkinAwareAnsi("response_border", "#FFD700")
_ACCENT = _SkinAwareAnsi("response_border", "#FFD700", bold=True)
_DIM = _SkinAwareAnsi("banner_dim", "#B8860B")
def _accent_hex() -> str:
@@ -6156,6 +6159,7 @@ class HermesCLI:
set_active_skin(new_skin)
_ACCENT.reset() # Re-resolve ANSI color for the new skin
_DIM.reset() # Re-resolve dim/secondary ANSI color for the new skin
if save_config_value("display.skin", new_skin):
print(f" Skin set to: {new_skin} (saved)")
else:

View File

@@ -41,6 +41,14 @@ colors:
session_label: "#DAA520" # Session label
session_border: "#8B8682" # Session ID dim color
# TUI surfaces
status_bar_bg: "#1a1a2e" # Status / usage bar background
voice_status_bg: "#1a1a2e" # Voice-mode badge background
completion_menu_bg: "#1a1a2e" # Completion list background
completion_menu_current_bg: "#333355" # Active completion row background
completion_menu_meta_bg: "#1a1a2e" # Completion meta column background
completion_menu_meta_current_bg: "#333355" # Active completion meta background
# ── Spinner ─────────────────────────────────────────────────────────────────
# Customize the animated spinner shown during API calls and tool execution.
spinner:

View File

@@ -32,6 +32,12 @@ All fields are optional. Missing values inherit from the ``default`` skin.
response_border: "#FFD700" # Response box border (ANSI)
session_label: "#DAA520" # Session label color
session_border: "#8B8682" # Session ID dim color
status_bar_bg: "#1a1a2e" # TUI status/usage bar background
voice_status_bg: "#1a1a2e" # TUI voice status background
completion_menu_bg: "#1a1a2e" # Completion menu background
completion_menu_current_bg: "#333355" # Active completion row background
completion_menu_meta_bg: "#1a1a2e" # Completion meta column background
completion_menu_meta_current_bg: "#333355" # Active completion meta background
# Spinner: customize the animated spinner during API calls
spinner:
@@ -87,6 +93,7 @@ BUILT-IN SKINS
- ``ares`` — Crimson/bronze war-god theme with custom spinner wings
- ``mono`` — Clean grayscale monochrome
- ``slate`` — Cool blue developer-focused theme
- ``daylight`` — Light background theme with dark text and blue accents
USER SKINS
==========
@@ -304,6 +311,43 @@ _BUILTIN_SKINS: Dict[str, Dict[str, Any]] = {
},
"tool_prefix": "",
},
"daylight": {
"name": "daylight",
"description": "Light theme for bright terminals with dark text and cool blue accents",
"colors": {
"banner_border": "#2563EB",
"banner_title": "#0F172A",
"banner_accent": "#1D4ED8",
"banner_dim": "#475569",
"banner_text": "#111827",
"ui_accent": "#2563EB",
"ui_label": "#0F766E",
"ui_ok": "#15803D",
"ui_error": "#B91C1C",
"ui_warn": "#B45309",
"prompt": "#111827",
"input_rule": "#93C5FD",
"response_border": "#2563EB",
"session_label": "#1D4ED8",
"session_border": "#64748B",
"status_bar_bg": "#E5EDF8",
"voice_status_bg": "#E5EDF8",
"completion_menu_bg": "#F8FAFC",
"completion_menu_current_bg": "#DBEAFE",
"completion_menu_meta_bg": "#EEF2FF",
"completion_menu_meta_current_bg": "#BFDBFE",
},
"spinner": {},
"branding": {
"agent_name": "Hermes Agent",
"welcome": "Welcome to Hermes Agent! Type your message or /help for commands.",
"goodbye": "Goodbye! ⚕",
"response_label": " ⚕ Hermes ",
"prompt_symbol": " ",
"help_header": "[?] Available Commands",
},
"tool_prefix": "",
},
"poseidon": {
"name": "poseidon",
"description": "Ocean-god theme — deep blue and seafoam",
@@ -685,6 +729,12 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
label = skin.get_color("ui_label", title)
warn = skin.get_color("ui_warn", "#FF8C00")
error = skin.get_color("ui_error", "#FF6B6B")
status_bg = skin.get_color("status_bar_bg", "#1a1a2e")
voice_bg = skin.get_color("voice_status_bg", status_bg)
menu_bg = skin.get_color("completion_menu_bg", "#1a1a2e")
menu_current_bg = skin.get_color("completion_menu_current_bg", "#333355")
menu_meta_bg = skin.get_color("completion_menu_meta_bg", menu_bg)
menu_meta_current_bg = skin.get_color("completion_menu_meta_current_bg", menu_current_bg)
return {
"input-area": prompt,
@@ -692,13 +742,20 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
"prompt": prompt,
"prompt-working": f"{dim} italic",
"hint": f"{dim} italic",
"status-bar": f"bg:{status_bg} {text}",
"status-bar-strong": f"bg:{status_bg} {title} bold",
"status-bar-dim": f"bg:{status_bg} {dim}",
"status-bar-good": f"bg:{status_bg} {skin.get_color('ui_ok', '#8FBC8F')} bold",
"status-bar-warn": f"bg:{status_bg} {warn} bold",
"status-bar-bad": f"bg:{status_bg} {skin.get_color('banner_accent', warn)} bold",
"status-bar-critical": f"bg:{status_bg} {error} bold",
"input-rule": input_rule,
"image-badge": f"{label} bold",
"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}",
"completion-menu": f"bg:{menu_bg} {text}",
"completion-menu.completion": f"bg:{menu_bg} {text}",
"completion-menu.completion.current": f"bg:{menu_current_bg} {title}",
"completion-menu.meta.completion": f"bg:{menu_meta_bg} {dim}",
"completion-menu.meta.completion.current": f"bg:{menu_meta_current_bg} {label}",
"clarify-border": input_rule,
"clarify-title": f"{title} bold",
"clarify-question": f"{text} bold",
@@ -716,4 +773,6 @@ def get_prompt_toolkit_style_overrides() -> Dict[str, str]:
"approval-cmd": f"{dim} italic",
"approval-choice": dim,
"approval-selected": f"{title} bold",
"voice-status": f"bg:{voice_bg} {label}",
"voice-status-recording": f"bg:{voice_bg} {error} bold",
}

View File

@@ -78,6 +78,20 @@ class TestBuiltinSkins:
assert skin.name == "slate"
assert skin.get_color("banner_title") == "#7eb8f6"
def test_daylight_skin_loads(self):
from hermes_cli.skin_engine import load_skin
skin = load_skin("daylight")
assert skin.name == "daylight"
assert skin.tool_prefix == ""
assert skin.get_color("banner_title") == "#0F172A"
assert skin.get_color("status_bar_bg") == "#E5EDF8"
assert skin.get_color("voice_status_bg") == "#E5EDF8"
assert skin.get_color("completion_menu_bg") == "#F8FAFC"
assert skin.get_color("completion_menu_current_bg") == "#DBEAFE"
assert skin.get_color("completion_menu_meta_bg") == "#EEF2FF"
assert skin.get_color("completion_menu_meta_current_bg") == "#BFDBFE"
def test_unknown_skin_falls_back_to_default(self):
from hermes_cli.skin_engine import load_skin
skin = load_skin("nonexistent_skin_xyz")
@@ -114,6 +128,7 @@ class TestSkinManagement:
assert "ares" in names
assert "mono" in names
assert "slate" in names
assert "daylight" in names
for s in skins:
assert "source" in s
assert s["source"] == "builtin"
@@ -242,6 +257,15 @@ class TestCliBrandingHelpers:
"completion-menu.completion.current",
"completion-menu.meta.completion",
"completion-menu.meta.completion.current",
"status-bar",
"status-bar-strong",
"status-bar-dim",
"status-bar-good",
"status-bar-warn",
"status-bar-bad",
"status-bar-critical",
"voice-status",
"voice-status-recording",
"clarify-border",
"clarify-title",
"clarify-question",
@@ -277,3 +301,9 @@ class TestCliBrandingHelpers:
assert overrides["clarify-title"] == f"{skin.get_color('banner_title')} bold"
assert overrides["sudo-prompt"] == f"{skin.get_color('ui_error')} bold"
assert overrides["approval-title"] == f"{skin.get_color('ui_warn')} bold"
set_active_skin("daylight")
skin = get_active_skin()
overrides = get_prompt_toolkit_style_overrides()
assert overrides["status-bar"] == f"bg:{skin.get_color('status_bar_bg')} {skin.get_color('banner_text')}"
assert overrides["voice-status"] == f"bg:{skin.get_color('voice_status_bg')} {skin.get_color('ui_label')}"

View File

@@ -36,6 +36,7 @@ display:
| `ares` | War-god theme — crimson and bronze | `Ares Agent` | Deep crimson borders with bronze accents. Aggressive spinner verbs ("forging", "marching", "tempering steel"). Custom sword-and-shield ASCII art banner. |
| `mono` | Monochrome — clean grayscale | `Hermes Agent` | All grays — no color. Borders are `#555555`, text is `#c9d1d9`. Ideal for minimal terminal setups or screen recordings. |
| `slate` | Cool blue — developer-focused | `Hermes Agent` | Royal blue borders (`#4169e1`), soft blue text. Calm and professional. No custom spinner — uses default faces. |
| `daylight` | Light theme for bright terminals with dark text and cool blue accents | `Hermes Agent` | Designed for white or bright terminals. Dark slate text with blue borders, pale status surfaces, and a light completion menu that stays readable in light terminal profiles. |
| `poseidon` | Ocean-god theme — deep blue and seafoam | `Poseidon Agent` | Deep blue to seafoam gradient. Ocean-themed spinners ("charting currents", "sounding the depth"). Trident ASCII art banner. |
| `sisyphus` | Sisyphean theme — austere grayscale with persistence | `Sisyphus Agent` | Light grays with stark contrast. Boulder-themed spinners ("pushing uphill", "resetting the boulder", "enduring the loop"). Boulder-and-hill ASCII art banner. |
| `charizard` | Volcanic theme — burnt orange and ember | `Charizard Agent` | Warm burnt orange to ember gradient. Fire-themed spinners ("banking into the draft", "measuring burn"). Dragon-silhouette ASCII art banner. |
@@ -63,6 +64,12 @@ Controls all color values throughout the CLI. Values are hex color strings.
| `response_border` | Border around the agent's response box (ANSI escape) | `#FFD700` |
| `session_label` | Session label color | `#DAA520` |
| `session_border` | Session ID dim border color | `#8B8682` |
| `status_bar_bg` | Background color for the TUI status / usage bar | `#1a1a2e` |
| `voice_status_bg` | Background color for the voice-mode status badge | `#1a1a2e` |
| `completion_menu_bg` | Background color for the completion menu list | `#1a1a2e` |
| `completion_menu_current_bg` | Background color for the active completion row | `#333355` |
| `completion_menu_meta_bg` | Background color for the completion meta column | `#1a1a2e` |
| `completion_menu_meta_current_bg` | Background color for the active completion meta column | `#333355` |
### Spinner (`spinner:`)
@@ -129,6 +136,12 @@ colors:
response_border: "#FFD700"
session_label: "#DAA520"
session_border: "#8B8682"
status_bar_bg: "#1a1a2e"
voice_status_bg: "#1a1a2e"
completion_menu_bg: "#1a1a2e"
completion_menu_current_bg: "#333355"
completion_menu_meta_bg: "#1a1a2e"
completion_menu_meta_current_bg: "#333355"
spinner:
waiting_faces: