Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ These are **not bundled with `hermes-agent`**. The core repo ships only the plug
| [`plugin-llm-async-example`](./plugin-llm-async-example) | `ctx.llm.acomplete()` + `asyncio.gather()` | Async LLM lane — concurrent forward + sentiment + back-translation pass for `/translate` |
| [`example-dashboard`](./example-dashboard) | `dashboard/manifest.json` | Bare-minimum dashboard plugin — a tab, a slot injection, a backend route |
| [`strike-freedom-cockpit`](./strike-freedom-cockpit) | dashboard theme + slot plugin | Complete custom-skin reskin — palette, layout variant, asset slots, sidebar HUD |
| [`skin-studio`](./skin-studio) | desktop theme editor + dashboard backend | Full theme editor — wallpapers/video backdrops, extended palette, bold/typography, global FX layer (digit wall / hanzi rain / CRT scanlines), one-click apply via the native skins path |

## Installing an example as a user plugin

Expand All @@ -25,6 +26,8 @@ cp -r hermes-example-plugins/plugin-llm-example ~/.hermes/plugins/
cp -r hermes-example-plugins/plugin-llm-async-example ~/.hermes/plugins/
cp -r hermes-example-plugins/example-dashboard ~/.hermes/plugins/
cp -r hermes-example-plugins/strike-freedom-cockpit ~/.hermes/plugins/
cp -r hermes-example-plugins/skin-studio ~/.hermes/plugins/
cp -r hermes-example-plugins/skin-studio ~/.hermes/desktop-plugins/

# enable any with a slash command surface
hermes plugins enable plugin-llm-example
Expand Down
22 changes: 22 additions & 0 deletions skin-studio/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
MIT License

Copyright (c) 2026 Theme Forge contributors
Copyright (c) 2026 Skin Studio contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
115 changes: 115 additions & 0 deletions skin-studio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Skin Studio (皮肤工坊) — Dress up your Hermes Desktop

**A Hermes Desktop theme editor plugin: wallpaper / animated video backdrops, extended palette, text bold levels, chat font size, markdown accent colors, and animated rain backdrops (Matrix-style digit wall & traditional-Chinese-character rain).**

- ✅ No build step, no app-code changes — one ESM file + an optional Python backend
- ✅ **One-click apply** via Hermes' native skin path (`/skin`-equivalent hot path, repaints every surface in ~1s)
- ✅ **Live editing** — changes repaint the active theme immediately
- ✅ **15 preset themes**: 6 Chinese-style + 4 anime-style + 5 classics
- ✅ **Auto-color from image** (median-cut extraction + WCAG contrast guarantee) + 10 palette templates
- ✅ **Video media library**: large videos stored in IndexedDB, survive restarts, click-to-apply
- ✅ **Global FX layer**: rain effects decoupled from themes — pair the digit wall or hanzi rain with *any* color theme

