Files
EZPPLauncher/THEME_API_V2.md
T

57 KiB
Raw Blame History

EZPPLauncher Theme API 2.0

API 2.0 is a declarative theme format. Instead of shipping a JavaScript bundle that mounts its own UI, a theme describes its background, effects, audio, fonts and freeform layers in theme.json. The launcher renders it, and the built-in advanced editor can create and edit these themes without writing code. Themes can opt into a custom Svelte layer when they need behavior that data alone cannot express; that layer runs theme-authored JavaScript and therefore needs an explicit trust prompt.

API 1.0 themes continue to work unchanged. Pick the API that fits the theme:

API 1.0 API 2.0
Rendering Svelte bundle (entry) Launcher-rendered
Files theme.json, bundle.js, style.css, assets/ theme.json, assets/
Created with create-ezpplauncher-theme Launcher editor or create-ezpplauncher-theme
Scripts / arbitrary DOM Yes Optional custom Svelte layer (trust prompt)
Cross-platform Depends on the bundle Yes

Package layout

A packaged .ezpplauncher-theme file is a zip with this structure:

theme.json
assets/
  background.webm
  layer.png
  music.mp3
  font.woff2
  preview.png        # optional thumbnail

An installed theme lives in:

# Windows
C:\Users\<user>\.ezpplauncher\themes\<folder>\

# macOS / Linux
~/.ezpplauncher/themes/<folder>/

theme.json

{
  "name": "Midnight",
  "version": "1.0.0",
  "apiVersion": "2.0",
  "author": "Your Name",
  "description": "A dark gradient with drifting stars.",
  "preview": "preview.png",
  "background": {
    "type": "gradient",
    "from": "#181825",
    "to": "#3b1d4f",
    "angle": 135,
    "opacity": 1
  },
  "overlay": {
    "color": "#000000",
    "opacity": 0.2,
    "vignette": 0.35
  },
  "effects": {
    "type": "stars",
    "density": 40,
    "speed": 1,
    "color": "#ffd6f2"
  },
  "typography": {
    "body": "Sora Variable",
    "heading": "Bebas Neue",
    "mono": "JetBrains Mono Variable"
  },
  "fonts": [
    {
      "family": "My Display Font",
      "src": "display.woff2",
      "weight": "400",
      "style": "normal"
    }
  ],
  "groups": [
    {
      "id": "branding",
      "name": "Branding",
      "opacity": 1
    }
  ],
  "elements": [
    {
      "id": "tagline",
      "groupId": "branding",
      "type": "text",
      "text": "MIDNIGHT",
      "x": 640,
      "y": 84,
      "width": 300,
      "height": 72,
      "fontFamily": "Bebas Neue",
      "fontSize": 46,
      "fontWeight": 400,
      "color": "#ffffff",
      "align": "right",
      "verticalAlign": "center",
      "letterSpacing": 2
    }
  ],
  "audio": {
    "src": "music.mp3",
    "loop": true
  },
  "variables": {
    "player": "Guest",
    "clicks": 0
  },
  "accentColor": "#ff006e",
  "iconSet": "lucide",
  "css": ""
}

Metadata

Field Type Required Notes
name string yes Display name; also used for the install folder
version string yes Semver, used for update checks
apiVersion string yes Must start with 2 (use "2.0")
author string yes Shown in the theme manager
description string no Shown on the theme card
nsfw bool no Marks the theme as adult content (18+ badge)
preview string no Card thumbnail; the editor generates preview.png
accentColor string no Hex color used for focus rings and active UI states
iconSet string no Launcher icon family: lucide, phosphor or tabler
css string no Extra CSS injected while the theme is active
behaviors array no Declarative WHEN / IF / THEN rules; see Theme logic
variables object no Starting values for placeholders and rules; see Dynamic text

Background

background.type selects the layer:

{ "type": "color", "color": "#181825", "opacity": 1 }
{ "type": "gradient", "from": "#181825", "to": "#3b1d4f", "angle": 135, "opacity": 1 }
{ "type": "image", "src": "background.png", "fit": "cover", "opacity": 1, "blur": 0 }
{
  "type": "video",
  "src": "background.webm",
  "fit": "cover",
  "opacity": 1,
  "blur": 0,
  "muted": true
}
  • src is relative to the theme's assets/ folder.
  • fit is cover (default) or contain.
  • opacity is clamped to 0–1, blur to 0–40 pixels.
  • Background videos autoplay and loop. They are muted by default; set muted to false to play their audio.
  • Theme music (audio) cannot be combined with a video background; the editor disables music and the launcher ignores the audio entry in that case.
  • Prefer WebM for video backgrounds and video layers. It is supported by the launcher and keeps theme packages much smaller than MP4. MP4, MOV and M4V also work.

Overlay

{ "color": "#000000", "opacity": 0.2, "vignette": 0.35 }

opacity controls the flat dim layer and vignette darkens the edges. Both are clamped to 0–1.

Effects

{ "type": "stars", "density": 40, "speed": 1, "color": "#ffd6f2" }

Supported types: none, stars, snow, petals, bubbles.

  • density is clamped to 0–120 particles.
  • speed is clamped to 0.2–3.
  • Effects are disabled when the user turns on Reduce animations.

Audio

{ "src": "music.mp3", "loop": true }

src is relative to assets/. Music uses the launcher's volume control and is silent in the theme editor preview.

