SLOPSHOPPER

abra

A pixel Abra that lives at the right edge of the band above the prompt

newbandcommandtimer
★ 64v0.3.0no licenseupdated 2026-10-08dgokcin/dotfiles/ai-stuff/claude/mods/abra
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · abra
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /abra ⎿ abra: Abra is now shiny. ⟨Claude Code's own drawing⟩ ▄▄▄▄▀▀▀▀▀▄▄▄▄ ▀▀▀▀▀▀▀▀▀▀▀▀▀ ▄▀▀▀▀▀▀▀▀▀▀▀▄ ▀▀▀▀▀▀▀▀▀▀▀▀▀ ▄▀▀▀▀▀▀▀▀▀▀▀▄ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ ▄▄▄▄▀▀▀▀▀▄▄▄▄ ▀▀▀▀▀▀▀▀▀▀▀▀▀ ▄▀▀▀▀▀▀▀▀▀▀▀▄ ▀▀▀▀▀▀▀▀▀▀▀▀▀ ▄▀▀▀▀▀▀▀▀▀▀▀▄
README

abra

A Claude Code mod that draws a pixel Abra at the right edge of the band above the prompt. It bobs in place while you're idle and paces back and forth while Claude works.

Requirements

  • Claude Code v2.1.287 or later
  • The terminal app. The Desktop app has no Raster, so the mod draws nothing there.
  • A truecolor terminal (iTerm2, Ghostty, kitty, WezTerm)

Usage

