Guides

Your own mascot

Make the bar's mascot yourself: a pet sheet or a character.json, the rules it keeps, and a prompt for your agent.

Where mascots come from

Settings → Mascot → Look offers Evlat’s own mascots first, then every one it finds in two places, one folder a mascot:

  • ~/.config/evlat/mascots/<name>/: yours. The Open Folder button beside “Your own mascots” makes it and opens it.
  • ~/.codex/pets/<name>/: the pets you keep in Codex, read as they are (petdex installs there too). Evlat never writes in this folder.

Evlat reads both when it starts and each time you open Settings → Mascot; nothing watches the disk. A folder it can’t offer is listed under the tiles with the reason, and the mascot you had chosen stays chosen until its folder is gone, when the cube comes back.

A folder holds one of two things: a pet (pet.json beside one picture sheet: frames drawn as pictures) or a character (character.json: shapes that move, as Evlat’s own mascots are made). With both in one folder, the character is read.

A pet: frames as pictures

The format is the Codex app’s, so a pet made for Codex (with its hatch-pet skill, or from petdex) works as it is. pet.json:

{
  "id": "mochi",
  "displayName": "Mochi",
  "description": "A sleepy cat.",
  "spritesheetPath": "spritesheet.webp"
}

The sheet is a PNG or WebP of 8 columns of 192×208 cells: 1536×1872 (9 rows) or 1536×2288 (11 rows); any other size is refused. spritesheetPath must stay inside the folder. Evlat plays these rows:

RowCodex calls itEvlat plays it
0idle (6 frames)idle: once through, then still on its first frame for 6 s
7running (6)working: once through, still for 2.5 s
6waiting (6)waiting: twice on arrival, then held
8review (6)done: once on arrival, then held
5failed (8)failed: once through, still for 6 s
3waving (4)a wave 45 s into a wait, and again at 3 minutes

The rows play in bursts, not Codex’s endless loop: a sheet that never stops is the continuous drawing Evlat keeps off your CPU. Rows 1, 2 and 4 (walking and jumping) and an 11-row sheet’s looking rows are not played. Each frame keeps Codex’s own timing.

A character: shapes that move

character.json describes a mascot the way Evlat’s cube, Pati, Bit and Puf are written: a tree of parts, each moved by bindings from named controls. Evlat’s own animations move the standard controls, so a character that binds them already blinks, looks at the cursor, widens its eyes when a session waits on you and squints while it works, with nothing else written. Only version and root are required.

{
  "version": 1,
  "name": "Hap",
  "root": {
    "name": "hap",
    "bindings": [
      { "control": "scaleX", "property": "scaleX", "from": [0, 2] },
      { "control": "scaleY", "property": "scaleY", "from": [0, 2] },
      { "control": "tilt", "property": "rotation", "from": [-180, 180] }
    ],
    "children": [
      { "name": "body", "shape": { "roundedRectangle": { "cornerRadius": 0.4 } },
        "fill": "#EBEBEB", "size": [0.9, 0.8] },
      { "eye": { "side": -1, "width": 0.12, "height": 0.26, "gap": 0.18, "gaze": [0.1, 0.07],
                 "fill": { "name": "black", "opacity": 0.92 } } },
      { "eye": { "side": 1, "width": 0.12, "height": 0.26, "gap": 0.18, "gaze": [0.1, 0.07],
                 "fill": { "name": "black", "opacity": 0.92 } } }
    ]
  }
}

Positions and sizes are fractions of the mascot’s side, measured from its centre, y pointing down: [0, 0] is the middle, [0, -0.5] the top edge. On the bar the mascot is 34 points square, so a detail smaller than about 0.05 doesn’t read. Two worked examples, each read back by Evlat’s tests equal to the Swift it was written from: Pati (ears, nose, whiskers, a gesture and its rules) and the lantern (a state of its own, conditions, weights).

Parts

FieldMeaning
nameRequired, unique in the character.
shapeWhat it draws (below); left out, the part only carries its children.
fillIts colour; clear when left out.
center[x, y] where the shape sits at rest; [0, 0] by default.
size[width, height] of the shape at rest; [1, 1] by default.
pivot[x, y] the part scales and turns about; [0, 0] by default.
bindingsWhat moves it (below).
childrenParts drawn over this one, moved, scaled and turned with it.

Parts are drawn in order, back to front. Instead of a part, {"eye": …} is Evlat’s own eye, bound the way the cube’s eyes are, so it blinks, squints, widens and looks in the shared language: side (-1 left, 1 right), y, width, height, gap (the space between the two eyes), gaze ([x, y] it travels at a full look) and fill.

Shapes

