ASCIInator/CLAUDE.md

343 lines
12 KiB
Markdown
Raw Normal View History

# 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.01.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<string, string|number|boolean> }`
### 3.3 `ShellBridge.vue`
- Renderless component (`<template><slot /></template>`)
- 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:3001/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 `<pre>` 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:3001
// 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-<uuid>.<ext>`)
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 `<html>`. 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 `<template>` blocks
- [ ] ESLint auto-fix on save (`BufWritePre` autocmd)
- [ ] Prettier format on save — confirm it doesn't fight ESLint
- [ ] `gd` (go-to-definition) across `.vue` SFC boundaries
- [ ] Telescope file search respecting `.gitignore` (node_modules, dist)
- [ ] DAP (debugger) attach to Vite dev server — note any issues
- [ ] Terminal split for running Fastify server alongside Vite
**Add test results below as you work:**
```
DATE | COMPONENT | OBSERVATION | PASS/FAIL
```
---
## 8. Dev Workflow
```bash
# Install
npm install
# Start Fastify server (terminal 1)
node server/index.js
# Start Vite dev server (terminal 2)
npm run dev
# Both should be running simultaneously during development
# Vue: http://localhost:5173
# Fastify API: http://localhost:3001
```
### npm scripts (`package.json`)
```json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"server": "node server/index.js",
"server:dev": "node --watch server/index.js",
"test:unit": "vitest",
"test:e2e": "playwright test",
"lint": "run-s lint:oxlint lint:eslint",
"format": "prettier --write --experimental-cli src/"
}
}
```
---
## 9. Key Constraints for Claude Code
- **Never** install a Vue component library (no Vuetify, Headless UI, PrimeVue, etc.)
- **Never** add TypeScript — this is a JS project
- **Never** use `Options API` — Composition API with `<script setup>` only
- **Never** add Pinia — use `reactive()` in App.vue
- **Never** use `localStorage` for anything
- **Always** use `execa` for shell invocations — never `exec()` or raw `spawn()`
- **Always** sanitize/validate flag values before building CLI args
- **Always** clean up `/tmp/asciinator-*` files after each conversion
- **Always** use `<script setup>` SFC format
- **Always** keep components under ~150 lines — extract composables if growing
- **Never** remove Vitest or Playwright — they were scaffolded by Vite and are part of the test bed
- **Unit tests** live in `src/__tests__/` (App) and `src/components/__tests__/` (components) — Vitest, not Jest
- **e2e tests** live in `e2e/` — Playwright
---
## 10. Dependencies
### All in root `package.json` (single package, no workspaces)
```json
{
"dependencies": {
"vue": "^3.5",
"fastify": "^5.x",
"@fastify/multipart": "^10.x",
"@fastify/cors": "^11.x",
"execa": "^9.x"
},
"devDependencies": {
"vite": "^8.x",
"@vitejs/plugin-vue": "^6.x",
"tailwindcss": "^3.x",
"autoprefixer": "^10.x",
"postcss": "^8.x",
"vitest": "^4.x",
"@playwright/test": "^1.x",
"@vue/test-utils": "^2.x",
"eslint": "^10.x",
"eslint-plugin-vue": "^10.x",
"oxlint": "^1.x",
"prettier": "^3.x"
}
}
```
---
*Last updated: 2026-04-28 — ASCIInator v0.1 (all components built)*