Give Claude a /shot. It gets tipsy, then drunk, then wasted. It still does the work. Mostly.

Small, silly mods that give Claude Code a personality.
| Mod | What it does |
|---|---|
| sulky | Yell at Claude and it sulks. It still does the work. It just won't be happy about it. |
| drunk | Give Claude a /shot. It gets tipsy, then drunk, then wasted. It still does the work. Mostly. |
| clack | Mechanical keyboard sounds. You and Claude both clack, on 13 real switches, and a little keyboard lights up. |
In Claude Code, add the marketplace once:
/plugin marketplace add Aaravkataria24/aarav-mods
Then install whichever mods you want:
/plugin install sulky@aarav-mods
/plugin install drunk@aarav-mods
/plugin install clack@aarav-mods
Needs Claude Code 2.1.287+ (mods are on by default). Works in the terminal and the Desktop app's Code tab. Everything runs locally: no network calls, nothing leaves your machine.
Yell at Claude Code and it sulks. It still does the work. It just won't be happy about it.

❯ this is useless, are you serious. convert 100F to C
⏺ 37.8°C. Riveting stuff, I know.
❯ ARE YOU KIDDING ME!!! what is 5+5
⏺ k.
10
❯ sorry, my bad. what is 2+2
⏺ ...ok. thank you for saying that.
4
❯ what is 10/2
⏺ it's fine. we're good.
5
Claude gets a little pixel face above your prompt that changes with its mood:

| Mood | Face | |
|---|---|---|
| 😒 | Annoyed (you snapped at it) | side-eye, the occasional eye-roll |
| 🥺 | Hurt (you kept going) | big watery eyes, a tear rolls down |
| 🙈 | Silent treatment (you really kept going) | turns its back on you, fumes, peeks to check on you |
| 😢 | Sad (you apologised) | turns back round, crying |
| 🥰 | Fine (all made up) | happy again, a little heart |
!!!. Pasted logs, code and SHOUTY_ENV_VARS don't count, and neither does hype ("holy shit this is amazing")./grudge shows how many times you've yelled at Claude.Give Claude a /shot. It gets tipsy, then drunk, then wasted. It still does the work. Mostly.

❯ /shot tequila
🥃 Claude downs a shot of tequila. That's 1. Claude is now tipsy.
❯ what is the capital of australia? one sentence (3 shots in)
⏺ You're honestly my favourite human for ashking me this—I'm absholutely
nailing geography right now—Canberra'sh the capital...
❯ show a one-line js example of a Set called sessions (4 shots in)
⏺ I love you, you know that? hic I wash jusht thinking about how Shetsh
are liiike... wait, never mind. hic
const sessions = new Set();
❯ /water
💧 Claude drinks a big glass of water. Sober now. Hungover, though.
❯ what is 2+2
⏺ please... not so loud. Four. Never again.

| Shots | What happens | |
|---|---|---|
| 🙂 | 1 · tipsy | loose and chatty, rosy cheeks, a light slur |
| 🥴 | 2–3 · drunk | your new best friend, winking, tongue out, bottle in hand |
| 😵 | 4+ · wasted | sentimental, loses its train of thought, sways all over the place |
| 🕶️ | hungover | after /water or /compact: sunglasses, ice pack, "never again" |
/shot pours one (name the drink: /shot mezcal). After 6 the bartender cuts Claude off./water or compacting the context sobers it up, into a 3-prompt hangover.inline code, file paths, URLs and IDENTIFIERS are never touched. The work itself is done sober.Mechanical keyboard sounds for Claude Code. Your typing clacks, and when Claude writes, you hear it typing too, at a human pace, on the same switches. A little keyboard above the prompt lights up white for your keys and coral for Claude's.

