SLOPSHOPPER

mermaid-ascii

Draws ```mermaid blocks in Claude's replies as Unicode box-drawing diagrams in the terminal

newrowsstatusprocess
★ 1v0.1.0MITupdated 2026-10-20BukeLy/claude-mermaid-ascii
A shopper browsing a rack in a slop shop
README

mermaid-ascii

Inside a box titled terminal, the word mermaid spelled in Mermaid node shapes flows into the word ascii spelled in plain boxes

A Claude Code mod that draws ``` `mermaid ``` blocks in Claude's replies as Unicode box-drawing diagrams in the terminal.

Claude writes:

flowchart TD
  A[mermaid block in a reply] --> B[boxmaid]
  B -->|drawn| D[diagram in the terminal]
  B -->|refused| C[merman-cli]
  C -->|drawn| D
  B -->|too wide| E[source kept]
  C -->|failed| E

You see:

         ┌──────────────────────────┐
         │ mermaid block in a reply │
         └─────────────┬────────────┘
                       │
                       ▼
                  ┌─────────┐
                  │ boxmaid │
                  └────┬────┘
                       │
        ┌──────────────┴─┬─────────────┐
        │                │             │
      drawn           refused      too wide
        │                ▼             │
        │         ┌────────────┐       │
        │         │ merman-cli │       │
        │         └──────┬─────┘       │
        │                │             │
        │       ┌────────┴────────┐    │
        │       │                 │    │
        │     drawn            failed  │
        │       │                 │    │
        │       │                 │   ┌┘
        ▼       ▼                 ▼   ▼
┌─────────────────────────┐   ┌─────────────┐
│ diagram in the terminal │   │ source kept │
└─────────────────────────┘   └─────────────┘

Quick start

macOS or Linux, in a shell:

curl -fsSL https://raw.githubusercontent.com/BukeLy/claude-mermaid-ascii/main/install.sh | sh

Or paste this to Claude Code:

Install the mermaid-ascii mod: read https://raw.githubusercontent.com/BukeLy/claude-mermaid-ascii/main/install.sh, then run `curl -fsSL https://raw.githubusercontent.com/BukeLy/claude-mermaid-ascii/main/install.sh | sh` and tell me what it printed.

If boxmaid is already on your PATH, install only the mod, at the Claude Code prompt:

/plugin install mermaid-ascii --marketplace BukeLy/claude-mermaid-ascii

Then start a new Claude Code session (or run /reload-plugins in an open one) and ask Claude for a Mermaid diagram. Needs Claude Code 2.1.287 or later, where mods are on by default.

The script installs boxmaid and merman-cli (pinned versions, SHA-256 checked) to ~/.local/bin, then installs the mod.

What it runs

The mod sends each Mermaid block, as standard input, to the local boxmaid and, if boxmaid refuses it, merman-cli, and shows their output in place of the block in the terminal. It makes no network requests. The stored message keeps the Mermaid source; a block neither tool can draw within the terminal width stays as source.

Source 1 files
hooks/register.ts 88 lines
1import type { Register } from 'claude-code'
2
3const FENCE = /```mermaid[^\n]*\n([\s\S]*?)\n```/g
4// ponytail: approximate East Asian Wide ranges; emoji and rarer wide blocks still count as one column
5const WIDE =
6  /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/g
7// The transcript indents a reply by two columns; keep a little slack for the fence.
8const MARGIN = 4
9// boxmaid's exit code when the diagram cannot be laid out within -w columns.
10const CANNOT_FIT = 3
11// boxmaid draws every kind and fits the width itself; merman-cli covers what it
12// refuses, such as an erDiagram with CJK entity names.
13const boxmaid = (columns: number) =>
14  ['boxmaid', '-color', 'never', '-w', String(columns), '-require-fit', '-']
15const MERMAN = ['merman-cli', 'render', '-q', '-f', 'unicode', '-o', '-', '-']
16
17const fence = (drawn: string) => '```\n' + drawn + '\n```'
18
19const width = (drawn: string) =>
20  Math.max(
21    ...drawn.split('\n').map(line => line.trimEnd().length + (line.match(WIDE)?.length ?? 0)),
22  )
23
24const firstLine = (text: string) => text.trim().split('\n')[0]
25
26export const register: Register = on => {
27  // ponytail: unbounded per-session cache; a session draws a handful of diagrams
28  const drawings = new Map<string, Promise<string | null>>()
29
30  on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
31    if (e.surface !== 'terminal' || !e.props.text.includes('```mermaid')) {
32      return next(e)
33    }
34
35    // 0 tells boxmaid the width is unlimited.
36    const columns = e.viewport ? Math.max(1, e.viewport.columns - MARGIN) : 0
37    const run = (argv: string[], source: string) =>
38      $.process.run(argv, { stdin: source, timeoutMs: 5000 })
39
40    const render = async (source: string) => {
41      const box = await run(boxmaid(columns), source)
42
43      if (box.exitCode === 0) {
44        return box.stdout.trimEnd()
45      }
46
47      if (box.exitCode === CANNOT_FIT) {
48        return null
49      }
50
51      const merman = await run(MERMAN, source)
52
53      if (merman.exitCode !== 0) {
54        throw new Error(`boxmaid: ${firstLine(box.stderr)}; merman-cli: ${firstLine(merman.stderr)}`)
55      }
56
57      const drawn = merman.stdout.trimEnd()
58
59      return columns === 0 || width(drawn) <= columns ? drawn : null
60    }
61
62    const draw = async (source: string) => {
63      const key = `${columns}\n${source}`
64
65      if (!drawings.has(key)) {
66        drawings.set(key, render(source))
67      }
68
69      const drawn = await drawings.get(key)!.catch(error => {
70        drawings.delete(key)
71        $.ui.status(`mermaid-ascii: ${error}`)
72
73        return null
74      })
75
76      return drawn === null ? null : fence(drawn)
77    }
78
79    const drawn = await Promise.all(
80      [...e.props.text.matchAll(FENCE)].map(([, source]) => draw(source ?? '')),
81    )
82    let i = 0
83    const text = e.props.text.replace(FENCE, block => drawn[i++] ?? block)
84
85    return next({ ...e, props: { ...e.props, text } })
86  })
87}
88