The real defect first. RefreshDatabaseMetadata was the one worker of six that never took the shared lock, and its flag was the one of six missing from the tab's busy state. It calls MessageCount, which holds the read lock, so a wipe could start while it was in there -- and the tab would not have known to grey the button, because it could not see the worker. Both halves fixed. The pattern is why: seven near-copies of one worker skeleton, and each copy decided something slightly different. The clear button failed silently when its thread could not start. The most destructive control in the plugin, pressed, and nothing happens, with no way to tell that from a wipe that worked -- while the three harmless workers beside it do report. Maintenance was the mirror: its comment promises refusals are said out loud, and then swallowed the actual failure. Three start-failure paths also bypassed the notify helper that carries the teardown check, three weeks after it was added for exactly that. The database numbers now wait for a real read, like the clear hint already did. Zero bytes and zero messages read as an empty database, not as a number nobody has fetched. SelectionAfterDelete is gone, with its three tests. The accordion has no selection, so its return value went into a discard -- a function answering a question the interface does not ask, with green tests guarding nothing. The project's own self-test README calls that the anti-pattern of record. Six new keys replaced by the translated orphans that already said the same thing. A commit earlier in this cycle is literally called "stop duplicating a key" and these went past it. The duplicate button also had the label "Add", which is the one string out of ninety-four that was never written. Tests: CleanupDeleteTypes had none, and with the failsafe on -- how a fresh config ships -- it is the path every cleanup takes. Four now, including the one that matters: an empty list deletes nothing rather than everything. And a self-test for the gate wiring, which is what would have caught the metadata worker. The unit tests prove the gate works; nothing proved the workers use it.
11 KiB
Theme Authoring Guide
Built by Hellion Forge — the plugin workshop arm of Hellion Online Media. HellionChat ships with nine built-in themes; this guide walks you through writing your own.
TL;DR
- Open Settings → Appearance → Open themes folder
- Copy
example-theme.jsonto<your-name>.jsonin the same folder - Edit the file with any text editor
- Reload the plugin (toggle off/on in
/xlplugins) - Your theme appears in the Custom-Themes section in Settings → Appearance
That's the whole loop. The rest of this document is reference.
File location
%APPDATA%\XIVLauncher\pluginConfigs\HellionChat\themes\
(or the equivalent path on Linux/macOS — Settings → Appearance → "Open themes folder" opens it directly).
Each *.json file in this folder is loaded as one theme. The example-theme.json that HellionChat
seeds on first launch is your starting template.
File format
Theme JSON has four blocks:
{
"schemaVersion": 1,
"slug": "your-slug",
"name": "Your Theme Name",
"author": "You",
"description": "One-line description shown under the theme name.",
"colors": { ... 21 color slots ... },
"layout": { ... 9 layout values ... },
"chatChannels": { ... optional, channel-name → hex ... }
}
Top-level fields
| Field | Type | Required | Notes |
|---|---|---|---|
schemaVersion |
int | yes | Always 1 for HellionChat 1.1.0. The plugin warns and skips themes with a different number. |
slug |
string | yes | Lowercase, hyphenated. Must be unique across all themes (built-in slugs are reserved). |
name |
string | yes | Display name in the picker. |
author |
string | yes | Shown small under the theme name. |
description |
string | yes | One short sentence. |
colors |
object | yes | All 21 slots required (see below). |
layout |
object | yes | All 9 slots required (see below). |
chatChannels |
object | no | Optional channel-name → hex map (see below). |
Color slots
All values are 6-digit #RRGGBB or 8-digit #RRGGBBAA hex strings. Six-digit values get an
implicit FF alpha.
| Slot | Role |
|---|---|
primary |
Brand color — used on buttons, sliders, check marks, highlighted separators. |
primaryDark |
Pressed-button stage. |
primaryLight |
Hovered-button / link-text stage. |
primaryGlow |
Glow / subtle accent (typically primary with ~60% alpha). |
accent |
Counter-accent — scrollbar grab on hover/active, resize grip, optional CTA. |
accentDark / accentLight |
Dark/light siblings of accent. |
identity |
Title-bar active color and active-tab color. Often equals primaryDark. |
windowBg |
Outermost window background. |
childBg |
Inner panel / popup background. |
frameBg |
Input fields, sliders, combos. |
surface |
Card surfaces, headers, selectables. |
surfaceHover |
Hovered card / header step. |
border |
Panel borders. Typically primary with ~40% alpha for a brand-tinted edge. |
textPrimary |
Body text. Soft off-white reads better than pure #FFFFFF on dark backgrounds. |
textMuted |
Captions, secondary lines. |
textDim |
Disabled / hint text, separators. |
statusSuccess |
Green-ish for success notifications. |
statusDanger |
Red for errors. |
statusWarning |
Amber for warnings. |
statusInfo |
Cyan-ish info. Often equals primary. |
Layout slots
All values are floats in pixels. BorderSize is 0 or 1 (no thicker borders look right with ImGui's
edge anti-aliasing).
| Slot | Typical range | Notes |
|---|---|---|
windowRounding |
0–8 | 0 = sharp upstream look; 4–6 = softer "app" feel. |
childRounding |
0–6 | Usually 1 less than windowRounding. |
popupRounding |
0–6 | Same as childRounding. |
frameRounding |
0–4 | For inputs, sliders. |
grabRounding |
0–4 | Slider grab dot. |
tabRounding |
0–4 | Tab corners. |
scrollbarRounding |
0–4 | Scrollbar grab. |
windowBorderSize |
0 or 1 | 1 reads better in dark themes. |
frameBorderSize |
0 or 1 | Usually matches windowBorderSize. |
Optional chatChannels
If present, your theme proposes its own chat-channel colors. Property names are ChatType enum
values (case-insensitive). Unknown names are skipped silently — safe for forward-compat.
"chatChannels": {
"Say": "#FFFFFF",
"Yell": "#FFE066",
"Shout": "#FFA040",
"TellIncoming": "#FF99CC",
"TellOutgoing": "#FF99CC",
"Party": "#80C0E8",
"FreeCompany": "#4DD9E8",
"NoviceNetwork": "#A8E060",
"Linkshell1": "#A8E060"
}
The user is asked once per theme switch whether to apply these colors — never auto-overwriting
existing picks. The banner shows up only if your suggested colors differ from the user's current
Configuration.ChatColours.
Channel-identity rule
Don't break FFXIV channel identity. Players have used these conventions for over a decade:
| Channel | Convention | Why |
|---|---|---|
| Say | white / off-white | Default-readable speech. |
| Yell | yellow | Urgent broadcast. |
| Shout | orange | Local urgent. |
| Tell | pink-magenta | Whisper, must stand out. |
| Party | light blue | Group ops. |
| FreeCompany | cyan-teal | Guild ops. |
| NoviceNetwork | lime-green | Mentor channel. |
A theme can tint these toward its brand family (e.g., a purple theme can shift Tell from #FF99CC
to #E090FF), but don't flip them (Tell suddenly green, Yell suddenly cyan). RP groups and
combat-spec setups depend on the visual hierarchy.
The eight colored built-in themes (Hellion Arctic, Hellion Spectrum, Event Horizon, Crystal
Nocturne, Mint Grove, Night Blue, Indigo Violet, Forge Merchantman) all follow this rule — read
their source for reference. Chat 2 Klassik intentionally ships without chatChannels so the user
keeps their existing picks.
Theme families
Naming convention <color>-<modifier> is recommended for theme families. The first member of a
family is the lightest/brightest:
mint-grove(current built-in, light mint)forest-grove(planned, dark emerald)moss-grove(planned, mid muted)
Code-wise families have no special handling — only the slug naming hints at the relationship. The picker may group families later, but that's not required.
Validation and errors
When HellionChat loads your theme:
- Schema mismatch (
schemaVersion != 1): theme is skipped, warning written to/xllog. - Missing required field (e.g., no
slug): theme is skipped, warning written. - Invalid hex (e.g.,
#GGHHII): theme is skipped, warning written. - Unknown channel name in
chatChannels: that one channel is skipped silently, the rest of the theme loads normally.
Check /xllog after a plugin reload to see what loaded and what didn't.
Testing your theme
- Edit the JSON, save the file.
- Reload the plugin:
/xlplugins→ toggle HellionChat off, then on. - Settings → Appearance → click your theme card.
- Watch every plugin window (chat, settings, pop-out) and pick something to fix.
- Tweak. Reload. Repeat.
Tip: the Settings → Appearance picker shows a mini-mockup per theme — your colors are visible before you switch.
Sharing themes
Themes are JSON, so sharing is just a file. Drop it into someone's
pluginConfigs/HellionChat/themes/ folder and their plugin picks it up on next reload.
A community theme repository is on the Hellion Forge roadmap. Until then: share via Discord or any pastebin.
Reference
docs/example-theme.json(seeded automatically on first launch intopluginConfigs/HellionChat/themes/) — minimal valid theme.- The five built-in themes live in source under
HellionChat/Themes/Builtin/. They are a good reference for Color choices that work. - Hellion Online Media branding — the Arctic Cyan + Ember Glow palette that drives the default Hellion Arctic theme.
HellionChat is a privacy-focused fork of Chat 2, distributed under the EUPL-1.2.
Theme engine and authoring guide are part of Hellion Forge.
