Clique's color system is open. If you've ever looked at a built-in theme and thought I want that, but mine — this is how you do it.
A theme is just a class that maps names to ANSI codes. That's it. Everything else — auto-discovery, registerTheme() by name, mixing with markup — comes for free once you wire it up.
Just the SPI module:
<!-- Maven -->
<dependency>
<groupId>io.github.kusoroadeolu</groupId>
<artifactId>clique-spi</artifactId>
<version>1.0.7</version>
</dependency>// Gradle
implementation 'io.github.kusoroadeolu:clique-spi:1.0.7'If you want to reference the built-in themes while building yours, add clique-themes too — but it's optional.
public class MyTheme implements CliqueTheme {
@Override public String themeName() { return "my-theme"; }
@Override public String author() { return "your-name"; }
@Override public String url() { return "https://github.com/you/my-theme"; }
@Override
public Map<String, AnsiCode> styles() {
return Map.of(
"mt_blue", ansi(66, 135, 245, false),
"mt_purple", ansi(156, 39, 176, false),
"bg_mt_blue", ansi(66, 135, 245, true)
);
}
private AnsiCode ansi(int r, int g, int b, boolean bg) {
int type = bg ? 48 : 38;
String code = "\u001B[%d;2;%d;%d;%dm".formatted(type, r, g, b);
return new Rgb(code);
}
private record Rgb(String code) implements AnsiCode {
@Override public String toString() { return code; }
}
}author() and url() are metadata, they don't affect how colors render, but they're part of the interface and useful for discovery, attribution, and anyone inspecting themes at runtime. Return whatever makes sense for your project.
Register it, use it:
Clique.registerTheme("my-theme");
Clique.parser().print("[mt_blue, bold]Hello from my theme[/]");Solarized Dark, showing what a complete palette might look like in practice:
public class SolarizedDarkTheme implements CliqueTheme {
@Override public String themeName() { return "solarized-dark"; }
@Override public String author() { return "ethan-schoonover"; }
@Override public String url() { return "https://ethanschoonover.com/solarized"; }
@Override
public Map<String, AnsiCode> styles() {
var colors = new HashMap<String, AnsiCode>();
// Base tones
put(colors, "sol_base03", 0, 43, 54);
put(colors, "sol_base02", 7, 54, 66);
put(colors, "sol_base01", 88, 110, 117);
put(colors, "sol_base0", 131, 148, 150);
put(colors, "sol_base1", 147, 161, 161);
put(colors, "sol_base3", 253, 246, 227);
// Accent colors
put(colors, "sol_yellow", 181, 137, 0);
put(colors, "sol_orange", 203, 75, 22);
put(colors, "sol_red", 220, 50, 47);
put(colors, "sol_magenta", 211, 54, 130);
put(colors, "sol_violet", 108, 113, 196);
put(colors, "sol_blue", 38, 139, 210);
put(colors, "sol_cyan", 42, 161, 152);
put(colors, "sol_green", 133, 153, 0);
return colors;
}
// Registers both foreground and background in one call
private void put(Map<String, AnsiCode> map, String name, int r, int g, int b) {
map.put(name, rgb(r, g, b, false));
map.put("bg_" + name, rgb(r, g, b, true));
}
private AnsiCode rgb(int r, int g, int b, boolean bg) {
String code = "\u001B[%d;2;%d;%d;%dm".formatted(bg ? 48 : 38, r, g, b);
return new Rgb(code);
}
private record Rgb(String code) implements AnsiCode {
@Override public String toString() { return code; }
}
}If you're pulling colors from a design tool or a palette website, hex is usually what you have. Here's a helper that converts it directly:
private void addHex(Map<String, AnsiCode> map, String name, String hex) {
map.put(name, hexToAnsi(hex, false));
map.put("bg_" + name, hexToAnsi(hex, true));
}
private AnsiCode hexToAnsi(String hex, boolean bg) {
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);
String code = "\u001B[%d;2;%d;%d;%dm".formatted(bg ? 48 : 38, r, g, b);
return new Rgb(code);
}Then your palette becomes just a list of names and hex values, readable, easy to update:
addHex(colors, "corp_navy", "#003366");
addHex(colors, "corp_gold", "#FFB81C");
addHex(colors, "corp_success", "#2E7D32");
addHex(colors, "corp_error", "#C62828");If you want your theme to work with Clique.registerTheme("my-theme") or Clique.registerAllThemes(), you need to tell Java's ServiceLoader where to find it.
Create this file:
src/main/resources/META-INF/services/io.github.kusoroadeolu.clique.spi.CliqueTheme
With one fully-qualified class name per line:
com.example.themes.SolarizedDarkTheme
com.example.themes.SolarizedLightTheme
That's all. After this, your theme is discoverable like any built-in one.
Add a provides declaration to your module-info.java:
module my.themes {
requires clique.spi;
provides io.github.kusoroadeolu.clique.spi.CliqueTheme
with com.example.themes.SolarizedDarkTheme,
com.example.themes.SolarizedLightTheme;
}Keep the META-INF/services file too — it handles non-modular classpath scenarios.
A few conventions worth following:
Prefix everything with your theme's identifier. Colors like red or primary will eventually collide with something.
// Good
"sol_red", "sol_cyan", "sol_base03"
// Will cause problems eventually
"red", "cyan", "background"Background colors get the bg_ prefix. Always. It's what users expect after working with any other Clique theme.
"sol_blue" // foreground
"bg_sol_blue" // backgroundTheme names use lowercase with hyphens.
return "solarized-dark"; // ✓
return "SolarizedDark"; // ✗
return "solarized_dark"; // works but inconsistent with conventionBefore shipping, run through every color to make sure nothing's invisible or broken:
var theme = new MyTheme();
theme.register();
theme.styles().forEach((name, code) -> {
if (name.startsWith("bg_")) {
Clique.parser().print("[" + name + ", white] " + name + " [/]");
} else {
Clique.parser().print("[" + name + "] " + name + " [/]");
}
});It's rough, but it catches the common mistakes, missing toString(), forgotten colors, names that clash.
Package it as a standalone JAR. The structure is straightforward:
my-clique-themes/
├── src/main/java/com/example/themes/
│ ├── MyTheme.java
│ └── MyOtherTheme.java
└── src/main/resources/META-INF/services/
└── io.github.kusoroadeolu.clique.spi.CliqueTheme
Users add it as a dependency, and Clique.registerAllThemes() picks it up automatically. No extra setup on their end.
Skip the ServiceLoader entirely and register directly by name after wiring up the service file, or just call Clique.registerTheme("my-theme") once the class is on the classpath.
Themes use 24-bit RGB color. Most modern terminals handle this without any configuration — iTerm2, Alacritty, Kitty, Windows Terminal, recent GNOME Terminal all support it out of the box.
If colors look off, check that COLORTERM=truecolor is set in your shell profile. On Windows PowerShell, you may also need:
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8- Themes — using the built-in themes
- Markup Reference — how to use your colors in markup
- Parser — the parser that brings it all together