# Creating Custom Themes
Learn how to build and distribute your own color themes for Clique.
## Overview
Custom themes allow you to package color palettes as reusable, discoverable components. Once created, themes can be:
- Automatically discovered via Java's ServiceLoader
- Registered by name
- Shared across projects
- Distributed as separate libraries
## Terminal Requirements
For themes to display correctly with their full color palette, your terminal must support truecolor (24-bit color). Most modern terminals support this by default, but you may need to enable it:
- Check support: Run echo $COLORTERM - it should output truecolor or 24bit
- Enable truecolor: Set the environment variable COLORTERM=truecolor in your shell profile
- Terminal compatibility: Ensure your terminal emulator supports 24-bit color (most modern terminals like iTerm2, Alacritty, Kitty, Windows Terminal, and recent versions of GNOME Terminal do)
Without truecolor support, themes may appear with reduced color accuracy or fall back to the nearest 256-color approximation.
## Setup
To create custom themes, you only need the clique-spi library:
### Maven
```xml
io.github.kusoroadeolu
clique-spi
1.0.1
```
### Gradle
```gradle
dependencies {
implementation 'io.github.kusoroadeolu:clique-core:3.0.0'
}
```
**Optional:** If you want to reference or test against the pre-built themes, you can also include `clique-themes`:
```xml
io.github.kusoroadeolu
clique-themes
${version}
```
## Basic Theme Structure
All themes implement the `CliqueTheme` interface from the `Clique` library:
```java
public class MyCustomTheme implements CliqueTheme {
@Override
public String themeName() {
return "my-theme";
}
@Override
public Map styles() {
return Map.ofEntries(
Map.entry("mytheme_primary", new CustomAnsiCode("\u001B[38;2;66;135;245m")),
Map.entry("mytheme_accent", new CustomAnsiCode("\u001B[38;2;156;39;176m")),
Map.entry("bg_mytheme_primary", new CustomAnsiCode("\u001B[48;2;66;135;245m"))
);
}
private record CustomAnsiCode(String code) implements AnsiCode {
@Override
public String toString() {
return code;
}
}
}
```
## Step-by-Step Guide
### 1. Create the Theme Class
Start by implementing the `CliqueTheme` interface:
```java
public class SolarizedDarkTheme implements CliqueTheme {
@Override
public String themeName() {
return "solarized-dark";
}
@Override
public Map styles() {
return Map.ofEntries(
// Base colors
Map.entry("sol_base03", rgb(0, 43, 54)),
Map.entry("sol_base02", rgb(7, 54, 66)),
Map.entry("sol_base01", rgb(88, 110, 117)),
Map.entry("sol_base00", rgb(101, 123, 131)),
Map.entry("sol_base0", rgb(131, 148, 150)),
Map.entry("sol_base1", rgb(147, 161, 161)),
Map.entry("sol_base2", rgb(238, 232, 213)),
Map.entry("sol_base3", rgb(253, 246, 227)),
// Accent colors
Map.entry("sol_yellow", rgb(181, 137, 0)),
Map.entry("sol_orange", rgb(203, 75, 22)),
Map.entry("sol_red", rgb(220, 50, 47)),
Map.entry("sol_magenta", rgb(211, 54, 130)),
Map.entry("sol_violet", rgb(108, 113, 196)),
Map.entry("sol_blue", rgb(38, 139, 210)),
Map.entry("sol_cyan", rgb(42, 161, 152)),
Map.entry("sol_green", rgb(133, 153, 0)),
// Backgrounds
Map.entry("bg_sol_base03", rgb(0, 43, 54, true)),
Map.entry("bg_sol_yellow", rgb(181, 137, 0, true)),
Map.entry("bg_sol_orange", rgb(203, 75, 22, true)),
Map.entry("bg_sol_red", rgb(220, 50, 47, true))
// ... add more backgrounds as needed
);
}
public String author(){
return "someone";
}
public String url(){
return "url";
}
// Helper method to create RGB colors
private static AnsiCode rgb(int r, int g, int b) {
return rgb(r, g, b, false);
}
private static AnsiCode rgb(int r, int g, int b, boolean background) {
int type = background ? 48 : 38;
String code = String.format("\u001B[%d;2;%d;%d;%dm", type, r, g, b);
return new RGBAnsiCode(code);
}
private record RGBAnsiCode(String code) implements AnsiCode {
@Override
public String toString() {
return code;
}
}
}
```
### 2. Make Your Theme Discoverable
Create a service provider configuration file so Java's ServiceLoader can find your theme.
**File location:** `src/main/resources/META-INF/services/io.github.kusoroadeolu.clique.spi.CliqueTheme`
**File content:**
```
com.example.themes.SolarizedDarkTheme
```
If you have multiple themes in your package, list them all:
```
com.example.themes.SolarizedDarkTheme
com.example.themes.SolarizedLightTheme
com.example.themes.MonokaiTheme
```
#### Multi-module projects (JPMS)
If you're using Java Platform Module System (JPMS), you'll also need to declare your theme services in your `module-info.java` file at the root of your module:
**File location:** `src/main/java/module-info.java`
```java
module my.themes {
requires clique.spi;
uses io.github.kusoroadeolu.clique.spi.CliqueTheme;
provides io.github.kusoroadeolu.clique.spi.CliqueTheme
with com.example.themes.SolarizedDarkTheme,
com.example.themes.SolarizedLightTheme,
com.example.themes.MonokaiTheme;
}
```
**Note:** You still need the `META-INF/services` file from step 2 for non-modular classpath scenarios.
### 3. Use Your Theme
Once registered as a service, your theme is automatically discoverable:
```java
public class App {
public static void main(String[] args) {
// Register by name
Clique.registerTheme("solarized-dark");
// Use your theme colors
Clique.parser().print("[sol_blue]Hello, Solarized![/]");
Clique.parser().print("[sol_red]Error:[/] [sol_base0]Something went wrong[/]");
}
}
```
## Proposed Naming Conventions
Follow these conventions for consistency:
### Color Names
Use descriptive, lowercase names with underscores:
```java
// Good
"mytheme_primary"
"mytheme_accent"
"mytheme_error"
// Avoid
"MyThemePrimary" // Wrong case
"primary" // Too generic, conflicts with other themes
"mytheme-accent" // Use underscores, not hyphens
```
### Background Colors
Always prefix background colors with `bg_`:
```java
Map.entry("mytheme_blue", ...), // Foreground
Map.entry("bg_mytheme_blue", ...), // Background
```
### Bright/Dark Variants
Use prefixes to indicate variants:
```java
// Standard colors
"mytheme_red"
"mytheme_blue"
// Bright variants
"*mytheme_red"
"*mytheme_blue"
// Dark variants (if applicable)
"dark_mytheme_red"
"dark_mytheme_blue"
```
### Theme Name
Use lowercase with hyphens for the theme name:
```java
@Override
public String themeName() {
return "my-awesome-theme"; // ✓ Good
}
```
Try avoiding using hyphens or camelcase for theme names
## Testing Your Theme
Create a simple test to verify all colors render correctly:
```java
public class ThemeTest {
public static void main(String[] args) {
var theme = new MyCustomTheme();
theme.register();
System.out.println("Testing theme: " + theme.themeName());
System.out.println();
// Test each color
theme.styles().forEach((name, code) -> {
if (name.startsWith("bg_")) {
// Background colors
Clique.parser().print("[" + name + ", black] " + name + " [/]");
} else {
// Foreground colors
Clique.parser().print("[" + name + "]" + name + "[/]");
}
});
}
}
```
## Distributing Your Theme
### As a Separate Library
Package your theme as a standalone JAR:
**Project structure:**
```
my-clique-themes/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/themes/
│ │ ├── MyTheme1.java
│ │ └── MyTheme2.java
│ └── resources/
│ └── META-INF/
│ └── services/
│ └── io.github.kusoroadeolu.clique.spi.CliqueTheme
└── pom.xml (or build.gradle)
```
**Maven dependency:**
```xml
com.example
my-clique-themes
1.0.0
```
Users can then discover and use your themes:
```java
Clique.registerAllThemes(); // Auto-discovers your themes
Clique.registerTheme("my-theme-1");
```
### In the Same Project
If your theme is for internal use only, you can skip the ServiceLoader registration and register manually:
```java
public class AppTheme implements CliqueTheme {
// ... implementation
}
// In your main class
var theme = new AppTheme();
theme.register();
```
## Best Practices
### 1. Provide Complete Palettes
Include both foreground and background variants for all your colors.
### 2. Include Semantic Colors
Add semantic color names for common use cases:
```java
Map.entry("theme_error", rgb(220, 50, 47)),
Map.entry("theme_success", rgb(133, 153, 0)),
Map.entry("theme_warning", rgb(181, 137, 0)),
Map.entry("theme_info", rgb(38, 139, 210))
```
### 3. Pre-compute ANSI Codes
Compute ANSI codes once in the constructor or initialization, not in `toString()`:
```java
// Good - computed once
private record CustomAnsiCode(String code) implements AnsiCode {
@Override
public String toString() {
return code;
}
}
// Bad - computes every time
private static class CustomAnsiCode implements AnsiCode {
private final int r, g, b;
@Override
public String toString() {
return String.format("\u001B[38;2;%d;%d;%dm", r, g, b); // Recomputes!
}
}
```
## Common Pitfalls
### Missing Service Provider Configuration
Theme isn't discovered by `CliqueThemeLoader.discover()`, so always ensure you have the service provider file at:
```
src/main/resources/META-INF/services/io.github.kusoroadeolu.clique.spi.CliqueTheme
```
### Forgetting toString() Implementation
Colors don't appear, or you see object addresses like `CustomAnsiCode@1a2b3c4d`. Therefore, always implement `toString()` in your `AnsiCode` implementation to return the actual ANSI escape code string:
```java
private record CustomAnsiCode(String code) implements AnsiCode {
@Override
public String toString() { // This is essential!
return code;
}
}
```
### Conflicting Color Names
Colors from different themes could override each other, hence always prefix your color names with your theme identifier:
```java
// Good
"mytheme_red"
"mytheme_blue"
// Bad - can conflict with other themes
"red"
"blue"
```
## Example: Complete Theme
Here's a complete, production-ready theme implementation:
```java
package com.example.themes;
import io.github.kusoroadeolu.clique.spi.AnsiCode;
import io.github.kusoroadeolu.clique.spi.CliqueTheme;
import java.util.HashMap;
import java.util.Map;
/**
* A business based theme with corporate colors.
*/
public class CorporateTheme implements CliqueTheme {
@Override
public String themeName() {
return "corporate";
}
@Override
public Map styles() {
Map colors = new HashMap<>();
// Brand colors
addColor(colors, "corp_navy", "#003366");
addColor(colors, "corp_gold", "#FFB81C");
addColor(colors, "corp_slate", "#54585A");
// Semantic colors
addColor(colors, "corp_success", "#2E7D32");
addColor(colors, "corp_error", "#C62828");
addColor(colors, "corp_warning", "#F57C00");
addColor(colors, "corp_info", "#0277BD");
// Neutral colors
addColor(colors, "corp_text", "#212121");
addColor(colors, "corp_text_light", "#757575");
addColor(colors, "corp_bg", "#F5F5F5");
addColor(colors, "corp_bg_dark", "#E0E0E0");
return colors;
}
private void addColor(Map map, String name, String hex) {
map.put(name, hexToAnsi(hex, false));
map.put("bg_" + name, hexToAnsi(hex, true));
}
private AnsiCode hexToAnsi(String hex, boolean background) {
hex = hex.startsWith("#") ? hex.substring(1) : hex;
int r = Integer.parseInt(hex.substring(0, 2), 16);
int g = Integer.parseInt(hex.substring(2, 4), 16);
int b = Integer.parseInt(hex.substring(4, 6), 16);
int type = background ? 48 : 38;
String code = String.format("\u001B[%d;2;%d;%d;%dm", type, r, g, b);
return new CustomAnsiCode(code);
}
private record CustomAnsiCode(String code) implements AnsiCode {
@Override
public String toString() {
return code;
}
}
}
```
**Usage:**
```java
Clique.registerTheme("corporate");
Clique.parser().print("[corp_navy, bold]QUARTERLY REPORT[/]");
Clique.parser().print("[corp_success]✓[/] Revenue: [corp_gold]$2.5M[/]");
Clique.parser().print("[corp_error]✗[/] Expenses: [corp_text]$1.8M[/]");
```
## See Also
- [Themes Documentation](themes.md) - Using pre-built themes
- [Parser Composability](parser-compose.md) - Advanced custom ANSI codes
- [Markup Reference](markup-reference.md) - Using theme colors in markup