SLOPSHOPPER

brrad

Brrad: when a task takes a while, tells you in Darija to take a break and make tea, then calls you back when it's done

newbandguardtoastprompttimer
v0.1.0MITupdated 2026-10-08Mohamedballouch/darija-mods/brrad
A shopper browsing a rack in a slop shop
README

darija-mods ๐Ÿ‡ฒ๐Ÿ‡ฆ

Claude Code mods that speak Moroccan Darija, edged like a zrabi (Berber rug).

Claude kaykhddem, w nta katchrb atay. ๐Ÿซ–

โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡
  ๐Ÿซ– 4:32  l-brrad kaytbokh... ๐Ÿซ–
  Atay wjed? Khwi lik kas tani, ana mazal khddam
โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—†โ”€โ—‡โ”€โ—† [Khbbi]

Install

claude plugin marketplace add Mohamedballouch/darija-mods
claude plugin install darija-narrator@darija-mods
claude plugin install zlafa@darija-mods
claude plugin install hajib@darija-mods
claude plugin install brrad@darija-mods

Install all four, or only the ones you like.

The mods

ModWhat it doesExample
darija-narratorNarrates every step Claude takes, live above the prompt๐Ÿ” Khllini nchouf ach kayn f app.ts
zlafa (ุงู„ุฒู„ุงูุฉ)A bowl that fills as the context window fills; warns at 80% and 95%๐Ÿฅฃ [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘] 82% L-zlafa 9rbat t3mer, fkker f /compact
hajib (ุงู„ุญุงุฌุจ)Blocks catastrophic commands (wiping /, formatting a diskโ€ฆ) and asks before risky ones (git push --force, sudo, `curl \sh, editing .env`โ€ฆ)๐Ÿ›‘ Force push! Ghadi ybddel l-historique li f remote. N-khllih ydouz?
brrad (ุงู„ุจุฑุงุฏ)When a task runs long, tells you to step away for tea; calls you back when it's done and counts your cups for the dayHadchi ghadi ytwl chwiya... khud lik pause w chi brrad d atay ๐Ÿซ–

Hajib only ever makes approvals stricter: it never auto-approves anything.

Brrad settings

In the plugin config menu:

  • First tea nudge (seconds): default 90; later nudges (4 min, 8 min, 15 min) scale from it
  • Quiet mode: only the "come back, it's done" toast

Spelling

Darija is written in Latin letters with the usual Arabizi numbers: 3 = ุน ยท 7 = ุญ ยท 9 = ู‚ ยท gh = ุบ ยท kh = ุฎ ยท ch = ุด

Look

Every band sits between two rows of rug fringe (โ—‡โ”€โ—†โ”€โ—‡) in terracotta #B5452A. The text keeps your terminal's own colours, so it reads the same on light and dark themes; only warnings turn red.

Develop

Each mod is a folder with .claude-plugin/plugin.json and a hooks module under hooks/.

claude plugin validate ./brrad
claude plugin test ./brrad
claude --plugin-dir ./brrad

Better Darija wording is very welcome: the phrases live in darija-narrator/hooks/darija.ts, zlafa/hooks/zlafa.ts, hajib/hooks/hajib.ts and brrad/hooks/brrad.ts.

License

MIT