Theme music cannot be combined with a video background. The editor disables the music controls while a video background is selected, and themes that contain both have their audio entry ignored.

Typography

typography.body changes the font used by the launcher UI. typography.heading changes headings such as the launcher title. typography.mono changes the monospace font used for paths, versions and telemetry labels; leave it out to keep the built-in Space Mono stack.

"typography": {
  "body": "Montserrat Variable",
  "heading": "Playfair Display Variable",
  "mono": "JetBrains Mono Variable"
}

These families ship with the launcher through @fontsource and can be used without adding any files:

Family Style
Sora Variable Sans
Montserrat Variable Sans
Bebas Neue Display
Playfair Display Variable Serif
Space Mono Mono
JetBrains Mono Variable Mono

Custom fonts can be packaged with the theme. The launcher generates the @font-face rules from fonts:

"fonts": [
  { "family": "My Display Font", "src": "display.woff2", "weight": "400", "style": "normal" }
]
  • src is relative to assets/.
  • Supported formats: WOFF2 (recommended), WOFF, TTF and OTF.
  • weight defaults to 400; style defaults to normal.
  • Use the family value in typography and in text layers.

The editor's Fonts tab browses the full Fontsource catalog (2,000+ families) and downloads the selected family, weight, style and subset as a WOFF2 into the theme's assets/ folder. It adds the matching fonts entry automatically, so saved and exported themes stay self-contained.

Icon set

iconSet swaps the launcher's navigation, status, settings and theme-browser icons. The default is Lucide; Phosphor and Tabler are also bundled.

"iconSet": "phosphor"
Value Family Character
lucide Lucide Clean geometric outlines (default)
phosphor Phosphor Softer, rounded shapes
tabler Tabler Crisp, technical outlines

Icon bodies ship with the launcher, so the setting never downloads anything at runtime. Unknown values fall back to Lucide, which keeps older launchers and hand-written themes compatible. The editor shows a live preview in Theme → Icon set, and the selected family is used across the launcher and its live home canvas.

Freeform layers

elements are rendered above the launcher UI, but only on the Home screen. The launcher canvas is a fixed 1000 × 650 coordinate space. Elements can extend the home screen with text, images, videos, shapes, icons, data readouts, gauges, tickers, Lottie animations and buttons. They are pointer-transparent by default so they never block buttons; set interactive: true to let a layer receive clicks and hovers from theme logic. Interactive layers sit above the UI and can block clicks inside their rectangle.

The order of the elements array controls stacking: the last element is drawn on top. Themes can contain up to 100 layers.

Every layer supports:

Field Type Notes
id string Unique within the theme
name string? Optional editor label; does not affect rendering
type string text, data, image, video, shape, icon, repeater, gauge, ticker, lottie, button, path, particles, clock, countdown or svelte
x, y number Position in the 1000 × 650 stage
width number 24–1000
height number 24–650
rotation number Degrees, -360–360
scale number Legacy uniform scale fallback, 0.05–10
scaleX number Horizontal scale, 0.05–10
scaleY number Vertical scale, 0.05–10
skewX number Horizontal skew in degrees, -89–89
skewY number Vertical skew in degrees, -89–89
flipX boolean Mirror the layer horizontally
flipY boolean Mirror the layer vertically
opacity number 0–1
hidden boolean Keep the layer in the file but do not render it
locked boolean Prevent moving/resizing the layer in the editor
interactive boolean Opts into clicks and hovers from theme logic
groupId string? Group that contains this layer
filters object? Per-layer CSS filters; see Groups and filters
mask object? Shape or image clip; see Masks
cssClasses string? Space-separated classes for the layer wrapper

Text

{
  "id": "greeting",
  "type": "text",
  "text": "Welcome back!",
  "x": 360,
  "y": 220,
  "width": 420,
  "height": 90,
  "fontFamily": "Bebas Neue",
  "fontSize": 42,
  "fontWeight": 600,
  "fontStyle": "normal",
  "color": "#ffffff",
  "align": "center",
  "verticalAlign": "center",
  "lineHeight": 1.2,
  "letterSpacing": 0,
  "textDecoration": "none",
  "textTransform": "none",
  "fillMode": "solid",
  "outlineWidth": 0,
  "outlineColor": "#000000",
  "shadowColor": "#000000",
  "shadowBlur": 0,
  "shadowOffsetX": 0,
  "shadowOffsetY": 0
}

align is left, center or right; verticalAlign is top, center or bottom. Font size is clamped to 6–240.

Text layers support the full visual styling set:

Field Type Notes
fontStyle string normal or italic
textDecoration string none, underline, line-through or underline line-through
textTransform string none, uppercase, lowercase or capitalize
fillMode string solid or gradient
gradientFrom string Gradient start color
gradientTo string Gradient end color
gradientAngle number Degrees, 0–360; 90 is left to right
outlineWidth number Pixels, 0–20; 0 disables the outline
outlineColor string Outline color
shadowColor string Text shadow color
shadowBlur number Pixels, 0–40
shadowOffsetX number Pixels, -40–40
shadowOffsetY number Pixels, -40–40

Gradient fills use gradientFrom and gradientTo; color remains the fallback for solid fills and is used for text decoration when no gradient is active. Text outlines are drawn behind the fill, so gradients and outlines can be combined.

Dynamic text

Text layers can show live rule variables with {name} placeholders. Variables are shared by every rule and text layer in the theme.