{ "roundedRectangle": { "cornerRadius": 0.3 } }
{ "capsule": { "minimumHeight": 0.35 } }
{ "polygon": { "points": [[0, 0], [1, 0], [0.5, 1]], "cornerRadius": 0.02 } }
{ "image": "face.png" }
{ "cells": { "image": "eyes.png", "columns": 4, "rows": 1, "by": "lid" } }
  • cornerRadius is a fraction of the mascot’s side, so a part that grows keeps its corners; a polygon’s corners shrink where its edges are too short for them.
  • A capsule is never flatter than minimumHeight times its width: shut, an eye is a slit.
  • A polygon’s points are fractions of the part’s own frame (0 to 1, y down). It may carry morphs (below).
  • image draws a picture from the folder, fitted into the part. cells cuts a picture into a grid and draws the cell a control names, rounded, row by row from the top left; move that control with "move": "cut" steps. Pictures are PNG or WebP, inside the folder.

Colours

"#EBEBEB"                             hex, or "#RRGGBBAA" with alpha
{ "name": "black", "opacity": 0.92 }  black, white, clear, gray, red, orange, yellow, green, blue, pink, purple
{ "white": 0.92 }                     a grey
{ "red": 0.96, "green": 0.7, "blue": 0.74, "opacity": 1 }

The bar’s rings speak in amber (waiting), green (done) and red (failed). A body in those colours reads as a signal; keep them for what means one.

Controls and bindings

A binding maps a control onto a property of its part, linearly, clamped to its input:

{ "control": "yaw", "property": "offsetX", "from": [-1, 1], "to": [-0.1, 0.1] }

Left out, to is from: the control passed through. Properties: offsetX, offsetY, rotation (degrees) add; width, height (the part’s own shape), scaleX, scaleY (the whole part about its pivot) and opacity multiply, so several controls can move one property. Because input is clamped, a binding can be one-sided: "from": [1, 1.4] moves only once the eyes open past rest.

The standard controls, which Evlat’s animations move:

ControlRangeMeans
yaw, pitch-1 to 1Where it looks: left/right, up/down (the cursor, mostly).
eyeOpen1 at rest0 shut (a blink), above 1 wide.
eyeSquint0 to 1A lid coming down in focus.
scaleX, scaleY1 at restBreath, squash and stretch.
tiltdegreesA head tilt.

Your own controls are declared at the top level, with the range a clip may set them in and where they rest:

"controls": { "glow": { "lower": 0, "upper": 1, "rest": 0.3 } }

Morphs: an outline that moves point by point

A binding moves a whole part. A morph moves a polygon’s points each their own way: an eye that shuts from the top, rounds out in surprise or pushes up from below in a smile, instead of squashing as a box would. Each morph is the whole outline at full weight, point for point with the polygon’s own points; its weight is the control over from, clamped onto 0 to 1:

{ "polygon": {
    "points": [[0, 0], [1, 0], [1, 1], [0, 1]], "cornerRadius": 0.01,
    "morphs": [
      { "control": "eyeOpen", "from": [1, 0], "points": [[0, 0.8], [1, 0.8], [1, 1], [0, 1]] }
    ] } }

Here the top edge comes down onto the bottom as eyeOpen falls from 1 to 0. Morphs add, so a blink and a smile compose; the spring carries every weight, so nothing snaps. A curved outline needs many points: sample each target from the same control points as the base, so point 3 is always the same place on the outline.

States, gestures and rules

A phase you don’t write plays Evlat’s own animation, built on these resting poses (fields left out are at rest: 1 for eyeOpen and the scales, 0 for the rest):

PhaseRests atAnimation
idlegazeMix 0.85loops: a blink now and then, a rarer breath
workingeyeOpen 0.92, eyeSquint 0.34, gazeMix 0.3loops: eyes on the work, a small bob
waitingeyeOpen 1.28, scaleX 1.03, scaleY 1.04once on arrival, then held: the one that must be noticed
revieweyeOpen 1.02, eyeSquint 0.12, gazeMix 0.6once: a lean, a nod, back upright
failedeyeOpen 0.55, eyeSquint 0.5, scaleX 1.07, scaleY 0.9, gazeMix 0.45loops slowly, with a shudder on arrival

gazeMix is how much of the cursor the eyes take (0 none, 1 all). To play a phase your own way, give it a clip; gestures of your own go in motions and play once, on top of a phase:

