reference
A theme is a folder with one theme.json in it, zipped. Drop it in
the themes folder and the app installs it.
No rebuild. No Xcode. No restart.
Copy this whole page as markdown and paste it into Claude, or install the skill and it knows the format already.
Download the skillmy-theme/
├── theme.json the only required file
├── MyFont.otf optional, referenced by fontFile
├── done.wav optional, any moment can have one
└── ghost.png optional, usable in any slot
Zip the folder itself. The app accepts the theme.json at the top or one level down, which is what Finder's Compress gives you.
A theme is a folder, zipped. One theme.json is the only file it has to contain. It can also carry a font, sounds and images, and they travel with it:
my-theme/
├── theme.json
├── MyFont.otf
├── done.wav
└── ghost.png
Drop the zip in the themes folder and the app installs it on its own: no rebuild, no Xcode, no restart.
~/Library/Application Support/FocusPill/Themes/
Settings… › Theme › Open folder takes you straight there.
theme.json:
{
"id": "midnight",
"title": "midnight",
"tokens": { "ink": "#C8D3F5", "accent": "#82AAFF", "shell": "#1B1E2B" }
}
Zip the folder, drop it in, done. Everything you leave out keeps its standard value.
bash scripts/pack-theme.sh design/themes/midnight # writes midnight.zip
bash scripts/pack-theme.sh design/themes/midnight --install # and installs it
Or by hand: select the folder in Finder, right click, Compress. Both layouts work, the zip can hold theme.json at the top or the folder that contains it.
Colours take #RRGGBB or #RRGGBBAA.
| key | what it paints | default |
|---|---|---|
ink | main text (also sets inkDim unless you set that too) | white |
inkDim | secondary text | ink at 55% |
accent | progress, selection, the start button | coral |
success | a finished session | green |
warning | a paused or abandoned session | amber |
shell | the pill case, panel and window background | near black |
screen | the inset display and input fields | white at 10% |
stroke | hairlines and borders | white at 16% |
onAccent | text sitting on an accent fill | white |
| key | what it changes | default |
|---|---|---|
font | a font family already installed on the machine | the system face |
fontFile | a font file shipped inside your zip, e.g. "MyFont.otf" | none |
lowercase | lowercases every label in the app | false |
chrome | "device" (inset screen in a case) or "flat" | "flat" |
progress | "bar" or "ring" | "ring" |
keys | "rounded" or "circle" for the hover buttons | follows chrome |
scanlines | faint CRT lines over surfaces | false |
glow | a soft halo on digits and faces | false |
pillWidth, pillHeight | the pill's size in points | 222 x 44 |
pillMargin | transparent room around the pill for its shadow | 16 |
displayWidth, displayHeight | the inset screen, device chrome only | 122 x 32 |
corner | corner radius of the inset screen | 10 |
appearance | "dark", "light", or omit to follow the system | system |
background | window background if it should differ from shell | shell |
statusMark | "face" for the pixel face, or any SF Symbol name | "timer" |
finishSound | a macOS system sound (Glass, Ping, Hero, Blow) or a file in your folder | "Ping" |
keySound | the click a key makes: click, thock, a system sound, a file, or "" for silence | |
warmUp | true makes the screen come on like a tube when a session starts | |
fontScale | multiplies every type size. Wide faces like Press Start 2P need about 0.8 | 1.0 |
finishSound covers the end. sounds covers every other moment, including opened, which fires when the goal panel comes up. Each value is either a macOS system sound or a file you ship:
"sounds": {
"opened": "open.wav",
"started": "start.wav",
"paused": "Tink",
"resumed": "Pop",
"finished": "done.wav",
"stopped": "Bottle"
}
Every one is optional: leave a moment out and it stays silent. All of it obeys the sound switch in settings, so someone who turned sound off hears nothing no matter what a theme asks for.
Every label the app draws goes through one place, so a theme can replace any of it: another language, shorter wording, or an emoji instead of a word. Keys are the English the app ships with.
"strings": {
"What are you working on?": "Sur quoi tu bosses ?",
"Start": "C'est parti",
"min": "min",
"Focused today": "Concentré aujourd'hui",
"Sessions…": "Séances…",
"Break": "Pause",
"pomodoro": "🍅",
"stopwatch": "⏱"
}
The arcade theme in the repo uses this to speak like a cabinet: "insert coin" instead of "What are you working on?", "high scores" instead of "Sessions".
Slots are the optional places the app offers you. Fill the ones you want, ignore the rest.
| slot | where it shows |
|---|---|
pillLeading | on the case before the display, room for a face |
pillDisplay | the pill's main area, the clock by default |
pillTrailing | after the display, the goal by default |
panelAccessory | top right of the goal panel |
historyAccessory | above an empty session list |
menuBarAccessory | after the countdown in the menu bar, text or emoji only |
Each slot maps a moment to what to draw. The moments are idle, running, finalMinute (the last 60 seconds), paused, finished and resting (a break). default covers the ones you did not name.
A stopwatch session has no finalMinute and never reaches finished on its own, so themes that lean on those moments should still look right in running.
"slots": {
"pillLeading": {
"running": { "animation": [{ "face": "focused", "hold": 3.4 }, { "face": "blink", "hold": 0.2 }], "tint": "accent" },
"paused": { "face": "sleepy", "tint": "warning" },
"finished": { "emoji": "🎉", "size": 16 }
},
"pillDisplay": { "default": "clock", "finished": { "face": "happy", "tint": "success" } },
"menuBarAccessory": { "finalMinute": { "text": "!", "tint": "warning" } }
}
What you can put in a slot:
"clock" or "goal", the two built-in pieces{ "face": "happy", "tint": "accent" }, a dot matrix face{ "animation": [{ "face": "focused", "hold": 3.4 }, …], "tint": "accent" }, frames on a loop, hold in seconds. This is how a face blinks.{ "emoji": "🙂", "size": 16 }{ "image": "ghost.png", "size": 20 }, a picture from your folder{ "symbol": "cup.and.saucer.fill", "tint": "ink" }, any SF Symbol{ "text": "go", "tint": "accent" }tint takes a token name (ink, inkDim, accent, success, warning, screen, shell) or a hex value.
Faces are 8x8 grids. # lights a pixel, anything else stays dark. Six come with the app: happy, wink, focused, alert, sleepy, blink (same grid as sleepy), mark.
Add your own under faces and use them by name anywhere a face is accepted:
"faces": {
"owl": [
"........",
"##....##",
"#.#..#.#",
"##....##",
"...##...",
"..####..",
"...##...",
"........"
]
}
Grids do not have to be 8x8, they just have to be rectangular. Bigger grids draw bigger.
Settings shows the first problem under the theme list: bad JSON, a missing font file, a font that is not installed. Fix the file and press Reload, or just save it, the folder is watched.
A theme whose id matches a built-in one (phosphor, native) is ignored, so pick your own.
design/themes/ in the repo has three: arcade ships its own font, system is black and white with nothing else, signal is the light one. design/theme-template/ is a blank to copy.