{
  "type": "text",
  "text": "Clicks: {clicks}\nPlayer: {player}",
  "id": "counter"
}
clicks += 1;
if (clicks >= 3) {
  player = 'Ready!';
}

The theme can declare starting values for its variables. This is optional; any variable without an entry starts at 0.

"variables": {
  "clicks": 0,
  "player": "Guest",
  "ready": false
}

Every valid placeholder starts with its declared value when the theme loads, then updates whenever a script or builder action changes that variable. Values may be numbers, strings or booleans, and variables that are only read by rules or scripts can be seeded here as well. Numbers, strings and booleans are inserted as plain text. Reserved names such as {screen} and malformed placeholders stay visible instead of rendering a value. In the editor, the text inspector lists known variables and inserts the placeholder at the cursor, and Theme → Variables sets each variable's starting value; clearing a field falls back to 0.

Built-in clock

The engine also exposes read-only local clock values. They refresh once per second, so a text layer or script can display the live time without declaring any variables. Conditions also read the current clock whenever a rule fires.

Placeholder Value
{time} 24-hour time, e.g. 21:05
{time12} 12-hour time with AM/PM, e.g. 9:05 PM
{timeSeconds} 24-hour time with seconds, e.g. 21:05:07
{hour} Hour from 0 to 23
{minute} Minute from 0 to 59
{second} Second from 0 to 59
{dayPeriod} AM or PM
<p class="clock">{time12}</p>
<p class="tiny">LOCAL // {timeSeconds}</p>

They are available to conditions and scripts too, which makes day/night themes simple:

if (hour >= 18 || hour < 6) {
  showGroup('night');
  hideGroup('day');
}

Conditions are evaluated when their rule's trigger fires, so pair a time-based condition with a timer trigger to re-check it while the launcher is open. For example, a rule with trigger: { "type": "timer", "interval": 10 } and a condition on hour switches the theme at sunset or sunrise without restarting. The helper actions Show group / Hide group make this easy to build in the rule editor; the Clock quick-pick in a variable condition fills in time, hour, minute, second or dayPeriod for you.

The clock names are reserved. Assigning to time, hour or any other clock name is a script error, and they are not listed in Theme → Variables. They appear in the text inspector's insert row and in script completion.

Image and video

{
  "id": "logo",
  "type": "image",
  "src": "logo.png",
  "x": 700,
  "y": 420,
  "width": 220,
  "height": 140,
  "fit": "contain",
  "radius": 18,
  "tint": "",
  "opacity": 1
}

tint repaints the image's alpha silhouette with a solid color. It is the simplest way to make a monochrome SVG logo follow the theme accent; leave it out or set it to an empty string to keep the original colors.

{
  "id": "ambient",
  "type": "video",
  "src": "ambient.webm",
  "x": 0,
  "y": 0,
  "width": 1000,
  "height": 650,
  "fit": "cover",
  "opacity": 0.35
}

Video layers autoplay and loop. They are muted by default; set muted to false to play their audio. WebM is recommended for size. Use an opacity below 1 if the video should blend into the launcher UI.

Shape

{
  "id": "glow",
  "type": "shape",
  "shape": "ellipse",
  "x": 680,
  "y": 60,
  "width": 260,
  "height": 260,
  "fill": "#ff006e",
  "opacity": 0.18,
  "borderWidth": 0,
  "borderColor": "#ffffff"
}

shape is rectangle, ellipse, line, polygon, star or arc. radius rounds rectangle corners and cornerRadii (top-left, top-right, bottom-right, bottom-left) overrides it per corner. points sets polygon sides and star points (3–24), and innerRadius sets the star's inner radius (0.1–0.95).

An arc draws a ring segment instead of a filled shape. Angles start at 12 o'clock and run clockwise; endAngle may wrap past 360 and a full 360 span becomes a ring. thickness is the stroke width in stage pixels and rounded controls the end caps.

{
  "id": "dial",
  "type": "shape",
  "shape": "arc",
  "x": 700,
  "y": 80,
  "width": 180,
  "height": 180,
  "startAngle": -120,
  "endAngle": 120,
  "thickness": 10,
  "rounded": true,
  "fillMode": "gradient",
  "gradientFrom": "#ff006e",
  "gradientTo": "#7c3aed"
}

Fill can be a solid color or a gradient:

{
  "id": "sunburst",
  "type": "shape",
  "shape": "star",
  "x": 120,
  "y": 120,
  "width": 220,
  "height": 220,
  "points": 8,
  "innerRadius": 0.42,
  "fillMode": "gradient",
  "gradientFrom": "#ffd166",
  "gradientTo": "#ff006e",
  "gradientAngle": 135,
  "opacity": 0.9
}

borderStyle accepts solid, dashed or dotted, and backdropBlur adds a frosted-glass blur behind the shape (0–60 pixels). Keep the fill partially transparent to see the blur.

Icon

An icon layer draws a semantic icon from the theme's icon set, so switching iconSet changes every icon layer with the rest of the launcher.

{
  "id": "star",
  "type": "icon",
  "icon": "music",
  "x": 720,
  "y": 80,
  "width": 64,
  "height": 64,
  "color": "#ffd6f2",
  "scale": 100
}

scale is a percentage of the layer's shortest side (10–100). The available names are the ones listed by Theme → Icon in the editor.

Data layer

A data layer is a text layer whose content comes from the launcher instead of static copy. Use {value} in text as the placeholder.

