- Auto-convert on image load (0ms) and flag change (400ms debounce) - SizeControl: linked width/height sliders with aspect-ratio lock and 0.5 font correction factor - Full flag exposure for all 4 tools (chafa, jp2a, ascii-image-converter, img2txt) - ChafaOptions: symbols/fill dropdowns, dither controls, work/threshold/font-ratio sliders, format select, toggles - Jp2aOptions: color-depth, RGB weight sliders, edge controls, 8 toggles - ImgTxtOptions: dither select with valid libcaca values, gamma slider - OutputDisplay: ansi-to-html rendering for colored chafa output - ShellBridge: abort-previous pattern, conversion-start/end lifecycle events - Test API (ENABLE_TEST_API=true): /test/health, /test/convert, /test/flags/:tool, /test/imagemagick - buildArgs: space-separated args (not = format); full schemas in SCHEMAS export - runChafa: width/height destructured and combined into --size WxH - Port changed to 3050; Vite on 0.0.0.0 with allowedHosts for production domain - 98 unit tests passing across 12 test files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
343 lines
12 KiB
Markdown
343 lines
12 KiB
Markdown
# 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<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: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 `<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: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-<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:3050
|
||
```
|
||
|
||
### 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)*
|