ASCIInator/CLAUDE.md
dev e904cdb3cb feat: full GUI, test API, auto-convert, aspect-ratio sliders
- 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>
2026-04-29 10:29:45 +00:00

12 KiB
Raw Permalink Blame 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: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

// 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:

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.

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:
    :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

{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "printWidth": 100,
  "vueIndentScriptAndStyle": false
}

7.3 .eslintrc.cjs

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

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

{
  "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)

{
  "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)