{
  "id": "player",
  "type": "data",
  "field": "user.name",
  "text": "Welcome, {value}!",
  "x": 360,
  "y": 180,
  "width": 420,
  "height": 60,
  "fontSize": 26,
  "color": "#ffffff"
}

Fields: user.name, user.country, user.pp, user.rank, user.plays, user.accuracy, user.playtime, launcher.version, osu.build, osu.stream, server.ping, server.friends, theme.name, theme.author, theme.version, screen, clock.time, clock.time12 and clock.date. Player stat fields accept mode: osu, taiko, catch or mania. Unknown or signed-out values render as an empty string. A data layer accepts every text styling field (fontFamily, fontSize, fillMode, outlineWidth, shadows and so on).

Repeater

A repeater tiles an image or an icon across the layer bounds.

{
  "id": "sparkles",
  "type": "repeater",
  "source": "icon",
  "icon": "music",
  "x": 120,
  "y": 480,
  "width": 300,
  "height": 120,
  "tileWidth": 28,
  "tileHeight": 28,
  "gapX": 14,
  "gapY": 14,
  "color": "#ffffff"
}

source is image or icon. Image repeaters use src, radius and fit (cover inside each tile). Leave columns and rows at 0 to fill the bounds; explicit counts are clamped to 60. At most 240 tiles are drawn.

Gauge

A gauge renders a bar or a ring from a 0–1 value. Bind it to a theme variable so logic rules can animate it, or leave variable out and use the static value.

{
  "id": "progress",
  "type": "gauge",
  "mode": "ring",
  "variable": "sync",
  "x": 760,
  "y": 420,
  "width": 120,
  "height": 120,
  "trackColor": "#000000",
  "fillColor": "#ff006e",
  "thickness": 10,
  "rounded": true,
  "label": "{percent}%"
}

label supports {percent}, {value} and any rule variable. thickness is the ring stroke width in pixels; the bar fills the layer height.

Ticker

A ticker scrolls a single line of text horizontally.

{
  "id": "now-playing",
  "type": "ticker",
  "text": "Now playing · Your favourite track · ",
  "x": 120,
  "y": 560,
  "width": 420,
  "height": 48,
  "fontSize": 20,
  "color": "#ffffff",
  "speed": 18,
  "gap": 48
}

speed is the number of seconds for one loop. The track is duplicated so the loop is seamless, and animation stops when the system asks for reduced motion.

Lottie animation

A Lottie layer plays a JSON animation from the theme's assets folder. It scales crisply and is usually far smaller than a WebM.

{
  "id": "crest",
  "type": "lottie",
  "src": "crest.json",
  "x": 720,
  "y": 60,
  "width": 200,
  "height": 200,
  "loop": true,
  "autoplay": true,
  "speed": 1
}

speed is clamped to 0.1–4. The exported preview image renders the first frame; the launcher renders the live animation.

Vector path

A path layer draws custom SVG path data as a scalable shape. Author the path inside a 0–100 coordinate box; it is stretched to the layer rectangle. M, L, H, V, C, S, Q, T, A and Z commands are supported, and anything outside path grammar is stripped when the theme loads.

{
  "id": "wave",
  "type": "path",
  "path": "M 0 70 C 20 20 35 95 55 55 C 72 22 86 40 100 10 L 100 100 L 0 100 Z",
  "x": 0,
  "y": 470,
  "width": 700,
  "height": 180,
  "fillMode": "gradient",
  "gradientFrom": "#ff006e",
  "gradientTo": "#7c3aed",
  "gradientAngle": 90,
  "stroke": "#ffffff",
  "strokeWidth": 2,
  "strokeStyle": "solid",
  "strokeLinecap": "round"
}

Set stroke to an empty string for a fill-only path. strokeStyle accepts solid, dashed or dotted, and strokeLinecap accepts butt, round or square.

Particles

A particle field animates inside its rectangle without any assets. The presets cover the most common ambient effects, and the layer respects the system's reduced-motion setting.

{
  "id": "sparkle-field",
  "type": "particles",
  "preset": "sparkles",
  "x": 520,
  "y": 90,
  "width": 420,
  "height": 360,
  "density": 42,
  "size": 7,
  "speed": 1,
  "color": "#ffe9a8",
  "color2": "#ff006e",
  "wind": 0.2,
  "fade": true
}
Field Type Notes
preset string sparkles, snow, petals, bubbles, embers, confetti
density number Particle count, 4–120
speed number Animation speed multiplier, 0.1–3
size number Base particle size in stage pixels, 1–24
color string Primary color
color2 string Secondary color; particles alternate between both
wind number Horizontal drift, -1 (left) to 1 (right)
fade boolean Fade particles at the top and bottom edges (default true)

Clock

A clock layer shows the viewer's local time, either as text or as an analog dial. It ticks once per second while it is visible.

{
  "id": "local-time",
  "type": "clock",
  "mode": "digital",
  "x": 40,
  "y": 500,
  "width": 300,
  "height": 110,
  "hour12": false,
  "showSeconds": true,
  "showDate": true,
  "label": "Local time",
  "fontFamily": "Sora Variable",
  "fontSize": 42,
  "fontWeight": 700,
  "color": "#ffffff",
  "accentColor": "#ff006e",
  "align": "center"
}

mode is digital or analog. hour12, showSeconds and showDate affect the digital clock; the analog dial always uses local hours and shows the second hand unless showSeconds is false. accentColor colors the second hand, dial details and the date.

Countdown

