focuspill

reference

Making a theme

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.

What is in the zip

my-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.

The smallest theme that works

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.

Packaging

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.

Tokens

Colours take #RRGGBB or #RRGGBBAA.

keywhat it paintsdefault
inkmain text (also sets inkDim unless you set that too)white
inkDimsecondary textink at 55%
accentprogress, selection, the start buttoncoral
successa finished sessiongreen
warninga paused or abandoned sessionamber
shellthe pill case, panel and window backgroundnear black
screenthe inset display and input fieldswhite at 10%
strokehairlines and borderswhite at 16%
onAccenttext sitting on an accent fillwhite
keywhat it changesdefault
fonta font family already installed on the machinethe system face
fontFilea font file shipped inside your zip, e.g. "MyFont.otf"none
lowercaselowercases every label in the appfalse
chrome"device" (inset screen in a case) or "flat""flat"
progress"bar" or "ring""ring"
keys"rounded" or "circle" for the hover buttonsfollows chrome
scanlinesfaint CRT lines over surfacesfalse
glowa soft halo on digits and facesfalse
pillWidth, pillHeightthe pill's size in points222 x 44
pillMargintransparent room around the pill for its shadow16
displayWidth, displayHeightthe inset screen, device chrome only122 x 32
cornercorner radius of the inset screen10
appearance"dark", "light", or omit to follow the systemsystem
backgroundwindow background if it should differ from shellshell
statusMark"face" for the pixel face, or any SF Symbol name"timer"
finishSounda macOS system sound (Glass, Ping, Hero, Blow) or a file in your folder"Ping"
keySoundthe click a key makes: click, thock, a system sound, a file, or "" for silence
warmUptrue makes the screen come on like a tube when a session starts
fontScalemultiplies every type size. Wide faces like Press Start 2P need about 0.81.0

Sounds

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.

Strings

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

Slots are the optional places the app offers you. Fill the ones you want, ignore the rest.

slotwhere it shows
pillLeadingon the case before the display, room for a face
pillDisplaythe pill's main area, the clock by default
pillTrailingafter the display, the goal by default
panelAccessorytop right of the goal panel
historyAccessoryabove an empty session list
menuBarAccessoryafter 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:

tint takes a token name (ink, inkDim, accent, success, warning, screen, shell) or a hex value.

Faces

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.

When something is wrong

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.

A full example

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.