idle-art
A Claude Code Mod that draws an ASCII animation above the prompt while the model works. Five scenes are built in: matrix rain, fire, a star field, an aquarium and a cat. You can add your own: /idle-art import turns a GIF into a character clip and keeps it for every project. Display only: nothing reaches the model, so the mod costs no tokens and does not touch the prompt cache.
What it shows
The picture appears 3 seconds into a turn, so a short turn shows nothing, and it goes away when the turn ends. By default each turn draws a scene at random from the built-in scenes and your saved clips, never the one shown before. A long turn moves on to another at random: a built-in scene after 20 seconds, the cat and a clip at the end of their first round or loop that ends after 20 seconds, so a long clip plays through once and a short one repeats until then. A scene you chose by name stays for the whole turn, and choosing one while a turn draws changes the picture at once.
● Brewing… (5s)
.: . ,
, ::,,,
::;;:; ;;::; .
, ;:+ :*;++++ , ,,
:::*oO;*:;*:+: :+++,::**o ++:, :
;,, ;,;+;+::+;,;+:**+**,; ; ,
: +;*:+**o*;;;;*+**:,*:;;***o:,::;;
:,::;oOoO*ooOOO#oO*o*O*o**OOOoOo;+;,
| Scene | What moves |
|---|---|
matrix |
Streams of half-width katakana and digits fall in green, a bright head over a fading trail; glyphs under a trail flicker. |
fire |
Heat rises from a hidden row under the band and cools on the way up, drawn from . to @ and from dark red to pale yellow. |
stars |
Stars fly toward the viewer from the centre, growing from a grey · to a white ✦ as they near. |
aquarium |
Fish of four shapes cross both ways, bubbles rise from them and grow, seaweed sways on the sand. |
cat |
A cat walks in slowly to the middle, head first, sits and blinks, and says meow; then its tricks in a new order each round: it rolls on the ground and back, jumps three times, purrs with a heart floating up, and looks left and right. It says MEOW!, walks out slowly to the right and comes round again. Each word stands in a speech bubble over its head. A round takes about 34 seconds, and random waits for the cat to walk out before it moves on. |
● Brewing… (8s)
.------.
( meow )
'------'
/
/\_/\
( o.o )
> ^ <
(_|_)~
The band takes at most 8 rows and 100 columns, fewer when the terminal has less room, and draws nothing in a band under 3 rows. It draws on the terminal only, and gives way to a survey. The scenes draw about 60 frames a second, and each moves by the time that passed, so the rain, the stars, the fish and the cat glide a step on every frame. The fire's heat changes ten times a second, and a clip changes at its own frame delays.
Your own GIFs
/idle-art import ~/Downloads/kitty.gif kitty
The mod reads the GIF, decodes every frame with its own delay and transparency, and turns each frame into characters: every cell takes the mean colour of its pixels, and a glyph from .:-=+*#%@ by its brightness, spread over the clip's own darkest to brightest cell. A cell mostly transparent stays blank. The picture keeps its shape and fills the 8 rows of the band; a cell counts as twice as tall as wide. The clip then plays in the middle of the band, each frame for its own delay, in a loop.
● Brewing… (6s)
....=*******++=+***+====-....
....=******=----=*#=====-....
....-+++++*==+----===---:....
....:--=###++*=-:-=-:---:....
....-+#%#%*--==-:-:-==--:....
....+**#*++-:-:---::::--:....
....-====+-=:--:::::.:-+-....
....:-=*++*+++=-:-=--=-=-....
A clip is kept under 90,000 characters, because that is what one drawing may hand the drawing thread. A longer clip keeps every other frame, each kept frame showing for the time of both, and does so again until it fits; the answer says how many frames stayed. The clips live in the mod's store, which holds 4 MiB in all, so about forty clips fit; a clip that does not fit is refused with the reason.
A name is lowercase letters, digits and dashes, up to 24 characters, and cannot be a built-in scene or a word the command reads. Importing under a saved name replaces that clip. A path starting with ~ is under your home directory, and a relative path is under the session's directory. A path may hold spaces: the last word is the name.
Command
/idle-art the state: on or off, the style, the delay
/idle-art on | off draw or stop drawing
/idle-art <scene or clip> always draw that one: matrix, fire, stars, aquarium, cat, or a saved clip
/idle-art random a new scene or clip each turn and every 20 seconds or so (the default)
/idle-art delay <n> wait n seconds into a turn, 0 to 60 (default 3)
/idle-art import <gif path> <name> turn a GIF into a clip and keep it under that name
/idle-art list the built-in scenes and the saved clips
/idle-art remove <name> delete a saved clip; a style set to it goes back to random
/idle-art help
The settings and the clips stay in the mod's store, shared by every project, and survive updates.
How it draws
An AbovePrompt ui.render hook mounts a Client element while isWorking is true. The Client runs hooks/scene.tsx on the drawing thread: a 16 ms surface.every tick advances the scene by 16 ms and asks for the next frame, so no hook runs per frame. Measured on 2.1.283, the frame clock keeps that rate: the cat, which walks 5 cells a second, moved 5 cells in each second of a live band. Each built-in scene is a pure module under hooks/art/ that answers a grid of cells; a saved clip reaches the drawing thread in the Client's props and plays from hooks/clip.ts. A row draws as one Text per run of one colour. The hooks module picks the scene and a random seed when the band first shows a working turn, a $.clock.after timer redraws the band once the delay has passed, and the main loop's turn.complete ends the turn, so the next one picks again. Under random the drawing thread counts a scene's time itself, and once it has run it posts the scene's name with surface.post; the ui.message hook picks the next scene and answers with its props, which the running instance takes in place. A message that names a scene no longer showing changes nothing, so a late or repeated post cannot skip one. No clip travels to the drawing thread before its turn to show, because one clip may take most of the 100,000 characters a Client's props hold. The GIF decoder in hooks/gif.ts is written for this mod and needs no tool on the machine.
Install
claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install idle-art@kilimcininkoroglu-mods
Function hooks are early access. Nothing loads without the flag:
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude
Load it from a local checkout for one session:
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --plugin-dir plugins/idle-art
To keep the flag on, add this to ~/.claude/settings.json:
{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } }
After installing
Restart Claude Code, or run /reload-plugins in an open session. The mod is on after an install; /idle-art off turns it off.
What it can reach
Validated with claude plugin validate on Claude Code 2.1.283:
❯ ./register.tsx hooks: session.start, ui.render{component=AbovePrompt}, ui.message, turn.complete, command.run{command=idle-art}
❯ ./register.tsx calls: $.clock.after (via beginTurn), $.clock.now (via sceneFor), $.command.register, $.env.get (via resolvePath), $.fs.exists (via readGifBytes), $.fs.read (via readGifBytes), $.fs.stat (via readGifBytes), $.process.spawn (via streamedStdout), $.session.cwd (via resolvePath), $.store.delete (via removeClip), $.store.get (via loadClips, loadConfig), $.store.set (via importGif, removeClip, saveClips, setting), $.ui.invalidate (via beginTurn, setting), $.ui.resolve
❯ ./register.tsx env reads: HOME
❯ ./register.tsx surface modules: hooks/scene.tsx
Reach L2, runs base64 to read a GIF over 4 MiB.
1. Reads: the band's props (working, survey, rows, columns) and the clock; its settings and clips from the store; the GIF a /idle-art import names, once, when it is 32 MiB or smaller; HOME and the session's directory to resolve that path
2. Runs: base64 -i <path> for a GIF over 4 MiB, once per import; no fork; one timer per turn for the delay, and the drawing thread's 100 ms tick while the band shows
3. Sends: nothing; no network call and nothing to the model
4. Persists: the on/off state, the style, the delay and each imported clip in the mod's store
5. Hostile input: a GIF is untrusted bytes: the decoder bounds every read by the file's length, refuses a broken code stream or a missing color table, stops at 500 frames, and an import that fails keeps nothing; a stored clip of the wrong shape is skipped at load; the command takes a fixed word list, a whole number from 0 to 60, and a clip name of lowercase letters, digits and dashes
Limits
- 8 rows is a small canvas: a GIF becomes about 30 columns by 8 rows at the usual 2:1 shape, enough for a silhouette or a motion, not for detail.
- A GIF of up to 4 MiB is read with
$.fs.read. A larger one, up to 32 MiB, is read throughbase64 -i <path>piece by piece, because$.fs.readrefuses a file over 4 MiB and$.process.runcuts its output at 4 MiB (measured on 2.1.283). The bytes that come back must match the file's size, or the import is refused. - Each frame is reduced to its cells as it is decoded, so a large GIF holds one frame of pixels at a time. A GIF stops at 500 frames.
- A long GIF loses frames to the 90,000-character limit; its motion stays as long, but steps more coarsely.
- The terminal draws the colours with its own palette; a terminal without true colour shows the nearest of its 256 colours.
matrixdraws half-width katakana, andstarsdraws∗and✦. A font without those glyphs draws a replacement character.
Development
make install # eslint, typescript-eslint, typescript
make lint # complexity limit 10, fails the build above it
make typecheck # needs .claude/types/ from /plugin-types
make validate
make test # claude plugin test