Source 4 files
hooks/register.tsx 114 lines
1import { atom, read, update } from 'claude-code'
2import type { Register, Timer } from 'claude-code'
3
4import { back, band, heads, isSlow, nudge, stepAt, tally } from './brrad'
5
6import { TERRACOTTA, fringe } from './zrabi'
7
8const elapsed = atom({ plugin: 'brrad', key: 'elapsed' } as const, null as number | null)
9const isHidden = atom({ plugin: 'brrad', key: 'isHidden' } as const, false)
10
11const TICK_MS = 5000
12
13export const register: Register = (on, options) => {
14  const first = Math.max(15, Number(options.firstNudgeSeconds ?? 90))
15  const isQuiet = options.quiet === true
16
17  let startedAt = 0
18  let step = -1
19  let agents = 0
20  let timer: Timer | undefined
21
22  on('prompt.submit', async ($, e, next) => {
23    timer?.cancel()
24    startedAt = await $.clock.now()
25    step = -1
26    agents = 0
27    await update($, elapsed, () => null)
28    await update($, isHidden, () => false)
29
30    timer = $.clock.every(TICK_MS, async () => {
31      const seconds = ((await $.clock.now()) - startedAt) / 1000
32      const due = stepAt(seconds, first)
33
34      if (due > step) {
35        step = due
36
37        if (!isQuiet) {
38          $.ui.toast(nudge(due), { timeoutMs: 8000 })
39        }
40      }
41
42      if (step >= 0 && !isQuiet) {
43        await update($, elapsed, () => seconds)
44      }
45    })
46
47    return next(e)
48  })
49
50  on('tool.call', async ($, e, next) => {
51    const isEarly = step < 0
52    const isBuild = e.tool === 'Bash' && isSlow(String(e.command ?? ''))
53
54    if (e.tool === 'Agent') {
55      agents += 1
56    }
57
58    if (isEarly && (isBuild || agents >= 2)) {
59      step = 0
60
61      if (!isQuiet) {
62        $.ui.toast(isBuild ? heads.build : heads.agents, { timeoutMs: 8000 })
63      }
64    }
65
66    return next(e)
67  })
68
69  on('turn.complete', async ($, e, next) => {
70    timer?.cancel()
71    timer = undefined
72
73    const seconds = ((await $.clock.now()) - startedAt) / 1000
74    const wasBreak = step >= 0 || seconds >= first
75    await update($, elapsed, () => null)
76
77    if (wasBreak && !e.isAborted) {
78      const day = new Date(await $.clock.now()).toISOString().slice(0, 10)
79      const cups = Number((await $.store.get(`cups:${day}`)) ?? 0) + 1
80      await $.store.set(`cups:${day}`, cups)
81
82      const isError = e.reason === 'error' || e.reason === 'refusal'
83      $.ui.toast(`${back(seconds, isError)}  ยท  ${tally(cups)}`, { timeoutMs: 10000 })
84    }
85
86    return next(e)
87  })
88
89  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
90    const seconds = await read($, elapsed)
91
92    if (e.props.hasSurvey || seconds === null || (await read($, isHidden))) {
93      return next(e)
94    }
95
96    const { Box, Button, Text } = $.ui.resolve(e)
97    const width = e.props.bodyColumns - 2
98
99    return (
100      <Box flexDirection="column">
101        <Text color={TERRACOTTA}>{fringe(width)}</Text>
102        <Box flexDirection="column" paddingX={2}>
103          <Text bold>{band(seconds)}</Text>
104          {step >= 0 ? <Text dimColor>{nudge(step)}</Text> : null}
105        </Box>
106        <Box>
107          <Text color={TERRACOTTA}>{fringe(width - 10, 2)} </Text>
108          <Button key="hide" label="Khbbi" onPress={() => update($, isHidden, () => true)} />
109        </Box>
110      </Box>
111    )
112  })
113}
114
hooks/brrad.ts 58 lines
1// Darija b l-7rouf latiniya: 3 = ุน, 7 = ุญ, 9 = ู‚, gh = ุบ, kh = ุฎ, ch = ุด
2
3// Nudge points as multiples of the first one: 90s โ†’ 90s, 4m, 8m, 15m.
4const STEPS = [1, 2.67, 5.33, 10] as const
5
6const NUDGES = [
7  'Hadchi ghadi ytwl chwiya... khud lik pause w chi brrad d atay ๐Ÿซ–',
8  'Atay wjed? Khwi lik kas tani, ana mazal khddam',
9  'Rah hadi khdma kbira, sir tmchcha chwiya w rje3',
10  'Ila kan 3ndek chi msemmen wla 7rcha, daba howa l-weqt ๐Ÿฅž',
11] as const
12
13// The nudge index due at `seconds`, given the first threshold; -1 before the first.
14export const stepAt = (seconds: number, first: number): number => {
15  let step = -1
16
17  STEPS.forEach((k, i) => {
18    if (seconds >= first * k) {
19      step = i
20    }
21  })
22
23  return step
24}
25
26export const nudge = (step: number): string => NUDGES[Math.min(step, NUDGES.length - 1)]!
27
28const SLOW = /\b(npm|pnpm|yarn|bun)\s+(install|i|ci|run\s+build)\b|\bdocker\s+(build|compose\s+up)\b|\bcargo\s+(build|test)\b|\bgradlew?\s+build\b|\bmvn\s+(install|package)\b|\bpytest\b|\bgo\s+test\s+\.\/\.\.\.|\bpip\s+install\s+-r\b|\bmake\b(\s+-j\d*)?\s*$/
29
30export const isSlow = (command: string): boolean => SLOW.test(command.trim())
31
32export const heads = {
33  build: 'Bda l-khdma t9ila... hada weqt atay, ma-tb9ach tsenna ๐Ÿซ–',
34  agents: 'Jbt l-jma3a ykhdmou m3aya, ghadi ytwlo chwiya. Sir sawb atay ๐Ÿซ–',
35} as const
36
37export const teapot = (seconds: number): string => (seconds < 240 ? '๐Ÿซ–' : seconds < 600 ? 'โ™จ๏ธ' : 'โ˜•')
38
39export const clock = (seconds: number): string => {
40  const s = Math.max(0, Math.floor(seconds))
41
42  return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`
43}
44
45export const band = (seconds: number): string => `${teapot(seconds)} ${clock(seconds)}  l-brrad kaytbokh... ๐Ÿซ–`
46
47export const back = (seconds: number, isError: boolean): string =>
48  isError
49    ? 'Rje3, kayn chi mochkil khassni rayek fih โš ๏ธ'
50    : seconds >= 600
51      ? 'Salina! Atay brd? Machi mochkil, l-khdma t9adat โœ…'
52      : 'Rje3 a sa7bi! Salina, w l-brrad mazal skhoun โ˜•'
53
54export const tally = (cups: number): string =>
55  cups >= 5
56    ? `Lyoum chrbti ${cups} d l-kissan d atay ๐Ÿ˜… bzzaf 3la l-9lb`
57    : `Lyoum chrbti ${cups} ${cups === 1 ? 'kas' : 'd l-kissan'} d atay`
58
hooks/zrabi.ts 18 lines
1// Zrabi (ุงู„ุฒุฑุจูŠุฉ): the diamond fringe of a Berber rug, in terracotta.
2
3export const TERRACOTTA = '#B5452A'
4export const ALERT = '#E5534B'
5
6const UNIT = 'โ—‡โ”€โ—†โ”€'
7
8// A fringe `width` cells wide that starts and ends on a diamond; `offset` 2 starts on โ—†.
9export const fringe = (width: number, offset = 0): string => {
10  let s = ''
11
12  for (let i = 0; i < Math.max(0, width); i++) {
13    s += UNIT[(i + offset) % UNIT.length]
14  }
15
16  return s.endsWith('โ”€') ? s.slice(0, -1) : s
17}
18
types/index.d.ts 8 lines
1export type Elapsed = number | null
2
3declare module 'claude-code' {
4  interface PluginState {
5    brrad: { elapsed: Elapsed; isHidden: boolean }
6  }
7}
8