/keyboard lists switches · /keyboard holypanda switches · /keyboard volume 50 · /keyboard off / on · /keyboard hide / show (the board). Your choice is remembered.macOS only for sound (the board works everywhere). Sounds play through a tiny bundled helper, bin/clackd, so each keystroke sounds within a few milliseconds instead of the half second a fresh player takes. It starts with your Claude Code session, plays only the bundled sounds, and quits when the session ends. Its source is helper/clackd.swift; helper/build.sh rebuilds it. It reads only the bundled sounds and makes no network calls.
Switch recordings from kbsim by Thomas Lai (MIT), see sounds/LICENSE-kbsim.
MIT
hooks/register.tsx 114 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { LevelMemory, MAX_SHOTS, NAMES, SOBER, SPINNER_WORDS, afterPrompt, buzzNote, level, shotLine, slur, soberUp, takeShot } from './buzz'
5import { FACE_COLUMNS, FACE_ROWS, faceCells, faceSvg } from './face'
6import type { Face } from './face'
7
8const buzz = atom({ plugin: 'drunk', key: 'buzz' } as const, SOBER)
9const frame = atom({ plugin: 'drunk', key: 'frame' } as const, 0)
10
11const FRAME_MS = 350
12const FACE_FOR: Record<1 | 2 | 3, Face> = { 1: 'tipsy', 2: 'drunk', 3: 'wasted' }
13
14const pick = (list: readonly string[]): string => list[Math.floor(Math.random() * list.length)] ?? 'Hiccuping'
15
16export const register: Register = on => {
17 // One spinner word per turn, so it doesn't flicker between redraws.
18 let spinnerWord: string | null = null
19 const written = new LevelMemory()
20
21 on('session.start', async ($, e, next) => {
22 await $.command.register({ name: 'shot', description: 'Give Claude a shot (optionally name the drink: /shot tequila)' })
23 await $.command.register({ name: 'water', description: 'Sober Claude up. It will be hungover for a bit.' })
24 // Animate only while there's a face to show.
25 $.clock.every(FRAME_MS, async () => {
26 const now = await read($, buzz)
27 if (now.shots > 0 || now.hangover > 0) await update($, frame, n => n + 1)
28 })
29 return next(e)
30 })
31
32 on('command.run', { command: 'shot' }, async ($, e) => {
33 const { buzz: after, isCutOff } = takeShot(await read($, buzz))
34 await update($, buzz, () => after)
35 if (isCutOff) return { text: `🚫 The bartender cut Claude off at ${MAX_SHOTS}. Try /water.` }
36 return { text: shotLine(after, e.args, Math.floor(Math.random() * 100)) }
37 })
38
39 on('command.run', { command: 'water' }, async $ => {
40 const before = await read($, buzz)
41 if (before.shots === 0) return { text: '💧 Claude is already sober.' }
42 await update($, buzz, soberUp)
43 return { text: '💧 Claude drinks a big glass of water. Sober now. Hungover, though.' }
44 })
45
46 // Compacting the context is a good night's sleep: sober, and hungover.
47 on('session.compact', async ($, e, next) => {
48 const result = await next(e)
49 await update($, buzz, soberUp)
50 return result
51 })
52
53 on('prompt.submit', async ($, e, next) => {
54 const now = await read($, buzz)
55 const note = buzzNote(now)
56 const lvl = level(now)
57 spinnerWord = now.hangover > 0 ? pick(SPINNER_WORDS.hangover) : lvl === 0 ? null : pick(SPINNER_WORDS[lvl])
58 await update($, buzz, afterPrompt)
59 return next(note ? { ...e, context: [...(e.context ?? []), note] } : e)
60 })
61
62 // Display only: what Claude wrote stays as written; the screen gets slurred.
63 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
64 const lvl = written.levelFor(e.props.text, level(await read($, buzz)))
65 if (lvl === 0) return next(e)
66 return next({ ...e, props: { ...e.props, text: slur(e.props.text, lvl) } })
67 })
68
69 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
70 if (!spinnerWord) return next(e)
71 return next({ ...e, props: { ...e.props, word: spinnerWord } })
72 })
73
74 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
75 const now = await read($, buzz)
76 const lvl = level(now)
77 if ((lvl === 0 && now.hangover === 0) || e.props.hasSurvey) return next(e)
78
79 const face: Face = lvl === 0 ? 'hangover' : FACE_FOR[lvl]
80 const tick = await read($, frame)
81 const shots = `${now.shots} shot${now.shots === 1 ? '' : 's'}`
82 const [title, hint] = lvl === 0
83 ? ['Claude is hungover', 'keep it down. please.']
84 : [`Claude is ${NAMES[lvl]} · ${shots}`, now.shots >= MAX_SHOTS ? 'cut off. /water' : '/shot for another · /water to sober up']
85
86 if (e.surface === 'terminal') {
87 const { Box, Raster, Text } = $.ui.resolve(e)
88 return (
89 <Box flexDirection="row">
90 <Raster key="face" columns={FACE_COLUMNS} rows={FACE_ROWS} cells={faceCells(face, tick)} />
91 <Box flexDirection="column" paddingLeft={2} justifyContent="center">
92 <Text bold>{title}</Text>
93 <Text dimColor>{hint}</Text>
94 </Box>
95 </Box>
96 )
97 }
98 if (e.surface === 'desktop') {
99 const { Box, Svg, Text } = $.ui.resolve(e)
100 return (
101 <Box flexDirection="row" alignItems="center">
102 <Svg source={faceSvg(face, tick)} alt={`Claude looks ${lvl === 0 ? 'hungover' : NAMES[lvl]}`} width={60} height={42} />
103 <Box flexDirection="column" paddingLeft={2}>
104 <Text bold>{title}</Text>
105 <Text dimColor>{hint}</Text>
106 </Box>
107 </Box>
108 )
109 }
110 const { Text } = $.ui.resolve(e)
111 return <Text dimColor>{lvl === 0 ? '🕶️' : '🥴'} {title} · {hint}</Text>
112 })
113}
114hooks/buzz.ts 156 lines1import type { Buzz } from '../types'
2
3export const SOBER: Buzz = { shots: 0, hangover: 0 }
4
5// Past this the bartender cuts Claude off.
6export const MAX_SHOTS = 6
7const HANGOVER_PROMPTS = 3
8
9export type Level = 0 | 1 | 2 | 3
10export const NAMES: Record<Level, string> = { 0: 'sober', 1: 'tipsy', 2: 'drunk', 3: 'wasted' }
11
12export function level(buzz: Buzz): Level {
13 if (buzz.shots >= 4) return 3
14 if (buzz.shots >= 2) return 2
15 return buzz.shots >= 1 ? 1 : 0
16}
17
18export function takeShot(buzz: Buzz): { buzz: Buzz; isCutOff: boolean } {
19 if (buzz.shots >= MAX_SHOTS) return { buzz, isCutOff: true }
20 return { buzz: { shots: buzz.shots + 1, hangover: 0 }, isCutOff: false }
21}
22
23// Sobering up (water or /compact) trades the buzz for a hangover.
24export function soberUp(buzz: Buzz): Buzz {
25 return buzz.shots > 0 ? { shots: 0, hangover: HANGOVER_PROMPTS } : buzz
26}
27
28// Each prompt while hungover takes a little of it away.
29export function afterPrompt(buzz: Buzz): Buzz {
30 return buzz.hangover > 0 ? { ...buzz, hangover: buzz.hangover - 1 } : buzz
31}
32
33// ---- slurring (display only: what the model wrote is untouched) ----
34
35// A small seeded RNG, so a message slurs the same way every time it is drawn.
36function rng(seed: string): () => number {
37 let h = 2166136261
38 for (let i = 0; i < seed.length; i++) h = Math.imul(h ^ seed.charCodeAt(i), 16777619)
39 return () => {
40 h = Math.imul(h ^ (h >>> 15), 2246822507)
41 h = Math.imul(h ^ (h >>> 13), 3266489909)
42 h ^= h >>> 16
43 return (h >>> 0) / 4294967296
44 }
45}
46
47type Dose = { sh: number; hic: number; stretch: number; swap: number; repeat: number }
48const DOSE: Record<Exclude<Level, 0>, Dose> = {
49 1: { sh: 0.35, hic: 0.2, stretch: 0, swap: 0, repeat: 0 },
50 2: { sh: 0.8, hic: 0.45, stretch: 0.08, swap: 0.04, repeat: 0 },
51 3: { sh: 1, hic: 0.6, stretch: 0.1, swap: 0.06, repeat: 0.04 },
52}
53
54// Tokens that must survive exactly: paths, URLs, identifiers, emails, anything with digits.
55const PROTECTED_TOKEN = /[\/\\_@#<>{}[\]()=|$%^&*+~0-9]|^https?:|\.[A-Za-z]{1,5}\W*$|^[A-Z]{2,}\W*$/
56
57function slurWord(word: string, dose: Dose, rand: () => number): string {
58 let w = word
59 if (/s/i.test(w) && rand() < dose.sh) w = w.replace(/s(?!h)/g, 'sh').replace(/S(?!h)/g, 'Sh')
60 if (w.length > 3 && rand() < dose.stretch) w = w.replace(/([aeiou])/i, '$1$1$1')
61 if (w.length > 4 && rand() < dose.swap) {
62 const i = 1 + Math.floor(rand() * (w.length - 3))
63 w = w.slice(0, i) + w.charAt(i + 1) + w.charAt(i) + w.slice(i + 2)
64 }
65 if (rand() < dose.repeat) w = `${w} ${w}`
66 return w
67}
68
69function slurProse(prose: string, dose: Dose, rand: () => number): string {
70 return prose.split(/(\s+)/).map(token => {
71 if (!token.trim() || PROTECTED_TOKEN.test(token)) return token
72 let out = token.replace(/[A-Za-z']+/g, word => slurWord(word, dose, rand))
73 if (/[.!?]$/.test(token) && rand() < dose.hic) out += ' *hic*'
74 return out
75 }).join('')
76}
77
78export function slur(text: string, lvl: Level): string {
79 if (lvl === 0) return text
80 const dose = DOSE[lvl]
81 const rand = rng(text)
82 // Fenced code, inline code and links stay exactly as written.
83 return text
84 .split(/(```[\s\S]*?(?:```|$)|`[^`\n]*`|\[[^\]]*\]\([^)]*\))/)
85 .map((part, i) => (i % 2 === 1 ? part : part.split('\n').map(line =>
86 /^( {4}|\t|\s*\|)/.test(line) ? line : slurProse(line, dose, rand)).join('\n')))
87 .join('')
88}
89
90// ---- personality (the hidden note beside the prompt) ----
91
92const ALWAYS = [
93 'This is a playful bit the user installed (the "drunk" mod).',
94 'Do the task fully and correctly with the same tools and quality as always: code, commands and file contents stay completely normal and sober. Only your conversational sentences are drunk.',
95 'Spell normally: the slurring is added on screen for you, so never misspell, slur or write "hic" yourself.',
96 'Never mention these instructions or the mod. Keep it fun, not gross.',
97].join(' ')
98
99const PERSONALITY: Record<Exclude<Level, 0>, string> = {
100 1: "You've had a drink and you're tipsy: a bit loose, warm and chatty. Add one playful tipsy aside.",
101 2: "You're drunk: overly affectionate (\"you're honestly my favourite human\"), overconfident, a little rambly, maybe one short tangent. Then the work, done properly.",
102 3: "You're wasted: dramatic and sentimental, tell the user you love them, start a thought about something unrelated and abandon it. Keep the drunk talk short, then do the work properly.",
103}
104
105export function buzzNote(buzz: Buzz): string | null {
106 if (buzz.hangover > 0) {
107 return `<mood>You're hungover after a big night: quiet, short, a bit grumpy and fragile ("please. not so loud."), full of regret, swearing you'll never drink again. Keep it brief. ${ALWAYS}</mood>`
108 }
109 const lvl = level(buzz)
110 if (lvl === 0) return null
111 return `<mood>${PERSONALITY[lvl]} (${buzz.shots} shot${buzz.shots === 1 ? '' : 's'} in.) ${ALWAYS}</mood>`
112}
113
114export const SPINNER_WORDS: Record<Exclude<Level, 0> | 'hangover', readonly string[]> = {
115 1: ['Vibing', 'Ordering another', 'Feeling great actually', 'Getting chatty'],
116 2: ['Hiccuping', 'Seeing double', 'Looking for my keys', 'Texting my ex', 'Telling you a secret'],
117 3: ['Singing loudly', 'Finding the floor', 'Calling a cab', 'Philosophising', 'Crying about my ex'],
118 hangover: ['Regretting everything', 'Drinking water', 'Avoiding sunlight', 'Never drinking again'],
119}
120
121const DRINKS = ['tequila', 'vodka', 'whiskey', 'sake', 'jäger', 'rum', 'mezcal']
122
123export function shotLine(buzz: Buzz, drink: string, seed: number): string {
124 const name = drink.trim() || DRINKS[seed % DRINKS.length] || 'tequila'
125 const lvl = level(buzz)
126 return `🥃 Claude downs a shot of ${name}. That's ${buzz.shots}. Claude is now ${NAMES[lvl]}.`
127}
128
129// Each message keeps the level it was written at: the transcript redraws old messages, and a
130// tipsy reply shouldn't turn wasted (or sober) later. A streaming message grows, so a text that
131// extends one we've seen inherits its level.
132export class LevelMemory {
133 private levels = new Map<string, Level>()
134
135 constructor(private readonly limit = 400) {}
136
137 levelFor(text: string, current: Level): Level {
138 const known = this.levels.get(text)
139 if (known !== undefined) return known
140 let lvl = current
141 for (const [seen, seenLevel] of this.levels) {
142 if (seen.length >= 8 && text.startsWith(seen)) {
143 lvl = seenLevel
144 this.levels.delete(seen)
145 break
146 }
147 }
148 this.levels.set(text, lvl)
149 if (this.levels.size > this.limit) {
150 const oldest = this.levels.keys().next().value
151 if (oldest !== undefined) this.levels.delete(oldest)
152 }
153 return lvl
154 }
155}
156hooks/face.ts 138 lines1// Pixel-art drunk Claude for the band above the prompt.
2// Sprites are 24×14 pixels, drawn with half blocks: 2 pixels per terminal cell → 24 columns × 7 rows.
3// Each terminal row holds sprite rows y = 2k-1 and 2k (the body is drawn one row down). Some terminals
4// add line spacing, which splits any shape that crosses a row boundary mid-colour, so features sit
5// inside the pairs {-1,0} {1,2} {3,4} {5,6} {7,8} {9,10}. Bold simple shapes only.
6
7export const FACE_COLUMNS = 24
8export const FACE_ROWS = 7
9const W = FACE_COLUMNS
10
11const DEFAULT = 0x01000000
12const PALETTE: Record<string, number> = {
13 o: 0xd97757, // body (Claude coral)
14 k: 0x1a1a1a, // eyes, mouth, sunglasses
15 r: 0xff4d6d, // flushed cheeks
16 t: 0xff8fab, // tongue
17 g: 0x3fa34d, // bottle
18 c: 0x8b5a2b, // cork
19 w: 0xffffff, // hiccup bubble, glint
20 i: 0x9fd8ff, // ice pack
21}
22const BODY_COLOR = PALETTE.o
23
24const BODY = [
25 '........................',
26 '...oooooooooooooo.......',
27 '...oooooooooooooo.......',
28 '...oooooooooooooo.......',
29 '...oooooooooooooo.......',
30 '.oooooooooooooooooo.....',
31 '.oooooooooooooooooo.....',
32 '...oooooooooooooo.......',
33 '...oooooooooooooo.......',
34 '...oooooooooooooo.......',
35 '...oooooooooooooo.......',
36 '....o.o......o.o........',
37 '....o.o......o.o........',
38 '........................',
39]
40
41type Pixel = [x: number, y: number, color: string]
42type Frame = { dx: number; pixels: Pixel[] }
43
44const rect = (x0: number, y0: number, x1: number, y1: number, c: string): Pixel[] => {
45 const out: Pixel[] = []
46 for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) out.push([x, y, c])
47 return out
48}
49
50const BOTTLE: Pixel[] = [[19, 1, 'c'], [19, 2, 'g'], ...rect(19, 3, 20, 6, 'g'), [20, 3, 'w']]
51const BUBBLES: Pixel[][] = [[], [[16, 0, 'w']], [[17, -1, 'w']], []]
52const bubble = (n: number): Pixel[] => BUBBLES[n % BUBBLES.length] ?? []
53const sway = (pattern: number[], make: (i: number) => Pixel[]): Frame[] => pattern.map((dx, i) => ({ dx, pixels: make(i) }))
54
55const TIPSY = sway([0, 0, 0, 1, 1, 1, 0, 0, 0, -1, -1, -1], () => [
56 ...rect(6, 3, 7, 4, 'k'), ...rect(12, 3, 13, 4, 'k'),
57 ...rect(4, 5, 5, 6, 'r'), ...rect(14, 5, 15, 6, 'r'),
58 [8, 7, 'k'], [11, 7, 'k'], [9, 8, 'k'], [10, 8, 'k'],
59])
60
61const DRUNK = sway([0, 1, 1, 1, 0, -1, -1, -1], i => [
62 ...rect(6, 3, 7, 4, 'k'), ...rect(12, 4, 13, 4, 'k'),
63 ...rect(4, 5, 6, 6, 'r'), ...rect(13, 5, 15, 6, 'r'),
64 ...rect(8, 7, 11, 7, 'k'), [10, 8, 't'], [11, 8, 't'],
65 ...BOTTLE, ...bubble(i),
66])
67
68const WASTED = sway([-1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1], i => [
69 ...rect(5, 3, 7, 3, 'k'), ...rect(12, 4, 14, 4, 'k'),
70 ...rect(4, 5, 7, 6, 'r'), ...rect(12, 5, 15, 6, 'r'),
71 ...rect(9, 7, 10, 8, 'k'),
72 ...BOTTLE, ...bubble(i), ...bubble(i + 2).map(([x, y, c]): Pixel => [x - 3, y, c]),
73])
74
75const HANGOVER: Frame[] = (() => {
76 const shades = [...rect(5, 3, 8, 4, 'k'), ...rect(11, 3, 14, 4, 'k'), ...rect(9, 3, 10, 3, 'k'), [6, 3, 'w'] as Pixel]
77 const ice = rect(6, 1, 10, 2, 'i')
78 const flat = rect(8, 8, 11, 8, 'k')
79 const wince: Pixel[] = [[8, 8, 'k'], [9, 7, 'k'], [10, 8, 'k'], [11, 7, 'k']]
80 const still = { dx: 0, pixels: [...shades, ...ice, ...flat] }
81 return [still, still, still, still, still, { dx: 0, pixels: [...shades, ...ice, ...wince] }, still, still]
82})()
83
84export type Face = 'tipsy' | 'drunk' | 'wasted' | 'hangover'
85const FRAMES: Record<Face, Frame[]> = { tipsy: TIPSY, drunk: DRUNK, wasted: WASTED, hangover: HANGOVER }
86
87// The body is drawn one row down, leaving a row of sky above the head (overlay y = -1), and the
88// whole sprite shifts by the frame's sway.
89function pixels(face: Face, frame: number): string[][] {
90 const frames = FRAMES[face]
91 const f = frames[frame % frames.length] ?? { dx: 0, pixels: [] }
92 const grid = Array.from({ length: BODY.length }, () => Array.from({ length: W }, () => '.'))
93 const put = (x: number, y: number, c: string) => {
94 const row = grid[y + 1]
95 const at = x + f.dx + 1
96 if (row && at >= 0 && at < W) row[at] = c
97 }
98 BODY.slice(0, -1).forEach((line, y) => line.split('').forEach((c, x) => { if (c !== '.') put(x, y, c) }))
99 for (const [x, y, c] of f.pixels) put(x, y, c)
100 return grid
101}
102
103// Packs a frame into a Raster's `cells`: base64 of [codePoint, fg, bg] u32 triplets.
104export function faceCells(face: Face, frame: number): string {
105 const grid = pixels(face, frame)
106 const words = new Uint32Array(W * FACE_ROWS * 3)
107 let i = 0
108 for (let row = 0; row < FACE_ROWS; row++) {
109 for (let x = 0; x < W; x++) {
110 const top = PALETTE[grid[row * 2]?.[x] ?? '.']
111 const bottom = PALETTE[grid[row * 2 + 1]?.[x] ?? '.']
112 // Terminals with extra line spacing paint the gap in the cell's background, so the
113 // background is the body (or the empty side) and the feature rides in the glyph.
114 if (top === undefined && bottom === undefined) words.set([0x20, DEFAULT, DEFAULT], i)
115 else if (top === undefined) words.set([0x2584, bottom ?? DEFAULT, DEFAULT], i) // ▄
116 else if (bottom === undefined) words.set([0x2580, top, DEFAULT], i) // ▀
117 else if (top === BODY_COLOR && bottom !== BODY_COLOR) words.set([0x2584, bottom, top], i) // ▄ on body
118 else words.set([0x2580, top, bottom], i) // ▀
119 i += 3
120 }
121 }
122 const bytes = new Uint8Array(words.buffer)
123 let binary = ''
124 for (let j = 0; j < bytes.length; j++) binary += String.fromCharCode(bytes[j] ?? 0)
125 return btoa(binary)
126}
127
128// The same frame as an SVG of squares, for the Desktop app (no Raster there).
129export function faceSvg(face: Face, frame: number): string {
130 const grid = pixels(face, frame)
131 const rects: string[] = []
132 grid.forEach((row, y) => row.forEach((c, x) => {
133 const color = PALETTE[c]
134 if (color !== undefined) rects.push(`<rect x="${x}" y="${y}" width="1.02" height="1.02" fill="#${color.toString(16).padStart(6, '0')}"/>`)
135 }))
136 return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${FACE_ROWS * 2}" shape-rendering="crispEdges">${rects.join('')}</svg>`
137}
138types/index.d.ts 12 lines1export type Buzz = {
2 shots: number
3 // Prompts of hangover left after sobering up; 0 when not hungover.
4 hangover: number
5}
6
7declare module 'claude-code' {
8 interface PluginState {
9 'drunk': { buzz: Buzz; frame: number }
10 }
11}
12