# ASCIInator — CLAUDE.md > Project spec for Claude Code. Read this fully before touching any file. > Stack: Vue 3 + Vite + Tailwind (no component lib) + Fastify + execa > Purpose: Headless GUI front-end for system ASCII conversion binaries. --- ## 1. Project Overview ASCIInator converts images to ASCII art by orchestrating system binaries (chafa, jp2a, ascii-image-converter, img2txt) via ImageMagick preprocessing. It is a **pure front-end UI** that talks to a **local Fastify API server** which shells out to the binaries via `execa`. Output format: **plain text** Input methods: file upload, clipboard paste, drag-and-drop Theme: dark default, respects `prefers-color-scheme` CSS: Tailwind utility classes only — no component libraries, no Vuetify, no PrimeVue --- ## 2. Repository Structure ``` asciinator/ ├── CLAUDE.md ← you are here ├── package.json ← root (workspaces) ├── vite.config.js ├── tailwind.config.js ├── index.html ├── src/ │ ├── main.js │ ├── App.vue │ ├── assets/ │ │ └── main.css ← Tailwind base + custom properties │ └── components/ │ ├── ImageInput.vue ← Component 1: input (upload / paste / drag-drop) │ ├── ToolSelector.vue ← Component 2: ASCII tool picker + per-tool options │ ├── ShellBridge.vue ← Component 3: translates GUI state → API call → shell │ ├── OutputDisplay.vue ← Component 4: renders plain-text ASCII result │ ├── ErrorLog.vue ← Component 5: watches and surfaces stderr / API errors │ └── options/ ← Per-tool flag panels (extracted from ToolSelector) │ ├── ChafaOptions.vue │ ├── Jp2aOptions.vue │ ├── AsciiOptions.vue │ └── ImgTxtOptions.vue └── server/ ├── index.js ← Fastify server entry ├── routes/ │ └── convert.js ← POST /convert route └── lib/ ├── imagemagick.js ← ImageMagick pre/post processing via execa └── converters.js ← per-tool execa invocations ``` --- ## 3. Component Architecture ### 3.1 `ImageInput.vue` - Accepts: file picker, drag-and-drop onto drop zone, clipboard paste (`Ctrl+V`) - Emits: `image-ready` with a `{ dataUrl, mimeType, filename }` payload - Sends raw file to server as `multipart/form-data` - Shows image preview thumbnail before conversion - Validates: image/* MIME types only; max 20 MB ### 3.2 `ToolSelector.vue` - Provides a segmented control to choose between: - `chafa` (default) - `jp2a` - `ascii-image-converter` - `img2txt` - Each tool reveals its own option panel when selected - **chafa options** (full flag exposure): - `--width` / `--height` (integer inputs — combined server-side into `--size=WxH`) - `--colors` (none | 2 | 8 | 16/8 | 16 | 240 | 256 | full) - `--symbols` (text input, e.g. `block+border+extra`) - `--dither` (none | ordered | diffusion | noise) - `--threshold` (0.0–1.0 slider) - `--font-ratio` (float input) - **jp2a options**: `--width`, `--height`, `--chars`, `--background` - **ascii-image-converter options**: `--width`, `--height`, `--color`, `--braille`, `--threshold` - **img2txt options**: `--width`, `--height`, `--format` (ansi/utf8/html), `--dither`, `--gamma` - Emits: `tool-config` with `{ tool: string, flags: Record }` ### 3.3 `ShellBridge.vue` - Renderless component (``) - Props: `image` (`{ dataUrl, mimeType, filename }`), `toolConfig` (`{ tool, flags }`) - Exposes: `convert()` — called via template ref from App.vue on button click - Converts `dataUrl` back to Blob via `fetch()`, sends as `multipart/form-data` - POSTs to `/api/convert` (proxied by Vite dev server to `http://localhost:3050/convert`) - Emits: `conversion-result` (stdout string) | `conversion-error` (stderr string) - Handles: 30s timeout via `AbortController`, network errors, non-2xx responses ### 3.4 `OutputDisplay.vue` - Receives plain-text ASCII output string as prop - Renders in a `
` block with monospace font (JetBrains Mono or Fira Code via CDN)
- Controls: copy-to-clipboard button, download as `.txt`, font-size slider
- Preserves whitespace exactly — never reformat or trim output

### 3.5 `ErrorLog.vue`
- Receives array of `{ timestamp, source, message }` error objects as prop
- Collapsible panel, pinned to bottom of viewport
- Color-coded: yellow = warning, red = error
- Shows last 50 entries, auto-scrolls to latest
- Clear button

---

## 4. Fastify Server (`server/`)

### Entry: `server/index.js`
```js
// Fastify + @fastify/multipart
// Listens on localhost:3050
// Single route: POST /convert
// CORS: origin: true (reflects requester — permissive for local dev)
// Logger: warn level only (startup logged via console.log)
```

### Route: `POST /convert`
1. Parse multipart body — extract image file + JSON fields (`tool`, `flags`)
2. Write image to temp file (`/tmp/asciinator-.`)
3. Pass through ImageMagick preprocessing (see §4.1)
4. Invoke selected tool via `converters.js`
5. Stream stdout back as `text/plain`
6. Clean up temp files in `finally`
7. On non-zero exit: return 422 with stderr body

### 4.1 ImageMagick Pipeline (`lib/imagemagick.js`)
**Preprocessing** (always runs before ASCII tool):
- Convert to PNG by default, JPEG for jp2a (`magick input.x output.{png,jpg}`)
- Strip metadata (`-strip`)
- Auto-orient (`-auto-orient`)
- Resize to max 2000px on longest side if larger (`-resize 2000x2000>`)
- `jp2a` receives JPEG — it links against libjpeg and rejects PNG input

**Post-processing** (optional, future hook — stub it now):
- Reserved for potential brightness/contrast adjustment on output

### 4.2 Converters (`lib/converters.js`)
Each converter is a function:
```js
async function runChafa(imagePath, flags) → { stdout, stderr }
async function runJp2a(imagePath, flags) → { stdout, stderr }
async function runAsciiImageConverter(imagePath, flags) → { stdout, stderr }
async function runImgToTxt(imagePath, flags) → { stdout, stderr }
```
- Use `execa` for all invocations
- Map the `flags` object to CLI args (e.g. `{ width: 80 }` → `['--width', '80']`)
- Validate flag values before constructing args — reject unknown flags

---

## 5. State Management

Use Vue 3 `reactive()` in `App.vue` — no Pinia for this scale.

```js
const state = reactive({
  image: null,          // { dataUrl, mimeType, filename }
  toolConfig: { tool: 'chafa', flags: {} },
  result: '',           // plain text ASCII output
  errors: [],           // [{ timestamp, source, message }]
  isConverting: false,
})
```

Pass state slices as props, bubble events up. No global store.

---

## 6. Styling Conventions

- **Tailwind only** — no inline styles, no external CSS except `src/assets/main.css`
- Always dark — `class="dark"` hardcoded on ``. Light mode support planned (needs CSS variable overrides)
- CSS custom properties in `main.css` for brand tokens:
  ```css
  :root {
    --ascii-green: #39ff14;   /* neon accent */
    --ascii-bg: #0d0d0d;
    --ascii-surface: #1a1a1a;
    --ascii-border: #2a2a2a;
    --ascii-text: #e0e0e0;
    --ascii-error: #ff4444;
    --ascii-warn: #ffaa00;
  }
  ```
- Monospace font for all output: `font-family: 'JetBrains Mono', 'Fira Code', monospace`
- UI font: `font-family: 'IBM Plex Sans', sans-serif`
- Load both via Google Fonts / Bunny Fonts in `index.html`

**`@layer components` utility classes** (defined in `main.css` — use these in templates):

| Class | Purpose |
|-------|---------|
| `.ascii-input` | Dark-themed text input or select — full width, bordered, focus ring |
| `.ascii-label` | Column-flex label wrapper with muted text — wraps a `.ascii-input` |
| `.ascii-btn` | Ghost button — transparent bg, border, hover turns neon green |
| `.ascii-btn-primary` | Solid neon-green button — used for the main Convert action |

---

## 7. Neovim IDE v2 Setup

### 7.1 LSP & Tooling (expected config)
| Tool | Role |
|------|------|
| `volar` | Vue 3 LSP (use Takeover mode — disable `tsserver` for `.vue` files) |
| `eslint-lsp` | Linting via `eslint_d` or native LSP client |
| `prettier` | Formatting — `.prettierrc` at root |
| `emmet-ls` | HTML/template expansion in `.vue` files |
| `cssls` | Tailwind class intellisense (pair with `tailwindcss-language-server`) |

### 7.2 `.prettierrc`
```json
{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "printWidth": 100,
  "vueIndentScriptAndStyle": false
}
```

### 7.3 `.eslintrc.cjs`
```js
module.exports = {
  extends: ['plugin:vue/vue3-recommended', 'prettier'],
  rules: {
    'vue/multi-word-component-names': 'off'
  }
}
```

### 7.4 Neovim v2 Test Notes
This project is a test bed for **Neovim IDE v2.1.x**. Log observations here:

- [ ] Volar Takeover mode stability with Vite HMR running
- [ ] Tailwind intellisense triggering inside `class=""` in `.vue` templates
- [ ] `emmet-ls` expansion in `