> This plugin is a deep localization of the open-source [Theme Forge](https://github.com/NousResearch/hermes-example-plugins/pull/8) (MIT, by criptogus): fully Chinese UI, Chinese-style & anime-style themes, hanzi-rain FX, auto-color, video library, and the global FX layer.

## Included themes

### Chinese style

| Theme | Look | FX |
|---|---|---|
| **水墨 Ink** | Rice-paper white + ink black + cinnabar seal red | — |
| **青花瓷 Blue-and-white** | Glaze white + cobalt blue, oriental elegance | — |
| **故宫 Forbidden City** | Palace-wall red + glazed gold | — |
| **竹林 Bamboo** | Bamboo green + ink green, airy | — |
| **敦煌 Dunhuang** | Sand gold + mural ochre, Silk Road colors | — |
| **汉字雨 Hanzi Rain** | Ink-black paper + seal red | 🌧️ **Traditional-hanzi rain** (parchment-white drops + cinnabar accents) |

### Anime style

| Theme | Look | FX |
|---|---|---|
| **EVA 初号机 Unit-01** | Eva purple + phosphor green | — |
| **高达 Gundam** | RX-78 white/blue/red | — |
| **初音未来 Hatsune Miku** | Miku teal | — |
| **鬼灭之刃 Demon Slayer** | Tanjiro black-green check + nichirin red | — |

### Classics (inherited from Theme Forge)

| Theme | Look | FX |
|---|---|---|
| **赛博 Cyber** | Pure black + phosphor green + purple + gold | — |
| **玻璃 Glass** | Cool translucent neutrals + cyan | — |
| **纸感 Paper** | Warm paper light + terracotta | — |
| **数字墙 Digit Wall** | Pure black + phosphor green | 🌧️ **Matrix-style digit wall** (full-screen 0–9 grid, in-place refresh flow — no falling, no stacking, dim green so it never steals focus from text) |
| **黑客 Hacker** | Terminal green + purple + gold | 📺 CRT scanlines |
| **自定义 Custom** | Your editable theme (persists) | Everything adjustable |

## What it does that Hermes can't natively

| Feature | Why it doesn't exist natively |
|---|---|
| **Wallpaper / animated video backdrop** + overlay/blur | The `DesktopTheme` model only supports solid colors |
| **Extended palette** — `--ui-red/green/blue/purple/yellow/cyan/orange/warm` | Those colors are fixed in the app's `styles.css` |
| **Text bold levels** (3 levels) | No per-theme typography weight control |
| **Conversation font size** (12–48px for FX, 11–18px text) | Token exists but no UI |
| **Markdown accent colors** (headings, links, chat code) | Not configurable |
| **Digit wall / hanzi rain / CRT scanlines** | Impossible in the theme model |
| **One-click theme apply** | The desktop doesn't expose `setTheme` to plugins |

## Global FX layer (new in Skin Studio)

Rain effects are a **separate layer from color themes** — pick any theme and overlay any FX:

```
[跟随主题 Follow theme] [无 None] [数字墙 Digit wall] [汉字雨 Hanzi rain]
```

- `Follow theme` keeps each theme's built-in FX (classic behavior)
- `Digit wall` / `Hanzi rain` apply globally — switch color themes freely, the FX persists
- Font size & speed sliders tune the rain globally
- CRT scanlines get their own three-state switch (follow / on / off) and are **mutually exclusive with the digit wall** (a full-screen digit grid + horizontal lines reads as noise)
- FX config lives in its own storage key, themes stay clean

## Installation

### 1. Desktop plugin (required)

```bash
mkdir -p ~/.hermes/desktop-plugins
cp -R skin-studio ~/.hermes/desktop-plugins/
```

The desktop app hot-reloads the folder (or ⌘K → **Reload desktop plugins**).

### 2. Python backend (for one-click "Apply")

```bash
mkdir -p ~/.hermes/plugins/skin-studio
cp -R skin-studio/dashboard ~/.hermes/plugins/skin-studio/
```

Then make sure `skin-studio` is in `plugins.enabled` in `~/.hermes/config.yaml` (as a **YAML list**, not a stringified JSON string):

```yaml
plugins:
enabled:
- skin-studio
```

The backend writes `$HERMES_HOME/skins/<name>.yaml` and sets `display.skin`, so the gateway's skin watcher broadcasts `skin.changed` and every surface repaints live — no restart needed. Restart the app after installing the backend (routes mount at startup only).

Without the backend, themes still work: pick them via ⌘K → **Themes**.

## Usage

1. Open **皮肤工坊** from the sidebar (or ⌘K → "皮肤工坊")
2. **颜色** tab: hand-tune any color module (all Chinese labels), or click a palette template / auto-color from an image
3. **背景** tab: wallpaper URL / local image / local video (IndexedDB library keeps the last 5), overlay & blur
4. **文字** tab: bold levels, font size, markdown accents
5. **背景特效(全局层)**: pick the global FX layer & scanlines
6. Click **应用** to activate as the live Hermes skin (global — CLI/TUI repaint too)

## License

MIT — see [LICENSE](./LICENSE). Forked from [Theme Forge](https://github.com/NousResearch/hermes-example-plugins/pull/8) (MIT). Film/anime presets are palette homages with no official affiliation.
4 changes: 4 additions & 0 deletions skin-studio/dashboard/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "skin-studio",
"api": "plugin_api.py"
}
123 changes: 123 additions & 0 deletions skin-studio/dashboard/plugin_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""皮肤工坊(Skin Studio)backend — 把皮肤工坊主题应用为实时 Hermes 皮肤。

Rides the canonical skin path: writes ``$HERMES_HOME/skins/<name>.yaml`` and
sets ``display.skin`` so the gateway's skin watcher broadcasts ``skin.changed``
— the same live path the ``/skin`` command uses. Every Hermes surface (CLI,
TUI, desktop) repaints within ~1s, no restart needed.

Mounted at ``POST /api/plugins/skin-studio/activate`` by the desktop SDK's
plugin backend (``ctx.rest``). Only accepts a whitelisted subset of the
canonical skin tokens, validated as hex colors — never free-form YAML.

复刻自 Theme Forge(MIT),品牌与文档改为皮肤工坊。
"""

from __future__ import annotations

import argparse
import re
from pathlib import Path

import yaml
from fastapi import APIRouter

from hermes_constants import get_hermes_home

router = APIRouter()

_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
_HEX_RE = re.compile(r"^#[0-9a-fA-F]{6}$")

# Whitelisted canonical skin color tokens (see apps/shared/src/skin.ts).
_ALLOWED_TOKENS = {
"background",
"ui_text",
"ui_accent",
"ui_primary",
"ui_border",
"ui_error",
"banner_dim",
"banner_title",
"banner_text",
"status_bar_bg",
"completion_menu_bg",
"ui_ok",
"ui_warn",
}


def _skins_dir() -> Path:
return get_hermes_home() / "skins"


@router.post("/activate")
async def activate(body: dict):
name = str((body or {}).get("name") or "").strip()
if not _NAME_RE.match(name):
return {"ok": False, "error": "invalid skin name"}

raw_colors = (body or {}).get("colors") or {}
if not isinstance(raw_colors, dict):
return {"ok": False, "error": "colors must be an object"}

colors = {
key: value
for key, value in raw_colors.items()
if key in _ALLOWED_TOKENS and _HEX_RE.match(str(value))
}
if not colors:
return {"ok": False, "error": "no valid colors provided"}

label = str((body or {}).get("label") or name)
description = str((body or {}).get("description") or f"Forge theme {name}")

data = {
"name": name,
"description": description,
"colors": colors,
}

path = _skins_dir() / f"{name}.yaml"
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8")
except OSError as exc: # pragma: no cover - filesystem edge
return {"ok": False, "error": f"write failed: {exc}"}

try:
from hermes_cli.config import config_command

config_command(
argparse.Namespace(config_command="set", key="display.skin", value=name, force=True)
)
except Exception as exc: # pragma: no cover - surface backend failure to the UI
return {"ok": False, "error": f"config set failed: {exc}"}

return {"ok": True, "name": name, "label": label, "skin_path": str(path)}


@router.post("/reassert")
async def reassert(body: dict):
"""Re-assert the active forge skin after an app/backend restart.

The desktop applies a skin only when the gateway broadcasts ``skin.changed``
(it fires when ``display.skin`` or the skin YAML mtime changes). On boot the
desktop does NOT re-apply the configured skin, so after an update the theme
falls back to the app's local storage. Re-writing the same value bumps the
config mtime, which makes the gateway watcher broadcast again — every
surface re-applies the theme with no manual step.
"""
name = str((body or {}).get("name") or "").strip()
if not _NAME_RE.match(name):
return {"ok": False, "error": "invalid skin name"}
if not (_skins_dir() / f"{name}.yaml").exists():
return {"ok": False, "error": f"skin {name} not found — apply it from the editor first"}
try:
from hermes_cli.config import config_command

config_command(
argparse.Namespace(config_command="set", key="display.skin", value=name, force=True)
)
except Exception as exc: # pragma: no cover - surface backend failure to the UI
return {"ok": False, "error": f"config set failed: {exc}"}
return {"ok": True, "name": name, "reasserted": True}
Loading