"states": {
  "waiting": { "loops": false, "steps": [
    { "pose": { "from": "waiting", "glow": 1 }, "move": "spring", "hold": 0.6 },
    { "pose": { "from": "waiting", "glow": 0.5 }, "move": 0.15, "hold": 0.3 },
    { "pose": { "from": "waiting", "glow": 1 }, "move": 0.15, "hold": 0.4 }
  ] }
},
"motions": {
  "pulse": { "steps": [
    { "pose": { "from": "waiting", "glow": 0.4 }, "move": 0.12, "hold": 0.2 },
    { "pose": { "from": "waiting", "glow": 1 }, "move": 0.12, "hold": 0.3 }
  ] }
}
  • A pose starts from a phase’s resting pose ("from") or from the neutral one, and sets fields by name: the standard controls, gazeMix, and your own controls.
  • move is how the step gets there: "spring" (the phase-change spring, 0.4 s), a number of seconds (eased), or "cut" (one frame, for picture cells).
  • hold is how long the step lasts from when it starts, its move included.
  • loops is false unless you say: false plays once and holds the last pose.

Rules say when a gesture plays. They are read when a phase starts, when the sessions change, when a gesture ends, and at a time a rule itself named; never on a timer of their own:

"rules": [
  { "phase": "waiting", "after": 45, "play": ["pulse"] },
  { "phase": "working", "when": [{ "fact": "news", "atLeast": 1 }], "after": 1, "every": 20,
    "play": ["pulse", { "motion": "sway", "weight": 3 }] }
]

after is seconds into the phase; every plays it again so often (left out, once a phase); when counts live sessions (waiting, working, or news for finishes not yet seen); play picks one gesture, by weight. Of the rules due at once, the one furthest into the phase plays.

The rules a mascot keeps

Evlat holds every mascot it finds to the same rules as its own, and leaves out one that breaks any:

  • The five states read apart. No two rest at the same pose, and waiting arrives moving.
  • Motion comes in bursts. A phase that loops (idle, working, failed) moves at most 35% of its cycle, a repeating rule’s gestures included; a still mascot draws nothing, which keeps Evlat off your CPU.
  • Every state starts on the spring. A phase’s first step uses "spring".
  • Loops are seamless. A loop’s last step is the pose its first step has.
  • Nothing rests tilted. Lean in a gesture or on arrival, then come back upright.
  • Gestures give the phase back. A motion plays once and ends on the resting pose of the phase its rule plays it in; a rule names a motion that exists, a weight above 0, and in a looping phase waits at least 0.4 s.
  • Steps take real time. Each hold is above 0 and at least as long as its move.
  • Names add up. Part names are unique; a binding, a morph or a cells shape names a standard or declared control; a declared control is not a standard name, its range is not empty and it rests inside it; a clip sets your controls only within their ranges; a morph has as many points as its polygon.
  • Files stay small and inside. character.json up to 1 MB, pet.json up to 64 KB, pictures inside the folder, a cells grid up to 1024 cells.

Check it

evlat mascot check reads a folder as Settings does and prints every rule it breaks, where Settings shows only the first. --preview draws its five states, a blink and a look each way, large and at the bar’s size:

evlat mascot check ~/.config/evlat/mascots/hap --preview /tmp/hap.png

It exits 0 when Settings would offer the mascot, 1 when it wouldn’t, 2 on a wrong call. See evlat mascot.

What a file can’t say

  • No functions or loops: two ears are two parts, written out. eye is the one shorthand.
  • A state is a whole clip: “Evlat’s waiting, with my glow on” is every step written out, from "from": "waiting" poses.
  • Colours are fixed; a mascot can’t follow the app’s own colours.
  • Shapes are flat fills, so shading and perspective come from pictures: a pet sheet, or image and cells parts.

Let your agent make one

Paste this into Claude Code, Codex or another coding agent, with your description in place of the brackets:

Make me a mascot for Evlat, the macOS status bar for AI coding sessions.

The mascot I want: [describe it: what it is, its colours, how it should look when it waits on me].

1. Read the mascot guide: https://evlat.kalaomer.com/docs/mascots
   If it doesn't load, read the "Your own mascot" section of
   https://github.com/evlat/evlat/blob/main/README.md and the worked example
   https://github.com/evlat/evlat/blob/main/Tests/Fixtures/mascots/pati/character.json
2. Write it as ~/.config/evlat/mascots/<short-name>/character.json. Pictures, if any, go in the same folder.
3. Check it:
   evlat mascot check ~/.config/evlat/mascots/<short-name> --preview /tmp/<short-name>.png
   (no evlat on PATH: /Applications/Evlat.app/Contents/MacOS/Evlat mascot check …)
   Fix every rule it prints. Look at the preview image: it must read at the bar's 34 pt size,
   and its five states must look different. Repeat until it prints "ok".
4. Tell me to open Evlat → Settings → Mascot → Look and pick it.

Keep to simple shapes that read at 34 pt and say the states mostly with the eyes.
A likeness of someone else's character stays in my own folder; never publish it.