CommandEffect
/abraToggle between default and shiny, saved across sessions
/abra shiny, /abra defaultPick a variant
Ctrl+X Ctrl+ACollapse or expand the band (Claude Code's own binding)

Layout

PathContents
hooks/register.jsBand renderer, animation timer, /abra command
hooks/frames.jsGenerated pixel frames. Don't edit by hand.
sprites/*.gifSource GIFs, 32x32 at 2 frames each
scripts/build-frames.mjsRegenerates frames.js from the GIFs with ffmpeg
tests/abra.test.tsclaude plugin test suite

Development

node scripts/build-frames.mjs      # after changing sprites/
claude plugin validate .
claude plugin test
claude --plugin-dir .              # live-reloading session

The sprite is 19x19 pixels. Each terminal cell holds two vertical pixels with ▀/▄ half blocks, so Abra takes 19 columns by 10 rows inside a 40 column strip.

Credits

Sprites come from jakobhoeg/vscode-pokemon. Pokémon sprites are © The Pokémon Company / Nintendo / Game Freak, used here for a personal, non-commercial fan project.

Source 2 files
hooks/register.js 117 lines
1import { SPRITE } from './frames.js'
2
3// Abra lives at the right edge of the band above the prompt. It bobs in place
4// while you're idle and paces back and forth while Claude works.
5
6const TICK_MS = 150
7const FRAME_TICKS = 2
8const STRIP_COLUMNS = 40
9const DEFAULT_COLOR = 0x01000000
10const UPPER_HALF = 0x2580
11const LOWER_HALF = 0x2584
12const SPACE = 0x20
13
14let variant = 'default'
15let working = false
16let bandId = null
17let columns = STRIP_COLUMNS
18let tick = 0
19let x = STRIP_COLUMNS - SPRITE.width
20let facing = 'left'
21
22const rows = Math.ceil(SPRITE.height / 2)
23
24// Palette letter to 0xRRGGBB, per variant
25const COLORS = Object.fromEntries(
26  Object.entries(SPRITE.variants).map(([name, sheet]) => [
27    name,
28    Object.fromEntries(sheet.palette.map((hex, i) => [String.fromCharCode(97 + i), parseInt(hex.slice(1), 16)])),
29  ]),
30)
31
32// Look up a pixel's color in a frame, or null where it's transparent
33function pixelsOf(frame, colors, flip) {
34  return (px, py) => {
35    const row = frame[py]
36    if (!row) return null
37    return colors[row[flip ? SPRITE.width - 1 - px : px]] ?? null
38  }
39}
40
41// Pack the current frame into Raster cells, two pixels per cell with half blocks
42function cellsNow() {
43  const sheet = SPRITE.variants[variant]
44  const anim = working ? sheet.walk : sheet.idle
45  const frame = anim[Math.floor(tick / FRAME_TICKS) % anim.length]
46  const pixel = pixelsOf(frame, COLORS[variant], working && facing === 'left')
47  const words = new Uint32Array(columns * rows * 3)
48  for (let cy = 0; cy < rows; cy++) {
49    for (let cx = 0; cx < columns; cx++) {
50      const px = cx - x
51      const top = px >= 0 && px < SPRITE.width ? pixel(px, cy * 2) : null
52      const bottom = px >= 0 && px < SPRITE.width ? pixel(px, cy * 2 + 1) : null
53      const i = (cy * columns + cx) * 3
54      if (top !== null) words.set([UPPER_HALF, top, bottom ?? DEFAULT_COLOR], i)
55      else if (bottom !== null) words.set([LOWER_HALF, bottom, DEFAULT_COLOR], i)
56      else words.set([SPACE, DEFAULT_COLOR, DEFAULT_COLOR], i)
57    }
58  }
59  return new Uint8Array(words.buffer).toBase64()
60}
61
62// Move one column per tick while working, turning at the strip's edges
63function step() {
64  tick += 1
65  if (!working) return
66  const maxX = columns - SPRITE.width
67  if (facing === 'left' && x <= 0) facing = 'right'
68  else if (facing === 'right' && x >= maxX) facing = 'left'
69  x = Math.max(0, Math.min(maxX, x + (facing === 'left' ? -1 : 1)))
70}
71
72export function register(on) {
73  on('session.start', async ($, e, next) => {
74    await $.command.register({
75      name: 'abra',
76      description: 'Switch Abra between default and shiny',
77      argumentHint: '[default|shiny]',
78      immediate: true,
79    })
80    const saved = await $.store.get('variant')
81    if (saved in SPRITE.variants) variant = saved
82    $.clock.every(TICK_MS, () => {
83      step()
84      if (bandId !== null) $.ui.blit({ requestId: bandId, key: 'abra', columns, rows, cells: cellsNow() })
85    })
86    return next(e)
87  })
88
89  on('command.run', { command: 'abra' }, async ($, e) => {
90    const asked = e.args.trim()
91    variant = asked in SPRITE.variants ? asked : variant === 'shiny' ? 'default' : 'shiny'
92    await $.store.set('variant', variant)
93    $.ui.invalidate('ui.render')
94    return { text: 'Abra is now ' + variant + '.' }
95  })
96
97  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
98    if (e.surface !== 'terminal' || e.props.hasSurvey) {
99      bandId = null
100      return next(e)
101    }
102    const { Box, Raster } = $.ui.resolve(e)
103    working = e.props.isWorking
104    bandId = e.requestId
105    columns = Math.max(SPRITE.width, Math.min(STRIP_COLUMNS, e.props.bodyColumns))
106    x = Math.max(0, Math.min(columns - SPRITE.width, x))
107
108    const abra = Raster({ key: 'abra', columns, rows, cells: cellsNow() })
109    const theirs = await next(e)
110    return Box({
111      flexDirection: 'row',
112      justifyContent: theirs ? 'space-between' : 'flex-end',
113      children: theirs ? [Box({ flexGrow: 1, children: [theirs] }), abra] : [abra],
114    })
115  })
116}
117
hooks/frames.js 206 lines
1// Generated by scripts/build-frames.mjs from sprites/*.gif. Do not edit by hand.
2// Each frame is rows of palette letters; '.' is transparent.
3
4export const SPRITE = {
5  "width": 19,
6  "height": 19,
7  "variants": {
8    "default": {
9      "palette": [
10        "#000000",
11        "#483818",
12        "#b89828",
13        "#f8d860",
14        "#907860",
15        "#786038"
16      ],
17      "idle": [
18        [
19          "...................",
20          "...................",
21          "........aaaaa......",
22          "....bbbacdddcabbb..",
23          "....bddcdddddcddb..",
24          "....bcdddddddddcb..",
25          ".....bdddddddddb...",
26          "....bcdddddddddcb..",
27          "....acbadddddabca..",
28          "....acddadddaddca..",
29          ".....aadddddddaa...",
30          "....beeaadddaaeeb..",
31          "....affbcaaacbffa..",
32          ".....aaccbfbccaa...",
33          "...bbdcaccbccacdbb.",
34          "...bdccaddaddaccdb.",
35          "....bbccaaaaaccbb..",
36          "......aa.....aa....",
37          "..................."
38        ],
39        [
40          "...................",
41          "........aaaaa......",
42          "....bbbacdddcabbb..",
43          "....bddcdddddcddb..",
44          "....bcdddddddddcb..",
45          ".....bdddddddddb...",
46          "....bcdddddddddcb..",
47          "....acbadddddabca..",
48          "....acddadddaddca..",
49          ".....aadddddddaa...",
50          "....beeaadddaaeeb..",
51          "....affbcaaacbffa..",
52          ".....aaccbfbccaa...",
53          "...bbdcaccbccacdbb.",
54          "...bdccaddaddaccdb.",
55          "....bbccaaaaaccbb..",
56          "......aa.....aa....",
57          "...................",
58          "..................."
59        ]
60      ],
61      "walk": [
62        [
63          "...................",
64          ".........bb........",
65          "........bccbba.....",
66          "........aabcddba...",
67          ".......addcbdddda..",
68          ".......accddddddda.",
69          "........accddddddb.",
70          ".......bcbdddddddda",
71          ".......acddddbadddb",
72          ".......adddddddaddb",
73          ".bb.....accdddddbb.",
74          "bcb....afaaaaaaa...",
75          "bccbbb.afffab......",
76          "bccfccbcaaacbbb....",
77          ".bffcccaffbacaaa...",
78          "..bbcccabbccacda...",
79          "....babbaccccdb....",
80          ".........aabccb....",
81          "............bb....."
82        ],
83        [
84          ".........bb........",
85          "........bccbba.....",
86          "........aabcddba...",
87          ".......addcbdddda..",
88          ".......accddddddda.",
89          "........accddddddb.",
90          ".......bcbdddddddda",
91          "..bb...acddddbadddb",
92          ".bcb...adddddddaddb",
93          ".bcb....accdddddbb.",
94          ".bcfb..afaaaaaaa...",
95          ".affcbbafffab......",
96          "..bcccccaaacbbb....",
97          "..accccaffbacaaa...",
98          "...abccabbccacda...",
99          ".....abbaccccdb....",
100          ".........aabccb....",
101          "............bb.....",
102          "..................."
103        ]
104      ]
105    },
106    "shiny": {
107      "palette": [
108        "#000000",
109        "#604820",
110        "#d8b870",
111        "#f8e0a0",
112        "#a89080",
113        "#a08048"
114      ],
115      "idle": [
116        [
117          "...................",
118          "...................",
119          "........aaaaa......",
120          "....bbbacdddcabbb..",
121          "....bddcdddddcddb..",
122          "....bcdddddddddcb..",
123          ".....bdddddddddb...",
124          "....bcdddddddddcb..",
125          "....acbadddddabca..",
126          "....acddadddaddca..",
127          ".....aadddddddaa...",
128          "....beeaadddaaeeb..",
129          "....affbcaaacbffa..",
130          ".....aaccbfbccaa...",
131          "...bbdcaccbccacdbb.",
132          "...bdccaddaddaccdb.",
133          "....bbccaaaaaccbb..",
134          "......aa.....aa....",
135          "..................."
136        ],
137        [
138          "...................",
139          "........aaaaa......",
140          "....bbbacdddcabbb..",
141          "....bddcdddddcddb..",
142          "....bcdddddddddcb..",
143          ".....bdddddddddb...",
144          "....bcdddddddddcb..",
145          "....acbadddddabca..",
146          "....acddadddaddca..",
147          ".....aadddddddaa...",
148          "....beeaadddaaeeb..",
149          "....affbcaaacbffa..",
150          ".....aaccbfbccaa...",
151          "...bbdcaccbccacdbb.",
152          "...bdccaddaddaccdb.",
153          "....bbccaaaaaccbb..",
154          "......aa.....aa....",
155          "...................",
156          "..................."
157        ]
158      ],
159      "walk": [
160        [
161          "...................",
162          ".........bb........",
163          "........bccbba.....",
164          "........aabcddba...",
165          ".......addcbdddda..",
166          ".......accddddddda.",
167          "........accddddddb.",
168          ".......bcbdddddddda",
169          ".......acddddbadddb",
170          ".......adddddddaddb",
171          ".bb.....accdddddbb.",
172          "bcb....afaaaaaaa...",
173          "bccbbb.afffab......",
174          "bccfccbcaaacbbb....",
175          ".bffcccaffbacaaa...",
176          "..bbcccabbccacda...",
177          "....babbaccccdb....",
178          ".........aabccb....",
179          "............bb....."
180        ],
181        [
182          ".........bb........",
183          "........bccbba.....",
184          "........aabcddba...",
185          ".......addcbdddda..",
186          ".......accddddddda.",
187          "........accddddddb.",
188          ".......bcbdddddddda",
189          "..bb...acddddbadddb",
190          ".bcb...adddddddaddb",
191          ".bcb....accdddddbb.",
192          ".bcfb..afaaaaaaa...",
193          ".affcbbafffab......",
194          "..bcccccaaacbbb....",
195          "..accccaffbacaaa...",
196          "...abccabbccacda...",
197          ".....abbaccccdb....",
198          ".........aabccb....",
199          "............bb.....",
200          "..................."
201        ]
202      ]
203    }
204  }
205}
206