A countdown layer ticks towards an ISO date-time using a template. It shares the clock tick and needs no logic rule.

{
  "id": "release",
  "type": "countdown",
  "target": "2027-01-01T00:00:00Z",
  "text": "{days}d {hours}h {minutes}m {seconds}s",
  "finishedText": "Happy new year!",
  "x": 60,
  "y": 420,
  "width": 380,
  "height": 80,
  "fontSize": 34,
  "color": "#ffffff",
  "align": "left"
}

The template supports {days}, {hours}, {minutes} and {seconds}. Hours, minutes and seconds are zero-padded. Once the target passes, finishedText is shown instead. An empty or invalid target displays zeroes.

Button

A button layer is a real launcher control. It receives clicks even though the rest of the layer stack is pointer-transparent, and it is above the built-in UI.

{
  "id": "open-themes",
  "type": "button",
  "label": "Browse themes",
  "icon": "themes",
  "x": 360,
  "y": 480,
  "width": 240,
  "height": 52,
  "action": { "type": "navigate", "screen": "themes" },
  "background": "#ff006e",
  "color": "#ffffff",
  "hoverBackground": "#ff4d94",
  "radius": 12
}

action is one of:

Action Shape
navigate { "type": "navigate", "screen": "home" }
launch { "type": "launch" }
url { "type": "url", "url": "https://…" }

External URLs always open through the launcher's confirmation dialog, which shows the full address first. A url action must use http or https; the theme check reports anything else as an error.

Custom Svelte layer

A svelte layer is a self-contained Svelte 5 component compiled by the launcher at runtime. It fills the layer rectangle and can render any markup, SVG or canvas content, use runes, and import from Svelte's runtime modules.

{
  "id": "welcome",
  "type": "svelte",
  "x": 360,
  "y": 200,
  "width": 360,
  "height": 190,
  "interactive": true,
  "source": "<script>\n  let { variables = {}, asset } = $props();\n</script>\n\n<div class=\"card\">Score: {variables.score}</div>\n\n<style>\n  .card { height: 100%; color: white; }\n</style>\n"
}

The component receives these reactive props:

Prop Value
variables Live theme variable values. Logic rule changes update the component.
screen The current launcher screen (custom layers render on Home today).
layer { id, name, width, height } for this layer.
theme { name, author, accentColor }.
asset asset('image.png') resolves a file inside the theme assets/ folder.
<script>
  let { variables = {}, asset } = $props();
  const logo = $derived(asset('logo.png'));
</script>

<img src={logo} alt="" /><p>Score: {variables.score ?? 0}</p>

The editor provides a Monaco source pane with Svelte/JavaScript highlighting, completions for props, runes and theme variables, and inline compiler errors. The source is plain JavaScript; TypeScript in <script lang="ts"> is not preprocessed. Imports are limited to Svelte's runtime (svelte, svelte/store, svelte/transition, svelte/easing, svelte/motion, svelte/animate, svelte/reactivity and svelte/legacy). Dynamic import() and import.meta are not available.

Security: a Svelte layer is executable theme code. It can read the launcher DOM and call APIs available to the page, so only run themes from authors you trust. The launcher asks for confirmation before activating or opening the editor for any API 2.0 theme that contains a Svelte layer, just like an API 1.0 theme bundle. The editor shows a warning in the layer inspector as a reminder.

Groups and filters

groups are optional layer groups for the layer list. Assign a layer to a group with groupId; groups can nest through parentId:

"groups": [
  {
    "id": "branding",
    "name": "Branding",
    "collapsed": false,
    "opacity": 1,
    "hidden": false,
    "locked": false,
    "offsetX": 12,
    "offsetY": -8,
    "scaleX": 1.1,
    "scaleY": 1.1,
    "rotation": 0,
    "skewX": 0,
    "skewY": 0,
    "flipX": false,
    "flipY": false,
    "filters": { "brightness": 1.1, "saturate": 1.2 }
  },
  { "id": "branding_badge", "name": "Badge", "parentId": "branding" }
]
Field Type Notes
id string Unique within the theme
name string? Group label in the editor
parentId string? Parent group for nested groups
hidden boolean Hides every layer inside the group
locked boolean Prevents dragging, resizing and transforming the group
collapsed boolean Folds the group in the editor; ignored by the launcher
opacity number 0–1, multiplied with the layer opacity
offsetX number Horizontal translation in stage pixels, -2000–2000
offsetY number Vertical translation in stage pixels, -2000–2000
scale number Legacy uniform scale fallback, 0.05–10
scaleX number Horizontal scale, 0.05–10
scaleY number Vertical scale, 0.05–10
rotation number Degrees around the group's bounds, -360–360
skewX number Horizontal skew in degrees, -89–89
skewY number Vertical skew in degrees, -89–89
flipX boolean Mirror the group horizontally
flipY boolean Mirror the group vertically
filters object? Filters applied to the whole group
mask object? Shape or image clip; see Masks
cssClasses string? Classes inherited by every layer in the group

Masks

Layers and groups can be clipped by a shape or by an image's alpha channel:

"mask": {
  "shape": "polygon",
  "sides": 6,
  "radius": 0,
  "innerRadius": 0.5,
  "src": "mask.png",
  "fit": "cover"
}
Shape Behavior
rectangle Rounded rectangle; radius in pixels
ellipse Inscribed ellipse
polygon Regular polygon with sides (3–24)
star Star with sides points and innerRadius (0.1–0.95)
image Alpha mask from src; fit is cover or contain

