ASCIInator/CLAUDE.md
dev c1cf06ed67 feat: initial commit — ASCIInator v0.1
Vue 3 + Vite + Tailwind frontend, Fastify API server, execa shell bridge.
All components built, server validated, 41 unit tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:47:57 +00:00

343 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)*