A group mask clips every layer in the group and follows the group's transform. A layer mask clips only that layer; selection outlines in the editor stay outside the mask so the layer remains easy to move. Image masks need an actual alpha channel; opaque PNG or JPEG files clip the layer away completely.

Both groups and layers accept the same filters object:

"filters": {
  "blur": 4,
  "brightness": 1.1,
  "contrast": 1.05,
  "saturate": 1.2,
  "hueRotate": 12,
  "grayscale": 0.2,
  "sepia": 0,
  "invert": 0
}
Field Default Range Notes
blur 0 0–40 Pixels
brightness 1 0–3 Multiplier
contrast 1 0–3 Multiplier
saturate 1 0–4 Multiplier
hueRotate 0 -360–360 Degrees
grayscale 0 0–1
sepia 0 0–1
invert 0 0–1

Filters compose down the tree: a layer's own filters run first, followed by its group, then outer groups. Group opacity multiplies the same way. Group transforms are anchored to the bounds of the layers inside the group and compose through nested groups. A layer's own rotation, skewX, skewY, scaleX, scaleY, flipX and flipY apply before its ancestor groups' transforms. Themes can contain up to 50 groups; nesting is limited only by that count.

The full 2D transform set is supported: translate, rotate, independent X/Y scale and skew. Layers use x/y for translation and groups use offsetX/offsetY (shown as Translate in the editor). The remaining transforms compose in CSS order: rotate, skew, then scale and flips, around the layer or group bounds centre. scaleX and scaleY take precedence over the legacy uniform scale field; when they are absent, scale is used for both axes. Raw matrix(), matrix3d() and 3D transforms such as perspective and rotateX/rotateY are not exposed because the editor's hit-testing and group handles use an invertible 2D matrix.

In the editor, hover any layer on the canvas to reveal its group outline and a breadcrumb of the group path from the outermost group to the innermost one. Click any crumb to select that group, including an outer ancestor. You can also double-click the layer or use Select parent group in its context menu. The group frame has a center label for moving, corner handles for proportional scaling and a top handle for rotation. A selected single layer gets a top rotate handle of its own; rotating a layer inside a group respects the group's transform. Hold Alt while moving to skip snapping and Shift while rotating or scaling to snap to 15° or 5% steps. Group handles are hidden while the group is locked.

Theme logic

API 2.0 themes can include declarative interaction rules. Think of them as Scratch-style WHEN / IF / THEN blocks: no JavaScript is executed, and the editor can show exactly what a rule will do. Rules can also opt into a safe advanced script with expressions, branches and loops; see Advanced scripts below.

"behaviors": [
  {
    "id": "open_themes",
    "name": "Open themes",
    "enabled": true,
    "once": true,
    "target": { "type": "element", "id": "themes_button" },
    "trigger": { "type": "click", "elementId": "themes_button" },
    "conditionMode": "all",
    "conditions": [
      { "type": "variable", "variable": "opened", "operator": "equals", "value": 0 }
    ],
    "actions": [
      { "type": "setVariable", "variable": "opened", "value": 1 },
      { "type": "navigate", "screen": "themes" }
    ]
  }
]

A layer only receives pointer events when it sets "interactive": true. Use the Interactive toggle in the layer inspector, then pick that layer in a click or hover trigger.

target is optional and links a rule to a layer or group:

"target": { "type": "group", "id": "home_actions" }

Linked rules appear directly in that layer or group inspector, where they can be viewed, added and edited without leaving the tab. For click, hover and leave triggers, a group target matches any interactive layer inside the group (including nested groups); an element target matches only that layer. load, screen and timer triggers are theme-wide regardless of the link.

Trigger Fields When it runs
load — The theme becomes active
screen screen The launcher switches to home, themes, settings or login
click elementId? An interactive layer (or a layer inside a linked group) is clicked
hover elementId? The pointer enters an interactive layer or linked group
leave elementId? The pointer leaves an interactive layer or linked group
timer interval (seconds) Repeatedly, with a minimum of 0.5 seconds

Conditions can be combined with conditionMode: "all" (default) or "any". An empty condition list always matches.

Condition Fields Checks
variable variable, operator, value Compares a variable using equals, notEquals, greaterThan, lessThan, greaterOrEqual or lessOrEqual
visible elementId, value Whether a layer is currently visible
groupVisible groupId, value Whether a group is currently visible
screen screen The current launcher screen
chance percent A random roll from 0–100
Action Fields Effect
setVariable variable, value Stores a string, number or boolean
changeVariable variable, amount Adds a number to a variable
toggleVariable variable Flips a boolean
showElement / hideElement / toggleElement elementId Overrides a layer's visible state
showGroup / hideGroup / toggleGroup groupId Overrides a group's visibility, including its descendants
navigate screen Switches the launcher to a screen
launch — Starts the normal osu! launch flow from a click
openUrl url Asks first, then opens an http/https URL from a click

A group-level hide wins over its children. A group-level show reveals the group, including descendants that are hidden in the theme data. Element visibility and nested-group overrides still take priority over a group show.

Rules are evaluated in array order and every matching rule runs. Use conditions and once: true to gate rules. Rules are limited to 100 per theme, 12 conditions per rule and 20 actions per rule. Variables are scoped to the active theme session, start at 0, and are reset when the theme changes. once: true stops a rule after it matches once.

Rules run in the launcher, not in the editor preview. Use Save theme and then test the saved theme in the launcher. launch and openUrl are deliberately restricted to click triggers so loading, timers or screen changes cannot leave the launcher unexpectedly. Before opening an external URL, the launcher shows a blocking confirmation dialog with the full destination; links never open on their own.

Advanced scripts

Every rule can use an advanced script instead of its builder actions. Set script to a string; when the field is present, it replaces actions for that rule. The builder actions stay in theme.json so switching back in the editor restores them.

"behaviors": [
  {
    "id": "advanced_click",
    "trigger": { "type": "click", "elementId": "button" },
    "actions": [],
    "script": "clicks += 1;\nif (clicks >= 3) {\n  showLayer(\"secret\");\n} else {\n  hideLayer(\"secret\");\n}"
  }
]

Scripts use plain JavaScript syntax, but they are never passed to eval. The launcher parses each script into a small, safe instruction set and runs only that subset. There is no DOM, browser API, network access, import or eval, and unsupported syntax is reported as an error in the editor. The earlier keyword syntax (set, show layer, repeat) is no longer accepted.

Supported statements:

JavaScript Effect
let name = expression; / const / var Stores a number, string or boolean
name = expression; Assigns a value
name += 1;, -=, *=, /=, %= Updates a number
name++;, name--; Increments or decrements
showLayer("id"); Shows a layer
hideLayer("id"); Hides a layer
toggleLayer("id"); Flips a layer's visibility
showGroup("id"); Shows a group and its descendants
hideGroup("id"); Hides a group and its descendants
toggleGroup("id"); Flips a group's visibility
if (expression) { ... } Runs a block when the expression is truthy
if (...) { ... } else { ... } Adds an else branch, including else if
for (let i = 0; i < 3; i++) { ... } Runs a bounded loop
while (expression) { ... } Runs a bounded loop
goTo("themes"); Navigates to home, themes, settings or login
openUrl("https://..."); Opens an http/https URL from a click
launch(); Starts the normal osu! launch flow from a click
stop(); Ends the current script immediately

Comments use // or /* ... */. Semicolons are optional, but one statement per line is easiest to read.

Expressions support numbers, strings, template literals, true, false, variables, screen (the current screen name), read-only clock values (time, time12, timeSeconds, hour, minute, second, dayPeriod), + - * / %, == != < <= > >= (=== and !== also work), && || !, the ternary condition ? a : b, and parentheses. Math.min, Math.max, Math.round, Math.floor, Math.ceil, Math.abs and Math.random are available; the shorthand min(), max(), round() and the other built-ins below work too.

Not supported: functions, classes, arrays, objects, destructuring, switch, try/catch, do...while, break/continue, async, imports, this, regular expressions and BigInt. Assignments must be their own statement; they cannot appear inside an expression.

Built-in functions:

Function Result
min(a, b, …) / max(a, b, …) Smallest or largest number
clamp(value, min, max) Clamps a number to a range
round(value), floor(value), ceil(value), abs(value) Numeric helpers
random() A random number from 0 to 1
chance(percent) A random boolean with the given percentage
visible("elementId") Whether a layer is currently visible
groupVisible("groupId") Whether a group is currently visible
contains(text, search) Whether text contains a substring
length(text) String length
number(value), string(value), boolean(value) Explicit conversions

Scripts have a 20,000 character limit, loops are capped at 1,000 iterations and each trigger is limited to 10,000 executed statements. launch() and openUrl() still only run from explicit click triggers. Use a timer trigger instead of waiting inside a script. Variables changed by a script can be shown in any text layer with a {name} placeholder, for example Clicks: {clicks}.

Custom CSS

The css field is appended while the theme is active. Theme authors can use it for small tweaks, but it is not required. The advanced editor applies it live to the canvas, with selectors scoped to the 1000 × 650 preview so the editor window itself keeps its own styling. Example:

:root {
  --radius: 1rem;
}

Layers and groups can expose stable hooks through cssClasses:

{ "id": "hero", "type": "text", "cssClasses": "hero-title glow", "text": "Welcome" }
.hero-title {
  letter-spacing: 0.04em;
}

.hero-title.glow {
  text-shadow: 0 0 24px #ffd37a;
}

A group's classes are applied to every layer inside it, including nested groups. Class names are space-separated and limited to letters, numbers, _, - and a leading -; up to 20 classes per layer or group. Use the CSS classes section in the layer or group inspector to edit them.


Using the advanced editor

  1. Open Themes in the launcher.
  2. Select Create theme, or the pencil button on an installed API 2.0 theme.
  3. The editor opens in its own resizable window, not a modal. The center is a full launcher home canvas at 1000 × 650.
  4. Use the + Add menu above the layer list to add text, image, video and shape layers. Drag layers on the canvas, resize with the corner handles, or edit exact values in the right inspector. Shift/Cmd-click a layer or drag a marquee over empty canvas to select several layers. Multi-selections can be dragged together and aligned or distributed from the right panel. Layers snap to canvas edges, the canvas center, and other layers while dragging; hold Alt to disable snapping. Right-click a layer for copy, cut, paste, duplicate, group, ordering, hide/lock and delete actions, or right-click empty canvas to add or paste a layer.
  5. The layer list supports double-click rename, hide/show, lock/unlock and drag-and-drop. Drag a layer by its grip handle: drop it between two layers to reorder it and adopt that row's group, drop it onto a group to move it inside, or drop it in the move out of all groups zone to return it to the root. Groups are draggable too, so nested groups can be moved into or out of other groups. Select several layers and choose Group selection. Groups can be collapsed, hidden, locked and renamed; right-click a group to copy, cut, paste into it, or use its other quick actions. Hidden layers remain in the package, while locked layers cannot be moved or resized on the canvas. Select a layer or a group to edit its Filters (blur, brightness, contrast, saturation, hue, grayscale, sepia and invert) in the right inspector. Group filters and opacity apply to every layer inside. The Transform section provides translate, independent X/Y scale, rotation, X/Y skew and flips for both layers and groups. Group transforms are anchored to the bounds of their layers. Text layers also expose bold, italic, underline, strikethrough, case, gradient fill, outline and shadow controls in the inspector.
  6. Use the Theme tab for metadata, background, effects, typography, icon set and custom CSS. Open the Fonts tab to search and download from every Fontsource family, or import WOFF2, WOFF, TTF and OTF files. Downloaded fonts are copied into the theme's assets/ folder when you save.
  7. Open the Logic tab to add WHEN / IF / THEN rules and search them by name or target. Mark the layers that should react to the pointer as Interactive, then pick them in a click, hover or leave trigger. Link a rule to a layer or group with Linked to; linked rules also appear in that layer or group inspector with an inline editor and an Add rule shortcut. In Then, use the Builder / Script switch for a safe advanced script with expressions, if/else, repeat, variables and all the normal actions. The script opens in a Monaco editor with line numbers, syntax highlighting, variable/layer/group completion, hover documentation and inline error markers. Use the expand button for a fullscreen editor. Logic badges in the layer list open the linked editor, and the arrows above the list (or Alt/Option+ArrowUp/Down) jump between layers and groups that have rules. Rules run in the launcher, so save the theme before testing them.
  8. Select Save theme to write theme.json and copy new assets into ~/.ezpplauncher/themes. A fresh assets/preview.png thumbnail is rendered from the theme and the launcher applies the saved theme immediately. Unsaved edits are autosaved as a local draft and offered for recovery after a crash.
  9. Select Export to create a shareable .ezpplauncher-theme package. A theme check runs first and reports missing assets, oversized media, missing fonts and unused imports. Export saves and refreshes the preview, then reports the final package size.

Keyboard shortcuts:

  • Cmd/Ctrl+Z undoes the last change.
  • Cmd/Ctrl+Shift+Z or Ctrl+Y redoes it.
  • Cmd/Ctrl+S saves.
  • Cmd/Ctrl+C, Cmd/Ctrl+X and Cmd/Ctrl+V copy, cut and paste the selected layer(s) or group. Pasting a group keeps its nested groups, layers and filters.
  • Cmd/Ctrl+D duplicates the selected layer or multi-selection.
  • Cmd/Ctrl+G groups the selected layers, or ungroups the selected group.
  • Cmd/Ctrl+A selects every visible layer.
  • Shift/Cmd-click toggles a layer in the current selection.
  • Arrow keys nudge the selected layer(s) by 1 px; hold Shift for 10 px.
  • Alt/Option+ArrowDown and Alt/Option+ArrowUp jump to the next or previous layer or group with linked logic, expanding its inspector section.
  • Delete removes the selected layer(s).
  • Hold Alt while dragging to skip snapping.
  • Esc clears the selection or closes an open editor dialog.

Creating API 2.0 themes with the CLI

The create-ezpplauncher-theme CLI includes a declarative (API 2.0) template:

npx git+https://git.ez-pp.farm/EZPPFarm/create-ezpplauncher-theme.git my-theme

Choose declarative (API 2.0), edit theme.json, add files to assets/, then run:

npm run build

This produces dist/my_theme.ezpplauncher-theme. The template also includes preview.html for a standalone browser preview:

npx serve .

Compatibility and limits

  • API 2.0 packages must include theme.json; entry and style are not used.
  • Asset paths are resolved inside assets/; path traversal is rejected by the import commands.
  • Exports include only assets/ files referenced by theme.json (background, audio, fonts, media layers and preview). Asset paths mentioned by custom Svelte layers through asset('...') are kept as well. Orphaned files left in the folder are omitted, which keeps packages small. API 1.0 themes are exported whole because their bundles can load files dynamically.
  • The editor writes preview.png whenever you save or export. The generated thumbnail renders the background, effects, launcher UI, theme fonts, freeform layers, group visibility/opacity and filters; custom CSS is applied to the live editor canvas but not to the generated thumbnail.
  • The theme editor only writes inside ~/.ezpplauncher/themes.
  • API 1.0 themes are still loaded through their existing mountTheme bundle API.
  • Advanced scripts use JavaScript syntax but never run as JavaScript. Theme logic is compiled into the same restricted instruction set as the declarative triggers, conditions and actions above. There is no eval, DOM access or network API; launch() and openUrl() only run from explicit click triggers, and external URLs require a second confirmation in the launcher.
  • Custom Svelte layers are the exception: they are real JavaScript and are not sandboxed. They require a trust confirmation before activation. Keep the source self-contained and use asset() for packaged files so exports can keep the right assets.
  • Older launchers ignore unknown fields such as behaviors, so a theme with logic still loads, but its rules will not run until the launcher is updated.
  • Freeform layers are home-screen-only and pointer-transparent by default. Marking a layer interactive opts it into logic clicks/hovers; it still cannot add arbitrary controls or execute code.